Merge "Update default alignment for base Button overload for consistency with other overloads." into androidx-main
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index 0b88e7c..2f5d5d7 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -42,7 +42,7 @@
         wasmJsMain {
             dependsOn(nonJvmMain)
             dependencies {
-                api(libs.kotlinStdlibJs)
+                implementation(libs.kotlinStdlibJs)
             }
         }
 
diff --git a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
index 12612a1..028e2dc 100644
--- a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
+++ b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
@@ -40,7 +40,7 @@
      * should be <code>{@value #AUTOFILL_HINT_EMAIL_ADDRESS}</code>).
      *
      * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
-     * hints.3
+     * hints.
      */
     public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
index 7aa48d6..dc61707 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
@@ -35,9 +35,13 @@
             try {
                 streamConfigurationMap?.outputFormats
             } catch (e: NullPointerException) {
-                Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+                Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+                null
+            } catch (e: IllegalArgumentException) {
+                Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
                 null
             }
+
         return outputFormats?.toTypedArray()
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
index a933a0a..d9e49d0 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
@@ -168,6 +168,8 @@
         val callback: RequestProcessor.Callback = mock()
 
         requestProcessorAdapter!!.setRepeating(requestToSet, callback)
+        advanceUntilIdle()
+
         val frame = cameraGraphSimulator!!.simulateNextFrame()
         val request = frame.request
         assertThat(request.streams.size).isEqualTo(1)
@@ -209,6 +211,8 @@
         val callback: RequestProcessor.Callback = mock()
 
         requestProcessorAdapter!!.submit(mutableListOf(requestToSubmit), callback)
+        advanceUntilIdle()
+
         val frame = cameraGraphSimulator!!.simulateNextFrame()
         val request = frame.request
         assertThat(request.streams.size).isEqualTo(1)
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
index 63a3e39..78d0670 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
@@ -34,10 +34,8 @@
 import androidx.camera.camera2.pipe.RequestFailure
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.media.ImageSource
-import kotlin.collections.removeFirst as removeFirstKt
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.withTimeout
 
 /**
  * This class creates a [CameraPipe] and [CameraGraph] instance using a [FakeCameraBackend].
@@ -166,7 +164,7 @@
         }
     }
 
-    public suspend fun simulateNextFrame(
+    public fun simulateNextFrame(
         advanceClockByNanos: Long = 33_366_666 // (2_000_000_000 / (60  / 1.001))
     ): FrameSimulator =
         generateNextFrame().also {
@@ -174,7 +172,7 @@
             it.simulateStarted(clockNanos)
         }
 
-    private suspend fun generateNextFrame(): FrameSimulator {
+    private fun generateNextFrame(): FrameSimulator {
         val captureSequenceProcessor = cameraController.currentCaptureSequenceProcessor
         check(captureSequenceProcessor != null) {
             "simulateCameraStarted() must be called before frames can be created!"
@@ -183,16 +181,19 @@
         // This checks the pending frame queue and polls for the next request. If no request is
         // available it will suspend until the next interaction with the request processor.
         if (pendingFrameQueue.isEmpty()) {
-            val requestSequence =
-                withTimeout(timeMillis = 250) { captureSequenceProcessor.nextRequestSequence() }
+            val captureSequence = captureSequenceProcessor.nextCaptureSequence()
+            checkNotNull(captureSequence) {
+                "Failed to simulate a CaptureSequence from $captureSequenceProcessor! Make sure " +
+                    "Requests have been submitted or that the repeating Request has been set."
+            }
 
             // Each sequence is processed as a group, and if a sequence contains multiple requests
             // the list of requests is processed in order before polling the next sequence.
-            for (request in requestSequence.captureRequestList) {
-                pendingFrameQueue.add(FrameSimulator(request, requestSequence))
+            for (request in captureSequence.captureRequestList) {
+                pendingFrameQueue.add(FrameSimulator(request, captureSequence))
             }
         }
-        return pendingFrameQueue.removeFirstKt()
+        return pendingFrameQueue.removeAt(0)
     }
 
     /** Utility function to simulate the production of a [FakeImage]s for one or more streams. */
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
new file mode 100644
index 0000000..7e718b1
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import androidx.camera.camera2.pipe.CameraId
+import kotlinx.atomicfu.atomic
+
+/**
+ * Utility class for tracking and creating Fake [CameraId] instances for use in testing.
+ *
+ * These id's are intentionally non-numerical to help prevent code that may assume that camera2
+ * camera ids are parsable.
+ */
+public object FakeCameraIds {
+    private val fakeCameraIds = atomic(0)
+    public val default: CameraId = CameraId("FakeCamera-default")
+
+    public fun next(): CameraId = CameraId("FakeCamera-${fakeCameraIds.getAndIncrement()}")
+}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
index 1280187..38f6520 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
@@ -27,12 +27,6 @@
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.Metadata
 import kotlin.reflect.KClass
-import kotlinx.atomicfu.atomic
-
-private val fakeCameraIds = atomic(0)
-
-internal fun nextFakeCameraId(): CameraId =
-    CameraId("FakeCamera-${fakeCameraIds.incrementAndGet()}")
 
 /** Utility class for interacting with objects that require pre-populated Metadata. */
 public open class FakeMetadata(private val metadata: Map<Metadata.Key<*>, Any?> = emptyMap()) :
@@ -56,7 +50,7 @@
 public class FakeCameraMetadata(
     private val characteristics: Map<CameraCharacteristics.Key<*>, Any?> = emptyMap(),
     metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
-    cameraId: CameraId = nextFakeCameraId(),
+    cameraId: CameraId = FakeCameraIds.default,
     override val keys: Set<CameraCharacteristics.Key<*>> = emptySet(),
     override val requestKeys: Set<CaptureRequest.Key<*>> = emptySet(),
     override val resultKeys: Set<CaptureResult.Key<*>> = emptySet(),
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
index 5010c90..161d428 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
@@ -25,48 +25,81 @@
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.StreamId
 import kotlinx.atomicfu.atomic
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.withTimeout
 
 /**
- * Fake implementation of a [CaptureSequenceProcessor] that passes events to a [Channel].
+ * Fake implementation of a [CaptureSequenceProcessor] that records events and simulates some low
+ * level behavior.
  *
  * This allows kotlin tests to check sequences of interactions that dispatch in the background
  * without blocking between events.
  */
 public class FakeCaptureSequenceProcessor(
-    private val cameraId: CameraId = CameraId("test-camera"),
+    private val cameraId: CameraId = FakeCameraIds.default,
     private val defaultTemplate: RequestTemplate = RequestTemplate(1)
 ) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
+    private val debugId = debugIds.incrementAndGet()
     private val lock = Any()
     private val sequenceIds = atomic(0)
-    private val eventChannel = Channel<Event>(Channel.UNLIMITED)
 
-    @GuardedBy("lock") private var pendingSequence: CompletableDeferred<FakeCaptureSequence>? = null
+    @GuardedBy("lock") private val captureQueue = mutableListOf<FakeCaptureSequence>()
 
-    @GuardedBy("lock") private val queue: MutableList<FakeCaptureSequence> = mutableListOf()
+    @GuardedBy("lock") private var repeatingCapture: FakeCaptureSequence? = null
 
-    @GuardedBy("lock") private var repeatingRequestSequence: FakeCaptureSequence? = null
+    @GuardedBy("lock") private var shutdown = false
 
-    @GuardedBy("lock") private var _rejectRequests = false
+    @GuardedBy("lock") private val _events = mutableListOf<Event>()
 
-    public var rejectRequests: Boolean
-        get() = synchronized(lock) { _rejectRequests }
-        set(value) {
-            synchronized(lock) { _rejectRequests = value }
+    @GuardedBy("lock") private var nextEventIndex = 0
+    public val events: List<Event>
+        get() = synchronized(lock) { _events }
+
+    /** Get the next event from queue with an option to specify a timeout for tests. */
+    public fun nextEvent(): Event {
+        synchronized(lock) {
+            val eventIdx = nextEventIndex++
+            check(_events.size > 0) {
+                "Failed to get next event for $this, there have been no interactions."
+            }
+            check(eventIdx < _events.size) {
+                "Failed to get next event. Last event was ${events[eventIdx - 1]}"
+            }
+            return events[eventIdx]
         }
+    }
 
-    private var _surfaceMap: Map<StreamId, Surface> = emptyMap()
-    public var surfaceMap: Map<StreamId, Surface>
-        get() = synchronized(lock) { _surfaceMap }
+    public fun clearEvents() {
+        synchronized(lock) {
+            _events.clear()
+            nextEventIndex = 0
+        }
+    }
+
+    public var rejectBuild: Boolean = false
+        get() = synchronized(lock) { field }
+        set(value) = synchronized(lock) { field = value }
+
+    public var rejectSubmit: Boolean = false
+        get() = synchronized(lock) { field }
+        set(value) = synchronized(lock) { field = value }
+
+    public var surfaceMap: Map<StreamId, Surface> = emptyMap()
+        get() = synchronized(lock) { field }
         set(value) =
             synchronized(lock) {
-                _surfaceMap = value
+                field = value
                 println("Configured surfaceMap for $this")
             }
 
+    @Volatile public var throwOnBuild: Boolean = false
+
+    @Volatile public var throwOnSubmit: Boolean = false
+
+    @Volatile public var throwOnStop: Boolean = false
+
+    @Volatile public var throwOnAbort: Boolean = false
+
+    @Volatile public var throwOnShutdown: Boolean = false
+
     override fun build(
         isRepeating: Boolean,
         requests: List<Request>,
@@ -75,136 +108,157 @@
         listeners: List<Request.Listener>,
         sequenceListener: CaptureSequenceListener
     ): FakeCaptureSequence? {
-        return FakeCaptureSequence.create(
-            cameraId = cameraId,
-            repeating = isRepeating,
-            requests = requests,
-            surfaceMap = surfaceMap,
-            defaultTemplate = defaultTemplate,
-            defaultParameters = defaultParameters,
-            requiredParameters = requiredParameters,
-            listeners = listeners,
-            sequenceListener = sequenceListener
-        )
+        throwTestExceptionIf(throwOnBuild)
+
+        val captureSequence =
+            FakeCaptureSequence.create(
+                cameraId,
+                isRepeating,
+                requests,
+                surfaceMap,
+                defaultTemplate,
+                defaultParameters,
+                requiredParameters,
+                listeners,
+                sequenceListener
+            )
+        synchronized(lock) {
+            if (rejectBuild || shutdown || captureSequence == null) {
+                println("$this: BuildRejected $captureSequence")
+                _events.add(BuildRejected(captureSequence))
+                return null
+            }
+        }
+        println("$this: Build $captureSequence")
+        return captureSequence
     }
 
     override fun submit(captureSequence: FakeCaptureSequence): Int {
-        println("submit $captureSequence")
+        throwTestExceptionIf(throwOnSubmit)
         synchronized(lock) {
-            if (rejectRequests) {
-                check(
-                    eventChannel
-                        .trySend(Event(requestSequence = captureSequence, rejected = true))
-                        .isSuccess
-                )
+            if (rejectSubmit || shutdown) {
+                println("$this: SubmitRejected $captureSequence")
+                _events.add(SubmitRejected(captureSequence))
                 return -1
             }
-            queue.add(captureSequence)
+            captureQueue.add(captureSequence)
             if (captureSequence.repeating) {
-                repeatingRequestSequence = captureSequence
+                repeatingCapture = captureSequence
             }
-            check(
-                eventChannel
-                    .trySend(Event(requestSequence = captureSequence, submit = true))
-                    .isSuccess
-            )
-            // If there is a non-null pending sequence, make sure we complete it here.
-            pendingSequence?.also {
-                pendingSequence = null
-                it.complete(captureSequence)
-            }
+            println("$this: Submit $captureSequence")
+            _events.add(Submit(captureSequence))
             return sequenceIds.incrementAndGet()
         }
     }
 
     override fun abortCaptures() {
+        throwTestExceptionIf(throwOnAbort)
+
         val requestSequencesToAbort: List<FakeCaptureSequence>
         synchronized(lock) {
-            requestSequencesToAbort = queue.toList()
-            queue.clear()
-            check(eventChannel.trySend(Event(abort = true)).isSuccess)
+            println("$this: AbortCaptures")
+            _events.add(AbortCaptures)
+            requestSequencesToAbort = captureQueue.toList()
+            captureQueue.clear()
         }
+
         for (sequence in requestSequencesToAbort) {
             sequence.invokeOnSequenceAborted()
         }
     }
 
     override fun stopRepeating() {
-        val requestSequence =
-            synchronized(lock) {
-                check(eventChannel.trySend(Event(stop = true)).isSuccess)
-                repeatingRequestSequence.also { repeatingRequestSequence = null }
-            }
-        requestSequence?.invokeOnSequenceAborted()
+        throwTestExceptionIf(throwOnStop)
+        synchronized(lock) {
+            println("$this: StopRepeating")
+            _events.add(StopRepeating)
+            repeatingCapture = null
+        }
     }
 
     override suspend fun shutdown() {
+        throwTestExceptionIf(throwOnShutdown)
         synchronized(lock) {
-            rejectRequests = true
-            check(eventChannel.trySend(Event(close = true)).isSuccess)
+            println("$this: Shutdown")
+            shutdown = true
+            _events.add(Shutdown)
         }
     }
 
-    /** Get the next event from queue with an option to specify a timeout for tests. */
-    public suspend fun nextEvent(timeMillis: Long = 500): Event =
-        withTimeout(timeMillis) { eventChannel.receive() }
+    override fun toString(): String {
+        return "FakeCaptureSequenceProcessor-$debugId($cameraId)"
+    }
 
-    public suspend fun nextRequestSequence(): FakeCaptureSequence {
-        while (true) {
-            val pending: Deferred<FakeCaptureSequence>
-            synchronized(lock) {
-                var sequence = queue.removeFirstOrNull()
-                if (sequence == null) {
-                    sequence = repeatingRequestSequence
-                }
-                if (sequence != null) {
-                    return sequence
-                }
+    /**
+     * Get the next CaptureSequence from this CaptureSequenceProcessor. If there are non-repeating
+     * capture requests in the queue, remove the first item from the queue. Otherwise, return the
+     * current repeating CaptureSequence, or null if there are no active CaptureSequences.
+     */
+    internal fun nextCaptureSequence(): FakeCaptureSequence? =
+        synchronized(lock) { captureQueue.removeFirstOrNull() ?: repeatingCapture }
 
-                if (pendingSequence == null) {
-                    pendingSequence = CompletableDeferred()
-                }
-                pending = pendingSequence!!
-            }
-
-            pending.await()
+    private fun throwTestExceptionIf(condition: Boolean) {
+        if (condition) {
+            throw RuntimeException("Test Exception")
         }
     }
 
-    /** TODO: It's probably better to model this as a sealed class. */
-    public data class Event(
-        val requestSequence: FakeCaptureSequence? = null,
-        val rejected: Boolean = false,
-        val abort: Boolean = false,
-        val close: Boolean = false,
-        val stop: Boolean = false,
-        val submit: Boolean = false
-    )
+    public open class Event
+
+    public object Shutdown : Event()
+
+    public object StopRepeating : Event()
+
+    public object AbortCaptures : Event()
+
+    public data class BuildRejected(val captureSequence: FakeCaptureSequence?) : Event()
+
+    public data class SubmitRejected(val captureSequence: FakeCaptureSequence) : Event()
+
+    public data class Submit(val captureSequence: FakeCaptureSequence) : Event()
 
     public companion object {
-        public suspend fun FakeCaptureSequenceProcessor.awaitEvent(
-            request: Request? = null,
-            filter: (event: Event) -> Boolean
-        ): Event {
+        private val debugIds = atomic(0)
+        public val Event.requests: List<Request>
+            get() = checkNotNull(captureSequence).captureRequestList
 
-            var event: Event
-            var loopCount = 0
-            while (loopCount < 10) {
-                loopCount++
-                event = this.nextEvent()
+        public val Event.requiredParameters: Map<*, Any?>
+            get() = checkNotNull(captureSequence).requiredParameters
 
-                if (request != null) {
-                    val contains =
-                        event.requestSequence?.captureRequestList?.contains(request) ?: false
-                    if (filter(event) && contains) {
-                        return event
-                    }
-                } else if (filter(event)) {
-                    return event
+        public val Event.defaultParameters: Map<*, Any?>
+            get() = checkNotNull(captureSequence).defaultParameters
+
+        // TODO: Decide if these should only work on successful submit or not.
+        public val Event.isRepeating: Boolean
+            get() = (this as? Submit)?.captureSequence?.repeating ?: false
+
+        public val Event.isCapture: Boolean
+            get() = (this as? Submit)?.captureSequence?.repeating == false
+
+        public val Event.isRejected: Boolean
+            get() =
+                when (this) {
+                    is BuildRejected,
+                    is SubmitRejected -> true
+                    else -> false
                 }
-            }
 
-            throw IllegalStateException("Failed to observe a submit event containing $request")
-        }
+        public val Event.isAbort: Boolean
+            get() = this is AbortCaptures
+
+        public val Event.isStopRepeating: Boolean
+            get() = this is StopRepeating
+
+        public val Event.isClose: Boolean
+            get() = this is Shutdown
+
+        public val Event.captureSequence: FakeCaptureSequence?
+            get() =
+                when (this) {
+                    is Submit -> captureSequence
+                    is BuildRejected -> captureSequence
+                    is SubmitRejected -> captureSequence
+                    else -> null
+                }
     }
 }
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
index 90465f2..f24a252 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
@@ -35,7 +35,7 @@
 public class FakeFrameMetadata(
     private val resultMetadata: Map<CaptureResult.Key<*>, Any?> = emptyMap(),
     extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
-    override val camera: CameraId = nextFakeCameraId(),
+    override val camera: CameraId = FakeCameraIds.default,
     override val frameNumber: FrameNumber = nextFakeFrameNumber(),
     override val extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
 ) : FakeMetadata(extraResultMetadata), FrameMetadata {
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
index d7373f1..f1e3fd5 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
@@ -36,13 +36,17 @@
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class CameraPipeSimulatorTest {
     private val testScope = TestScope()
-    private val frontCameraMetadata =
-        FakeCameraMetadata(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
-        )
     private val backCameraMetadata =
         FakeCameraMetadata(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+            cameraId = FakeCameraIds.next(),
+            characteristics =
+                mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+        )
+    private val frontCameraMetadata =
+        FakeCameraMetadata(
+            cameraId = FakeCameraIds.next(),
+            characteristics =
+                mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
         )
 
     private val streamConfig = CameraStream.Config.create(Size(640, 480), StreamFormat.YUV_420_888)
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
index d91dd8e..fed7ff7 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
@@ -31,22 +31,30 @@
 class FakeCameraDevicesTest {
     private val EXTERNAL_BACKEND_ID =
         CameraBackendId("androidx.camera.camera2.pipe.testing.FakeCameraDevicesTest")
-    private val metadata1 =
+    private val frontMetadata =
         FakeCameraMetadata(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
+            cameraId = FakeCameraIds.next(),
+            characteristics =
+                mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
         )
-    private val metadata2 =
+    private val backMetadata =
         FakeCameraMetadata(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+            cameraId = FakeCameraIds.next(),
+            characteristics =
+                mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
         )
-    private val metadata3 =
+    private val extMetadata =
         FakeCameraMetadata(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL)
+            cameraId = FakeCameraIds.next(),
+            characteristics =
+                mapOf(
+                    CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL
+                )
         )
     private val cameraMetadataMap =
         mapOf(
-            FAKE_CAMERA_BACKEND_ID to listOf(metadata1, metadata2),
-            EXTERNAL_BACKEND_ID to listOf(metadata3)
+            FAKE_CAMERA_BACKEND_ID to listOf(frontMetadata, backMetadata),
+            EXTERNAL_BACKEND_ID to listOf(extMetadata)
         )
 
     @Test
@@ -63,11 +71,13 @@
             )
         val devices = cameraDevices.getCameraIds()
         assertThat(devices)
-            .containsExactlyElementsIn(listOf(metadata1.camera, metadata2.camera))
+            .containsExactlyElementsIn(listOf(frontMetadata.camera, backMetadata.camera))
             .inOrder()
 
-        assertThat(cameraDevices.getCameraMetadata(metadata1.camera)).isSameInstanceAs(metadata1)
-        assertThat(cameraDevices.getCameraMetadata(metadata2.camera)).isSameInstanceAs(metadata2)
+        assertThat(cameraDevices.getCameraMetadata(frontMetadata.camera))
+            .isSameInstanceAs(frontMetadata)
+        assertThat(cameraDevices.getCameraMetadata(backMetadata.camera))
+            .isSameInstanceAs(backMetadata)
     }
 
     @Test
@@ -86,13 +96,13 @@
         assertThat(devices)
             .containsExactlyElementsIn(
                 listOf(
-                    metadata3.camera,
+                    extMetadata.camera,
                 )
             )
             .inOrder()
 
-        assertThat(cameraDevices.getCameraMetadata(metadata3.camera)).isNull()
-        assertThat(cameraDevices.getCameraMetadata(metadata3.camera, EXTERNAL_BACKEND_ID))
-            .isSameInstanceAs(metadata3)
+        assertThat(cameraDevices.getCameraMetadata(extMetadata.camera)).isNull()
+        assertThat(cameraDevices.getCameraMetadata(extMetadata.camera, EXTERNAL_BACKEND_ID))
+            .isSameInstanceAs(extMetadata)
     }
 }
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
index e276d6c..d418346 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
@@ -62,7 +62,6 @@
             )
 
         assertThat(metadata1).isNotEqualTo(metadata2)
-        assertThat(metadata1.camera).isNotEqualTo(metadata2.camera)
     }
 
     @Test
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
index d75b9f9..89467a3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
@@ -18,6 +18,8 @@
 
 import android.hardware.camera2.CameraCaptureSession
 import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
 import android.hardware.camera2.CaptureFailure
 import android.hardware.camera2.CaptureRequest
 import android.view.Surface
@@ -107,6 +109,25 @@
         ) {}
 
         /**
+         * This event provides clients with an estimate of the post-processing progress of a capture
+         * which could take significantly more time relative to the rest of the
+         * [CameraExtensionSession.capture] sequence. The callback will be triggered only by
+         * extensions that return true from calls
+         * [CameraExtensionCharacteristics.isCaptureProcessProgressAvailable]. If support for this
+         * callback is present, then clients will be notified at least once with progress value 100.
+         * The callback will be triggered only for still capture requests
+         * [CameraExtensionSession.capture] and is not supported for repeating requests
+         * [CameraExtensionSession.setRepeatingRequest].
+         *
+         * @param requestMetadata the data about the camera2 request that was sent to the camera.
+         * @param progress the value indicating the current post-processing progress (between 0 and
+         *   100 inclusive)
+         * @see
+         *   android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback.onCaptureProcessProgressed
+         */
+        public fun onCaptureProgress(requestMetadata: RequestMetadata, progress: Int) {}
+
+        /**
          * This event indicates that all of the metadata associated with this frame has been
          * produced. If [onPartialCaptureResult] was invoked, the values returned in the
          * totalCaptureResult map be a superset of the values produced from the
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
index 9d10ac0..ebc244d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
@@ -46,6 +46,8 @@
         frameNumber: FrameNumber
     )
 
+    fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int)
+
     fun onCaptureFailed(captureRequest: CaptureRequest, frameNumber: FrameNumber)
 
     fun onCaptureSequenceCompleted(captureSequenceId: Int, captureFrameNumber: Long)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
index 34a0a1d..cc5e292 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
@@ -184,6 +184,15 @@
         Debug.traceStop() // onCaptureCompleted
     }
 
+    override fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int) {
+        Debug.traceStart { "onCaptureProcessProgressed" }
+        // Load the request and throw if we are not able to find an associated request. Under
+        // normal circumstances this should never happen.
+        val request = readRequestMetadata(captureRequest)
+        invokeOnRequest(request) { it.onCaptureProgress(request, progress) }
+        Debug.traceStop()
+    }
+
     override fun onCaptureFailed(
         captureSession: CameraCaptureSession,
         captureRequest: CaptureRequest,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
index 7fb4d94..608dc0f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
@@ -278,6 +278,14 @@
             request: CaptureRequest
         ) {}
 
+        override fun onCaptureProcessProgressed(
+            session: CameraExtensionSession,
+            request: CaptureRequest,
+            progress: Int
+        ) {
+            captureCallback.onCaptureProcessProgressed(request, progress)
+        }
+
         override fun onCaptureFailed(session: CameraExtensionSession, request: CaptureRequest) {
             val frameNumber = frameQueue.remove()
             captureCallback.onCaptureFailed(request, FrameNumber(frameNumber))
@@ -342,6 +350,14 @@
             }
         }
 
+        override fun onCaptureProcessProgressed(
+            session: CameraExtensionSession,
+            request: CaptureRequest,
+            progress: Int
+        ) {
+            captureCallback.onCaptureProcessProgressed(request, progress)
+        }
+
         override fun onCaptureSequenceCompleted(session: CameraExtensionSession, sequenceId: Int) {
             val frameNumber = extensionSessionMap[session]
             captureCallback.onCaptureSequenceCompleted(sequenceId, frameNumber!!)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
index 80ce6b0f..cc5d57a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
@@ -373,8 +373,8 @@
                 if (success) {
                     lastRepeatingRequest = command.request
                     commands.removeAt(idx)
+                    commands.removeUpTo(idx) { it is StartRepeating }
                 }
-                commands.removeUpTo(idx) { it is StartRepeating }
             }
             is SubmitCapture -> {
                 if (!_captureProcessingEnabled.value) {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index 4b35f50..a9e597f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -477,14 +478,20 @@
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
-        // call. There should be a request to cancel AF and AE precapture metering.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
+        // call. There should be a request to cancel AF and AE precapture metering
+        val event1 = captureSequenceProcessor.nextEvent()
         if (cancelAf) {
-            assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+            assertThat(event1.requiredParameters)
+                .containsEntry(
+                    CaptureRequest.CONTROL_AF_TRIGGER,
+                    CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+                )
         }
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
+        assertThat(event1.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+            )
     }
 
     private fun testUnlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true) = runTest {
@@ -509,31 +516,37 @@
 
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
         // call. There should be a request to cancel AF and lock ae.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
+        val event1 = captureSequenceProcessor.nextEvent()
         if (cancelAf) {
-            assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+            assertThat(event1.requiredParameters)
+                .containsEntry(
+                    CaptureRequest.CONTROL_AF_TRIGGER,
+                    CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+                )
         }
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // Then another request to unlock ae.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+        val captureSequence2 = captureSequenceProcessor.nextEvent()
+        assertThat(captureSequence2.requiredParameters)
+            .containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
     }
 
-    private suspend fun assertCorrectCaptureSequenceInLock3AForCapture(
-        isAfTriggered: Boolean = true
-    ) {
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
+    private fun assertCorrectCaptureSequenceInLock3AForCapture(isAfTriggered: Boolean = true) {
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
             if (isAfTriggered) {
                 isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
             } else {
                 isNotEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
             }
         }
-        assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+        assertThat(event1.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+            )
     }
 
     companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
index 3085cc7..e9be1c9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
@@ -27,6 +27,10 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -147,14 +151,17 @@
 
         // We not check if the correct sequence of requests were submitted by lock3A call. The
         // request should be a repeating request to lock AE.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // The second request should be a single request to lock AF.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
-        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+        assertThat(event2.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
     }
 
     @Test
@@ -199,10 +206,12 @@
 
         // Check the correctness of the requests submitted by lock3A.
         // One repeating request was sent to monitor the state of AE to get converged.
-        captureSequenceProcessor.nextEvent().requestSequence
-        // Once AE is converged, another repeatingrequest is sent to lock AE.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.isRepeating).isTrue()
+
+        // Once AE is converged, another repeating request is sent to lock AE.
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
@@ -228,9 +237,12 @@
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // A single request to lock AF must have been used as well.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        val event3 = captureSequenceProcessor.nextEvent()
+        assertThat(event3.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
         globalScope.cancel()
     }
 
@@ -275,8 +287,8 @@
 
         // For a new AE scan we first send a request to unlock AE just in case it was
         // previously or internally locked.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
@@ -302,14 +314,17 @@
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // There should be one more request to lock AE after new scan is done.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // And one request to lock AF.
-        val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event3 = captureSequenceProcessor.nextEvent()
+        assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+        assertThat(event3.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
 
         globalScope.cancel()
     }
@@ -378,14 +393,17 @@
         // There should be one request to monitor AF to finish it's scan.
         captureSequenceProcessor.nextEvent()
         // One request to lock AE
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // And one request to lock AF.
-        val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event3 = captureSequenceProcessor.nextEvent()
+        assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+        assertThat(event3.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
         globalScope.cancel()
     }
 
@@ -451,21 +469,27 @@
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // One request to cancel AF to start a new scan.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+            )
         // There should be one request to monitor AF to finish it's scan.
         captureSequenceProcessor.nextEvent()
 
         // There should be one request to monitor lock AE.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // And one request to lock AF.
-        val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event3 = captureSequenceProcessor.nextEvent()
+        assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+        assertThat(event3.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
         globalScope.cancel()
     }
 
@@ -532,29 +556,26 @@
 
         // There should be one request to monitor AF to finish it's scan.
         val event = captureSequenceProcessor.nextEvent()
-        assertThat(event.requestSequence!!.repeating).isTrue()
-        assertThat(event.rejected).isFalse()
-        assertThat(event.abort).isFalse()
-        assertThat(event.close).isFalse()
-        assertThat(event.submit).isTrue()
+        assertThat(event.isRepeating).isTrue()
 
-        // One request to lock AE
+        // One request to lock AE (Repeating)
         val request2Event = captureSequenceProcessor.nextEvent()
-        assertThat(request2Event.requestSequence!!.repeating).isTrue()
-        assertThat(request2Event.submit).isTrue()
-        val request2 = request2Event.requestSequence!!
-        assertThat(request2).isNotNull()
-        assertThat(request2.requiredParameters).isNotEmpty()
-        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        assertThat(request2Event.isRepeating).isTrue()
+        assertThat(request2Event.requests.size).isEqualTo(1)
+        assertThat(request2Event.requiredParameters)
+            .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // And one request to lock AF.
         val request3Event = captureSequenceProcessor.nextEvent()
-        assertThat(request3Event.requestSequence!!.repeating).isFalse()
-        assertThat(request3Event.submit).isTrue()
-        val request3 = request3Event.requestSequence!!
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        assertThat(request3Event.isCapture).isTrue()
+        assertThat(request3Event.requests.size).isEqualTo(1)
+        assertThat(request3Event.requiredParameters)
+            .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+        assertThat(request3Event.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
 
         globalScope.cancel()
     }
@@ -620,22 +641,29 @@
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // One request to cancel AF to start a new scan.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+            )
+
         // There should be one request to unlock AE and monitor the current AF scan to finish.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
 
         // There should be one request to monitor lock AE.
-        val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event3 = captureSequenceProcessor.nextEvent()
+        assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // And one request to lock AF.
-        val request4 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request4!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
-        assertThat(request4.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event4 = captureSequenceProcessor.nextEvent()
+        assertThat(event4.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+        assertThat(event4.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
         globalScope.cancel()
     }
 
@@ -713,14 +741,17 @@
 
         // We not check if the correct sequence of requests were submitted by lock3A call. The
         // request should be a repeating request to lock AE.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
 
         // The second request should be a single request to lock AF.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
-        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event2.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
     }
 
     @Test
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
index e70b7a7..9305a51 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
@@ -25,6 +25,7 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -104,8 +105,8 @@
         assertThat(result.isCompleted).isFalse()
 
         // There should be one request to lock AE.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
 
         repeatingJob.cancel()
         repeatingJob.join()
@@ -164,9 +165,12 @@
         assertThat(result.isCompleted).isFalse()
 
         // There should be one request to unlock AF.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+            )
 
         repeatingJob.cancel()
         repeatingJob.join()
@@ -225,8 +229,8 @@
         assertThat(result.isCompleted).isFalse()
 
         // There should be one request to lock AWB.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AWB_LOCK]).isEqualTo(false)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AWB_LOCK, false)
 
         repeatingJob.cancel()
         repeatingJob.join()
@@ -287,12 +291,15 @@
         assertThat(result.isCompleted).isFalse()
 
         // There should be one request to unlock AF.
-        val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
-            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+        val event1 = captureSequenceProcessor.nextEvent()
+        assertThat(event1.requiredParameters)
+            .containsEntry(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+            )
         // Then request to unlock AE.
-        val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+        val event2 = captureSequenceProcessor.nextEvent()
+        assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
 
         repeatingJob.cancel()
         repeatingJob.join()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
index 728d859..711da09 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
@@ -17,21 +17,25 @@
 package androidx.camera.camera2.pipe.graph
 
 import android.os.Build
-import android.view.Surface
 import androidx.camera.camera2.pipe.CameraGraphId
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CaptureSequence
-import androidx.camera.camera2.pipe.CaptureSequenceProcessor
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequence
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.defaultParameters
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isAbort
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isStopRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
 import androidx.camera.camera2.pipe.testing.FakeMetadata.Companion.TEST_KEY
 import androidx.camera.camera2.pipe.testing.FakeSurfaces
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
@@ -73,8 +77,10 @@
             stream2 to fakeSurfaces.createFakeSurface()
         )
 
-    private val csp1 = SimpleCSP(fakeCameraId, surfaceMap)
-    private val csp2 = SimpleCSP(fakeCameraId, surfaceMap)
+    private val csp1 =
+        FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
+    private val csp2 =
+        FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
 
     private val grp1 = GraphRequestProcessor.from(csp1)
     private val grp2 = GraphRequestProcessor.from(csp2)
@@ -823,90 +829,32 @@
             assertThat(csp2.events[0].isClose).isTrue()
         }
 
-    private val SimpleCSP.SimpleCSPEvent.requests: List<Request>
-        get() = (this as SimpleCSP.Submit).captureSequence.captureRequestList
+    @Test
+    fun settingRepeatingRequestWhenRequestsAreRejectedDoesNotAttemptMultipleRepeatingRequests() =
+        testScope.runTest {
+            // Arrange
+            csp1.rejectSubmit = true
+            graphLoop.requestProcessor = grp1
+            graphLoop.repeatingRequest = request1
+            advanceUntilIdle()
 
-    private val SimpleCSP.SimpleCSPEvent.requiredParameters: Map<*, Any?>
-        get() = (this as SimpleCSP.Submit).captureSequence.requiredParameters
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRejected).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
 
-    private val SimpleCSP.SimpleCSPEvent.defaultParameters: Map<*, Any?>
-        get() = (this as SimpleCSP.Submit).captureSequence.defaultParameters
+            graphLoop.repeatingRequest = request2
+            advanceUntilIdle()
 
-    private val SimpleCSP.SimpleCSPEvent.isRepeating: Boolean
-        get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating ?: false
+            assertThat(csp1.events.size).isEqualTo(2)
+            assertThat(csp1.events[1].isRejected).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request2)
 
-    private val SimpleCSP.SimpleCSPEvent.isCapture: Boolean
-        get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating == false
+            csp1.rejectSubmit = false
+            graphLoop.invalidate()
+            advanceUntilIdle()
 
-    private val SimpleCSP.SimpleCSPEvent.isAbort: Boolean
-        get() = this is SimpleCSP.AbortCaptures
-
-    private val SimpleCSP.SimpleCSPEvent.isStopRepeating: Boolean
-        get() = this is SimpleCSP.StopRepeating
-
-    private val SimpleCSP.SimpleCSPEvent.isClose: Boolean
-        get() = this is SimpleCSP.Close
-
-    internal class SimpleCSP(
-        private val cameraId: CameraId,
-        private val surfaceMap: Map<StreamId, Surface>
-    ) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
-        val events = mutableListOf<SimpleCSPEvent>()
-        var throwOnBuild = false
-        private var closed = false
-        private val sequenceIds = atomic(0)
-
-        override fun build(
-            isRepeating: Boolean,
-            requests: List<Request>,
-            defaultParameters: Map<*, Any?>,
-            requiredParameters: Map<*, Any?>,
-            listeners: List<Request.Listener>,
-            sequenceListener: CaptureSequence.CaptureSequenceListener
-        ): FakeCaptureSequence? {
-            if (closed) return null
-            if (throwOnBuild) throw RuntimeException("Test Exception")
-            return FakeCaptureSequence.create(
-                cameraId = cameraId,
-                repeating = isRepeating,
-                requests = requests,
-                surfaceMap = surfaceMap,
-                defaultParameters = defaultParameters,
-                requiredParameters = requiredParameters,
-                listeners = listeners,
-                sequenceListener = sequenceListener
-            )
+            assertThat(csp1.events.size).isEqualTo(3)
+            assertThat(csp1.events[2].isRepeating).isTrue()
+            assertThat(csp1.events[2].requests).containsExactly(request2)
         }
-
-        override fun abortCaptures() {
-            events.add(AbortCaptures)
-        }
-
-        override fun stopRepeating() {
-            events.add(StopRepeating)
-        }
-
-        override suspend fun shutdown() {
-            closed = true
-            events.add(Close)
-        }
-
-        override fun submit(captureSequence: FakeCaptureSequence): Int? {
-            if (!closed) {
-                events.add(Submit(captureSequence))
-                return sequenceIds.incrementAndGet()
-            }
-            return null
-        }
-
-        sealed class SimpleCSPEvent
-
-        object Close : SimpleCSPEvent()
-
-        object StopRepeating : SimpleCSPEvent()
-
-        object AbortCaptures : SimpleCSPEvent()
-
-        data class Submit(val captureSequence: FakeCaptureSequence) : SimpleCSPEvent()
-    }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 13e29b9..c648043 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -28,7 +28,12 @@
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.awaitEvent
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
 import androidx.camera.camera2.pipe.testing.FakeGraphConfigs
 import androidx.camera.camera2.pipe.testing.FakeRequestListener
 import androidx.camera.camera2.pipe.testing.FakeThreads
@@ -36,10 +41,9 @@
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.withTimeoutOrNull
@@ -52,17 +56,20 @@
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class GraphProcessorTest {
+    private val testScope = TestScope()
+    private val fakeThreads = FakeThreads.fromTestScope(testScope)
+
     private val globalListener = FakeRequestListener()
     private val graphState3A = GraphState3A()
     private val graphListener3A = Listener3A()
     private val streamId = StreamId(0)
     private val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
 
-    private val fakeProcessor1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
-    private val fakeProcessor2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+    private val csp1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+    private val csp2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
 
-    private val graphRequestProcessor1 = GraphRequestProcessor.from(fakeProcessor1)
-    private val graphRequestProcessor2 = GraphRequestProcessor.from(fakeProcessor2)
+    private val grp1 = GraphRequestProcessor.from(csp1)
+    private val grp2 = GraphRequestProcessor.from(csp2)
 
     private val requestListener1 = FakeRequestListener()
     private val request1 = Request(listOf(StreamId(0)), listeners = listOf(requestListener1))
@@ -70,516 +77,375 @@
     private val requestListener2 = FakeRequestListener()
     private val request2 = Request(listOf(StreamId(0)), listeners = listOf(requestListener2))
 
+    private val graphProcessor =
+        GraphProcessorImpl(
+            fakeThreads,
+            CameraGraphId.nextId(),
+            FakeGraphConfigs.graphConfig,
+            graphState3A,
+            graphListener3A,
+            arrayListOf(globalListener)
+        )
+
     @After
     fun teardown() {
         surfaceMap[streamId]?.release()
     }
 
     @Test
-    fun graphProcessorSubmitsRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.submit(request1)
-        advanceUntilIdle()
+    fun graphProcessorSubmitsRequests() =
+        testScope.runTest {
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.submit(request1)
+            advanceUntilIdle()
 
-        // Make sure the requests get submitted to the request processor
-        val event = fakeProcessor1.nextEvent()
-        assertThat(event.requestSequence!!.captureRequestList).containsExactly(request1)
-        assertThat(event.requestSequence!!.requiredParameters)
-            .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
-    }
+            // Make sure the requests get submitted to the request processor
+            assertThat(csp1.events.size).isEqualTo(1)
 
-    @Test
-    fun graphProcessorSubmitsRequestsToMostRecentProcessor() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.onGraphStarted(graphRequestProcessor2)
-        graphProcessor.submit(request1)
-
-        val event1 = fakeProcessor1.nextEvent()
-        assertThat(event1.close).isTrue()
-
-        val event2 = fakeProcessor2.nextEvent()
-        assertThat(event2.submit).isTrue()
-        assertThat(event2.requestSequence!!.captureRequestList).containsExactly(request1)
-    }
-
-    @Test
-    fun graphProcessorSubmitsQueuedRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-
-        graphProcessor.submit(request1)
-        graphProcessor.submit(request2)
-
-        // Request1 and 2 should be queued and will be submitted even when the request
-        // processor is set after the requests are submitted.
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-
-        val event1 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
-        assertThat(event1.requestSequence!!.captureRequestList).hasSize(1)
-        assertThat(event1.requestSequence!!.captureRequestList).contains(request1)
-
-        val event2 = fakeProcessor1.nextEvent()
-        assertThat(event2.requestSequence!!.captureRequestList).hasSize(1)
-        assertThat(event2.requestSequence!!.captureRequestList).contains(request2)
-    }
-
-    @Test
-    fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-
-        graphProcessor.submit(listOf(request1, request2))
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        val event = fakeProcessor1.awaitEvent(request = request1) { it.submit }
-        assertThat(event.requestSequence!!.captureRequestList).hasSize(2)
-        assertThat(event.requestSequence!!.captureRequestList).contains(request1)
-        assertThat(event.requestSequence!!.captureRequestList).contains(request2)
-    }
-
-    @Test
-    fun graphProcessorDoesNotForgetRejectedRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-
-        fakeProcessor1.rejectRequests = true
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-
-        graphProcessor.submit(request1)
-        val event1 = fakeProcessor1.nextEvent()
-        assertThat(event1.rejected).isTrue()
-        assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
-        graphProcessor.submit(request2)
-        val event2 = fakeProcessor1.nextEvent()
-        assertThat(event2.rejected).isTrue()
-        assertThat(event2.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
-        graphProcessor.onGraphStarted(graphRequestProcessor2)
-        assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
-            .isSameInstanceAs(request1)
-        assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
-            .isSameInstanceAs(request2)
-    }
-
-    @Test
-    fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-
-        // Note: setting the requestProcessor, and calling submit() can both trigger a call
-        // to submit a request.
-        fakeProcessor1.rejectRequests = true
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.submit(request1)
-
-        // Check to make sure that submit is called at least once, and that request1 is rejected
-        // from the request processor.
-        fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
-        // Stop rejecting requests
-        fakeProcessor1.rejectRequests = false
-
-        graphProcessor.submit(request2)
-        // Cycle events until we get a submitted event with request1
-        val event2 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
-        assertThat(event2.rejected).isFalse()
-
-        // Assert that immediately after we get a successfully submitted request, the
-        //  next request is also submitted.
-        val event3 = fakeProcessor1.nextEvent()
-        assertThat(event3.requestSequence!!.captureRequestList).contains(request2)
-        assertThat(event3.submit).isTrue()
-        assertThat(event3.rejected).isFalse()
-    }
-
-    @Test
-    fun graphProcessorSetsRepeatingRequest() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.repeatingRequest = request1
-        graphProcessor.repeatingRequest = request2
-        advanceUntilIdle()
-
-        val event =
-            fakeProcessor1.awaitEvent(request = request2) {
-                it.submit && it.requestSequence?.repeating == true
-            }
-        assertThat(event.requestSequence!!.requiredParameters)
-            .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
-    }
-
-    @Test
-    fun graphProcessorDoesNotForgetRejectedRepeatingRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-
-        fakeProcessor1.rejectRequests = true
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-
-        graphProcessor.repeatingRequest = request1
-        val event1 = fakeProcessor1.nextEvent()
-        assertThat(event1.rejected).isTrue()
-        assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
-        graphProcessor.repeatingRequest = request2
-        val event2 = fakeProcessor1.nextEvent()
-        assertThat(event2.rejected).isTrue()
-        fakeProcessor1.awaitEvent(request = request2) {
-            !it.submit && it.requestSequence?.repeating == true
+            assertThat(csp1.events[0].isCapture).isTrue()
+            assertThat(csp1.events[0].requiredParameters)
+                .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
         }
 
-        fakeProcessor1.rejectRequests = false
-        graphProcessor.invalidate()
-
-        fakeProcessor1.awaitEvent(request = request2) {
-            it.submit && it.requestSequence?.repeating == true
-        }
-    }
-
     @Test
-    fun graphProcessorTracksRepeatingRequest() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
+    fun graphProcessorSubmitsRequestsToMostRecentProcessor() =
+        testScope.runTest {
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.onGraphStarted(grp2)
+            graphProcessor.submit(request1)
+            advanceUntilIdle()
 
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.repeatingRequest = request1
-        advanceUntilIdle()
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isClose).isTrue()
 
-        fakeProcessor1.awaitEvent(request = request1) {
-            it.submit && it.requestSequence?.repeating == true
+            assertThat(csp2.events.size).isEqualTo(1)
+            assertThat(csp2.events[0].isCapture).isTrue()
+            assertThat(csp2.events[0].requests).containsExactly(request1)
         }
 
-        graphProcessor.onGraphStarted(graphRequestProcessor2)
-        advanceUntilIdle()
+    @Test
+    fun graphProcessorSubmitsQueuedRequests() =
+        testScope.runTest {
+            graphProcessor.submit(request1)
+            graphProcessor.submit(request2)
 
-        fakeProcessor2.awaitEvent(request = request1) {
-            it.submit && it.requestSequence?.repeating == true
+            // Request1 and 2 should be queued and will be submitted even when the request
+            // processor is set after the requests are submitted.
+            graphProcessor.onGraphStarted(grp1)
+            advanceUntilIdle()
+
+            assertThat(csp1.events.size).isEqualTo(2)
+            assertThat(csp1.events[0].isCapture).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
+
+            assertThat(csp1.events[1].isCapture).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request2)
         }
-    }
 
     @Test
-    fun graphProcessorTracksRejectedRepeatingRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
+    fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() =
+        testScope.runTest {
+            graphProcessor.submit(listOf(request1, request2))
+            graphProcessor.onGraphStarted(grp1)
+            advanceUntilIdle()
 
-        fakeProcessor1.rejectRequests = true
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.repeatingRequest = request1
-        fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
-        graphProcessor.onGraphStarted(graphRequestProcessor2)
-        fakeProcessor2.awaitEvent(request = request1) {
-            it.submit && it.requestSequence?.repeating == true
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isCapture).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1, request2).inOrder()
         }
-    }
 
     @Test
-    fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
+    fun graphProcessorDoesNotForgetRejectedRequests() =
+        testScope.runTest {
+            csp1.rejectSubmit = true
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.submit(request1)
+            advanceUntilIdle()
 
-        graphProcessor.repeatingRequest = request1
-        graphProcessor.submit(request2)
-        delay(50)
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRejected).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
 
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
+            graphProcessor.submit(request2)
+            advanceUntilIdle()
+            assertThat(csp1.events.size).isEqualTo(2)
+            assertThat(csp1.events[1].isRejected).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request1) // Re-attempt #1
 
-        var hasRequest1Event = false
-        var hasRequest2Event = false
+            graphProcessor.onGraphStarted(grp2)
+            advanceUntilIdle()
 
-        // Loop until we see at least one repeating request, and one submit event.
-        launch {
-                while (!hasRequest1Event && !hasRequest2Event) {
-                    val event = fakeProcessor1.nextEvent()
-                    hasRequest1Event =
-                        hasRequest1Event ||
-                            event.requestSequence?.captureRequestList?.contains(request1) ?: false
-                    hasRequest2Event =
-                        hasRequest2Event ||
-                            event.requestSequence?.captureRequestList?.contains(request2) ?: false
-                }
-            }
-            .join()
-    }
+            // Assert that after a new request processor is set, it receives the queued up requests.
+            assertThat(csp2.events.size).isEqualTo(2)
+            assertThat(csp2.events[0].isCapture).isTrue()
+            assertThat(csp2.events[0].requests).containsExactly(request1)
+            assertThat(csp2.events[1].isCapture).isTrue()
+            assertThat(csp2.events[1].requests).containsExactly(request2).inOrder()
+        }
 
     @Test
-    fun graphProcessorAbortsQueuedRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
+    fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() =
+        testScope.runTest {
 
-        graphProcessor.repeatingRequest = request1
-        graphProcessor.submit(request2)
+            // Note: setting the requestProcessor, and calling submit() can both trigger a call
+            // to submit a request.
+            csp1.rejectSubmit = true
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.submit(request1)
+            advanceUntilIdle()
 
-        // Abort queued and in-flight requests.
-        graphProcessor.abort()
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
+            // Check to make sure that submit is called at least once, and that request1 is rejected
+            // from the request processor.
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRejected).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
 
-        val abortEvent1 =
-            withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
-        val abortEvent2 = requestListener2.onAbortedFlow.first()
-        val globalAbortEvent = globalListener.onAbortedFlow.first()
+            // Stop rejecting requests
+            csp1.rejectSubmit = false
 
-        assertThat(abortEvent1).isNull()
-        assertThat(abortEvent2.request).isSameInstanceAs(request2)
-        assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+            graphProcessor.submit(request2)
+            advanceUntilIdle()
 
-        val nextSequence = fakeProcessor1.nextRequestSequence()
-        assertThat(nextSequence.captureRequestList.first()).isSameInstanceAs(request1)
-        assertThat(nextSequence.requestMetadata[request1]!!.repeating).isTrue()
-    }
+            // Assert that immediately after we get a successfully submitted request, the
+            //  next request is also submitted.
+            assertThat(csp1.events.size).isEqualTo(3)
+            assertThat(csp1.events[1].isCapture).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request1)
+            assertThat(csp1.events[2].isCapture).isTrue()
+            assertThat(csp1.events[2].requests).containsExactly(request2)
+        }
 
     @Test
-    fun closingGraphProcessorAbortsSubsequentRequests() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-        graphProcessor.close()
-        advanceUntilIdle()
+    fun graphProcessorSetsRepeatingRequest() =
+        testScope.runTest {
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.repeatingRequest = request1
+            graphProcessor.repeatingRequest = request2
+            advanceUntilIdle()
 
-        // Abort queued and in-flight requests.
-        // graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.repeatingRequest = request1
-        graphProcessor.submit(request2)
-
-        val abortEvent1 =
-            withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
-        val abortEvent2 = requestListener2.onAbortedFlow.first()
-        assertThat(abortEvent1).isNull()
-        assertThat(abortEvent2.request).isSameInstanceAs(request2)
-    }
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRepeating).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request2)
+            assertThat(csp1.events[0].requiredParameters)
+                .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
+        }
 
     @Test
-    fun graphProcessorResubmitsParametersAfterGraphStarts() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
+    fun graphProcessorDoesNotForgetRejectedRepeatingRequests() =
+        testScope.runTest {
+            csp1.rejectSubmit = true
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.repeatingRequest = request1
+            advanceUntilIdle()
 
-        // Submit a repeating request first to make sure we have one in progress.
-        graphProcessor.repeatingRequest = request1
-        advanceUntilIdle()
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRejected).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
 
-        graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
-        advanceUntilIdle()
+            graphProcessor.repeatingRequest = request2
+            advanceUntilIdle()
 
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        advanceUntilIdle()
-        val event1 = fakeProcessor1.nextEvent()
-        assertThat(event1.requestSequence?.repeating).isTrue()
-        val event2 = fakeProcessor1.nextEvent()
-        assertThat(event2.requestSequence?.repeating).isFalse()
-        assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
-            .isFalse()
-    }
+            assertThat(csp1.events.size).isEqualTo(2)
+            assertThat(csp1.events[1].isRejected).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request2)
+
+            csp1.rejectSubmit = false
+            graphProcessor.invalidate()
+            advanceUntilIdle()
+
+            assertThat(csp1.events.size).isEqualTo(3)
+            assertThat(csp1.events[2].isRepeating).isTrue()
+            assertThat(csp1.events[2].requests).containsExactly(request2)
+        }
 
     @Test
-    fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
+    fun graphProcessorTracksRepeatingRequest() =
+        testScope.runTest {
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.repeatingRequest = request1
+            advanceUntilIdle()
 
-        // Submit a repeating request first to make sure we have one in progress.
-        graphProcessor.repeatingRequest = request1
-        graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
-        graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
-        advanceUntilIdle()
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRepeating).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
 
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        advanceUntilIdle()
+            graphProcessor.onGraphStarted(grp2)
+            advanceUntilIdle()
 
-        val event1 = fakeProcessor1.nextEvent()
-        assertThat(event1.requestSequence?.repeating).isTrue()
-        val event2 = fakeProcessor1.nextEvent()
-        assertThat(event2.requestSequence?.repeating).isFalse()
-        assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
-            .isFalse()
-        val event3 = fakeProcessor1.nextEvent()
-        assertThat(event3.requestSequence?.repeating).isFalse()
-        assertThat(event3.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
-            .isTrue()
-    }
+            assertThat(csp2.events.size).isEqualTo(1)
+            assertThat(csp2.events[0].isRepeating).isTrue()
+            assertThat(csp2.events[0].requests).containsExactly(request1)
+        }
 
     @Test
-    fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
+    fun graphProcessorTracksRejectedRepeatingRequests() =
+        testScope.runTest {
+            csp1.rejectSubmit = true
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.repeatingRequest = request1
+            advanceUntilIdle()
 
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        advanceUntilIdle()
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRejected).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
 
-        assertThrows<IllegalStateException> {
+            graphProcessor.onGraphStarted(grp2)
+            advanceUntilIdle()
+
+            assertThat(csp2.events.size).isEqualTo(1)
+            assertThat(csp2.events[0].isRepeating).isTrue()
+            assertThat(csp2.events[0].requests).containsExactly(request1)
+        }
+
+    @Test
+    fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() =
+        testScope.runTest {
+            graphProcessor.repeatingRequest = request1
+            graphProcessor.submit(request2)
+            advanceUntilIdle()
+
+            graphProcessor.onGraphStarted(grp1)
+            advanceUntilIdle()
+
+            assertThat(csp1.events.size).isEqualTo(2)
+            assertThat(csp1.events[0].isRepeating).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
+            assertThat(csp1.events[1].isCapture).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request2)
+        }
+
+    @Test
+    fun graphProcessorAbortsQueuedRequests() =
+        testScope.runTest {
+            graphProcessor.repeatingRequest = request1
+            graphProcessor.submit(request2)
+
+            // Abort queued and in-flight requests.
+            graphProcessor.abort()
+            graphProcessor.onGraphStarted(grp1)
+
+            val abortEvent1 = requestListener2.onAbortedFlow.first()
+            val globalAbortEvent = globalListener.onAbortedFlow.first()
+
+            assertThat(abortEvent1.request).isSameInstanceAs(request2)
+            assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+
+            advanceUntilIdle()
+
+            assertThat(csp1.events.size).isEqualTo(1)
+            assertThat(csp1.events[0].isRepeating).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
+        }
+
+    @Test
+    fun closingGraphProcessorAbortsSubsequentRequests() =
+        testScope.runTest {
+            graphProcessor.close()
+            advanceUntilIdle()
+
+            // Abort queued and in-flight requests.
+            // graphProcessor.onGraphStarted(graphRequestProcessor1)
+            graphProcessor.repeatingRequest = request1
+            graphProcessor.submit(request2)
+
+            val abortEvent1 =
+                withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
+            val abortEvent2 = requestListener2.onAbortedFlow.first()
+            assertThat(abortEvent1).isNull()
+            assertThat(abortEvent2.request).isSameInstanceAs(request2)
+        }
+
+    @Test
+    fun graphProcessorResubmitsParametersAfterGraphStarts() =
+        testScope.runTest {
+            // Submit a repeating request first to make sure we have one in progress.
+            graphProcessor.repeatingRequest = request1
+            graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+            graphProcessor.onGraphStarted(grp1)
+            advanceUntilIdle()
+
+            assertThat(csp1.events.size).isEqualTo(2)
+            assertThat(csp1.events[0].isRepeating).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
+            assertThat(csp1.events[1].isCapture).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request1)
+            assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+        }
+
+    @Test
+    fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() =
+        testScope.runTest {
+
+            // Submit a repeating request first to make sure we have one in progress.
+            graphProcessor.repeatingRequest = request1
+            graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
             graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+            advanceUntilIdle()
+
+            graphProcessor.onGraphStarted(grp1)
+            advanceUntilIdle()
+
+            assertThat(csp1.events.size).isEqualTo(3)
+            assertThat(csp1.events[0].isRepeating).isTrue()
+            assertThat(csp1.events[0].requests).containsExactly(request1)
+
+            assertThat(csp1.events[1].isCapture).isTrue()
+            assertThat(csp1.events[1].requests).containsExactly(request1)
+            assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+
+            assertThat(csp1.events[2].isCapture).isTrue()
+            assertThat(csp1.events[2].requests).containsExactly(request1)
+            assertThat(csp1.events[2].requiredParameters).containsEntry(CONTROL_AE_LOCK, true)
         }
-    }
 
     @Test
-    fun graphProcessorChangesGraphStateOnError() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
-            )
-        assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+    fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() =
+        testScope.runTest {
+            graphProcessor.onGraphStarted(grp1)
+            advanceUntilIdle()
 
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
-        graphProcessor.onGraphError(
-            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
-        )
-        assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
-    }
+            assertThrows<IllegalStateException> {
+                graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+            }
+        }
 
     @Test
-    fun graphProcessorDropsStaleErrors() = runTest {
-        val graphProcessor =
-            GraphProcessorImpl(
-                FakeThreads.fromTestScope(this),
-                CameraGraphId.nextId(),
-                FakeGraphConfigs.graphConfig,
-                graphState3A,
-                graphListener3A,
-                arrayListOf(globalListener)
+    fun graphProcessorChangesGraphStateOnError() =
+        testScope.runTest {
+            assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+            graphProcessor.onGraphStarted(grp1)
+            graphProcessor.onGraphError(
+                GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
             )
-        assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+            assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
+        }
 
-        graphProcessor.onGraphError(
-            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
-        )
-        assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+    @Test
+    fun graphProcessorDropsStaleErrors() =
+        testScope.runTest {
+            assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
 
-        graphProcessor.onGraphStarting()
-        graphProcessor.onGraphStarted(graphRequestProcessor1)
+            graphProcessor.onGraphError(
+                GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+            )
+            assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
 
-        // GraphProcessor should drop errors while the camera graph is stopping.
-        graphProcessor.onGraphStopping()
-        graphProcessor.onGraphError(
-            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
-        )
-        assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+            graphProcessor.onGraphStarting()
+            graphProcessor.onGraphStarted(grp1)
 
-        // GraphProcessor should also drop errors while the camera graph is stopped.
-        graphProcessor.onGraphStopped(graphRequestProcessor1)
-        graphProcessor.onGraphError(
-            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
-        )
-        assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
-    }
+            // GraphProcessor should drop errors while the camera graph is stopping.
+            graphProcessor.onGraphStopping()
+            graphProcessor.onGraphError(
+                GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+            )
+            assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+            // GraphProcessor should also drop errors while the camera graph is stopped.
+            graphProcessor.onGraphStopped(grp1)
+            graphProcessor.onGraphError(
+                GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+            )
+            assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+        }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
index ec4cc78..ace8b46 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
@@ -45,8 +45,8 @@
         // StreamConfigurationMap provided by Robolectric.
         try {
             return mStreamConfigurationMap.getOutputFormats();
-        } catch (NullPointerException e) {
-            Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e);
+        } catch (NullPointerException | IllegalArgumentException e) {
+            Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e);
             return null;
         }
     }
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 263b7a5..b596010 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -124,7 +124,7 @@
         FakeCameraControl fakeCameraControl =
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
 
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             // Notify the cancel after the capture request has been successfully submitted
             fakeCameraControl.notifyAllRequestsOnCaptureCancelled();
         });
@@ -154,7 +154,7 @@
                 ImageCapture.OnImageCapturedCallback.class);
         FakeCameraControl fakeCameraControl =
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             // Notify the failure after the capture request has been successfully submitted
             fakeCameraControl.notifyAllRequestsOnCaptureFailed();
         });
@@ -302,7 +302,7 @@
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
 
         // Simulates the case that the capture request failed after running in 300 ms.
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             CameraXExecutors.mainThreadExecutor().schedule(() -> {
                 fakeCameraControl.notifyAllRequestsOnCaptureFailed();
             }, 300, TimeUnit.MILLISECONDS);
@@ -395,7 +395,7 @@
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
         FakeCameraControl.OnNewCaptureRequestListener mockCaptureRequestListener =
                 mock(FakeCameraControl.OnNewCaptureRequestListener.class);
-        fakeCameraControl.setOnNewCaptureRequestListener(mockCaptureRequestListener);
+        fakeCameraControl.addOnNewCaptureRequestListener(mockCaptureRequestListener);
 
         // Act.
         mInstrumentation.runOnMainSync(
@@ -463,7 +463,7 @@
         FakeCameraControl fakeCameraControl =
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
         CountDownLatch latch = new CountDownLatch(1);
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             latch.countDown();
         });
 
@@ -492,7 +492,7 @@
     private void addExtraFailureNotificationsForRetry(FakeCameraControl cameraControl,
             int retryCount) {
         if (retryCount > 0) {
-            cameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+            cameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
                 addExtraFailureNotificationsForRetry(cameraControl, retryCount - 1);
                 cameraControl.notifyAllRequestsOnCaptureFailed();
             });
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 20b2ecd..0873c4b 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
@@ -1312,7 +1312,8 @@
 
         if (mTakePictureManager == null) {
             // mTakePictureManager is reused when the Surface is reset.
-            mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+            mTakePictureManager = getCurrentConfig().getTakePictureManagerProvider().newInstance(
+                    mImageCaptureControl);
         }
         mTakePictureManager.setImagePipeline(mImagePipeline);
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index b81133b..62245fd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -49,10 +49,11 @@
 public final class ImageProcessingUtil {
 
     private static final String TAG = "ImageProcessingUtil";
+    public static final String JNI_LIB_NAME = "image_processing_util_jni";
     private static int sImageCount = 0;
 
     static {
-        System.loadLibrary("image_processing_util_jni");
+        System.loadLibrary(JNI_LIB_NAME);
     }
 
     enum Result {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
index d4ab73e..ceec2f8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.processing.Operation;
 import androidx.camera.core.processing.Packet;
@@ -37,7 +38,8 @@
  *
  * <p>The {@link Bitmap} will be recycled and should not be used after the processing.
  */
-class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
 
     @NonNull
     @Override
@@ -79,16 +81,16 @@
      * Input of {@link Bitmap2JpegBytes} processor.
      */
     @AutoValue
-    abstract static class In {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public abstract static class In {
 
         abstract Packet<Bitmap> getPacket();
 
         abstract int getJpegQuality();
 
         @NonNull
-        static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
+        public static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
             return new AutoValue_Bitmap2JpegBytes_In(imagePacket, jpegQuality);
         }
     }
 }
-
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 4dccd13..cb378f1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -27,6 +27,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.impl.utils.Exif;
@@ -48,7 +49,9 @@
 /**
  * Saves JPEG bytes to disk.
  */
-class JpegBytes2Disk implements Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JpegBytes2Disk implements
+        Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
 
     private static final String TEMP_FILE_PREFIX = "CameraX";
     private static final String TEMP_FILE_SUFFIX = ".tmp";
@@ -287,7 +290,8 @@
      * Input packet.
      */
     @AutoValue
-    abstract static class In {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public abstract static class In {
 
         @NonNull
         abstract Packet<byte[]> getPacket();
@@ -296,7 +300,7 @@
         abstract ImageCapture.OutputFileOptions getOutputFileOptions();
 
         @NonNull
-        static In of(@NonNull Packet<byte[]> jpegBytes,
+        public static In of(@NonNull Packet<byte[]> jpegBytes,
                 @NonNull ImageCapture.OutputFileOptions outputFileOptions) {
             return new AutoValue_JpegBytes2Disk_In(jpegBytes, outputFileOptions);
         }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index fdf33ed..5b06259 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -16,36 +16,15 @@
 
 package androidx.camera.core.imagecapture;
 
-import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
-import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
-import static androidx.camera.core.impl.utils.Threads.checkMainThread;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkState;
-
-import static java.util.Objects.requireNonNull;
-
-import android.util.Log;
-
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Pair;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
 import java.util.List;
 
 /**
@@ -62,46 +41,13 @@
  *
  * <p>The thread safety is guaranteed by using the main thread.
  */
-public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
-
-    private static final String TAG = "TakePictureManager";
-
-    // Queue of new requests that have not been sent to the pipeline/camera.
-    @VisibleForTesting
-    final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
-    final ImageCaptureControl mImageCaptureControl;
-    ImagePipeline mImagePipeline;
-
-    // The current request being processed by the camera. Only one request can be processed by
-    // the camera at the same time. Null if the camera is idle.
-    @Nullable
-    private RequestWithCallback mCapturingRequest;
-    // The current requests that have not received a result or an error.
-    private final List<RequestWithCallback> mIncompleteRequests;
-
-    // Once paused, the class waits until the class is resumed to handle new requests.
-    boolean mPaused = false;
-
-    /**
-     * @param imageCaptureControl for controlling {@link ImageCapture}
-     */
-    @MainThread
-    public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
-        checkMainThread();
-        mImageCaptureControl = imageCaptureControl;
-        mIncompleteRequests = new ArrayList<>();
-    }
-
+public interface TakePictureManager {
     /**
      * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
      * output.
      */
     @MainThread
-    public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
-        checkMainThread();
-        mImagePipeline = imagePipeline;
-        mImagePipeline.setOnImageCloseListener(this);
-    }
+    void setImagePipeline(@NonNull ImagePipeline imagePipeline);
 
     /**
      * Adds requests to the queue.
@@ -109,201 +55,52 @@
      * <p>The requests in the queue will be executed based on the order being added.
      */
     @MainThread
-    public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
-        checkMainThread();
-        mNewRequests.offer(takePictureRequest);
-        issueNextRequest();
-    }
-
-    @MainThread
-    @Override
-    public void retryRequest(@NonNull TakePictureRequest request) {
-        checkMainThread();
-        Logger.d(TAG, "Add a new request for retrying.");
-        // Insert the request to the front of the queue.
-        mNewRequests.addFirst(request);
-        // Try to issue the newly added request in case condition allows.
-        issueNextRequest();
-    }
+    void offerRequest(@NonNull TakePictureRequest takePictureRequest);
 
     /**
      * Pauses sending request to camera.
      */
     @MainThread
-    public void pause() {
-        checkMainThread();
-        mPaused = true;
-
-        // Always retry because the camera may not send an error callback during the reset.
-        if (mCapturingRequest != null) {
-            mCapturingRequest.abortSilentlyAndRetry();
-        }
-    }
+    void pause();
 
     /**
      * Resumes sending request to camera.
      */
     @MainThread
-    public void resume() {
-        checkMainThread();
-        mPaused = false;
-        issueNextRequest();
-    }
+    void resume();
 
     /**
      * Clears the requests queue.
      */
     @MainThread
-    public void abortRequests() {
-        checkMainThread();
-        ImageCaptureException exception =
-                new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
-
-        // Clear pending request first so aborting in-flight request won't trigger another capture.
-        for (TakePictureRequest request : mNewRequests) {
-            request.onError(exception);
-        }
-        mNewRequests.clear();
-
-        // Abort the in-flight request after clearing the pending requests.
-        // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
-        List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
-        for (RequestWithCallback request : requestsSnapshot) {
-            // TODO: optimize the performance by not processing aborted requests.
-            request.abortAndSendErrorToApp(exception);
-        }
-    }
+    void abortRequests();
 
     /**
-     * Issues the next request if conditions allow.
+     * Returns whether any capture request is being processed currently.
      */
-    @MainThread
-    void issueNextRequest() {
-        checkMainThread();
-        Log.d(TAG, "Issue the next TakePictureRequest.");
-        if (hasCapturingRequest()) {
-            Log.d(TAG, "There is already a request in-flight.");
-            return;
-        }
-        if (mPaused) {
-            Log.d(TAG, "The class is paused.");
-            return;
-        }
-        if (mImagePipeline.getCapacity() == 0) {
-            Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
-            return;
-        }
-        TakePictureRequest request = mNewRequests.poll();
-        if (request == null) {
-            Log.d(TAG, "No new request.");
-            return;
-        }
-
-        RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
-        trackCurrentRequests(requestWithCallback);
-
-        // Send requests.
-        Pair<CameraRequest, ProcessingRequest> requests =
-                mImagePipeline.createRequests(request, requestWithCallback,
-                        requestWithCallback.getCaptureFuture());
-        CameraRequest cameraRequest = requireNonNull(requests.first);
-        ProcessingRequest processingRequest = requireNonNull(requests.second);
-        mImagePipeline.submitProcessingRequest(processingRequest);
-        ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
-        requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
-    }
-
-    /**
-     * Waits for the request to finish before issuing the next.
-     */
-    private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
-        checkState(!hasCapturingRequest());
-        mCapturingRequest = requestWithCallback;
-
-        // Waits for the capture to finish before issuing the next.
-        mCapturingRequest.getCaptureFuture().addListener(() -> {
-            mCapturingRequest = null;
-            issueNextRequest();
-        }, directExecutor());
-
-        // Track all incomplete requests so we can abort them when UseCase is detached.
-        mIncompleteRequests.add(requestWithCallback);
-        requestWithCallback.getCompleteFuture().addListener(() -> {
-            mIncompleteRequests.remove(requestWithCallback);
-        }, directExecutor());
-    }
-
-    /**
-     * Submit a request to camera and post-processing pipeline.
-     *
-     * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
-     */
-    @MainThread
-    private ListenableFuture<Void> submitCameraRequest(
-            @NonNull CameraRequest cameraRequest) {
-        checkMainThread();
-        mImageCaptureControl.lockFlashMode();
-        ListenableFuture<Void> captureRequestFuture =
-                mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
-        Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
-            @Override
-            public void onSuccess(@Nullable Void result) {
-                mImageCaptureControl.unlockFlashMode();
-            }
-
-            @Override
-            public void onFailure(@NonNull Throwable throwable) {
-                if (cameraRequest.isAborted()) {
-                    // When the pipeline is recreated, the in-flight request is aborted and
-                    // retried. On legacy devices, the camera may return CancellationException
-                    // for the aborted request which causes the retried request to fail. Return
-                    // early if the request has been aborted.
-                    return;
-                } else {
-                    int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
-                    if (throwable instanceof ImageCaptureException) {
-                        mImagePipeline.notifyCaptureError(
-                                CaptureError.of(requestId, (ImageCaptureException) throwable));
-                    } else {
-                        mImagePipeline.notifyCaptureError(
-                                CaptureError.of(requestId, new ImageCaptureException(
-                                        ERROR_CAPTURE_FAILED,
-                                        "Failed to submit capture request",
-                                        throwable)));
-                    }
-                }
-                mImageCaptureControl.unlockFlashMode();
-            }
-        }, mainThreadExecutor());
-        return captureRequestFuture;
-    }
-
     @VisibleForTesting
-    boolean hasCapturingRequest() {
-        return mCapturingRequest != null;
-    }
+    boolean hasCapturingRequest();
 
+    /**
+     * Returns the capture request being processed currently.
+     */
     @VisibleForTesting
     @Nullable
-    public RequestWithCallback getCapturingRequest() {
-        return mCapturingRequest;
-    }
+    RequestWithCallback getCapturingRequest();
 
+    /**
+     * Returns the requests that have not received a result or an error yet.
+     */
+    @NonNull
     @VisibleForTesting
-    List<RequestWithCallback> getIncompleteRequests() {
-        return mIncompleteRequests;
-    }
+    List<RequestWithCallback> getIncompleteRequests();
 
+    /**
+     * Returns the {@link ImagePipeline} instance used under the hood.
+     */
     @VisibleForTesting
     @NonNull
-    public ImagePipeline getImagePipeline() {
-        return mImagePipeline;
-    }
-
-    @Override
-    public void onImageClose(@NonNull ImageProxy image) {
-        mainThreadExecutor().execute(this::issueNextRequest);
-    }
+    ImagePipeline getImagePipeline();
 
     @AutoValue
     abstract static class CaptureError {
@@ -318,4 +115,18 @@
         }
     }
 
+    /**
+     * Interface for deferring creation of a {@link TakePictureManager}.
+     */
+    interface Provider {
+        /**
+         * Creates a new, initialized instance of a {@link TakePictureManager}.
+         *
+         * @param imageCaptureControl       Used by TakePictureManager to control an
+         *                                  {@link ImageCapture} instance.
+         * @return                          The {@code TakePictureManager} instance.
+         */
+        @NonNull
+        TakePictureManager newInstance(@NonNull ImageCaptureControl imageCaptureControl);
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
new file mode 100644
index 0000000..e356a50
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
@@ -0,0 +1,317 @@
+/*
+ * 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.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.core.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Manages {@link ImageCapture#takePicture} calls.
+ *
+ * <p>In coming requests are added to a queue and later sent to camera one at a time. Only one
+ * in-flight request is allowed at a time. The next request cannot be sent until the current one
+ * is completed by camera. However, it allows multiple concurrent requests for post-processing,
+ * as {@link ImagePipeline} supports parallel processing.
+ *
+ * <p>This class selectively propagates callbacks from camera and {@link ImagePipeline} to the
+ * app. e.g. it may choose to retry the request before sending the {@link ImageCaptureException}
+ * to the app.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+public class TakePictureManagerImpl implements TakePictureManager, OnImageCloseListener,
+        TakePictureRequest.RetryControl {
+
+    private static final String TAG = "TakePictureManagerImpl";
+
+    // Queue of new requests that have not been sent to the pipeline/camera.
+    @VisibleForTesting
+    final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
+    final ImageCaptureControl mImageCaptureControl;
+    ImagePipeline mImagePipeline;
+
+    // The current request being processed by the camera. Only one request can be processed by
+    // the camera at the same time. Null if the camera is idle.
+    @Nullable
+    private RequestWithCallback mCapturingRequest;
+    // The current requests that have not received a result or an error.
+    private final List<RequestWithCallback> mIncompleteRequests;
+
+    // Once paused, the class waits until the class is resumed to handle new requests.
+    boolean mPaused = false;
+
+    /**
+     * @param imageCaptureControl for controlling {@link ImageCapture}
+     */
+    @MainThread
+    public TakePictureManagerImpl(@NonNull ImageCaptureControl imageCaptureControl) {
+        checkMainThread();
+        mImageCaptureControl = imageCaptureControl;
+        mIncompleteRequests = new ArrayList<>();
+    }
+
+    /**
+     * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+     * output.
+     */
+    @MainThread
+    @Override
+    public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+        checkMainThread();
+        mImagePipeline = imagePipeline;
+        mImagePipeline.setOnImageCloseListener(this);
+    }
+
+    /**
+     * Adds requests to the queue.
+     *
+     * <p>The requests in the queue will be executed based on the order being added.
+     */
+    @MainThread
+    @Override
+    public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
+        checkMainThread();
+        mNewRequests.offer(takePictureRequest);
+        issueNextRequest();
+    }
+
+    @MainThread
+    @Override
+    public void retryRequest(@NonNull TakePictureRequest request) {
+        checkMainThread();
+        Logger.d(TAG, "Add a new request for retrying.");
+        // Insert the request to the front of the queue.
+        mNewRequests.addFirst(request);
+        // Try to issue the newly added request in case condition allows.
+        issueNextRequest();
+    }
+
+    /**
+     * Pauses sending request to camera.
+     */
+    @MainThread
+    @Override
+    public void pause() {
+        checkMainThread();
+        mPaused = true;
+
+        // Always retry because the camera may not send an error callback during the reset.
+        if (mCapturingRequest != null) {
+            mCapturingRequest.abortSilentlyAndRetry();
+        }
+    }
+
+    /**
+     * Resumes sending request to camera.
+     */
+    @MainThread
+    @Override
+    public void resume() {
+        checkMainThread();
+        mPaused = false;
+        issueNextRequest();
+    }
+
+    /**
+     * Clears the requests queue.
+     */
+    @MainThread
+    @Override
+    public void abortRequests() {
+        checkMainThread();
+        ImageCaptureException exception =
+                new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
+
+        // Clear pending request first so aborting in-flight request won't trigger another capture.
+        for (TakePictureRequest request : mNewRequests) {
+            request.onError(exception);
+        }
+        mNewRequests.clear();
+
+        // Abort the in-flight request after clearing the pending requests.
+        // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+        List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+        for (RequestWithCallback request : requestsSnapshot) {
+            // TODO: optimize the performance by not processing aborted requests.
+            request.abortAndSendErrorToApp(exception);
+        }
+    }
+
+    /**
+     * Issues the next request if conditions allow.
+     */
+    @MainThread
+    void issueNextRequest() {
+        checkMainThread();
+        Log.d(TAG, "Issue the next TakePictureRequest.");
+        if (hasCapturingRequest()) {
+            Log.d(TAG, "There is already a request in-flight.");
+            return;
+        }
+        if (mPaused) {
+            Log.d(TAG, "The class is paused.");
+            return;
+        }
+        if (mImagePipeline.getCapacity() == 0) {
+            Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
+            return;
+        }
+        TakePictureRequest request = mNewRequests.poll();
+        if (request == null) {
+            Log.d(TAG, "No new request.");
+            return;
+        }
+
+        RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+        trackCurrentRequests(requestWithCallback);
+
+        // Send requests.
+        Pair<CameraRequest, ProcessingRequest> requests =
+                mImagePipeline.createRequests(request, requestWithCallback,
+                        requestWithCallback.getCaptureFuture());
+        CameraRequest cameraRequest = requireNonNull(requests.first);
+        ProcessingRequest processingRequest = requireNonNull(requests.second);
+        mImagePipeline.submitProcessingRequest(processingRequest);
+        ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
+        requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
+    }
+
+    /**
+     * Waits for the request to finish before issuing the next.
+     */
+    private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+        checkState(!hasCapturingRequest());
+        mCapturingRequest = requestWithCallback;
+
+        // Waits for the capture to finish before issuing the next.
+        mCapturingRequest.getCaptureFuture().addListener(() -> {
+            mCapturingRequest = null;
+            issueNextRequest();
+        }, directExecutor());
+
+        // Track all incomplete requests so we can abort them when UseCase is detached.
+        mIncompleteRequests.add(requestWithCallback);
+        requestWithCallback.getCompleteFuture().addListener(() -> {
+            mIncompleteRequests.remove(requestWithCallback);
+        }, directExecutor());
+    }
+
+    /**
+     * Submit a request to camera and post-processing pipeline.
+     *
+     * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
+     */
+    @MainThread
+    private ListenableFuture<Void> submitCameraRequest(
+            @NonNull CameraRequest cameraRequest) {
+        checkMainThread();
+        mImageCaptureControl.lockFlashMode();
+        ListenableFuture<Void> captureRequestFuture =
+                mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
+        Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(@Nullable Void result) {
+                mImageCaptureControl.unlockFlashMode();
+            }
+
+            @Override
+            public void onFailure(@NonNull Throwable throwable) {
+                if (cameraRequest.isAborted()) {
+                    // When the pipeline is recreated, the in-flight request is aborted and
+                    // retried. On legacy devices, the camera may return CancellationException
+                    // for the aborted request which causes the retried request to fail. Return
+                    // early if the request has been aborted.
+                    return;
+                } else {
+                    int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
+                    if (throwable instanceof ImageCaptureException) {
+                        mImagePipeline.notifyCaptureError(
+                                CaptureError.of(requestId, (ImageCaptureException) throwable));
+                    } else {
+                        mImagePipeline.notifyCaptureError(
+                                CaptureError.of(requestId, new ImageCaptureException(
+                                        ERROR_CAPTURE_FAILED,
+                                        "Failed to submit capture request",
+                                        throwable)));
+                    }
+                }
+                mImageCaptureControl.unlockFlashMode();
+            }
+        }, mainThreadExecutor());
+        return captureRequestFuture;
+    }
+
+    @VisibleForTesting
+    @Override
+    public boolean hasCapturingRequest() {
+        return mCapturingRequest != null;
+    }
+
+    @VisibleForTesting
+    @Nullable
+    @Override
+    public RequestWithCallback getCapturingRequest() {
+        return mCapturingRequest;
+    }
+
+    @NonNull
+    @VisibleForTesting
+    @Override
+    public List<RequestWithCallback> getIncompleteRequests() {
+        return mIncompleteRequests;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    @Override
+    public ImagePipeline getImagePipeline() {
+        return mImagePipeline;
+    }
+
+    @Override
+    public void onImageClose(@NonNull ImageProxy image) {
+        mainThreadExecutor().execute(this::issueNextRequest);
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index 4ef3f3b..d49c06b9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -82,14 +82,14 @@
      * Gets the app provided options for on-disk capture.
      */
     @Nullable
-    abstract ImageCapture.OutputFileOptions getOutputFileOptions();
+    public abstract ImageCapture.OutputFileOptions getOutputFileOptions();
 
     /**
      * A snapshot of {@link ImageCapture#getViewPortCropRect()} when
      * {@link ImageCapture#takePicture} is called.
      */
     @NonNull
-    abstract Rect getCropRect();
+    public abstract Rect getCropRect();
 
     /**
      * A snapshot of {@link ImageCapture#getSensorToBufferTransformMatrix()} when
@@ -102,14 +102,14 @@
      * A snapshot of rotation degrees when {@link ImageCapture#takePicture} is called.
      */
     @ImageOutputConfig.RotationValue
-    abstract int getRotationDegrees();
+    public abstract int getRotationDegrees();
 
     /**
      * A snapshot of {@link ImageCaptureConfig#getJpegQuality()} when
      * {@link ImageCapture#takePicture} is called.
      */
     @IntRange(from = 1, to = 100)
-    abstract int getJpegQuality();
+    public abstract int getJpegQuality();
 
     /**
      * Gets the capture mode of the request.
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 347a25c..9d6f159 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
@@ -21,10 +21,16 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.camera.core.ExtendableBuilder;
+import androidx.camera.core.ImageCapture;
 import androidx.camera.core.UseCase;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
+import androidx.camera.core.imagecapture.TakePictureManagerImpl;
 import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.internal.TargetConfig;
 
+import java.util.Objects;
+
 /**
  * Configuration containing options for use cases.
  *
@@ -108,6 +114,10 @@
     Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
             Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
 
+    Option<TakePictureManager.Provider> OPTION_TAKE_PICTURE_MANAGER_PROVIDER =
+            Option.create("camerax.core.useCase.takePictureManagerProvider",
+                    TakePictureManager.Provider.class);
+
     // *********************************************************************************************
 
     /**
@@ -329,6 +339,22 @@
     }
 
     /**
+     * @return The {@link TakePictureManager} implementation for {@link ImageCapture} use case.
+     */
+    @NonNull
+    default TakePictureManager.Provider getTakePictureManagerProvider() {
+        return Objects.requireNonNull(retrieveOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+                new TakePictureManager.Provider() {
+                    @NonNull
+                    @Override
+                    public TakePictureManager newInstance(
+                            @NonNull ImageCaptureControl imageCaptureControl) {
+                        return new TakePictureManagerImpl(imageCaptureControl);
+                    }
+                }));
+    }
+
+    /**
      * 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/ImageCaptureExtTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
index be859db..45ba186 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
@@ -28,7 +28,6 @@
 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
-import java.io.File
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.MainScope
@@ -36,7 +35,9 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TemporaryFolder
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.Shadows
@@ -47,9 +48,14 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class ImageCaptureExtTest {
+    @get:Rule
+    val temporaryFolder =
+        TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
     private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val fakeOutputFileOptions =
-        ImageCapture.OutputFileOptions.Builder(File("fake_path")).build()
+    private val fakeOutputFileOptions by lazy {
+        ImageCapture.OutputFileOptions.Builder(temporaryFolder.newFile("fake_path")).build()
+    }
     private lateinit var cameraProvider: ProcessCameraProvider
     private lateinit var imageCapture: ImageCapture
 
@@ -88,12 +94,11 @@
     fun takePicture_inMemory_canGetImage(): Unit = runTest {
         // Arrange
         val imageProxy = FakeImageProxy(FakeImageInfo())
+        val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+        fakeTakePictureManager.enqueueImageProxy(imageProxy)
 
         // Arrange & Act.
         val takePictureAsync = MainScope().async { imageCapture.takePicture() }
-        Shadows.shadowOf(Looper.getMainLooper()).idle()
-        val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
-        imageCaptureCallback?.onCaptureSuccess(imageProxy)
 
         // Assert.
         Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -135,6 +140,7 @@
         var callbackCalled = false
         val progress = 100
         var resultProgress = 0
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
@@ -163,6 +169,7 @@
         var callbackCalled = false
         val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
         lateinit var resultBitmap: Bitmap
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
@@ -189,15 +196,14 @@
     fun takePicture_onDisk_canGetResult(): Unit = runTest {
         // Arrange
         val outputFileResults = ImageCapture.OutputFileResults(null)
+        val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+        fakeTakePictureManager.enqueueOutputFileResults(outputFileResults)
 
         // Arrange & Act.
         val takePictureAsync =
             MainScope().async {
                 imageCapture.takePicture(outputFileOptions = fakeOutputFileOptions)
             }
-        Shadows.shadowOf(Looper.getMainLooper()).idle()
-        val imageCaptureCallback = imageCapture.getTakePictureRequest()?.onDiskCallback
-        imageCaptureCallback?.onImageSaved(outputFileResults)
 
         // Assert.
         Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -245,6 +251,7 @@
         var callbackCalled = false
         val progress = 100
         var resultProgress = 0
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
@@ -274,6 +281,7 @@
         var callbackCalled = false
         val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
         lateinit var resultBitmap: Bitmap
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
index 7b775e8..87e05b0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
@@ -112,7 +112,7 @@
         return fileOptions
     }
 
-    internal override fun getCropRect(): Rect {
+    override fun getCropRect(): Rect {
         return Rect(0, 0, 640, 480)
     }
 
@@ -120,11 +120,11 @@
         return Matrix()
     }
 
-    internal override fun getRotationDegrees(): Int {
+    override fun getRotationDegrees(): Int {
         return ROTATION_DEGREES
     }
 
-    internal override fun getJpegQuality(): Int {
+    override fun getJpegQuality(): Int {
         return JPEG_QUALITY
     }
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index db131ac..4f02ad7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -50,7 +50,7 @@
     private val imagePipeline = FakeImagePipeline()
     private val imageCaptureControl = FakeImageCaptureControl()
     private val takePictureManager =
-        TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
+        TakePictureManagerImpl(imageCaptureControl).also { it.imagePipeline = imagePipeline }
     private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
 
     @After
diff --git a/camera/camera-testing/api/current.txt b/camera/camera-testing/api/current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/current.txt
+++ b/camera/camera-testing/api/current.txt
@@ -42,10 +42,12 @@
     ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     method public void addInteropConfig(androidx.camera.core.impl.Config);
+    method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
     method public void clearInteropConfig();
-    method public void clearNewCaptureRequestListener();
+    method @Deprecated public void clearNewCaptureRequestListener();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
     method public int getExposureCompensationIndex();
     method public int getFlashMode();
@@ -62,11 +64,14 @@
     method public void notifyAllRequestsOnCaptureCancelled();
     method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
     method public void notifyAllRequestsOnCaptureFailed();
+    method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+    method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
     method public void setFlashMode(int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
-    method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
-    method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/api/restricted_current.txt b/camera/camera-testing/api/restricted_current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/restricted_current.txt
+++ b/camera/camera-testing/api/restricted_current.txt
@@ -42,10 +42,12 @@
     ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     method public void addInteropConfig(androidx.camera.core.impl.Config);
+    method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
     method public void clearInteropConfig();
-    method public void clearNewCaptureRequestListener();
+    method @Deprecated public void clearNewCaptureRequestListener();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
     method public int getExposureCompensationIndex();
     method public int getFlashMode();
@@ -62,11 +64,14 @@
     method public void notifyAllRequestsOnCaptureCancelled();
     method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
     method public void notifyAllRequestsOnCaptureFailed();
+    method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+    method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
     method public void setFlashMode(int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
-    method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
-    method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 41958c4..c7f41a5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -28,6 +28,10 @@
 import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager;
 import androidx.camera.testing.impl.fakes.FakeCameraFactory;
 import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Convenience class for generating a fake {@link CameraXConfig}.
@@ -47,6 +51,9 @@
     @Nullable
     private static FakeCamera sFrontCamera = null;
 
+    @Nullable
+    private static FakeUseCaseConfigFactory sFakeUseCaseConfigFactory;
+
     /** Generates a fake {@link CameraXConfig}. */
     @NonNull
     public static CameraXConfig create() {
@@ -59,28 +66,26 @@
      */
     @NonNull
     public static CameraXConfig create(@Nullable CameraSelector availableCamerasSelector) {
+        FakeCameraFactory cameraFactory = createCameraFactory(availableCamerasSelector);
+
         final CameraFactory.Provider cameraFactoryProvider =
-                (ignored1, ignored2, ignored3, ignore4) -> {
-                    final FakeCameraFactory cameraFactory = new FakeCameraFactory(
-                            availableCamerasSelector);
-                    cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK,
-                            DEFAULT_BACK_CAMERA_ID,
-                            FakeAppConfig::getBackCamera);
-                    cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
-                            DEFAULT_FRONT_CAMERA_ID,
-                            FakeAppConfig::getFrontCamera);
-                    final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
-                    cameraFactory.setCameraCoordinator(cameraCoordinator);
-                    return cameraFactory;
-                };
+                (ignored1, ignored2, ignored3, ignore4) -> cameraFactory;
 
         final CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
                 (ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
 
+        List<FakeCamera> fakeCameras = new ArrayList<>();
+        for (String cameraId : cameraFactory.getAvailableCameraIds()) {
+            fakeCameras.add((FakeCamera) cameraFactory.getCamera(cameraId));
+        }
+
+        sFakeUseCaseConfigFactory = new FakeUseCaseConfigFactory(fakeCameras);
+
         final CameraXConfig.Builder appConfigBuilder = new CameraXConfig.Builder()
                 .setCameraFactoryProvider(cameraFactoryProvider)
                 .setDeviceSurfaceManagerProvider(surfaceManagerProvider)
-                .setUseCaseConfigFactoryProvider(ignored -> new FakeUseCaseConfigFactory());
+                .setUseCaseConfigFactoryProvider(
+                        ignored -> sFakeUseCaseConfigFactory);
 
         if (availableCamerasSelector != null) {
             appConfigBuilder.setAvailableCamerasLimiter(availableCamerasSelector);
@@ -89,6 +94,21 @@
         return appConfigBuilder.build();
     }
 
+    private static FakeCameraFactory createCameraFactory(
+            @Nullable CameraSelector availableCamerasSelector) {
+        FakeCameraFactory cameraFactory = new FakeCameraFactory(availableCamerasSelector);
+        cameraFactory.insertCamera(
+                CameraSelector.LENS_FACING_BACK,
+                DEFAULT_BACK_CAMERA_ID,
+                FakeAppConfig::getBackCamera);
+        cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
+                DEFAULT_FRONT_CAMERA_ID,
+                FakeAppConfig::getFrontCamera);
+        final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
+        cameraFactory.setCameraCoordinator(cameraCoordinator);
+        return cameraFactory;
+    }
+
     /**
      * Returns the default fake back camera that is used internally by CameraX.
      */
@@ -126,4 +146,20 @@
             return create();
         }
     }
+
+    /**
+     * Returns the {@link TakePictureManagerWrapper} being used for image capture.
+     *
+     * <p> Note that this may be null if {@link androidx.camera.core.ImageCapture} is still not set
+     * up and bound to a camera.
+     */
+    @Nullable
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static TakePictureManagerWrapper getTakePictureManager() {
+        if (sFakeUseCaseConfigFactory == null) {
+            return null;
+        }
+        return sFakeUseCaseConfigFactory.getTakePictureManager();
+    }
+
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index a3b256b..b4d661b 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -21,6 +21,7 @@
 
 import android.graphics.Rect;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.camera.core.FocusMeteringAction;
@@ -46,6 +47,8 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -76,13 +79,18 @@
      * <p> {@link CameraXExecutors#directExecutor} via default, unless some other executor is set
      * via {@link #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)}.
      */
-    @NonNull private final Executor mExecutor;
+    @NonNull
+    private final Executor mExecutor;
     private final ControlUpdateCallback mControlUpdateCallback;
     private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
     @ImageCapture.FlashMode
     private int mFlashMode = FLASH_MODE_OFF;
     private final ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
+    @Deprecated
     private Pair<Executor, OnNewCaptureRequestListener> mOnNewCaptureRequestListener;
+    @GuardedBy("mOnNewCaptureRequestListeners")
+    private final List<Pair<Executor, OnNewCaptureRequestListener>> mOnNewCaptureRequestListeners =
+            new ArrayList<>();
     private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
     private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
             new ArrayList<>();
@@ -127,7 +135,8 @@
      * Constructs an instance of {@link FakeCameraControl} with the
      * provided {@link ControlUpdateCallback}.
      *
-     * @param executor {@link Executor} used to invoke the {@code controlUpdateCallback}.
+     * @param executor              {@link Executor} used to invoke the {@code
+     * controlUpdateCallback}.
      * @param controlUpdateCallback {@link ControlUpdateCallback} to notify events.
      */
     public FakeCameraControl(@NonNull Executor executor,
@@ -180,6 +189,38 @@
     }
 
     /**
+     * Notifies last submitted request using {@link CameraCaptureCallback#onCaptureCompleted},
+     * which is invoked in the thread denoted by {@link #mExecutor}.
+     *
+     * @param result The {@link CameraCaptureResult} which is notified to all the callbacks.
+     */
+    public void notifyLastRequestOnCaptureCompleted(@NonNull CameraCaptureResult result) {
+        if (mSubmittedCaptureRequests.isEmpty() || mSubmittedCompleterList.isEmpty()) {
+            Logger.e(TAG,
+                    "notifyLastRequestOnCaptureCompleted: returning early since either "
+                            + "mSubmittedCaptureRequests or mSubmittedCompleterList is empty, "
+                            + "mSubmittedCaptureRequests = "
+                            + mSubmittedCaptureRequests + ", mSubmittedCompleterList"
+                            + mSubmittedCompleterList);
+            return;
+        }
+
+        CaptureConfig captureConfig = mSubmittedCaptureRequests.get(
+                mSubmittedCaptureRequests.size() - 1);
+        for (CameraCaptureCallback cameraCaptureCallback :
+                captureConfig.getCameraCaptureCallbacks()) {
+            mExecutor.execute(() -> cameraCaptureCallback.onCaptureCompleted(
+                    captureConfig.getId(), result));
+        }
+        mSubmittedCaptureRequests.remove(captureConfig);
+
+        CallbackToFutureAdapter.Completer<Void> completer = mSubmittedCompleterList.get(
+                mSubmittedCompleterList.size() - 1);
+        completer.set(null);
+        mSubmittedCompleterList.remove(completer);
+    }
+
+    /**
      * Notifies all submitted requests using {@link CameraCaptureCallback#onCaptureCompleted},
      * which is invoked in the thread denoted by {@link #mExecutor}.
      *
@@ -288,6 +329,7 @@
     public ListenableFuture<List<Void>> submitStillCaptureRequests(
             @NonNull List<CaptureConfig> captureConfigs,
             int captureMode, int flashType) {
+        Logger.d(TAG, "submitStillCaptureRequests: captureConfigs = " + captureConfigs);
         mSubmittedCaptureRequests.addAll(captureConfigs);
         mExecutor.execute(
                 () -> mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs));
@@ -299,12 +341,16 @@
             }));
         }
 
-        if (mOnNewCaptureRequestListener != null) {
-            Executor executor = Objects.requireNonNull(mOnNewCaptureRequestListener.first);
-            OnNewCaptureRequestListener listener =
-                    Objects.requireNonNull(mOnNewCaptureRequestListener.second);
+        synchronized (mOnNewCaptureRequestListeners) {
+            Logger.d(TAG, "submitStillCaptureRequests: mOnNewCaptureRequestListeners = "
+                    + mOnNewCaptureRequestListeners);
 
-            executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+            for (Pair<Executor, FakeCameraControl.OnNewCaptureRequestListener> listenerPair :
+                    mOnNewCaptureRequestListeners) {
+                Executor executor = Objects.requireNonNull(listenerPair.first);
+                OnNewCaptureRequestListener listener = Objects.requireNonNull(listenerPair.second);
+                executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+            }
         }
         return Futures.allAsList(fakeFutures);
     }
@@ -348,6 +394,58 @@
     }
 
     /**
+     * Adds a listener to be notified when there are new capture requests submitted.
+     *
+     * <p> Note that the listener will be executed on the calling thread directly using
+     * {@link CameraXExecutors#directExecutor}. To specify the execution thread, use
+     * {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
+     *
+     * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     */
+    public void addOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+        addOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
+    }
+
+    /**
+     * Adds a listener to be notified when there are new capture requests submitted.
+     *
+     * @param executor {@link Executor} used to notify the {@code listener}.
+     * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     */
+    public void addOnNewCaptureRequestListener(@NonNull Executor executor,
+            @NonNull OnNewCaptureRequestListener listener) {
+        synchronized (mOnNewCaptureRequestListeners) {
+            mOnNewCaptureRequestListeners.add(new Pair<>(executor, listener));
+        }
+    }
+
+    /**
+     * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+     */
+    public void removeOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+        removeOnNewCaptureRequestListeners(Collections.singletonList(listener));
+    }
+
+    /**
+     * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+     */
+    public void removeOnNewCaptureRequestListeners(
+            @NonNull List<OnNewCaptureRequestListener> listeners) {
+        synchronized (mOnNewCaptureRequestListeners) {
+            Iterator<Pair<Executor, OnNewCaptureRequestListener>> iterator =
+                    mOnNewCaptureRequestListeners.iterator();
+            while (iterator.hasNext()) {
+                Pair<Executor, OnNewCaptureRequestListener> element = iterator.next();
+                if (listeners.contains(element.second)) {
+                    iterator.remove();
+                }
+            }
+        }
+    }
+
+    /**
      * Sets a listener to be notified when there are new capture requests submitted.
      *
      * <p> Note that the listener will be executed on the calling thread directly using
@@ -355,8 +453,10 @@
      * {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
      *
      * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
-     * {@link CaptureConfig} parameters when new capture requests are submitted.
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     * @deprecated Use {@link #addOnNewCaptureRequestListener(OnNewCaptureRequestListener)} instead.
      */
+    @Deprecated // TODO: b/359458110 - Remove all usages
     public void setOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
         setOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
     }
@@ -366,17 +466,31 @@
      *
      * @param executor {@link Executor} used to notify the {@code listener}.
      * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
-     * {@link CaptureConfig} parameters when new capture requests are submitted.
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     * @deprecated Use
+     * {@link #addOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}
+     * instead.
      */
+    @Deprecated // TODO: b/359458110 - Remove all usages
     public void setOnNewCaptureRequestListener(@NonNull Executor executor,
             @NonNull OnNewCaptureRequestListener listener) {
         mOnNewCaptureRequestListener = new Pair<>(executor, listener);
+        addOnNewCaptureRequestListener(executor, listener);
     }
 
     /**
      * Clears any listener set via {@link #setOnNewCaptureRequestListener}.
+     *
+     * @deprecated Use {@link #removeOnNewCaptureRequestListener(OnNewCaptureRequestListener)}
+     * instead.
      */
+    @Deprecated // TODO: b/359458110 - Remove all usages
     public void clearNewCaptureRequestListener() {
+        if (mOnNewCaptureRequestListener == null) {
+            return;
+        }
+        removeOnNewCaptureRequestListener(
+                Objects.requireNonNull(mOnNewCaptureRequestListener.second));
         mOnNewCaptureRequestListener = null;
     }
 
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
index e960662..015bff9 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
@@ -16,8 +16,11 @@
 
 package androidx.camera.testing.impl
 
+import android.graphics.Bitmap
 import android.graphics.Rect
+import android.util.Size
 import android.view.Surface
+import androidx.camera.core.Logger
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -36,7 +39,7 @@
 private const val TAG = "CaptureSimulation"
 
 /** Simulates a capture frame being drawn on all of the provided surfaces. */
-public suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
+internal suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
     it.simulateCaptureFrame()
 }
 
@@ -45,7 +48,7 @@
  *
  * @throws IllegalStateException If [DeferrableSurface.getSurface] provides a null surface.
  */
-public suspend fun DeferrableSurface.simulateCaptureFrame() {
+internal suspend fun DeferrableSurface.simulateCaptureFrame() {
     val deferred = CompletableDeferred<Unit>()
 
     Futures.addCallback(
@@ -53,6 +56,7 @@
         object : FutureCallback<Surface?> {
             override fun onSuccess(surface: Surface?) {
                 if (surface == null) {
+                    Logger.w(TAG, "simulateCaptureFrame: surface obtained from $this is null!")
                     deferred.completeExceptionally(
                         IllegalStateException(
                             "Null surface obtained from ${this@simulateCaptureFrame}"
@@ -60,10 +64,9 @@
                     )
                     return
                 }
-                val canvas =
-                    surface.lockCanvas(Rect(0, 0, prescribedSize.width, prescribedSize.height))
                 // TODO: Draw something on the canvas (e.g. fake image bitmap or alternating color).
-                surface.unlockCanvasAndPost(canvas)
+                surface.simulateCaptureFrame(prescribedSize)
+
                 deferred.complete(Unit)
             }
 
@@ -77,6 +80,20 @@
     deferred.await()
 }
 
+/**
+ * Simulates a capture frame being drawn on a [Surface].
+ *
+ * @param canvasSize The canvas size for drawing.
+ * @param bitmap A bitmap to draw as the capture frame, if not null.
+ */
+internal fun Surface.simulateCaptureFrame(canvasSize: Size, bitmap: Bitmap? = null) {
+    val canvas = lockCanvas(Rect(0, 0, canvasSize.width, canvasSize.height))
+    if (bitmap != null) {
+        canvas.drawBitmap(bitmap, null, Rect(0, 0, canvasSize.width, canvasSize.height), null)
+    }
+    unlockCanvasAndPost(canvas)
+}
+
 // The following methods are adapters for Java invocations.
 
 /**
@@ -88,7 +105,7 @@
  * @return A [ListenableFuture] representing when the operation has been completed.
  */
 @JvmOverloads
-public fun List<DeferrableSurface>.simulateCaptureFrameAsync(
+internal fun List<DeferrableSurface>.simulateCaptureFrameAsync(
     executor: Executor = Dispatchers.Default.asExecutor()
 ): ListenableFuture<Void> {
     val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
@@ -104,7 +121,7 @@
  * @return A [ListenableFuture] representing when the operation has been completed.
  */
 @JvmOverloads
-public fun DeferrableSurface.simulateCaptureFrameAsync(
+internal fun DeferrableSurface.simulateCaptureFrameAsync(
     executor: Executor = Dispatchers.Default.asExecutor()
 ): ListenableFuture<Void> {
     val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
index 90c4bce..5acfa2bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.testing.impl.fakes;
 
+import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.media.Image;
 
@@ -47,6 +48,8 @@
     @NonNull
     private ImageInfo mImageInfo;
     private Image mImage;
+    @Nullable
+    private Bitmap mBitmap;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final Object mReleaseLock = new Object();
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -60,6 +63,11 @@
         mImageInfo = imageInfo;
     }
 
+    public FakeImageProxy(@NonNull ImageInfo imageInfo, @NonNull Bitmap bitmap) {
+        mImageInfo = imageInfo;
+        mBitmap = bitmap;
+    }
+
     @Override
     public void close() {
         synchronized (mReleaseLock) {
@@ -196,4 +204,13 @@
             return mReleaseFuture;
         }
     }
+
+    @NonNull
+    @Override
+    public Bitmap toBitmap() {
+        if (mBitmap != null) {
+            return mBitmap;
+        }
+        return ImageProxy.super.toBitmap();
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
index f364c4e..9ccd9bd 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
@@ -19,6 +19,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TAKE_PICTURE_MANAGER_PROVIDER;
 
 import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraDevice;
@@ -30,21 +31,47 @@
 import androidx.camera.core.ExperimentalZeroShutterLag;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCapture.CaptureMode;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.MutableOptionsBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A fake implementation of {@link UseCaseConfigFactory}.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class FakeUseCaseConfigFactory implements UseCaseConfigFactory {
-
     @Nullable
     private CaptureType mLastRequestedCaptureType;
 
+    @Nullable
+    private TakePictureManagerWrapper mTakePictureManager;
+
+    @NonNull
+    private final List<FakeCamera> mFakeCameras = new ArrayList<>();
+
+    /**
+     * Creates a {@link FakeUseCaseConfigFactory} instance.
+     */
+    public FakeUseCaseConfigFactory() {
+    }
+
+    /**
+     * Creates a {@link FakeUseCaseConfigFactory} instance with the available {@link FakeCamera}
+     * instances.
+     */
+    public FakeUseCaseConfigFactory(@NonNull List<FakeCamera> fakeCameras) {
+        mFakeCameras.addAll(fakeCameras);
+    }
+
     /**
      * Returns the configuration for the given capture type, or <code>null</code> if the
      * configuration cannot be produced.
@@ -66,6 +93,20 @@
         mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
                 new FakeSessionConfigOptionUnpacker());
 
+        if (captureType == CaptureType.IMAGE_CAPTURE) {
+            mutableConfig.insertOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+                    new TakePictureManager.Provider() {
+                        @NonNull
+                        @Override
+                        public TakePictureManager newInstance(
+                                @NonNull ImageCaptureControl imageCaptureControl) {
+                            mTakePictureManager = new TakePictureManagerWrapper(
+                                    imageCaptureControl, mFakeCameras);
+                            return mTakePictureManager;
+                        }
+                    });
+        }
+
         return OptionsBundle.from(mutableConfig);
     }
 
@@ -97,4 +138,12 @@
                 return CameraDevice.TEMPLATE_PREVIEW;
         }
     }
+
+    /**
+     * Returns the last provided {@link TakePictureManagerWrapper} instance.
+     */
+    @Nullable
+    public TakePictureManagerWrapper getTakePictureManager() {
+        return mTakePictureManager;
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
new file mode 100644
index 0000000..0de7dba
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.impl.wrappers
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Logger
+import androidx.camera.core.imagecapture.Bitmap2JpegBytes
+import androidx.camera.core.imagecapture.ImageCaptureControl
+import androidx.camera.core.imagecapture.ImagePipeline
+import androidx.camera.core.imagecapture.JpegBytes2Disk
+import androidx.camera.core.imagecapture.JpegBytes2Image
+import androidx.camera.core.imagecapture.RequestWithCallback
+import androidx.camera.core.imagecapture.TakePictureManager
+import androidx.camera.core.imagecapture.TakePictureManagerImpl
+import androidx.camera.core.imagecapture.TakePictureRequest
+import androidx.camera.core.processing.Packet
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.impl.ExifUtil
+import androidx.camera.testing.impl.TestImageUtil
+import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.impl.fakes.FakeImageInfo
+import androidx.camera.testing.impl.fakes.FakeImageProxy
+
+/**
+ * A [TakePictureManager] implementation wrapped around the real implementation
+ * [TakePictureManagerImpl].
+ *
+ * It is used for fake cameras and provides fake image capture results when required from a camera.
+ */
+public class TakePictureManagerWrapper(
+    imageCaptureControl: ImageCaptureControl,
+    private val fakeCameras: List<FakeCamera>
+) : TakePictureManager {
+    // Try to keep the fake as close to real as possible
+    private val managerDelegate = TakePictureManagerImpl(imageCaptureControl)
+
+    private val bitmap2JpegBytes = Bitmap2JpegBytes()
+    private val jpegBytes2Disk = JpegBytes2Disk()
+    private val jpegBytes2Image = JpegBytes2Image()
+
+    private val imageProxyQueue = ArrayDeque<ImageProxy>()
+    private val outputFileResultsQueue = ArrayDeque<ImageCapture.OutputFileResults>()
+
+    /** Whether to disable auto capture completion. */
+    public var disableAutoComplete: Boolean = false
+
+    override fun setImagePipeline(imagePipeline: ImagePipeline) {
+        managerDelegate.imagePipeline = imagePipeline
+    }
+
+    override fun offerRequest(takePictureRequest: TakePictureRequest) {
+        val listeners = mutableListOf<FakeCameraControl.OnNewCaptureRequestListener>()
+
+        fakeCameras.forEach { camera ->
+            if (camera.cameraControlInternal is FakeCameraControl) {
+                (camera.cameraControlInternal as FakeCameraControl).apply {
+                    val listener =
+                        FakeCameraControl.OnNewCaptureRequestListener {
+                            if (!disableAutoComplete) {
+                                completeCapturingRequest(this)
+                            }
+                        }
+                    listeners.add(listener)
+                    addOnNewCaptureRequestListener(listener)
+                }
+            } else {
+                Logger.w(
+                    TAG,
+                    "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+                )
+            }
+        }
+
+        managerDelegate.offerRequest(takePictureRequest)
+
+        fakeCameras.forEach { camera ->
+            if (camera.cameraControlInternal is FakeCameraControl) {
+                (camera.cameraControlInternal as FakeCameraControl)
+                    .removeOnNewCaptureRequestListeners(listeners)
+            } else {
+                Logger.w(
+                    TAG,
+                    "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+                )
+            }
+        }
+    }
+
+    override fun pause() {
+        managerDelegate.pause()
+    }
+
+    override fun resume() {
+        managerDelegate.resume()
+    }
+
+    override fun abortRequests() {
+        managerDelegate.abortRequests()
+    }
+
+    @VisibleForTesting
+    override fun hasCapturingRequest(): Boolean = managerDelegate.hasCapturingRequest()
+
+    @VisibleForTesting
+    override fun getCapturingRequest(): RequestWithCallback? = managerDelegate.capturingRequest
+
+    @VisibleForTesting
+    override fun getIncompleteRequests(): List<RequestWithCallback> =
+        managerDelegate.incompleteRequests
+
+    @VisibleForTesting
+    override fun getImagePipeline(): ImagePipeline = managerDelegate.imagePipeline
+
+    @VisibleForTesting
+    public fun completeCapturingRequest(fakeCameraControl: FakeCameraControl) {
+        Log.d(
+            TAG,
+            "completeCapturingRequest: capturingRequest = ${managerDelegate.capturingRequest}"
+        )
+        managerDelegate.capturingRequest?.apply {
+            onCaptureStarted()
+
+            // This ensures the future from CameraControlInternal#submitStillCaptureRequests() is
+            // completed and not garbage collected later
+            // TODO - notify all the new requests, not only the last one
+            fakeCameraControl.notifyLastRequestOnCaptureCompleted(FakeCameraCaptureResult())
+
+            onImageCaptured()
+
+            takePictureRequest.also { req ->
+                val outputFileOptions = req.outputFileOptions // enables smartcast for null check
+                if (req.onDiskCallback != null && outputFileOptions != null) {
+                    if (outputFileResultsQueue.isEmpty()) {
+                        onFinalResult(createOutputFileResults(req, outputFileOptions))
+                    } else {
+                        onFinalResult(outputFileResultsQueue.first())
+                        outputFileResultsQueue.removeFirst()
+                    }
+                } else {
+                    if (imageProxyQueue.isEmpty()) {
+                        onFinalResult(createImageProxy(req))
+                    } else {
+                        onFinalResult(imageProxyQueue.first())
+                        imageProxyQueue.removeFirst()
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Enqueues an [ImageProxy] to be used as result for the next image capture with
+     * [ImageCapture.OnImageCapturedCallback].
+     *
+     * Note that the provided [ImageProxy] is consumed by next image capture and is not available
+     * for following captures. If no result is available during a capture, CameraX will create a
+     * fake image by itself and provide result based on that.
+     */
+    public fun enqueueImageProxy(imageProxy: ImageProxy) {
+        imageProxyQueue.add(imageProxy)
+    }
+
+    /**
+     * Enqueues an [OutputFileResults] to be used as result for the next image capture with
+     * [ImageCapture.OnImageSavedCallback].
+     *
+     * Note that the provided [OutputFileResults] is consumed by next image capture and is not
+     * available for following captures. If no result is available during a capture, CameraX will
+     * create a fake image by itself and provide result based on that.
+     */
+    public fun enqueueOutputFileResults(outputFileResults: ImageCapture.OutputFileResults) {
+        outputFileResultsQueue.add(outputFileResults)
+    }
+
+    private fun createOutputFileResults(
+        takePictureRequest: TakePictureRequest,
+        outputFileOptions: OutputFileOptions
+    ): ImageCapture.OutputFileResults {
+        // TODO - Take a bitmap as input and use that directly
+        val bytesPacket =
+            takePictureRequest.convertBitmapToBytes(
+                TestImageUtil.createBitmap(
+                    takePictureRequest.cropRect.width(),
+                    takePictureRequest.cropRect.height()
+                )
+            )
+        return jpegBytes2Disk.apply(JpegBytes2Disk.In.of(bytesPacket, outputFileOptions))
+    }
+
+    private fun createImageProxy(
+        takePictureRequest: TakePictureRequest,
+    ): ImageProxy {
+        // TODO - Take a bitmap as input and use that directly
+        val bitmap =
+            TestImageUtil.createBitmap(
+                takePictureRequest.cropRect.width(),
+                takePictureRequest.cropRect.height()
+            )
+        if (canLoadImageProcessingUtilJniLib()) {
+            val bytesPacket =
+                takePictureRequest.convertBitmapToBytes(
+                    TestImageUtil.createBitmap(
+                        takePictureRequest.cropRect.width(),
+                        takePictureRequest.cropRect.height()
+                    )
+                )
+            return jpegBytes2Image.apply(bytesPacket).data
+        } else {
+            return bitmap.toFakeImageProxy()
+        }
+    }
+
+    private fun Bitmap.toFakeImageProxy(): ImageProxy {
+        return FakeImageProxy(FakeImageInfo(), this)
+    }
+
+    private fun TakePictureRequest.convertBitmapToBytes(bitmap: Bitmap): Packet<ByteArray> {
+        val inputPacket =
+            Packet.of(
+                bitmap,
+                ExifUtil.createExif(
+                    TestImageUtil.createJpegBytes(cropRect.width(), cropRect.height())
+                ),
+                cropRect,
+                rotationDegrees,
+                Matrix(),
+                FakeCameraCaptureResult()
+            )
+
+        return bitmap2JpegBytes.apply(Bitmap2JpegBytes.In.of(inputPacket, jpegQuality))
+    }
+
+    private fun canLoadImageProcessingUtilJniLib(): Boolean {
+        try {
+            System.loadLibrary(ImageProcessingUtil.JNI_LIB_NAME)
+            return true
+        } catch (e: UnsatisfiedLinkError) {
+            Logger.d(TAG, "canLoadImageProcessingUtilJniLib", e)
+            return false
+        }
+    }
+
+    private companion object {
+        private const val TAG = "TakePictureManagerWrap"
+    }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
index a61ada7..d7264b0 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
@@ -173,6 +173,42 @@
     }
 
     @Test
+    public void notifiesLastRequestOnCaptureCompleted() {
+        CameraCaptureResult captureResult = new FakeCameraCaptureResult();
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List<CameraCaptureResult> resultList = new ArrayList<>();
+        CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+            @Override
+            public void onCaptureCompleted(int captureConfigId,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                resultList.add(cameraCaptureResult);
+            }
+        }, new CameraCaptureCallback() {
+            @Override
+            public void onCaptureCompleted(int captureConfigId,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                resultList.add(cameraCaptureResult);
+            }
+        });
+        CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+            @Override
+            public void onCaptureCompleted(int captureConfigId,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                resultList.add(cameraCaptureResult);
+                latch.countDown();
+            }
+        });
+
+        mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+                ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+        mCameraControl.notifyLastRequestOnCaptureCompleted(captureResult);
+
+        awaitLatch(latch);
+        assertThat(resultList).containsExactlyElementsIn(Collections.singletonList(captureResult));
+    }
+
+    @Test
     public void canUpdateFlashModeToOff() {
         mCameraControl.setFlashMode(ImageCapture.FLASH_MODE_OFF);
         assertThat(mCameraControl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_OFF);
@@ -319,7 +355,7 @@
         List<CaptureConfig> notifiedCaptureConfigs = new ArrayList<>();
         CountDownLatch latch = new CountDownLatch(1);
 
-        mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             notifiedCaptureConfigs.addAll(captureConfigs);
             latch.countDown();
         });
@@ -335,7 +371,7 @@
         AtomicReference<Thread> listenerThread = new AtomicReference<>();
         CountDownLatch latch = new CountDownLatch(1);
 
-        mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             listenerThread.set(Thread.currentThread());
             latch.countDown();
         });
@@ -350,7 +386,7 @@
         AtomicReference<Thread> listenerThread = new AtomicReference<>();
         CountDownLatch latch = new CountDownLatch(1);
 
-        mCameraControl.setOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
+        mCameraControl.addOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
                 captureConfigs -> {
                     listenerThread.set(Thread.currentThread());
                     latch.countDown();
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index c361fe0..3defd64b 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -70,8 +70,6 @@
 import com.google.common.util.concurrent.ListenableFuture
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
 import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicReference
@@ -273,6 +271,8 @@
 
         instrumentation.runOnMainSync {
             val previewView = PreviewView(context)
+            // Specifies the content description and uses it to find the view to click
+            previewView.contentDescription = previewView.hashCode().toString()
             clickEventHelper = ClickEventHelper(previewView)
             previewView.setOnTouchListener(clickEventHelper)
             previewView.controller = fakeController
@@ -311,7 +311,6 @@
         private var uiDevice: UiDevice? = null
         private var limitedRetryCount = 0
         private var retriedCounter = 0
-        private var executor: ExecutorService? = null
 
         override fun onTouch(view: View, event: MotionEvent): Boolean {
             if (view != targetView) {
@@ -337,7 +336,6 @@
                 uiDevice = null
                 limitedRetryCount = 0
                 retriedCounter = 0
-                executor?.shutdown()
                 synchronized(lock) { isPerformingClick = false }
             }
 
@@ -359,34 +357,16 @@
                 }
             }
 
-            executor = Executors.newSingleThreadExecutor()
             limitedRetryCount = retryCount
             retriedCounter = 0
             this.uiDevice = uiDevice
             performSingleClickInternal()
         }
 
-        private fun performSingleClickInternal() {
-            executor!!.execute {
-                var needClearContentDescription = false
-                val originalContentDescription = targetView.contentDescription
-
-                if (originalContentDescription == null || originalContentDescription.isEmpty()) {
-                    needClearContentDescription = true
-                    targetView.contentDescription = targetView.hashCode().toString()
-                }
-
-                uiDevice!!
-                    .findObject(
-                        UiSelector().descriptionContains(targetView.contentDescription.toString())
-                    )
-                    .click()
-
-                if (needClearContentDescription) {
-                    targetView.contentDescription = originalContentDescription
-                }
-            }
-        }
+        private fun performSingleClickInternal() =
+            uiDevice!!
+                .findObject(UiSelector().descriptionContains(targetView.hashCode().toString()))
+                .click()
     }
 
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
index af52c44..ef4fe9e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
@@ -42,7 +42,6 @@
 import kotlinx.coroutines.withContext
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -87,7 +86,7 @@
     @Test
     fun canSubmitTakePictureRequest(): Unit = runBlocking {
         val countDownLatch = CountDownLatch(1)
-        cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+        cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
 
         imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
 
@@ -96,10 +95,9 @@
 
     // Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
     // reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runBlocking {
-        val callback = FakeOnImageCapturedCallback()
+        val callback = FakeOnImageCapturedCallback(closeImageOnSuccess = false)
         imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
         callback.awaitCapturesAndAssert(capturedImagesCount = 1)
         callback.results.first().image.toBitmap()
@@ -107,7 +105,6 @@
 
     // Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
     // reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runBlocking {
         val saveLocation = temporaryFolder.newFile()
@@ -126,7 +123,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindImage_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
         val initialCount = getMediaStoreCameraXImageCount()
diff --git a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 459187c..15cbefe 100644
--- a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -44,7 +44,6 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -92,7 +91,7 @@
     @Test
     fun canSubmitTakePictureRequest(): Unit = runTest {
         val countDownLatch = CountDownLatch(1)
-        cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+        cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
 
         imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
 
@@ -101,7 +100,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
         val callback = FakeOnImageCapturedCallback()
@@ -112,7 +110,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runTest {
         val saveLocation = temporaryFolder.newFile()
@@ -131,7 +128,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindFakeImageUri_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
         val callback = FakeOnImageSavedCallback()
diff --git a/car/app/app/api/1.7.0-beta02.txt b/car/app/app/api/1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/1.7.0-beta02.txt
+++ b/car/app/app/api/1.7.0-beta02.txt
@@ -900,6 +900,7 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
diff --git a/car/app/app/api/current.ignore b/car/app/app/api/current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/current.ignore
+++ b/car/app/app/api/current.ignore
@@ -1,3 +1,3 @@
 // Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
-    Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+    Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -900,6 +900,7 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
diff --git a/car/app/app/api/restricted_1.7.0-beta02.txt b/car/app/app/api/restricted_1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_1.7.0-beta02.txt
+++ b/car/app/app/api/restricted_1.7.0-beta02.txt
@@ -900,6 +900,7 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
diff --git a/car/app/app/api/restricted_current.ignore b/car/app/app/api/restricted_current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/restricted_current.ignore
+++ b/car/app/app/api/restricted_current.ignore
@@ -1,3 +1,3 @@
 // Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
-    Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+    Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -900,6 +900,7 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
index 473f447..413cd71 100644
--- a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
@@ -37,6 +37,17 @@
 
     /**
      * {@link Bundle} key used in the rootHints bundle passed to
+     * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)}
+     * to indicate the version of the caller. Note that this should only be used for analytics and
+     * is different than {@link #KEY_ROOT_HINT_MEDIA_SESSION_API}.
+     *
+     * <p>TYPE: string - the version info.
+     */
+    public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION =
+            "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
+
+    /**
+     * {@link Bundle} key used in the rootHints bundle passed to
      * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} to indicate
      * which version of the media api is used by the caller
      *
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index f968628..c7595d2 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -127,10 +127,6 @@
     samples(project(":compose:animation:animation:animation-samples"))
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 android {
     compileSdk 35
     namespace "androidx.compose.animation"
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 49c87a5..995c88c 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -36,10 +36,6 @@
     debugImplementation(project(":compose:ui:ui-tooling"))
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 android {
     compileSdk 35
     namespace "androidx.compose.animation.demos"
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
index 375751a..803c694 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
@@ -70,7 +70,10 @@
     val avatar = remember {
         movableContentWithReceiverOf<SceneScope> {
             Box(
-                Modifier.sharedElementBasedOnProgress(progressProvider)
+                Modifier.sharedElementBasedOnProgress(
+                        this@movableContentWithReceiverOf,
+                        progressProvider
+                    )
                     .background(Color(0xffff6f69), RoundedCornerShape(20))
                     .fillMaxSize()
             )
@@ -81,7 +84,10 @@
         movableContentWithReceiverOf<SceneScope, @Composable () -> Unit> { child ->
             Surface(
                 modifier =
-                    Modifier.sharedElementBasedOnProgress(progressProvider)
+                    Modifier.sharedElementBasedOnProgress(
+                            this@movableContentWithReceiverOf,
+                            progressProvider
+                        )
                         .background(Color(0xfffdedac)),
                 color = Color(0xfffdedac),
                 shape = RoundedCornerShape(10.dp)
@@ -166,44 +172,53 @@
     val progress: Float
 }
 
-context(LookaheadScope)
 @SuppressLint("PrimitiveInCollection")
-fun <T> Modifier.sharedElementBasedOnProgress(provider: ProgressProvider<T>) = composed {
-    val sizeMap = remember { mutableMapOf<T, IntSize>() }
-    val offsetMap = remember { mutableMapOf<T, Offset>() }
-    val calculateSize: (IntSize) -> IntSize = {
-        sizeMap[provider.targetState] = it
-        val (width, height) =
-            lerp(
-                sizeMap[provider.initialState]!!.toSize(),
-                sizeMap[provider.targetState]!!.toSize(),
-                provider.progress
-            )
-        IntSize(width.roundToInt(), height.roundToInt())
-    }
-
-    val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
-        with(it) {
-            coordinates?.let {
-                offsetMap[provider.targetState] =
-                    lookaheadScopeCoordinates.localLookaheadPositionOf(it)
-                val lerpedOffset =
+fun <T> Modifier.sharedElementBasedOnProgress(
+    lookaheadScope: LookaheadScope,
+    provider: ProgressProvider<T>
+) =
+    with(lookaheadScope) {
+        composed {
+            val sizeMap = remember { mutableMapOf<T, IntSize>() }
+            val offsetMap = remember { mutableMapOf<T, Offset>() }
+            val calculateSize: (IntSize) -> IntSize = {
+                sizeMap[provider.targetState] = it
+                val (width, height) =
                     lerp(
-                        offsetMap[provider.initialState]!!,
-                        offsetMap[provider.targetState]!!,
+                        sizeMap[provider.initialState]!!.toSize(),
+                        sizeMap[provider.targetState]!!.toSize(),
                         provider.progress
                     )
-                val currentOffset = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
-                (lerpedOffset - currentOffset).round()
-            } ?: IntOffset(0, 0)
+                IntSize(width.roundToInt(), height.roundToInt())
+            }
+
+            val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
+                with(it) {
+                    coordinates?.let {
+                        offsetMap[provider.targetState] =
+                            lookaheadScopeCoordinates.localLookaheadPositionOf(it)
+                        val lerpedOffset =
+                            lerp(
+                                offsetMap[provider.initialState]!!,
+                                offsetMap[provider.targetState]!!,
+                                provider.progress
+                            )
+                        val currentOffset =
+                            lookaheadScopeCoordinates.localPositionOf(
+                                it,
+                                androidx.compose.ui.geometry.Offset.Zero
+                            )
+                        (lerpedOffset - currentOffset).round()
+                    } ?: IntOffset(0, 0)
+                }
+            }
+            this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
+                val (width, height) = calculateSize(lookaheadSize)
+                val animatedConstraints = androidx.compose.ui.unit.Constraints.fixed(width, height)
+                val placeable = measurable.measure(animatedConstraints)
+                layout(placeable.width, placeable.height) {
+                    placeable.place(calculateOffset(this@approachLayout))
+                }
+            }
         }
     }
-    this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
-        val (width, height) = calculateSize(lookaheadSize)
-        val animatedConstraints = Constraints.fixed(width, height)
-        val placeable = measurable.measure(animatedConstraints)
-        layout(placeable.width, placeable.height) {
-            placeable.place(calculateOffset(this@approachLayout))
-        }
-    }
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
index 5562c9c..0879163 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
@@ -71,7 +71,9 @@
             Box(Modifier.padding(start = 50.dp, top = 200.dp, bottom = 100.dp)) {
                 val icon = remember { movableContentOf<Boolean> { MyIcon(it) } }
                 val title = remember {
-                    movableContentOf<Boolean> { Title(visible = it, Modifier.animatePosition()) }
+                    movableContentOf<Boolean> {
+                        Title(visible = it, Modifier.animatePosition(this@LookaheadScope))
+                    }
                 }
                 val details = remember { movableContentOf<Boolean> { Details(visible = it) } }
 
@@ -129,39 +131,41 @@
     }
 }
 
-context(LookaheadScope)
 @OptIn(ExperimentalAnimatableApi::class)
 @SuppressLint("UnnecessaryComposedModifier")
-fun Modifier.animatePosition(): Modifier = composed {
-    val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
-    val coroutineScope = rememberCoroutineScope()
-    this.approachLayout(
-        isMeasurementApproachInProgress = { false },
-        isPlacementApproachInProgress = {
-            offsetAnimation.updateTarget(
-                lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
-                coroutineScope,
-                spring(stiffness = Spring.StiffnessMediumLow)
-            )
-            !offsetAnimation.isIdle
-        }
-    ) { measurable, constraints ->
-        measurable.measure(constraints).run {
-            layout(width, height) {
-                val (x, y) =
-                    coordinates?.let { coordinates ->
-                        val origin = this.lookaheadScopeCoordinates
-                        val animOffset =
-                            offsetAnimation.updateTarget(
-                                origin.localLookaheadPositionOf(coordinates).round(),
-                                coroutineScope,
-                                spring(stiffness = Spring.StiffnessMediumLow),
-                            )
-                        val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
-                        animOffset - currentOffset.round()
-                    } ?: IntOffset.Zero
-                place(x, y)
+fun Modifier.animatePosition(lookaheadScope: LookaheadScope): Modifier =
+    with(lookaheadScope) {
+        composed {
+            val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
+            val coroutineScope = rememberCoroutineScope()
+            this.approachLayout(
+                isMeasurementApproachInProgress = { false },
+                isPlacementApproachInProgress = {
+                    offsetAnimation.updateTarget(
+                        lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
+                        coroutineScope,
+                        spring(stiffness = Spring.StiffnessMediumLow)
+                    )
+                    !offsetAnimation.isIdle
+                }
+            ) { measurable, constraints ->
+                measurable.measure(constraints).run {
+                    layout(width, height) {
+                        val (x, y) =
+                            coordinates?.let { coordinates ->
+                                val origin = this.lookaheadScopeCoordinates
+                                val animOffset =
+                                    offsetAnimation.updateTarget(
+                                        origin.localLookaheadPositionOf(coordinates).round(),
+                                        coroutineScope,
+                                        spring(stiffness = Spring.StiffnessMediumLow),
+                                    )
+                                val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
+                                animOffset - currentOffset.round()
+                            } ?: IntOffset.Zero
+                        place(x, y)
+                    }
+                }
             }
         }
     }
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
index a02fc9a..d294736 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
@@ -18,10 +18,6 @@
 
 import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.animateBounds
-import androidx.compose.animation.core.DeferredTargetAnimation
-import androidx.compose.animation.core.ExperimentalAnimatableApi
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.spring
 import androidx.compose.animation.demos.fancy.AnimatedDotsDemo
 import androidx.compose.animation.demos.statetransition.InfiniteProgress
 import androidx.compose.animation.demos.statetransition.InfinitePulsingHeart
@@ -43,22 +39,14 @@
 import androidx.compose.runtime.movableContentWithReceiverOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.approachLayout
 import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
 
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Preview
@@ -157,37 +145,5 @@
     }
 }
 
-context(LookaheadScope)
-@OptIn(ExperimentalAnimatableApi::class)
-fun Modifier.animateBoundsInScope(): Modifier = composed {
-    val sizeAnim = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
-    val offsetAnim = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
-    val scope = rememberCoroutineScope()
-    this.approachLayout(
-        isMeasurementApproachInProgress = {
-            sizeAnim.updateTarget(it, scope)
-            !sizeAnim.isIdle
-        },
-        isPlacementApproachInProgress = {
-            val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
-            offsetAnim.updateTarget(target.round(), scope, spring())
-            !offsetAnim.isIdle
-        }
-    ) { measurable, _ ->
-        val (animWidth, animHeight) = sizeAnim.updateTarget(lookaheadSize, scope, spring())
-        measurable.measure(Constraints.fixed(animWidth, animHeight)).run {
-            layout(width, height) {
-                coordinates?.let {
-                    val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it).round()
-                    val animOffset = offsetAnim.updateTarget(target, scope, spring())
-                    val current = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).round()
-                    val (x, y) = animOffset - current
-                    place(x, y)
-                } ?: place(0, 0)
-            }
-        }
-    }
-}
-
 private val colors =
     listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
index 2ea3bd2..ed920ea 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
@@ -55,13 +55,15 @@
                 Text(if (shouldAnimate) "Stop animating bounds" else "Animate bounds")
             }
             SubcomposeLayout(
-                Modifier.background(colors[3]).conditionallyAnimateBounds(shouldAnimate)
+                Modifier.background(colors[3])
+                    .conditionallyAnimateBounds(this@LookaheadScope, shouldAnimate)
             ) {
                 val constraints = it.copy(minWidth = 0)
                 val placeable =
                     subcompose(0) {
                             Box(
                                 Modifier.conditionallyAnimateBounds(
+                                        this@LookaheadScope,
                                         shouldAnimate,
                                         Modifier.width(if (isWide) 150.dp else 70.dp)
                                             .requiredHeight(400.dp)
@@ -75,6 +77,7 @@
                     subcompose(1) {
                             Box(
                                 Modifier.conditionallyAnimateBounds(
+                                        this@LookaheadScope,
                                         shouldAnimate,
                                         Modifier.width(if (isWide) 150.dp else 70.dp)
                                             .requiredHeight(400.dp)
@@ -91,6 +94,7 @@
                                 Box(
                                     Modifier.width(totalWidth.toDp())
                                         .conditionallyAnimateBounds(
+                                            this@LookaheadScope,
                                             shouldAnimate,
                                             Modifier.height(if (isWide) 150.dp else 70.dp)
                                         )
@@ -108,12 +112,12 @@
     }
 }
 
-context(LookaheadScope)
 @OptIn(ExperimentalSharedTransitionApi::class)
 private fun Modifier.conditionallyAnimateBounds(
+    lookaheadScope: LookaheadScope,
     shouldAnimate: Boolean,
     modifier: Modifier = Modifier
-) = if (shouldAnimate) this.animateBounds(this@LookaheadScope, modifier) else this.then(modifier)
+) = if (shouldAnimate) this.animateBounds(lookaheadScope, modifier) else this.then(modifier)
 
 private val colors =
     listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff2a9d84), Color(0xff264653))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
index 1b94fc1..ecd48cd 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
@@ -106,6 +106,9 @@
                     layout(placeable.width, placeable.height) {
                         val (x, y) =
                             offsetAnimation.updateTargetBasedOnCoordinates(
+                                this@SceneScope,
+                                this@layout,
+                                this@with,
                                 spring(stiffness = Spring.StiffnessMediumLow),
                             )
                         coordinates?.let {
@@ -153,25 +156,32 @@
         }
 }
 
-context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
 @OptIn(ExperimentalAnimatableApi::class)
 internal fun DeferredTargetAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
+    lookaheadScope: LookaheadScope,
+    placementScope: Placeable.PlacementScope,
+    coroutineScope: CoroutineScope,
     animationSpec: FiniteAnimationSpec<IntOffset>,
 ): IntOffset {
-    coordinates?.let { coordinates ->
-        with(this@PlacementScope) {
-            val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
-            val animOffset =
-                updateTarget(
-                    targetOffset.round(),
-                    this@CoroutineScope,
-                    animationSpec,
-                )
-            val current =
-                lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
-            return (animOffset - current)
+    with(lookaheadScope) {
+        with(placementScope) {
+            coordinates?.let { coordinates ->
+                with(placementScope) {
+                    val targetOffset =
+                        lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
+                    val animOffset =
+                        updateTarget(
+                            targetOffset.round(),
+                            coroutineScope,
+                            animationSpec,
+                        )
+                    val current =
+                        lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
+                    return (animOffset - current)
+                }
+            }
+
+            return IntOffset.Zero
         }
     }
-
-    return IntOffset.Zero
 }
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
index 25b32da..78916a7 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
@@ -90,180 +90,208 @@
         ) {
             // TODO: Double check on container transform scrolling
             if (it != null) {
-                DetailView(model = model, selected = it, model.items[6])
+                DetailView(
+                    this@AnimatedContent,
+                    this@SharedTransitionLayout,
+                    model = model,
+                    selected = it,
+                    model.items[6]
+                )
             } else {
-                GridView(model = model)
+                GridView(this@AnimatedContent, this@SharedTransitionLayout, model = model)
             }
         }
     }
 }
 
-context(SharedTransitionScope, AnimatedVisibilityScope)
 @Composable
-fun Details(kitty: Kitty) {
-    Column(
-        Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
-            .fillMaxHeight()
-            .wrapContentHeight(Alignment.Top)
-            .fillMaxWidth()
-            .background(Color.White)
-            .padding(start = 10.dp, end = 10.dp)
-    ) {
-        Row(verticalAlignment = Alignment.CenterVertically) {
-            Column {
-                Spacer(Modifier.size(20.dp))
-                Text(
-                    kitty.name,
-                    fontSize = 25.sp,
-                    modifier =
-                        Modifier.padding(start = 10.dp)
-                            .sharedBounds(
-                                rememberSharedContentState(key = kitty.name + kitty.id),
-                                this@AnimatedVisibilityScope
-                            )
-                )
-                Text(
-                    kitty.breed,
-                    fontSize = 22.sp,
-                    color = Color.Gray,
-                    modifier =
-                        Modifier.padding(start = 10.dp)
-                            .sharedBounds(
-                                rememberSharedContentState(key = kitty.breed + kitty.id),
-                                this@AnimatedVisibilityScope
-                            )
+fun Details(
+    sharedTransitionScope: SharedTransitionScope,
+    animatedVisibilityScope: AnimatedVisibilityScope,
+    kitty: Kitty
+) {
+    with(sharedTransitionScope) {
+        Column(
+            Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
+                .fillMaxHeight()
+                .wrapContentHeight(Alignment.Top)
+                .fillMaxWidth()
+                .background(Color.White)
+                .padding(start = 10.dp, end = 10.dp)
+        ) {
+            Row(verticalAlignment = Alignment.CenterVertically) {
+                Column {
+                    Spacer(Modifier.size(20.dp))
+                    Text(
+                        kitty.name,
+                        fontSize = 25.sp,
+                        modifier =
+                            Modifier.padding(start = 10.dp)
+                                .sharedBounds(
+                                    rememberSharedContentState(key = kitty.name + kitty.id),
+                                    animatedVisibilityScope
+                                )
+                    )
+                    Text(
+                        kitty.breed,
+                        fontSize = 22.sp,
+                        color = Color.Gray,
+                        modifier =
+                            Modifier.padding(start = 10.dp)
+                                .sharedBounds(
+                                    rememberSharedContentState(key = kitty.breed + kitty.id),
+                                    animatedVisibilityScope
+                                )
+                    )
+                    Spacer(Modifier.size(10.dp))
+                }
+                Spacer(Modifier.weight(1f))
+                Icon(
+                    Icons.Outlined.Favorite,
+                    contentDescription = null,
+                    Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
                 )
                 Spacer(Modifier.size(10.dp))
             }
-            Spacer(Modifier.weight(1f))
-            Icon(
-                Icons.Outlined.Favorite,
-                contentDescription = null,
-                Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
+            Box(
+                modifier =
+                    Modifier.padding(bottom = 10.dp)
+                        .height(2.dp)
+                        .fillMaxWidth()
+                        .background(Color(0xffeeeeee))
             )
-            Spacer(Modifier.size(10.dp))
+            Text(
+                text =
+                    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
+                        " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
+                        " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
+                        " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
+                        " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
+                        "\n" +
+                        "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
+                        " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
+                        " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
+                        " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
+                        " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
+                        " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
+                        " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
+                        " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
+                        " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
+                        " mauris at urna dictum ornare.\n" +
+                        "\n" +
+                        "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
+                        " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
+                        " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
+                        " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
+                        " Nullam mattis luctus orci at pulvinar.\n" +
+                        "\n" +
+                        "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
+                        " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
+                        " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
+                        " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
+                        " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
+                        " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
+                        " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
+                        " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
+                        " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
+                        " lobortis turpis.\n" +
+                        "\n",
+                color = Color.Gray,
+                fontSize = 15.sp,
+            )
         }
-        Box(
-            modifier =
-                Modifier.padding(bottom = 10.dp)
-                    .height(2.dp)
-                    .fillMaxWidth()
-                    .background(Color(0xffeeeeee))
-        )
-        Text(
-            text =
-                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
-                    " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
-                    " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
-                    " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
-                    " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
-                    "\n" +
-                    "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
-                    " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
-                    " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
-                    " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
-                    " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
-                    " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
-                    " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
-                    " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
-                    " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
-                    " mauris at urna dictum ornare.\n" +
-                    "\n" +
-                    "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
-                    " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
-                    " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
-                    " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
-                    " Nullam mattis luctus orci at pulvinar.\n" +
-                    "\n" +
-                    "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
-                    " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
-                    " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
-                    " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
-                    " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
-                    " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
-                    " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
-                    " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
-                    " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
-                    " lobortis turpis.\n" +
-                    "\n",
-            color = Color.Gray,
-            fontSize = 15.sp,
-        )
     }
 }
 
-context(AnimatedVisibilityScope, SharedTransitionScope)
-@Suppress("UNUSED_PARAMETER")
 @Composable
-fun DetailView(model: MyModel, selected: Kitty, next: Kitty?) {
-    Column(
-        Modifier.clickable(
-                interactionSource = remember { MutableInteractionSource() },
-                indication = null
-            ) {
-                model.selected = null
-            }
-            .sharedBounds(
-                rememberSharedContentState(key = "container + ${selected.id}"),
-                this@AnimatedVisibilityScope,
-                fadeIn(),
-                fadeOut(),
-                resizeMode = ScaleToBounds(ContentScale.Crop),
-                clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
-            )
-    ) {
-        Row(Modifier.fillMaxHeight(0.5f)) {
-            Image(
-                painter = painterResource(selected.photoResId),
-                contentDescription = null,
-                contentScale = ContentScale.Crop,
-                modifier =
-                    Modifier.padding(10.dp)
-                        .sharedElement(
-                            rememberSharedContentState(key = selected.id),
-                            this@AnimatedVisibilityScope,
-                            placeHolderSize = animatedSize
-                        )
-                        .fillMaxHeight()
-                        .aspectRatio(1f)
-                        .clip(RoundedCornerShape(20.dp))
-            )
-            if (next != null) {
+fun DetailView(
+    animatedVisibilityScope: AnimatedVisibilityScope,
+    sharedTransitionScope: SharedTransitionScope,
+    model: MyModel,
+    selected: Kitty,
+    next: Kitty?
+) {
+    with(sharedTransitionScope) {
+        Column(
+            Modifier.clickable(
+                    interactionSource = remember { MutableInteractionSource() },
+                    indication = null
+                ) {
+                    model.selected = null
+                }
+                .sharedBounds(
+                    rememberSharedContentState(key = "container + ${selected.id}"),
+                    animatedVisibilityScope,
+                    fadeIn(),
+                    fadeOut(),
+                    resizeMode = ScaleToBounds(ContentScale.Crop),
+                    clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
+                )
+        ) {
+            Row(Modifier.fillMaxHeight(0.5f)) {
                 Image(
-                    painter = painterResource(next.photoResId),
+                    painter = painterResource(selected.photoResId),
                     contentDescription = null,
                     contentScale = ContentScale.Crop,
                     modifier =
-                        Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
-                            .fillMaxWidth()
+                        Modifier.padding(10.dp)
+                            .sharedElement(
+                                rememberSharedContentState(key = selected.id),
+                                animatedVisibilityScope,
+                                placeHolderSize = animatedSize
+                            )
                             .fillMaxHeight()
+                            .aspectRatio(1f)
                             .clip(RoundedCornerShape(20.dp))
-                            .blur(10.dp)
                 )
+                if (next != null) {
+                    Image(
+                        painter = painterResource(next.photoResId),
+                        contentDescription = null,
+                        contentScale = ContentScale.Crop,
+                        modifier =
+                            Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
+                                .fillMaxWidth()
+                                .fillMaxHeight()
+                                .clip(RoundedCornerShape(20.dp))
+                                .blur(10.dp)
+                    )
+                }
             }
+            Details(sharedTransitionScope, animatedVisibilityScope, kitty = selected)
         }
-        Details(kitty = selected)
     }
 }
 
-context(AnimatedVisibilityScope, SharedTransitionScope)
 @Composable
-fun GridView(model: MyModel) {
-    Box(Modifier.background(lessVibrantPurple)) {
-        Box(
-            Modifier.padding(20.dp)
-                .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
-                .animateEnterExit(fadeIn(), fadeOut())
-        ) {
-            SearchBar()
-        }
-        LazyVerticalGrid(
-            columns = GridCells.Fixed(2),
-            contentPadding = PaddingValues(top = 90.dp)
-        ) {
-            items(6) {
-                Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
-                    KittyItem(model.items[it])
+fun GridView(
+    animatedVisibilityScope: AnimatedVisibilityScope,
+    sharedTransitionScope: SharedTransitionScope,
+    model: MyModel
+) {
+    with(animatedVisibilityScope) {
+        with(sharedTransitionScope) {
+            Box(Modifier.background(lessVibrantPurple)) {
+                Box(
+                    Modifier.padding(20.dp)
+                        .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
+                        .animateEnterExit(fadeIn(), fadeOut())
+                ) {
+                    SearchBar()
+                }
+                LazyVerticalGrid(
+                    columns = GridCells.Fixed(2),
+                    contentPadding = PaddingValues(top = 90.dp)
+                ) {
+                    items(6) {
+                        Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
+                            KittyItem(
+                                animatedVisibilityScope,
+                                sharedTransitionScope,
+                                model.items[it]
+                            )
+                        }
+                    }
                 }
             }
         }
@@ -284,54 +312,59 @@
     var selected: Kitty? by mutableStateOf(null)
 }
 
-context(AnimatedVisibilityScope, SharedTransitionScope)
 @Composable
-fun KittyItem(kitty: Kitty) {
-    Column(
-        Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
-            .sharedBounds(
-                rememberSharedContentState(key = "container + ${kitty.id}"),
-                this@AnimatedVisibilityScope,
+fun KittyItem(
+    animatedVisibilityScope: AnimatedVisibilityScope,
+    sharedTransitionScope: SharedTransitionScope,
+    kitty: Kitty
+) {
+    with(sharedTransitionScope) {
+        Column(
+            Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
+                .sharedBounds(
+                    rememberSharedContentState(key = "container + ${kitty.id}"),
+                    animatedVisibilityScope,
+                )
+                .background(Color.White, RoundedCornerShape(20.dp))
+        ) {
+            Image(
+                painter = painterResource(kitty.photoResId),
+                contentDescription = null,
+                contentScale = ContentScale.Crop,
+                modifier =
+                    Modifier.sharedElement(
+                            rememberSharedContentState(key = kitty.id),
+                            animatedVisibilityScope,
+                            placeHolderSize = animatedSize
+                        )
+                        .aspectRatio(1f)
+                        .clip(RoundedCornerShape(20.dp))
             )
-            .background(Color.White, RoundedCornerShape(20.dp))
-    ) {
-        Image(
-            painter = painterResource(kitty.photoResId),
-            contentDescription = null,
-            contentScale = ContentScale.Crop,
-            modifier =
-                Modifier.sharedElement(
-                        rememberSharedContentState(key = kitty.id),
-                        this@AnimatedVisibilityScope,
-                        placeHolderSize = animatedSize
-                    )
-                    .aspectRatio(1f)
-                    .clip(RoundedCornerShape(20.dp))
-        )
-        Spacer(Modifier.size(10.dp))
-        Text(
-            kitty.name,
-            fontSize = 18.sp,
-            modifier =
-                Modifier.padding(start = 10.dp)
-                    .sharedBounds(
-                        rememberSharedContentState(key = kitty.name + kitty.id),
-                        this@AnimatedVisibilityScope
-                    )
-        )
-        Spacer(Modifier.size(5.dp))
-        Text(
-            kitty.breed,
-            fontSize = 15.sp,
-            color = Color.Gray,
-            modifier =
-                Modifier.padding(start = 10.dp)
-                    .sharedBounds(
-                        rememberSharedContentState(key = kitty.breed + kitty.id),
-                        this@AnimatedVisibilityScope
-                    )
-        )
-        Spacer(Modifier.size(10.dp))
+            Spacer(Modifier.size(10.dp))
+            Text(
+                kitty.name,
+                fontSize = 18.sp,
+                modifier =
+                    Modifier.padding(start = 10.dp)
+                        .sharedBounds(
+                            rememberSharedContentState(key = kitty.name + kitty.id),
+                            animatedVisibilityScope
+                        )
+            )
+            Spacer(Modifier.size(5.dp))
+            Text(
+                kitty.breed,
+                fontSize = 15.sp,
+                color = Color.Gray,
+                modifier =
+                    Modifier.padding(start = 10.dp)
+                        .sharedBounds(
+                            rememberSharedContentState(key = kitty.breed + kitty.id),
+                            animatedVisibilityScope
+                        )
+            )
+            Spacer(Modifier.size(10.dp))
+        }
     }
 }
 
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
index 9a3c30d..3cd4c47 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
@@ -83,111 +83,114 @@
         )
     ) {
         SharedTransitionLayout {
-            HomePage(!showExpandedCard)
-            ExpandedCard(showExpandedCard)
+            HomePage(this@SharedTransitionLayout, !showExpandedCard)
+            ExpandedCard(this@SharedTransitionLayout, showExpandedCard)
         }
     }
 }
 
-context(SharedTransitionScope)
 @Composable
-fun HomePage(showCard: Boolean) {
-    Box(Modifier.fillMaxSize().background(Color.White)) {
-        Column {
-            SearchBarAndTabs()
-            Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
-                androidx.compose.animation.AnimatedVisibility(visible = showCard) {
-                    Column(
-                        Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
-                            .sharedBounds(
-                                rememberSharedContentState(key = "container"),
-                                this@AnimatedVisibility,
-                                clipInOverlayDuringTransition =
-                                    OverlayClip(RoundedCornerShape(20.dp))
-                            )
-                            .clip(shape = RoundedCornerShape(20.dp))
-                            .background(color = cardBackgroundColor),
-                    ) {
-                        Box {
-                            Column {
-                                Image(
-                                    painterResource(R.drawable.quiet_night),
-                                    contentDescription = null,
-                                    modifier =
-                                        Modifier.fillMaxWidth()
-                                            .sharedElement(
-                                                rememberSharedContentState(key = "quiet_night"),
-                                                this@AnimatedVisibility,
-                                                zIndexInOverlay = 0.5f,
-                                            ),
-                                    contentScale = ContentScale.FillWidth
+fun HomePage(sharedTransitionScope: SharedTransitionScope, showCard: Boolean) {
+    with(sharedTransitionScope) {
+        Box(Modifier.fillMaxSize().background(Color.White)) {
+            Column {
+                SearchBarAndTabs()
+                Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
+                    androidx.compose.animation.AnimatedVisibility(visible = showCard) {
+                        Column(
+                            Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "container"),
+                                    this@AnimatedVisibility,
+                                    clipInOverlayDuringTransition =
+                                        OverlayClip(RoundedCornerShape(20.dp))
                                 )
-                                Text(
-                                    text = longText,
-                                    color = Color.Gray,
-                                    fontSize = 15.sp,
-                                    modifier =
-                                        Modifier.fillMaxWidth()
-                                            .padding(start = 20.dp, end = 20.dp, top = 20.dp)
-                                            .height(14.dp)
-                                            .sharedElement(
-                                                rememberSharedContentState(key = "longText"),
-                                                this@AnimatedVisibility,
-                                            )
-                                            .clipToBounds()
-                                            .wrapContentHeight(
-                                                align = Alignment.Top,
-                                                unbounded = true
-                                            )
-                                            .skipToLookaheadSize(),
-                                )
-                            }
+                                .clip(shape = RoundedCornerShape(20.dp))
+                                .background(color = cardBackgroundColor),
+                        ) {
+                            Box {
+                                Column {
+                                    Image(
+                                        painterResource(R.drawable.quiet_night),
+                                        contentDescription = null,
+                                        modifier =
+                                            Modifier.fillMaxWidth()
+                                                .sharedElement(
+                                                    rememberSharedContentState(key = "quiet_night"),
+                                                    this@AnimatedVisibility,
+                                                    zIndexInOverlay = 0.5f,
+                                                ),
+                                        contentScale = ContentScale.FillWidth
+                                    )
+                                    Text(
+                                        text = longText,
+                                        color = Color.Gray,
+                                        fontSize = 15.sp,
+                                        modifier =
+                                            Modifier.fillMaxWidth()
+                                                .padding(start = 20.dp, end = 20.dp, top = 20.dp)
+                                                .height(14.dp)
+                                                .sharedElement(
+                                                    rememberSharedContentState(key = "longText"),
+                                                    this@AnimatedVisibility,
+                                                )
+                                                .clipToBounds()
+                                                .wrapContentHeight(
+                                                    align = Alignment.Top,
+                                                    unbounded = true
+                                                )
+                                                .skipToLookaheadSize(),
+                                    )
+                                }
 
-                            Text(
-                                text = title,
-                                fontFamily = FontFamily.Default,
-                                color = Color.White,
-                                fontSize = 20.sp,
-                                modifier =
-                                    Modifier.fillMaxWidth()
-                                        .align(Alignment.BottomCenter)
-                                        .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
-                                        .animateEnterExit(
-                                            fadeIn(tween(1000)) + slideInVertically { -it / 3 },
-                                            fadeOut(tween(50)) + slideOutVertically { -it / 3 }
-                                        )
-                                        .skipToLookaheadSize()
-                                        .background(
-                                            Brush.verticalGradient(
-                                                listOf(
-                                                    Color.Transparent,
-                                                    Color.Black,
-                                                    Color.Transparent
+                                Text(
+                                    text = title,
+                                    fontFamily = FontFamily.Default,
+                                    color = Color.White,
+                                    fontSize = 20.sp,
+                                    modifier =
+                                        Modifier.fillMaxWidth()
+                                            .align(Alignment.BottomCenter)
+                                            .renderInSharedTransitionScopeOverlay(
+                                                zIndexInOverlay = 1f
+                                            )
+                                            .animateEnterExit(
+                                                fadeIn(tween(1000)) + slideInVertically { -it / 3 },
+                                                fadeOut(tween(50)) + slideOutVertically { -it / 3 }
+                                            )
+                                            .skipToLookaheadSize()
+                                            .background(
+                                                Brush.verticalGradient(
+                                                    listOf(
+                                                        Color.Transparent,
+                                                        Color.Black,
+                                                        Color.Transparent
+                                                    )
                                                 )
                                             )
-                                        )
-                                        .padding(20.dp),
+                                            .padding(20.dp),
+                                )
+                            }
+                            InstallBar(
+                                Modifier.fillMaxWidth()
+                                    .zIndex(1f)
+                                    .sharedElementWithCallerManagedVisibility(
+                                        rememberSharedContentState(key = "install_bar"),
+                                        showCard,
+                                    )
                             )
                         }
-                        InstallBar(
-                            Modifier.fillMaxWidth()
-                                .zIndex(1f)
-                                .sharedElementWithCallerManagedVisibility(
-                                    rememberSharedContentState(key = "install_bar"),
-                                    showCard,
-                                )
-                        )
                     }
                 }
+                Cluster()
             }
-            Cluster()
+            Image(
+                painterResource(R.drawable.navigation_bar),
+                contentDescription = null,
+                Modifier.fillMaxWidth().align(Alignment.BottomCenter),
+                contentScale = ContentScale.FillWidth
+            )
         }
-        Image(
-            painterResource(R.drawable.navigation_bar),
-            contentDescription = null,
-            Modifier.fillMaxWidth().align(Alignment.BottomCenter),
-            contentScale = ContentScale.FillWidth
-        )
     }
 }
 
@@ -221,97 +224,98 @@
     }
 }
 
-context(SharedTransitionScope)
 @Composable
-fun ExpandedCard(visible: Boolean) {
-    AnimatedVisibility(
-        visible = visible,
-        Modifier.fillMaxSize(),
-        enter = fadeIn(),
-        exit = fadeOut()
-    ) {
-        Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
-            Column(
-                Modifier.align(Alignment.Center)
-                    .padding(20.dp)
-                    .sharedBounds(
-                        rememberSharedContentState(key = "container"),
-                        this@AnimatedVisibility,
-                        enter = EnterTransition.None,
-                        exit = ExitTransition.None,
-                        clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
-                    )
-                    .clip(shape = RoundedCornerShape(20.dp))
-                    .background(cardBackgroundColor)
-            ) {
+fun ExpandedCard(sharedTransitionScope: SharedTransitionScope, visible: Boolean) {
+    with(sharedTransitionScope) {
+        AnimatedVisibility(
+            visible = visible,
+            Modifier.fillMaxSize(),
+            enter = fadeIn(),
+            exit = fadeOut()
+        ) {
+            Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
                 Column(
-                    Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
-                        .animateEnterExit(
-                            fadeIn() + slideInVertically { it / 3 },
-                            fadeOut() + slideOutVertically { it / 3 }
+                    Modifier.align(Alignment.Center)
+                        .padding(20.dp)
+                        .sharedBounds(
+                            rememberSharedContentState(key = "container"),
+                            this@AnimatedVisibility,
+                            enter = EnterTransition.None,
+                            exit = ExitTransition.None,
+                            clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
                         )
-                        .skipToLookaheadSize()
-                        .background(
-                            Brush.verticalGradient(
-                                listOf(Color.Transparent, Color.Black, Color.Transparent)
-                            )
-                        )
-                        .padding(start = 20.dp, end = 20.dp),
+                        .clip(shape = RoundedCornerShape(20.dp))
+                        .background(cardBackgroundColor)
                 ) {
-                    Text(
-                        text = "Lorem ipsum",
-                        Modifier.padding(top = 20.dp, bottom = 10.dp)
-                            .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
-                            .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
-                        color = Color.Black,
-                        fontFamily = FontFamily.Default,
-                        fontWeight = FontWeight.Bold,
-                        fontSize = 15.sp
+                    Column(
+                        Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
+                            .animateEnterExit(
+                                fadeIn() + slideInVertically { it / 3 },
+                                fadeOut() + slideOutVertically { it / 3 }
+                            )
+                            .skipToLookaheadSize()
+                            .background(
+                                Brush.verticalGradient(
+                                    listOf(Color.Transparent, Color.Black, Color.Transparent)
+                                )
+                            )
+                            .padding(start = 20.dp, end = 20.dp),
+                    ) {
+                        Text(
+                            text = "Lorem ipsum",
+                            Modifier.padding(top = 20.dp, bottom = 10.dp)
+                                .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
+                                .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
+                            color = Color.Black,
+                            fontFamily = FontFamily.Default,
+                            fontWeight = FontWeight.Bold,
+                            fontSize = 15.sp
+                        )
+                        Text(
+                            text = title,
+                            color = Color.White,
+                            fontSize = 30.sp,
+                            modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+                        )
+                    }
+                    Image(
+                        painterResource(R.drawable.quiet_night),
+                        contentDescription = null,
+                        modifier =
+                            Modifier.fillMaxWidth()
+                                .sharedElement(
+                                    rememberSharedContentState("quiet_night"),
+                                    this@AnimatedVisibility,
+                                ),
+                        contentScale = ContentScale.FillWidth
                     )
+
                     Text(
-                        text = title,
-                        color = Color.White,
-                        fontSize = 30.sp,
-                        modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+                        text = longText,
+                        color = Color.Gray,
+                        fontSize = 15.sp,
+                        modifier =
+                            Modifier.fillMaxWidth()
+                                .padding(start = 15.dp, end = 10.dp, top = 10.dp)
+                                .height(50.dp)
+                                .sharedElement(
+                                    rememberSharedContentState("longText"),
+                                    this@AnimatedVisibility,
+                                )
+                                .clipToBounds()
+                                .wrapContentHeight(align = Alignment.Top, unbounded = true)
+                                .skipToLookaheadSize(),
+                    )
+
+                    InstallBar(
+                        Modifier.fillMaxWidth()
+                            .zIndex(1f)
+                            .sharedElement(
+                                rememberSharedContentState("install_bar"),
+                                this@AnimatedVisibility,
+                            )
                     )
                 }
-                Image(
-                    painterResource(R.drawable.quiet_night),
-                    contentDescription = null,
-                    modifier =
-                        Modifier.fillMaxWidth()
-                            .sharedElement(
-                                rememberSharedContentState("quiet_night"),
-                                this@AnimatedVisibility,
-                            ),
-                    contentScale = ContentScale.FillWidth
-                )
-
-                Text(
-                    text = longText,
-                    color = Color.Gray,
-                    fontSize = 15.sp,
-                    modifier =
-                        Modifier.fillMaxWidth()
-                            .padding(start = 15.dp, end = 10.dp, top = 10.dp)
-                            .height(50.dp)
-                            .sharedElement(
-                                rememberSharedContentState("longText"),
-                                this@AnimatedVisibility,
-                            )
-                            .clipToBounds()
-                            .wrapContentHeight(align = Alignment.Top, unbounded = true)
-                            .skipToLookaheadSize(),
-                )
-
-                InstallBar(
-                    Modifier.fillMaxWidth()
-                        .zIndex(1f)
-                        .sharedElement(
-                            rememberSharedContentState("install_bar"),
-                            this@AnimatedVisibility,
-                        )
-                )
             }
         }
     }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
index cdd939a..5493358 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
@@ -808,14 +808,12 @@
         val moveAngle = Math.atan(moveOffset.x / moveOffset.y.toDouble())
 
         rule.runOnIdle {
-            assertEquals(
-                downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat(),
-                onDragStartedOffset.x
-            )
-            assertEquals(
-                downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat(),
-                onDragStartedOffset.y
-            )
+            assertThat(downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat())
+                .isWithin(0.5f)
+                .of(onDragStartedOffset.x)
+            assertThat(downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat())
+                .isWithin(0.5f)
+                .of(onDragStartedOffset.y)
         }
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 9d07996..6570da8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -172,7 +172,7 @@
     onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
 ) =
     detectDragGestures(
-        onDragStart = { change, _ -> onDragStart(change.position) },
+        onDragStart = { _, slopTriggerChange, _ -> onDragStart(slopTriggerChange.position) },
         onDragEnd = { onDragEnd.invoke() },
         onDragCancel = onDragCancel,
         shouldAwaitTouchSlop = { true },
@@ -200,7 +200,8 @@
  *
  * @param onDragStart A lambda to be called when the drag gesture starts, it contains information
  *   about the last known [PointerInputChange] relative to the containing element and the post slop
- *   delta.
+ *   delta, slopTriggerChange. It also contains information about the down event where this gesture
+ *   started and the overSlopOffset.
  * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
  *   up [PointerInputChange] that finished the gesture.
  * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
@@ -224,7 +225,10 @@
  * @see detectDragGesturesAfterLongPress to detect gestures after long press
  */
 internal suspend fun PointerInputScope.detectDragGestures(
-    onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+    onDragStart:
+        (
+            down: PointerInputChange, slopTriggerChange: PointerInputChange, overSlopOffset: Offset
+        ) -> Unit,
     onDragEnd: (change: PointerInputChange) -> Unit,
     onDragCancel: () -> Unit,
     shouldAwaitTouchSlop: () -> Boolean,
@@ -242,7 +246,6 @@
         }
         val down = awaitFirstDown(requireUnconsumed = false)
         var drag: PointerInputChange?
-        var initialDelta = Offset.Zero
         overSlop = Offset.Zero
 
         if (awaitTouchSlop) {
@@ -257,13 +260,12 @@
                         overSlop = over
                     }
             } while (drag != null && !drag.isConsumed)
-            initialDelta = overSlop
         } else {
             drag = initialDown
         }
 
         if (drag != null) {
-            onDragStart.invoke(drag, initialDelta)
+            onDragStart.invoke(down, drag, overSlop)
             onDrag(drag, overSlop)
             val upEvent =
                 drag(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index f0490ff..82ad253 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -48,7 +48,6 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.sign
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.coroutineScope
@@ -463,23 +462,27 @@
             // re-create tracker when pointer input block restarts. This lazily creates the tracker
             // only when it is need.
             val velocityTracker = VelocityTracker()
-            val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
-                { startEvent, initialDelta ->
-                    if (canDrag.invoke(startEvent)) {
+
+            val onDragStart:
+                (
+                    down: PointerInputChange,
+                    slopTriggerChange: PointerInputChange,
+                    postSlopOffset: Offset
+                ) -> Unit =
+                { down, slopTriggerChange, postSlopOffset ->
+                    if (canDrag.invoke(down)) {
                         if (!isListeningForEvents) {
                             if (channel == null) {
                                 channel = Channel(capacity = Channel.UNLIMITED)
                             }
                             startListeningForEvents()
                         }
-                        val overSlopOffset = initialDelta
-                        val xSign = sign(startEvent.position.x)
-                        val ySign = sign(startEvent.position.y)
-                        val adjustedStart =
-                            startEvent.position -
-                                Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
-
-                        channel?.trySend(DragStarted(adjustedStart))
+                        velocityTracker.addPointerInputChange(down)
+                        val dragStartedOffset = slopTriggerChange.position - postSlopOffset
+                        // the drag start event offset is the down event + touch slop value
+                        // or in this case the event that triggered the touch slop minus
+                        // the post slop offset
+                        channel?.trySend(DragStarted(dragStartedOffset))
                     }
                 }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index f03d8b3..423483f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -422,10 +422,17 @@
             consumedScroll = consumedScroll,
             measureResult =
                 layout(layoutWidth, layoutHeight) {
-                    // place normal items
-                    positionedItems.fastForEach { it.place(this, isLookingAhead) }
-                    // stickingItems should be placed after all other items
-                    stickingItems.fastForEach { it.place(this, isLookingAhead) }
+                    // Tagging as motion frame of reference placement, meaning the placement
+                    // contains scrolling. This allows the consumer of this placement offset to
+                    // differentiate this offset vs. offsets from structural changes. Generally
+                    // speaking, this signals a preference to directly apply changes rather than
+                    // animating, to avoid a chasing effect to scrolling.
+                    withMotionFrameOfReferencePlacement {
+                        // place normal items
+                        positionedItems.fastForEach { it.place(this, isLookingAhead) }
+                        // stickingItems should be placed after all other items
+                        stickingItems.fastForEach { it.place(this, isLookingAhead) }
+                    }
 
                     // we attach it during the placement so LazyListState can trigger re-placement
                     placementScopeInvalidator.attachToScope()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 22d00c9..66ed93c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -390,8 +390,15 @@
             consumedScroll = consumedScroll,
             measureResult =
                 layout(layoutWidth, layoutHeight) {
-                    positionedItems.fastForEach { it.place(this) }
-                    stickingItems.fastForEach { it.place(this) }
+                    // Tagging as motion frame of reference placement, meaning the placement
+                    // contains scrolling. This allows the consumer of this placement offset to
+                    // differentiate this offset vs. offsets from structural changes. Generally
+                    // speaking, this signals a preference to directly apply changes rather than
+                    // animating, to avoid a chasing effect to scrolling.
+                    withMotionFrameOfReferencePlacement {
+                        positionedItems.fastForEach { it.place(this) }
+                        stickingItems.fastForEach { it.place(this) }
+                    }
                     // we attach it during the placement so LazyGridState can trigger re-placement
                     placementScopeInvalidator.attachToScope()
                 },
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 15bba67..608b82b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -897,8 +897,15 @@
             consumedScroll = consumedScroll,
             measureResult =
                 layout(layoutWidth, layoutHeight) {
-                    positionedItems.fastForEach { item ->
-                        item.place(scope = this, context = this@measure)
+                    // Tagging as motion frame of reference placement, meaning the placement
+                    // contains scrolling. This allows the consumer of this placement offset to
+                    // differentiate this offset vs. offsets from structural changes. Generally
+                    // speaking, this signals a preference to directly apply changes rather than
+                    // animating, to avoid a chasing effect to scrolling.
+                    withMotionFrameOfReferencePlacement {
+                        positionedItems.fastForEach { item ->
+                            item.place(scope = this, context = this@measure)
+                        }
                     }
 
                     // we attach it during the placement so LazyStaggeredGridState can trigger
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
index 1333583..a6f83cd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
@@ -465,7 +465,14 @@
             firstVisiblePageScrollOffset = currentFirstPageScrollOffset,
             measureResult =
                 layout(layoutWidth, layoutHeight) {
-                    positionedPages.fastForEach { it.place(this) }
+                    // Tagging as motion frame of reference placement, meaning the placement
+                    // contains scrolling. This allows the consumer of this placement offset to
+                    // differentiate this offset vs. offsets from structural changes. Generally
+                    // speaking, this signals a preference to directly apply changes rather than
+                    // animating, to avoid a chasing effect to scrolling.
+                    withMotionFrameOfReferencePlacement {
+                        positionedPages.fastForEach { it.place(this) }
+                    }
                     // we attach it during the placement so PagerState can trigger re-placement
                     placementScopeInvalidator.attachToScope()
                 },
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index f37fbdb..2f637a0 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -111,10 +111,6 @@
     samples(project(":compose:material3:adaptive:adaptive-samples"))
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 // Screenshot tests related setup
 android {
     compileSdk 35
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index dbbcdc2..7992414 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -109,10 +109,6 @@
     metalavaK2UastEnabled = false
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 // Screenshot tests related setup
 android {
     compileSdk 35
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 1cb594e..24f0da1 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -109,10 +109,6 @@
     metalavaK2UastEnabled = false
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 // Screenshot tests related setup
 android {
     compileSdk 35
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 61654d2..3e166ef 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.requiredSize
@@ -40,6 +41,7 @@
 import androidx.compose.material3.internal.getString
 import androidx.compose.material3.tokens.SheetBottomTokens
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -84,9 +86,11 @@
 import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.coerceAtMost
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
 import androidx.compose.ui.unit.width
 import androidx.compose.ui.zIndex
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -96,6 +100,8 @@
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import junit.framework.TestCase
+import junit.framework.TestCase.assertEquals
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -903,4 +909,121 @@
             with(density!!) { rule.rootHeight().toPx() - peekHeight.toPx() - snackbarSize!!.height }
         assertThat(snackbarBottomOffset).isWithin(1f).of(expectedSnackbarBottomOffset)
     }
+
+    @Test
+    fun bottomSheetScaffold_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+        var offset by mutableStateOf(IntOffset(0, 0))
+        val offsets =
+            listOf(
+                IntOffset(0, 0),
+                IntOffset(5, 20),
+                IntOffset(25, 0),
+                IntOffset(100, 10),
+            )
+        var sheetCoords: LayoutCoordinates? = null
+        var rootCoords: LayoutCoordinates? = null
+        val state = SheetState(false, density = Density(1f))
+        var sheetValue by mutableStateOf(SheetValue.Hidden)
+        rule.setContent {
+            Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+                LaunchedEffect(sheetValue) {
+                    if (sheetValue == SheetValue.Hidden) {
+                        state.hide()
+                    } else if (sheetValue == SheetValue.PartiallyExpanded) {
+                        state.partialExpand()
+                    } else {
+                        state.expand()
+                    }
+                }
+                BottomSheetScaffold(
+                    sheetContent = {
+                        Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+                    },
+                    scaffoldState =
+                        BottomSheetScaffoldState(state, remember { SnackbarHostState() })
+                ) {
+                    Box(Modifier.fillMaxSize())
+                }
+            }
+        }
+
+        SheetValue.values().forEach {
+            sheetValue = it
+            rule.waitForIdle()
+
+            repeat(4) {
+                offset = offsets[it]
+                rule.runOnIdle {
+                    val excludeOffset =
+                        rootCoords!!
+                            .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+                            .round()
+                    val includeSheetOffset =
+                        rootCoords!!
+                            .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+                            .round()
+                    assertEquals(
+                        includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+                        excludeOffset
+                    )
+                }
+            }
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+        var offset by mutableStateOf(IntOffset(0, 0))
+        val offsets =
+            listOf(
+                IntOffset(0, 0),
+                IntOffset(5, 20),
+                IntOffset(25, 0),
+                IntOffset(100, 10),
+            )
+        var sheetCoords: LayoutCoordinates? = null
+        val state = SheetState(false, density = Density(1f))
+        var sheetValue by mutableStateOf(SheetValue.Hidden)
+        rule.setContent {
+            LaunchedEffect(sheetValue) {
+                if (sheetValue == SheetValue.Hidden) {
+                    state.hide()
+                } else if (sheetValue == SheetValue.PartiallyExpanded) {
+                    state.partialExpand()
+                } else {
+                    state.expand()
+                }
+            }
+            ModalBottomSheet({}, sheetState = state) {
+                Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+            }
+        }
+
+        fun LayoutCoordinates.root(): LayoutCoordinates =
+            if (parentLayoutCoordinates != null) parentLayoutCoordinates!!.root() else this
+
+        SheetValue.values().forEach {
+            sheetValue = it
+            rule.waitForIdle()
+            val rootCoords = sheetCoords!!.root()
+
+            repeat(4) {
+                offset = offsets[it]
+                rule.runOnIdle {
+                    val excludeOffset =
+                        rootCoords
+                            .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+                            .round()
+                    val includeSheetOffset =
+                        rootCoords
+                            .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+                            .round()
+                    assertEquals(
+                        includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+                        excludeOffset
+                    )
+                }
+            }
+        }
+    }
 }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
index 883c847..bfe9fa0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -53,6 +54,8 @@
 
     private val wrap = Modifier.wrapContentSize(Alignment.Center)
     private val wrapperTestTag = "splitButtonWrapper"
+    private val leadingButtonTag = "leadingButton"
+    private val trailingButtonTag = "trailingButton"
 
     @Test
     fun splitButton() {
@@ -323,6 +326,82 @@
         assertAgainstGolden("splitButton_textLeadingButton_${scheme.name}")
     }
 
+    @Test
+    fun splitButton_leadingButton_pressed() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                SplitButton(
+                    leadingButton = {
+                        SplitButtonDefaults.LeadingButton(
+                            onClick = { /* Do Nothing */ },
+                            modifier = Modifier.testTag(leadingButtonTag),
+                        ) {
+                            Icon(
+                                Icons.Filled.Edit,
+                                modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+                                contentDescription = "Localized description",
+                            )
+                            Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+                            Text("My Button")
+                        }
+                    },
+                    trailingButton = {
+                        SplitButtonDefaults.TrailingButton(
+                            onClick = {},
+                            checked = false,
+                        ) {
+                            Icon(
+                                Icons.Outlined.KeyboardArrowDown,
+                                contentDescription = "Localized description",
+                                Modifier.size(SplitButtonDefaults.TrailingIconSize)
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertPressed(leadingButtonTag, "splitButton_leadingButton_pressed_${scheme.name}")
+    }
+
+    @Test
+    fun splitButton_trailingButton_pressed() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                SplitButton(
+                    leadingButton = {
+                        SplitButtonDefaults.LeadingButton(
+                            onClick = { /* Do Nothing */ },
+                        ) {
+                            Icon(
+                                Icons.Filled.Edit,
+                                modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+                                contentDescription = "Localized description",
+                            )
+                            Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+                            Text("My Button")
+                        }
+                    },
+                    trailingButton = {
+                        SplitButtonDefaults.TrailingButton(
+                            onClick = {},
+                            checked = false,
+                            modifier = Modifier.testTag(trailingButtonTag),
+                        ) {
+                            Icon(
+                                Icons.Outlined.KeyboardArrowDown,
+                                contentDescription = "Localized description",
+                                Modifier.size(SplitButtonDefaults.TrailingIconSize)
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertPressed(trailingButtonTag, "splitButton_trailingButton_pressed_${scheme.name}")
+    }
+
     private fun assertAgainstGolden(goldenName: String) {
         rule
             .onNodeWithTag(wrapperTestTag)
@@ -330,6 +409,21 @@
             .assertAgainstGolden(screenshotRule, goldenName)
     }
 
+    private fun assertPressed(tag: String, goldenName: String) {
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(tag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        assertAgainstGolden(goldenName)
+    }
+
     // Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
     // This makes sure that the default method name and the initial Scuba image generated
     // name is as expected.
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
index 2ca56a9..72c2356 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.animation.core.FloatSpringSpec
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation
@@ -39,16 +40,22 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MonotonicFrameClock
 import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.withFrameNanos
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.SubcomposeLayout
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.StateRestorationTester
@@ -59,11 +66,13 @@
 import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.util.concurrent.TimeUnit
+import junit.framework.TestCase.assertEquals
 import kotlin.math.roundToInt
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
@@ -697,7 +706,7 @@
                 initialValue = A,
                 defaultPositionalThreshold,
                 defaultVelocityThreshold,
-                animationSpec = defaultAnimationSpec,
+                animationSpec = defaultAnimationSpec
             )
         anchoredDraggableState.updateAnchors(
             DraggableAnchors {
@@ -735,40 +744,6 @@
         dragJob.cancel()
     }
 
-    @Test
-    fun anchoredDraggable_anchoredDrag_doesNotUpdateOnConfirmValueChange() = runTest {
-        val anchoredDraggableState =
-            AnchoredDraggableState(
-                initialValue = B,
-                defaultPositionalThreshold,
-                defaultVelocityThreshold,
-                animationSpec = defaultAnimationSpec,
-                confirmValueChange = { false }
-            )
-        anchoredDraggableState.updateAnchors(
-            DraggableAnchors {
-                A at 0f
-                B at 200f
-            }
-        )
-
-        assertThat(anchoredDraggableState.targetValue).isEqualTo(B)
-
-        val unexpectedTarget = A
-        val targetUpdates = Channel<Float>()
-        val dragJob =
-            launch(Dispatchers.Unconfined) {
-                anchoredDraggableState.anchoredDrag(unexpectedTarget) { anchors, latestTarget ->
-                    targetUpdates.send(anchors.positionOf(latestTarget))
-                    suspendIndefinitely()
-                }
-            }
-
-        val firstTarget = targetUpdates.receive()
-        assertThat(firstTarget).isEqualTo(200f)
-        dragJob.cancel()
-    }
-
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun anchoredDraggable_dragCompletesExceptionally_cleansUp() = runTest {
@@ -1019,6 +994,64 @@
             )
     }
 
+    @Test
+    fun draggableAnchors_draggableOffsetTaggedAsMotionFrameOfReference() {
+        var offset by mutableStateOf(IntOffset(0, 0))
+        val offsets =
+            listOf(
+                IntOffset(0, 0),
+                IntOffset(5, 20),
+                IntOffset(25, 0),
+                IntOffset(100, 10),
+            )
+        var coords: LayoutCoordinates? = null
+        var rootCoords: LayoutCoordinates? = null
+        val state =
+            AnchoredDraggableState(
+                initialValue = 0,
+                positionalThreshold = defaultPositionalThreshold,
+                velocityThreshold = defaultVelocityThreshold,
+                animationSpec = { spring() }
+            )
+        var value by mutableIntStateOf(0)
+        rule.setContent {
+            Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+                LaunchedEffect(value) { state.snapTo(value) }
+                Box(
+                    Modifier.draggableAnchors(state, Orientation.Vertical) { _, _ ->
+                            DraggableAnchors { repeat(5) { it at it * 100f } } to 0
+                        }
+                        .fillMaxSize()
+                ) {
+                    Box(Modifier.fillMaxSize().onGloballyPositioned { coords = it })
+                }
+            }
+        }
+
+        repeat(5) {
+            value = it
+            rule.waitForIdle()
+
+            repeat(4) {
+                offset = offsets[it]
+                rule.runOnIdle {
+                    val excludeOffset =
+                        rootCoords!!
+                            .localPositionOf(coords!!, includeMotionFrameOfReference = false)
+                            .round()
+                    val includeOffset =
+                        rootCoords!!
+                            .localPositionOf(coords!!, includeMotionFrameOfReference = true)
+                            .round()
+                    assertEquals(
+                        includeOffset - IntOffset(0, state.requireOffset().roundToInt()),
+                        excludeOffset
+                    )
+                }
+            }
+        }
+    }
+
     private suspend fun suspendIndefinitely() = suspendCancellableCoroutine<Unit> {}
 
     private class HandPumpTestFrameClock : MonotonicFrameClock {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index 4cf6819..63959a5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -477,7 +477,7 @@
     /** Default size for the leading button end corners and trailing button start corners */
     // TODO update token to dp size and use it here
     val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
-    private val InnerCornerSizePressed = ShapeDefaults.CornerSmall
+    private val InnerCornerSizePressed = ShapeDefaults.CornerMedium
 
     /**
      * Default percentage size for the leading button start corners and trailing button end corners
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index 09b667b..1bf6f05 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -565,7 +565,7 @@
         if (anchors.hasAnchorFor(targetValue)) {
             try {
                 dragMutex.mutate(dragPriority) {
-                    dragTarget = if (confirmValueChange(targetValue)) targetValue else currentValue
+                    dragTarget = targetValue
                     restartable(inputs = { anchors to [email protected] }) {
                         (latestAnchors, latestTarget) ->
                         anchoredDragScope.block(latestAnchors, latestTarget)
@@ -856,7 +856,14 @@
                 } else state.requireOffset()
             val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
             val yOffset = if (orientation == Orientation.Vertical) offset else 0f
-            placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+            // Tagging as motion frame of reference placement, meaning the placement
+            // contains scrolling. This allows the consumer of this placement offset to
+            // differentiate this offset vs. offsets from structural changes. Generally
+            // speaking, this signals a preference to directly apply changes rather than
+            // animating, to avoid a chasing effect to scrolling.
+            withMotionFrameOfReferencePlacement {
+                placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+            }
         }
     }
 }
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index 7335b2c..d298a52 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -17,6 +17,7 @@
 package androidx.compose.runtime.benchmark
 
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.Text
@@ -30,8 +31,13 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
 import androidx.compose.ui.unit.dp
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -236,6 +242,21 @@
     fun benchmark_f_compose_Rect_100() = runBlockingTestWithFrameClock {
         measureComposeFocused { repeat(100) { Rect() } }
     }
+
+    @UiThreadTest
+    @Test
+    fun benchmark_g_group_eliding_focused_1000() = runBlockingTestWithFrameClock {
+        measureCompose { repeat(1000) { MyLayout { SimpleText("Value: $it") } } }
+    }
+}
+
+@Composable
+fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+    Layout(content = content, measurePolicy = EmptyMeasurePolicy, modifier = modifier)
+}
+
+internal val EmptyMeasurePolicy = MeasurePolicy { _, constraints ->
+    layout(constraints.minWidth, constraints.minHeight) {}
 }
 
 class ColorModel(color: Color = Color.Black) {
@@ -254,6 +275,12 @@
 }
 
 @Composable
+private fun SimpleText(text: String) {
+    val measurer = rememberTextMeasurer()
+    Box(modifier = Modifier.drawBehind { drawText(measurer, text) })
+}
+
+@Composable
 private fun Rect(color: Color) {
     val modifier = remember(color) { Modifier.background(color) }
     Column(modifier) {}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index cce9013..fe4b799 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -18,6 +18,7 @@
 
 import androidx.collection.MutableIntObjectMap
 import androidx.collection.MutableIntSet
+import androidx.collection.MutableObjectList
 import androidx.compose.runtime.snapshots.fastAny
 import androidx.compose.runtime.snapshots.fastFilterIndexed
 import androidx.compose.runtime.snapshots.fastForEach
@@ -1290,6 +1291,12 @@
     /** This a count of the [nodeCount] of the explicitly started groups. */
     private val nodeCountStack = IntStack()
 
+    /**
+     * Deferred slot writes for open groups to avoid thrashing the slot table when slots are added
+     * to parent group which already has children.
+     */
+    private var deferredSlotWrites: MutableIntObjectMap<MutableObjectList<Any?>>? = null
+
     /** The current group that will be started by [startGroup] or skipped by [skipGroup] */
     var currentGroup = 0
         private set
@@ -1439,6 +1446,19 @@
      * being inserted.
      */
     fun update(value: Any?): Any? {
+        if (insertCount > 0 && currentSlot != slotsGapStart) {
+            // Defer write as doing it now would thrash the slot table.
+            val deferred =
+                (deferredSlotWrites ?: MutableIntObjectMap())
+                    .also { deferredSlotWrites = it }
+                    .getOrPut(parent) { MutableObjectList() }
+            deferred.add(value)
+            return Composer.Empty
+        }
+        return rawUpdate(value)
+    }
+
+    private fun rawUpdate(value: Any?): Any? {
         val result = skip()
         set(value)
         return result
@@ -1664,7 +1684,7 @@
         groups.dataIndex(groupIndexToAddress(groupIndex + groupSize(groupIndex)))
 
     private val currentGroupSlotIndex: Int
-        get() = currentSlot - slotsStartIndex(parent)
+        get() = currentSlot - slotsStartIndex(parent) + (deferredSlotWrites?.get(parent)?.size ?: 0)
 
     /**
      * Advance [currentGroup] by [amount]. The [currentGroup] group cannot be advanced outside the
@@ -1850,6 +1870,14 @@
         val newGroupSize = currentGroup - groupIndex
         val isNode = groups.isNode(groupAddress)
         if (inserting) {
+            // Check for deferred slot writes
+            val deferredSlotWrites = deferredSlotWrites
+            deferredSlotWrites?.get(groupIndex)?.let {
+                it.forEach { value -> rawUpdate(value) }
+                deferredSlotWrites.remove(groupIndex)
+            }
+
+            // Close the group
             groups.updateGroupSize(groupAddress, newGroupSize)
             groups.updateNodeCount(groupAddress, newNodes)
             nodeCount = nodeCountStack.pop() + if (isNode) 1 else newNodes
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 9328a61..2d820aa 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -4608,6 +4608,19 @@
         revalidate()
     }
 
+    @Test // regression test for b/362291064
+    fun avoidsThrashingTheSlotTable() = compositionTest {
+        val count = 100
+        var data by mutableIntStateOf(0)
+        compose { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+        validate { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+        data++
+        advance()
+        revalidate()
+    }
+
     private inline fun CoroutineScope.withGlobalSnapshotManager(block: CoroutineScope.() -> Unit) {
         val channel = Channel<Unit>(Channel.CONFLATED)
         val job = launch { channel.consumeEach { Snapshot.sendApplyNotifications() } }
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
index e1b9966..30b464c 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
@@ -111,7 +111,7 @@
     @Composable
     private fun BoxWithMissingContentDescription() {
         Box(
-            Modifier.size(20.dp).semantics {
+            Modifier.size(48.dp).semantics {
                 // The SemanticsModifier will make this node importantForAccessibility
                 // Having no content description is now a violation
                 this.contentDescription = ""
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -13,5 +13,9 @@
     Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
 
 
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+    Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
 RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
     Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index cc929b6..afed775 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2338,20 +2338,20 @@
   }
 
   public interface MeasureResult {
-    method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+    method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
     method public int getHeight();
     method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
     method public int getWidth();
     method public void placeChildren();
-    property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+    property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
     property public abstract int height;
     property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
     property public abstract int width;
   }
 
   @androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
   }
 
   @kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -13,5 +13,9 @@
     Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
 
 
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+    Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
 RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
     Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c9b7cf..29ad080 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2341,20 +2341,20 @@
   }
 
   public interface MeasureResult {
-    method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+    method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
     method public int getHeight();
     method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
     method public int getWidth();
     method public void placeChildren();
-    property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+    property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
     property public abstract int height;
     property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
     property public abstract int width;
   }
 
   @androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
   }
 
   @kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
new file mode 100644
index 0000000..6f6e935
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import kotlin.test.assertEquals
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PlacementScopeMotionFrameOfReferenceTest {
+    @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @Test
+    fun testLazyList() {
+        var offset by mutableStateOf(IntOffset(0, 0))
+        val coords = arrayOfNulls<LayoutCoordinates>(30)
+        var rootCoords: LayoutCoordinates? = null
+        val state = LazyListState()
+        val offsets =
+            listOf(
+                IntOffset(0, 0),
+                IntOffset(5, 20),
+                IntOffset(25, 0),
+                IntOffset(100, 10),
+            )
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                Box(
+                    Modifier.layout { m, c ->
+                            m.measure(c).run {
+                                layout(width, height) {
+                                    rootCoords = coordinates
+                                    place(0, 0)
+                                }
+                            }
+                        }
+                        .offset { offset }
+                ) {
+                    LazyColumn(state = state, modifier = Modifier.requiredHeight(100.dp)) {
+                        items(30) { index ->
+                            Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+                        }
+                    }
+                }
+            }
+        }
+        repeat(4) {
+            val itemId = it * 5
+            offset = offsets[it]
+            rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+            repeat(5) {
+                assertEquals(
+                    offset,
+                    coords[itemId + it]!!
+                        .let {
+                            rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+                        }
+                        .round()
+                )
+                assertEquals(
+                    offset + IntOffset(0, it * 20),
+                    coords[itemId + it]!!
+                        .let {
+                            rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+                        }
+                        .round()
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testLazyGrid() {
+        var offset by mutableStateOf(IntOffset(0, 0))
+        val coords = arrayOfNulls<LayoutCoordinates>(60)
+        var rootCoords: LayoutCoordinates? = null
+        val state = LazyGridState()
+        val offsets =
+            listOf(
+                IntOffset(0, 0),
+                IntOffset(5, 20),
+                IntOffset(25, 0),
+                IntOffset(100, 10),
+            )
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                Box(
+                    Modifier.layout { m, c ->
+                            m.measure(c).run {
+                                layout(width, height) {
+                                    rootCoords = coordinates
+                                    place(0, 0)
+                                }
+                            }
+                        }
+                        .offset { offset }
+                ) {
+                    LazyVerticalGrid(
+                        GridCells.Fixed(2),
+                        modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp),
+                        state = state
+                    ) {
+                        items(60) { index ->
+                            Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+                        }
+                    }
+                }
+            }
+        }
+        repeat(4) {
+            val itemId = it * 5 * 2
+            offset = offsets[it]
+            rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+            rule.waitForIdle()
+            repeat(5) {
+                assertEquals(
+                    offset,
+                    coords[itemId + it]!!
+                        .let {
+                            rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+                        }
+                        .round()
+                )
+                assertEquals(
+                    offset + IntOffset(0 + it % 2 * 20, it / 2 * 20),
+                    coords[itemId + it]!!
+                        .let {
+                            rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+                        }
+                        .round()
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testLazyStaggeredGrid() {
+        var offset by mutableStateOf(IntOffset(0, 0))
+        val coords = arrayOfNulls<LayoutCoordinates>(60)
+        var rootCoords: LayoutCoordinates? = null
+        val state = LazyStaggeredGridState()
+        val offsets =
+            listOf(
+                IntOffset(0, 0),
+                IntOffset(5, 20),
+                IntOffset(25, 0),
+                IntOffset(100, 10),
+            )
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                Box(
+                    Modifier.layout { m, c ->
+                            m.measure(c).run {
+                                layout(width, height) {
+                                    rootCoords = coordinates
+                                    place(0, 0)
+                                }
+                            }
+                        }
+                        .offset { offset }
+                ) {
+                    LazyVerticalStaggeredGrid(
+                        state = state,
+                        columns = StaggeredGridCells.Fixed(2),
+                        modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp)
+                    ) {
+                        items(60) { index ->
+                            Box(
+                                Modifier.size(20.dp, ((index % 2) * 5).dp + 15.dp)
+                                    .onGloballyPositioned { coords[index] = it }
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        repeat(4) {
+            val itemId = it * 10
+            offset = offsets[it]
+            rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+            repeat(5) {
+                assertEquals(
+                    offset,
+                    coords[itemId + it]!!
+                        .let {
+                            rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+                        }
+                        .round()
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testPager() {
+        var offset by mutableStateOf(IntOffset(0, 0))
+        val coords = arrayOfNulls<LayoutCoordinates>(30)
+        var rootCoords: LayoutCoordinates? = null
+        val state = PagerState { 30 }
+        val offsets =
+            listOf(
+                IntOffset(0, 0),
+                IntOffset(5, 20),
+                IntOffset(25, 0),
+                IntOffset(100, 10),
+            )
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                Box(
+                    Modifier.layout { m, c ->
+                            m.measure(c).run {
+                                layout(width, height) {
+                                    rootCoords = coordinates
+                                    place(0, 0)
+                                }
+                            }
+                        }
+                        .offset { offset }
+                ) {
+                    HorizontalPager(
+                        state,
+                        pageSize = PageSize.Fixed(20.dp),
+                        modifier = Modifier.requiredHeight(20.dp).requiredWidth(100.dp)
+                    ) { index ->
+                        Box(Modifier.size(20.dp, 20.dp).onGloballyPositioned { coords[index] = it })
+                    }
+                }
+            }
+        }
+        repeat(4) {
+            val itemId = it * 5
+            offset = offsets[it]
+            rule.runOnIdle { runBlocking { state.scrollToPage(itemId) } }
+            repeat(5) {
+                assertEquals(
+                    offset,
+                    requireNotNull(coords[itemId + it]) { "item $itemId, it = $it" }
+                        .let {
+                            rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+                        }
+                        .round(),
+                )
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
index 2273b6a..3ea0bed 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -104,6 +105,52 @@
         assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
     }
 
+    @Test
+    fun visitChildrenInOtherLayoutNodesInDrawOrder_zIndex() {
+        // Arrange.
+        abstract class TrackedNode : Modifier.Node()
+        val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            Box(Modifier.elementOf(node)) {
+                Box(Modifier.elementOf(child1).zIndex(10f))
+                Box(Modifier.elementOf(child2).zIndex(-10f))
+                Box(Modifier.elementOf(child3))
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitChildren(Nodes.Any, zOrder = true) {
+                @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+    }
+
+    @Test
+    fun visitChildrenInOtherLayoutNodesInDrawOrder_subcompose() {
+        // Arrange.
+        val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            ReverseMeasureLayout(
+                Modifier.elementOf(node),
+                { Box(Modifier.elementOf(child1)) },
+                { Box(Modifier.elementOf(child2)) },
+                { Box(Modifier.elementOf(child3)) }
+            )
+        }
+
+        // Act.
+        rule.runOnIdle { node.visitChildren(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+    }
+
     @Ignore("b/278765590")
     @Test
     fun skipsUnattachedLocalChild() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
index 63f82d9..6f1e610 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -134,6 +135,58 @@
             .inOrder()
     }
 
+    @Test
+    fun visitsItemsAcrossLayoutNodesInDrawOrder_zIndex() {
+        // Arrange.
+        abstract class TrackedNode : Modifier.Node()
+        val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            Box(Modifier.elementOf(node)) {
+                Box(Modifier.elementOf(child1).zIndex(10f))
+                Box(Modifier.elementOf(child2).zIndex(-10f))
+                Box(Modifier.elementOf(child3))
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+                @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+                true
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+    }
+
+    @Test
+    fun visitsItemsAcrossLayoutNodesInDrawOrder_subcompose() {
+        // Arrange.
+        val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            ReverseMeasureLayout(
+                Modifier.elementOf(node),
+                { Box(Modifier.elementOf(child1)) },
+                { Box(Modifier.elementOf(child2)) },
+                { Box(Modifier.elementOf(child3)) }
+            )
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+                visitedChildren.add(it)
+                true
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+    }
+
     @Ignore("b/278765590")
     @Test
     fun skipsUnattachedItems() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
index 59d8aed..c2b585b5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -67,9 +68,6 @@
         assertThat(visitedChildren).containsExactly(localChild1, localChild2).inOrder()
     }
 
-    // TODO(ralu): I feel that this order of visiting children is incorrect, and we should
-    //  visit children in the order of composition. So instead of a stack, we probably need
-    //  to use a queue to hold the intermediate nodes.
     @Test
     fun differentLayoutNodes() {
         // Arrange.
@@ -79,10 +77,10 @@
         val visitedChildren = mutableListOf<Modifier.Node>()
         rule.setContent {
             Box(Modifier.elementOf(node).elementOf(child1).elementOf(child2)) {
-                Box(Modifier.elementOf(child5).elementOf(child6)) {
-                    Box(Modifier.elementOf(child7).elementOf(child8))
+                Box(Modifier.elementOf(child3).elementOf(child4)) {
+                    Box(Modifier.elementOf(child5).elementOf(child6))
                 }
-                Box { Box(Modifier.elementOf(child3).elementOf(child4)) }
+                Box { Box(Modifier.elementOf(child7).elementOf(child8)) }
             }
         }
 
@@ -95,6 +93,54 @@
             .inOrder()
     }
 
+    @Test
+    fun differentLayoutNodesInDrawOrder_zIndex() {
+        // Arrange.
+        abstract class TrackedNode : Modifier.Node()
+        val (node, child1, child2, child3, child4) = List(5) { object : TrackedNode() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            Box(Modifier.elementOf(node)) {
+                Box(Modifier.elementOf(child1))
+                Box(Modifier.elementOf(child2).zIndex(10f)) {
+                    Box(Modifier.elementOf(child3).zIndex(-10f))
+                }
+                Box { Box(Modifier.elementOf(child4)) }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitSubtree(Nodes.Any, zOrder = true) {
+                @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child4, child2, child3).inOrder()
+    }
+
+    @Test
+    fun differentLayoutNodesInDrawOrder_subcompose() {
+        // Arrange.
+        val (node, child1, child2, child3, child4) = List(5) { object : Modifier.Node() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            ReverseMeasureLayout(
+                Modifier.elementOf(node),
+                { Box(Modifier.elementOf(child1)) },
+                { Box(Modifier.elementOf(child2)) { Box(Modifier.elementOf(child3)) } },
+                { Box { Box(Modifier.elementOf(child4)) } }
+            )
+        }
+
+        // Act.
+        rule.runOnIdle { node.visitSubtree(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child2, child3, child4).inOrder()
+    }
+
     @Ignore("b/278765590")
     @Test
     fun skipsUnattached() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
deleted file mode 100644
index 74a3c0e..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
+++ /dev/null
@@ -1,59 +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.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import org.junit.Assert
-import org.junit.Test
-
-class NestedVectorStackTests {
-
-    @Test
-    fun testPushPopOrder() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-        stack.push(mutableVectorOf(4, 5, 6))
-        stack.push(mutableVectorOf())
-        stack.push(mutableVectorOf(7))
-        stack.push(mutableVectorOf(8, 9))
-        val result = buildString {
-            while (stack.isNotEmpty()) {
-                append(stack.pop())
-            }
-        }
-        Assert.assertEquals("987654321", result)
-    }
-
-    @Test
-    fun testPopInBetweenPushes() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3, 4))
-        stack.pop()
-        stack.push(mutableVectorOf(4, 5, 6))
-        stack.pop()
-        stack.pop()
-        stack.push(mutableVectorOf())
-        stack.push(mutableVectorOf(5, 6, 7))
-        stack.push(mutableVectorOf(8, 9))
-        val result = buildString {
-            while (stack.isNotEmpty()) {
-                append(stack.pop())
-            }
-        }
-        Assert.assertEquals("987654321", result)
-    }
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
index 29b9c0a..e77e8d6 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
@@ -16,7 +16,10 @@
 
 package androidx.compose.ui.node
 
+import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
 import androidx.compose.ui.platform.InspectorInfo
 
 /**
@@ -38,3 +41,30 @@
         name = "testNode"
     }
 }
+
+@Composable
+internal fun ReverseMeasureLayout(modifier: Modifier, vararg contents: @Composable () -> Unit) =
+    SubcomposeLayout(modifier) { constraints ->
+        var layoutWidth = constraints.minWidth
+        var layoutHeight = constraints.minHeight
+        val subcomposes = mutableListOf<List<Placeable>>()
+
+        // Measure in reverse order
+        contents.reversed().forEachIndexed { index, content ->
+            subcomposes.add(
+                0,
+                subcompose(index, content).map {
+                    it.measure(constraints).also { placeable ->
+                        layoutWidth = maxOf(layoutWidth, placeable.width)
+                        layoutHeight = maxOf(layoutHeight, placeable.height)
+                    }
+                }
+            )
+        }
+
+        layout(layoutWidth, layoutHeight) {
+
+            // But place in direct order - it sets direct draw order
+            subcomposes.forEach { placeables -> placeables.forEach { it.place(0, 0) } }
+        }
+    }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
index 8d0b0ee..9ba98f3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
@@ -332,6 +332,90 @@
         }
     }
 
+    @Test
+    fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureOne() = runBlocking {
+        val state = LazyListState()
+
+        // starting with view
+        createActivity(state)
+        checkVisibility(composeView(), View.GONE)
+        checkVisibility(recyclerView(), View.VISIBLE)
+
+        regularGestureOne(R.id.view_list)
+        rule.waitForIdle()
+        recyclerView().awaitScrollIdle()
+
+        val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+        // switch visibilities
+        rule.runOnUiThread {
+            rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+            rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+        }
+
+        checkVisibility(composeView(), View.VISIBLE)
+        checkVisibility(recyclerView(), View.GONE)
+
+        assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+        // Inject the same events in compose view
+        rule.runOnUiThread {
+            for (event in recyclerView().motionEvents) {
+                composeView().dispatchTouchEvent(event)
+            }
+        }
+
+        rule.runOnIdle {
+            val currentTopInCompose = state.firstVisibleItemIndex
+            val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+            val message =
+                "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+            assertTrue(message) { diff <= ItemDifferenceThreshold }
+        }
+    }
+
+    @Test
+    fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureTwo() = runBlocking {
+        val state = LazyListState()
+
+        // starting with view
+        createActivity(state)
+        checkVisibility(composeView(), View.GONE)
+        checkVisibility(recyclerView(), View.VISIBLE)
+
+        regularGestureTwo(R.id.view_list)
+        rule.waitForIdle()
+        recyclerView().awaitScrollIdle()
+
+        val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+        // switch visibilities
+        rule.runOnUiThread {
+            rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+            rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+        }
+
+        checkVisibility(composeView(), View.VISIBLE)
+        checkVisibility(recyclerView(), View.GONE)
+
+        assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+        // Inject the same events in compose view
+        rule.runOnUiThread {
+            for (event in recyclerView().motionEvents) {
+                composeView().dispatchTouchEvent(event)
+            }
+        }
+
+        rule.runOnIdle {
+            val currentTopInCompose = state.firstVisibleItemIndex
+            val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+            val message =
+                "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+            assertTrue(message) { diff <= ItemDifferenceThreshold }
+        }
+    }
+
     private fun createActivity(state: LazyListState) {
         rule.activityRule.scenario.createActivityWithComposeContent(
             R.layout.android_compose_lists_fling
@@ -385,7 +469,7 @@
 @Composable
 fun TestComposeList(state: LazyListState) {
     LazyColumn(Modifier.fillMaxSize(), state = state) {
-        items(1000) {
+        items(2000) {
             Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(Color.Black)) {
                 Text(text = it.toString(), color = Color.White)
             }
@@ -394,7 +478,7 @@
 }
 
 private class ListAdapter : RecyclerView.Adapter<ListViewHolder>() {
-    val items = (0 until 1000).map { it.toString() }
+    val items = (0 until 2000).map { it.toString() }
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
         return ListViewHolder(
@@ -451,4 +535,4 @@
     }
 }
 
-private const val ItemDifferenceThreshold = 3
+private const val ItemDifferenceThreshold = 1
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
index 4fef305..2e76112 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
@@ -24,10 +24,9 @@
 import androidx.activity.ComponentActivity
 import androidx.annotation.LayoutRes
 import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
 import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.draggable2D
+import androidx.compose.foundation.gestures.rememberDraggable2DState
 import androidx.compose.foundation.gestures.rememberDraggableState
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -38,14 +37,10 @@
 import androidx.compose.ui.background
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChangedIgnoreConsumed
 import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
-import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.ViewConfiguration
@@ -70,9 +65,7 @@
 import com.google.errorprone.annotations.CanIgnoreReturnValue
 import kotlin.math.absoluteValue
 import kotlin.test.assertTrue
-import kotlinx.coroutines.coroutineScope
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -272,7 +265,6 @@
     }
 
     @Test
-    @Ignore("b/299092669")
     fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_orthogonal() {
         // Arrange
         createActivity(true)
@@ -306,6 +298,70 @@
         assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
     }
 
+    @Test
+    fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationOne() {
+        // Arrange
+        createActivity()
+        checkVisibility(composeView, View.GONE)
+        checkVisibility(draggableView, View.VISIBLE)
+
+        // Act: Use system to send motion events and collect them.
+        regularGestureOne(R.id.draggable_view)
+
+        val latestVelocityInViewY = draggableView.latestVelocity.y
+
+        // switch visibility
+        rule.runOnUiThread {
+            composeView.visibility = View.VISIBLE
+            draggableView.visibility = View.GONE
+        }
+
+        checkVisibility(composeView, View.VISIBLE)
+        checkVisibility(draggableView, View.GONE)
+
+        assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+        // Inject the same events in compose view
+        for (event in draggableView.motionEvents) {
+            composeView.dispatchTouchEvent(event)
+        }
+
+        // assert
+        assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+    }
+
+    @Test
+    fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationTwo() {
+        // Arrange
+        createActivity()
+        checkVisibility(composeView, View.GONE)
+        checkVisibility(draggableView, View.VISIBLE)
+
+        // Act: Use system to send motion events and collect them.
+        regularGestureTwo(R.id.draggable_view)
+
+        val latestVelocityInViewY = draggableView.latestVelocity.y
+
+        // switch visibility
+        rule.runOnUiThread {
+            composeView.visibility = View.VISIBLE
+            draggableView.visibility = View.GONE
+        }
+
+        checkVisibility(composeView, View.VISIBLE)
+        checkVisibility(draggableView, View.GONE)
+
+        assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+        // Inject the same events in compose view
+        for (event in draggableView.motionEvents) {
+            composeView.dispatchTouchEvent(event)
+        }
+
+        // assert
+        assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+    }
+
     private fun createActivity(twoDimensional: Boolean = false) {
         rule.activityRule.scenario.createActivityWithComposeContent(
             R.layout.velocity_tracker_compose_vs_view
@@ -394,6 +450,24 @@
         )
 }
 
+internal fun regularGestureOne(id: Int) {
+    Espresso.onView(withId(id))
+        .perform(
+            espressoSwipe(
+                SwiperWithTime(100),
+                GeneralLocation.CENTER,
+                GeneralLocation.BOTTOM_CENTER
+            )
+        )
+}
+
+internal fun regularGestureTwo(id: Int) {
+    Espresso.onView(withId(id))
+        .perform(
+            espressoSwipe(SwiperWithTime(70), GeneralLocation.CENTER, GeneralLocation.TOP_CENTER)
+        )
+}
+
 private fun espressoSwipe(
     swiper: Swiper,
     start: CoordinatesProvider,
@@ -418,7 +492,10 @@
                 .background(Color.Black)
                 .then(
                     if (twoDimensional) {
-                        Modifier.draggable2D(onDragStopped)
+                        Modifier.draggable2D(
+                            rememberDraggable2DState {},
+                            onDragStopped = onDragStopped
+                        )
                     } else {
                         Modifier.draggable(
                             rememberDraggableState(onDelta = {}),
@@ -431,32 +508,6 @@
     }
 }
 
-fun Modifier.draggable2D(onDragStopped: (Velocity) -> Unit) =
-    this.pointerInput(Unit) {
-        coroutineScope {
-            awaitEachGesture {
-                val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
-                val initialDown =
-                    awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
-                tracker.addPointerInputChange(initialDown)
-
-                awaitTouchSlopOrCancellation(initialDown.id) { change, _ ->
-                    tracker.addPointerInputChange(change)
-                    change.consume()
-                }
-
-                val lastEvent =
-                    awaitDragOrUp(initialDown.id) {
-                        tracker.addPointerInputChange(it)
-                        it.consume()
-                        it.positionChangedIgnoreConsumed()
-                    }
-                lastEvent?.let { tracker.addPointerInputChange(it) }
-                onDragStopped(tracker.calculateVelocity())
-            }
-        }
-    }
-
 private fun ActivityScenario<*>.createActivityWithComposeContent(
     @LayoutRes layout: Int,
     content: @Composable () -> Unit,
@@ -512,8 +563,8 @@
     return down.size == 1 && move.isNotEmpty() && up.size == 1
 }
 
-// 1% tolerance
-private const val VelocityDifferenceTolerance = 0.1f
+// 5% tolerance
+private const val VelocityDifferenceTolerance = 0.05f
 
 /** Copied from androidx.test.espresso.action.Swipe */
 internal data class SwiperWithTime(val gestureDurationMs: Int) : Swiper {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index 5abbb34..5906b9f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -15,8 +15,11 @@
  */
 package androidx.compose.ui.window
 
-import android.content.res.Configuration
+import android.util.DisplayMetrics
 import android.view.KeyEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
@@ -35,8 +38,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.gesture.MotionEvent
+import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.input.pointer.PointerCoords
 import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.assertIsDisplayed
@@ -259,9 +265,9 @@
     fun canFillScreenWidth_dependingOnProperty() {
         var box1Width = 0
         var box2Width = 0
-        lateinit var configuration: Configuration
+        lateinit var displayMetrics: DisplayMetrics
         rule.setContent {
-            configuration = LocalConfiguration.current
+            displayMetrics = LocalView.current.context.resources.displayMetrics
             Dialog(
                 onDismissRequest = {},
                 properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -272,7 +278,7 @@
                 Box(Modifier.fillMaxSize().onSizeChanged { box2Width = it.width })
             }
         }
-        val expectedWidth = with(rule.density) { configuration.screenWidthDp.dp.roundToPx() }
+        val expectedWidth = with(rule.density) { displayMetrics.widthPixels }
         assertThat(box1Width).isEqualTo(expectedWidth)
         assertThat(box2Width).isLessThan(box1Width)
     }
@@ -313,6 +319,75 @@
         }
     }
 
+    @Test
+    fun dismissWhenClickingOutsideContent() {
+        var dismissed = false
+        var clicked = false
+        lateinit var composeView: View
+        val clickBoxTag = "clickBox"
+        rule.setContent {
+            Dialog(
+                onDismissRequest = { dismissed = true },
+                properties =
+                    DialogProperties(
+                        usePlatformDefaultWidth = false,
+                        decorFitsSystemWindows = false
+                    )
+            ) {
+                composeView = LocalView.current
+                Box(Modifier.size(10.dp).testTag(clickBoxTag).clickable { clicked = true })
+            }
+        }
+
+        // click inside the compose view
+        rule.onNodeWithTag(clickBoxTag).performClick()
+
+        rule.waitForIdle()
+
+        assertThat(dismissed).isFalse()
+        assertThat(clicked).isTrue()
+
+        clicked = false
+
+        // click outside the compose view
+        rule.waitForIdle()
+        var root = composeView
+        while (root.parent is View) {
+            root = root.parent as View
+        }
+
+        rule.runOnIdle {
+            val x = root.width / 4f
+            val y = root.height / 4f
+            val down =
+                MotionEvent(
+                    eventTime = 0,
+                    action = ACTION_DOWN,
+                    numPointers = 1,
+                    actionIndex = 0,
+                    pointerProperties = arrayOf(PointerProperties(0)),
+                    pointerCoords = arrayOf(PointerCoords(x, y)),
+                    root
+                )
+            root.dispatchTouchEvent(down)
+            val up =
+                MotionEvent(
+                    eventTime = 10,
+                    action = ACTION_UP,
+                    numPointers = 1,
+                    actionIndex = 0,
+                    pointerProperties = arrayOf(PointerProperties(0)),
+                    pointerCoords = arrayOf(PointerCoords(x, y)),
+                    root
+                )
+            root.dispatchTouchEvent(up)
+        }
+        rule.waitForIdle()
+
+        assertThat(dismissed).isTrue()
+        assertThat(clicked).isFalse()
+    }
+
     private fun setupDialogTest(
         closeDialogOnDismiss: Boolean = true,
         dialogProperties: DialogProperties = DialogProperties(),
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
index e85b14c..cde57e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
@@ -15,12 +15,16 @@
  */
 package androidx.compose.ui.window
 
+import android.content.res.Configuration
+import android.os.Build
 import android.view.View
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.ime
 import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.safeDrawingPadding
 import androidx.compose.material.TextField
 import androidx.compose.runtime.SideEffect
 import androidx.compose.ui.Alignment
@@ -29,10 +33,18 @@
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.core.graphics.Insets
@@ -41,8 +53,10 @@
 import androidx.core.view.WindowInsetsControllerCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
 import org.junit.Assert.assertNotEquals
 import org.junit.Rule
 import org.junit.Test
@@ -129,6 +143,90 @@
         assertNotEquals(Insets.NONE, imeInsets)
     }
 
+    @Test
+    fun dialogCanTakeEntireScreen() {
+        var size = IntSize.Zero
+        var displayWidth = 0
+        var displayHeight = 0
+        var insetsLeft = 0
+        var insetsTop = 0
+        var insetsRight = 0
+        var insetsBottom = 0
+        var textTop = 0
+        var controller: SoftwareKeyboardController? = null
+        rule.setContent {
+            val displayMetrics = LocalView.current.resources.displayMetrics
+            controller = LocalSoftwareKeyboardController.current
+            displayWidth = displayMetrics.widthPixels
+            displayHeight = displayMetrics.heightPixels
+            Box(Modifier.fillMaxSize()) {
+                Dialog(
+                    {},
+                    properties =
+                        DialogProperties(
+                            decorFitsSystemWindows = false,
+                            usePlatformDefaultWidth = false
+                        )
+                ) {
+                    val insets = WindowInsets.safeDrawing
+
+                    Box(
+                        Modifier.fillMaxSize()
+                            .layout { m, c ->
+                                val p = m.measure(c)
+                                size = IntSize(p.width, p.height)
+                                insetsTop = insets.getTop(this)
+                                insetsLeft = insets.getLeft(this, layoutDirection)
+                                insetsBottom = insets.getBottom(this)
+                                insetsRight = insets.getRight(this, layoutDirection)
+                                layout(p.width, p.height) { p.place(0, 0) }
+                            }
+                            .safeDrawingPadding()
+                    ) {
+                        TextField(
+                            value = "Hello",
+                            onValueChange = {},
+                            Modifier.align(Alignment.BottomStart).testTag("textField").onPlaced {
+                                layoutCoordinates ->
+                                textTop = layoutCoordinates.positionInRoot().y.roundToInt()
+                            }
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        if (
+            Build.VERSION.SDK_INT >= 35 &&
+                rule.activity.applicationContext.applicationInfo.targetSdkVersion >= 35
+        ) {
+            // On SDK >= 35, the metrics is the size of the entire screen
+            assertThat(size.width).isEqualTo(displayWidth)
+            assertThat(size.height).isEqualTo(displayHeight)
+        } else {
+            // On SDK < 35, the metrics is the size of the screen with some insets removed
+            assertThat(size.width).isAtLeast(displayWidth)
+            assertThat(size.height).isAtLeast(displayHeight)
+        }
+        // There is going to be some insets
+        assertThat(maxOf(insetsLeft, insetsTop, insetsRight, insetsBottom)).isNotEqualTo(0)
+
+        val hardKeyboardHidden =
+            rule.runOnUiThread { rule.activity.resources.configuration.hardKeyboardHidden }
+        if (hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
+            return // can't launch the IME when the hardware keyboard is up.
+        }
+        val bottomInsetsBeforeIme = insetsBottom
+        val textTopBeforeIme = textTop
+        rule.onNodeWithTag("textField").requestFocus()
+        rule.waitUntil {
+            controller?.show()
+            insetsBottom != bottomInsetsBeforeIme
+        }
+        rule.runOnIdle { assertThat(textTop).isLessThan(textTopBeforeIme) }
+    }
+
     private fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? {
         if (view is DialogWindowProvider) {
             return view
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 83c30cd..475f5dba 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -20,13 +20,16 @@
 import android.graphics.Outline
 import android.os.Build
 import android.view.ContextThemeWrapper
+import android.view.Gravity
 import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.View
+import android.view.View.OnLayoutChangeListener
 import android.view.ViewGroup
 import android.view.ViewOutlineProvider
 import android.view.Window
 import android.view.WindowManager
+import android.widget.FrameLayout
 import androidx.activity.ComponentDialog
 import androidx.activity.addCallback
 import androidx.compose.runtime.Composable
@@ -57,8 +60,11 @@
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxBy
-import androidx.compose.ui.util.fastRoundToInt
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -77,16 +83,19 @@
  * @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
  *   dialog's window.
  * @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
- *   the platform default, which is smaller than the screen width.
+ *   the platform default, which is smaller than the screen width. It is recommended to use
+ *   [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
+ *   using the entire screen and avoiding UI glitches on some devices when the IME animates in.
  * @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
  *   `false` to use WindowInsets. If `false`, the
  *   [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
  *   [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] and `android:windowIsFloating` is set to
- *   `false` for Android [R][Build.VERSION_CODES.R] and earlier.
+ *   `false` when [decorFitsSystemWindows] is false. When
+ *   `targetSdk` >= [Build.VERSION_CODES.VANILLA_ICE_CREAM], [decorFitsSystemWindows] can only be
+ *   `false` and this property doesn't have any effect.
  */
 @Immutable
-actual class DialogProperties
-constructor(
+actual class DialogProperties(
     actual val dismissOnBackPress: Boolean = true,
     actual val dismissOnClickOutside: Boolean = true,
     val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
@@ -218,6 +227,7 @@
     private var content: @Composable () -> Unit by mutableStateOf({})
 
     var usePlatformDefaultWidth = false
+    var decorFitsSystemWindows = false
 
     override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
         private set
@@ -229,50 +239,16 @@
         createComposition()
     }
 
-    override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        if (usePlatformDefaultWidth) {
-            super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
-        } else {
-            // usePlatformDefaultWidth false, so don't want to limit the dialog width to the Android
-            // platform default. Therefore, we create a new measure spec for width, which
-            // corresponds to the full screen width. We do the same for height, even if
-            // ViewRootImpl gives it to us from the first measure.
-            val displayWidthMeasureSpec =
-                MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.AT_MOST)
-            val displayHeightMeasureSpec =
-                MeasureSpec.makeMeasureSpec(displayHeight, MeasureSpec.AT_MOST)
-            super.internalOnMeasure(displayWidthMeasureSpec, displayHeightMeasureSpec)
-        }
-    }
-
-    override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
-        super.internalOnLayout(changed, left, top, right, bottom)
-        // Now set the content size as fixed layout params, such that ViewRootImpl knows
-        // the exact window size.
-        if (!usePlatformDefaultWidth) {
-            val child = getChildAt(0) ?: return
-            window.setLayout(child.measuredWidth, child.measuredHeight)
-        }
-    }
-
-    private val displayWidth: Int
-        get() {
-            val density = context.resources.displayMetrics.density
-            return (context.resources.configuration.screenWidthDp * density).fastRoundToInt()
-        }
-
-    private val displayHeight: Int
-        get() {
-            val density = context.resources.displayMetrics.density
-            return (context.resources.configuration.screenHeightDp * density).fastRoundToInt()
-        }
-
     @Composable
     override fun Content() {
         content()
     }
 }
 
+private fun adjustedDecorFitsSystemWindows(dialogProperties: DialogProperties, context: Context) =
+    dialogProperties.decorFitsSystemWindows &&
+        context.applicationInfo.targetSdkVersion < Build.VERSION_CODES.VANILLA_ICE_CREAM
+
 private class DialogWrapper(
     private var onDismissRequest: () -> Unit,
     private var properties: DialogProperties,
@@ -288,16 +264,16 @@
          */
         ContextThemeWrapper(
             composeView.context,
-            if (
-                Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || properties.decorFitsSystemWindows
-            ) {
+            if (adjustedDecorFitsSystemWindows(properties, composeView.context)) {
                 R.style.DialogWindowTheme
             } else {
                 R.style.FloatingDialogWindowTheme
             }
         )
     ),
-    ViewRootForInspector {
+    ViewRootForInspector,
+    OnApplyWindowInsetsListener,
+    OnLayoutChangeListener {
 
     private val dialogLayout: DialogLayout
 
@@ -308,15 +284,12 @@
     override val subCompositionView: AbstractComposeView
         get() = dialogLayout
 
-    private val defaultSoftInputMode: Int
-
     init {
         val window = window ?: error("Dialog has no window")
-        defaultSoftInputMode =
-            window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST
         window.requestFeature(Window.FEATURE_NO_TITLE)
         window.setBackgroundDrawableResource(android.R.color.transparent)
-        WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
+        val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+        WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
         dialogLayout =
             DialogLayout(context, window).apply {
                 // Set unique id for AbstractComposeView. This allows state restoration for the
@@ -336,10 +309,8 @@
                         override fun getOutline(view: View, result: Outline) {
                             result.setRect(0, 0, view.width, view.height)
                             // We set alpha to 0 to hide the view's shadow and let the composable to
-                            // draw
-                            // its own shadow. This still enables us to get the extra space needed
-                            // in the
-                            // surface.
+                            // draw its own shadow. This still enables us to get the extra space
+                            // needed in the surface.
                             result.alpha = 0f
                         }
                     }
@@ -359,7 +330,38 @@
 
         // Turn of all clipping so shadows can be drawn outside the window
         (window.decorView as? ViewGroup)?.disableClipping()
-        setContentView(dialogLayout)
+        // Center the ComposeView in a FrameLayout
+        val frameLayout = FrameLayout(context)
+        frameLayout.addView(
+            dialogLayout,
+            FrameLayout.LayoutParams(
+                    FrameLayout.LayoutParams.WRAP_CONTENT,
+                    FrameLayout.LayoutParams.WRAP_CONTENT
+                )
+                .also { it.gravity = Gravity.CENTER }
+        )
+        frameLayout.setOnClickListener { onDismissRequest() }
+        ViewCompat.setOnApplyWindowInsetsListener(frameLayout, this)
+        ViewCompat.setWindowInsetsAnimationCallback(
+            frameLayout,
+            object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+                override fun onProgress(
+                    insets: WindowInsetsCompat,
+                    runningAnimations: MutableList<WindowInsetsAnimationCompat>
+                ): WindowInsetsCompat {
+                    return insets.inset(
+                        dialogLayout.left,
+                        dialogLayout.top,
+                        frameLayout.width - dialogLayout.right,
+                        frameLayout.height - dialogLayout.bottom
+                    )
+                }
+            }
+        )
+        dialogLayout.addOnLayoutChangeListener(this)
+        frameLayout.addOnLayoutChangeListener(this)
+
+        setContentView(frameLayout)
         dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
         dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
         dialogLayout.setViewTreeSavedStateRegistryOwner(
@@ -430,21 +432,42 @@
         this.properties = properties
         setSecurePolicy(properties.securePolicy)
         setLayoutDirection(layoutDirection)
-        if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
-            // Undo fixed size in internalOnLayout, which would suppress size changes when
-            // usePlatformDefaultWidth is true.
-            window?.setLayout(
-                WindowManager.LayoutParams.WRAP_CONTENT,
-                WindowManager.LayoutParams.WRAP_CONTENT
-            )
-        }
         dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
-            if (properties.decorFitsSystemWindows) {
-                window?.setSoftInputMode(defaultSoftInputMode)
-            } else {
-                @Suppress("DEPRECATION")
-                window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+        val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+        dialogLayout.decorFitsSystemWindows = decorFitsSystemWindows
+        val window = window
+        if (window != null) {
+            val softInput =
+                when {
+                    decorFitsSystemWindows ->
+                        WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
+                    Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
+                        @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+                    else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+                }
+            window.setSoftInputMode(softInput)
+            val attrs = window.attributes
+            val measurementWidth =
+                if (properties.usePlatformDefaultWidth) {
+                    WindowManager.LayoutParams.WRAP_CONTENT
+                } else {
+                    WindowManager.LayoutParams.MATCH_PARENT
+                }
+            val measurementHeight =
+                if (properties.usePlatformDefaultWidth || decorFitsSystemWindows) {
+                    WindowManager.LayoutParams.WRAP_CONTENT
+                } else {
+                    WindowManager.LayoutParams.MATCH_PARENT
+                }
+            if (
+                attrs.width != measurementWidth ||
+                    attrs.height != measurementHeight ||
+                    attrs.gravity != Gravity.CENTER
+            ) {
+                attrs.width = measurementWidth
+                attrs.height = measurementHeight
+                attrs.gravity = Gravity.CENTER
+                window.attributes = attrs
             }
         }
     }
@@ -466,6 +489,28 @@
         // Prevents the dialog from dismissing itself
         return
     }
+
+    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+        val left = dialogLayout.left
+        val top = dialogLayout.top
+        val right = v.width - dialogLayout.right
+        val bottom = v.height - dialogLayout.bottom
+        return insets.inset(left, top, right, bottom)
+    }
+
+    override fun onLayoutChange(
+        v: View,
+        left: Int,
+        top: Int,
+        right: Int,
+        bottom: Int,
+        oldLeft: Int,
+        oldTop: Int,
+        oldRight: Int,
+        oldBottom: Int
+    ) {
+        v.requestApplyInsets()
+    }
 }
 
 @Composable
diff --git a/compose/ui/ui/src/androidMain/res/values/styles.xml b/compose/ui/ui/src/androidMain/res/values/styles.xml
index e1211d4..d0e837b 100644
--- a/compose/ui/ui/src/androidMain/res/values/styles.xml
+++ b/compose/ui/ui/src/androidMain/res/values/styles.xml
@@ -19,11 +19,13 @@
     <style name="DialogWindowTheme">
         <item name="android:windowClipToOutline">false</item>
     </style>
-    <!-- Style for decorFitsSystemWindows = false on API 30 and earlier. WindowInsets won't
-     be set on Dialogs without android:windowIsFloating set to false. -->
+    <!-- Style for decorFitsSystemWindows = false -->
     <style name="FloatingDialogWindowTheme">
         <item name="android:windowClipToOutline">false</item>
         <item name="android:dialogTheme">@style/FloatingDialogTheme</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:backgroundDimEnabled">true</item>
     </style>
     <style name="FloatingDialogTheme">
         <item name="android:windowIsFloating">false</item>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index c1363c7..0328f65 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -209,14 +209,14 @@
             layout(d)
         }
         val recorder = Recorder()
-        x.visitSubtree(Nodes.Draw, recorder)
+        x.visitSubtree(Nodes.Draw, block = recorder)
         assertThat(recorder.recorded)
             .isEqualTo(
                 listOf(
                     a.wrapped,
                     b,
-                    d,
                     c.wrapped,
+                    d,
                 )
             )
     }
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
deleted file mode 100644
index 951cb3d..0000000
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class NestedVectorStackTest {
-
-    @Test
-    fun testEnumerationOrder() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-        stack.push(mutableVectorOf(4, 5, 6))
-
-        Truth.assertThat(stack.enumerate()).isEqualTo(listOf(6, 5, 4, 3, 2, 1))
-    }
-
-    @Test
-    fun testEnumerationOrderPartiallyPoppingMiddleVectors() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-
-        Truth.assertThat(stack.pop()).isEqualTo(3)
-
-        stack.push(mutableVectorOf(4, 5, 6))
-
-        Truth.assertThat(stack.pop()).isEqualTo(6)
-
-        Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4, 2, 1))
-    }
-
-    @Test
-    fun testEnumerationOrderFullyPoppingMiddleVectors() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-
-        Truth.assertThat(stack.pop()).isEqualTo(3)
-        Truth.assertThat(stack.pop()).isEqualTo(2)
-        Truth.assertThat(stack.pop()).isEqualTo(1)
-
-        stack.push(mutableVectorOf(4, 5, 6))
-
-        Truth.assertThat(stack.pop()).isEqualTo(6)
-
-        Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4))
-    }
-}
-
-internal fun <T> NestedVectorStack<T>.enumerate(): List<T> {
-    val result = mutableListOf<T>()
-    var item: T? = pop()
-    while (item != null) {
-        result.add(item)
-        item = if (isNotEmpty()) pop() else null
-    }
-    return result
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index 8010fb9..d64d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -90,7 +90,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index dc853ae..1bdba48 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -351,7 +351,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
@@ -365,7 +365,7 @@
             override val height: Int
                 get() = h
 
-            override val alignmentLines: Map<AlignmentLine, Int>
+            override val alignmentLines: Map<out AlignmentLine, Int>
                 get() = alignmentLines
 
             override val rulers: (RulerScope.() -> Unit)?
@@ -385,7 +385,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
@@ -399,7 +399,7 @@
             override val height: Int
                 get() = h
 
-            override val alignmentLines: Map<AlignmentLine, Int>
+            override val alignmentLines: Map<out AlignmentLine, Int>
                 get() = alignmentLines
 
             override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
index a2b16cc..e7a3137 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
@@ -19,7 +19,7 @@
      * Alignment lines that can be used by parents to align this layout. This only includes the
      * alignment lines of this layout and not children.
      */
-    val alignmentLines: Map<AlignmentLine, Int>
+    val alignmentLines: Map<out AlignmentLine, Int>
 
     /**
      * An optional lambda function used to create [Ruler]s for child layout. This may be
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
index 36e7ea8..978165e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
@@ -47,7 +47,7 @@
     fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+        alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
         placementBlock: Placeable.PlacementScope.() -> Unit
     ) = layout(width, height, alignmentLines, null, placementBlock)
 
@@ -69,7 +69,7 @@
     fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+        alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
         rulers: (RulerScope.() -> Unit)? = null,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 442dada..08a5f0c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -911,7 +911,7 @@
         override fun layout(
             width: Int,
             height: Int,
-            alignmentLines: Map<AlignmentLine, Int>,
+            alignmentLines: Map<out AlignmentLine, Int>,
             rulers: (RulerScope.() -> Unit)?,
             placementBlock: Placeable.PlacementScope.() -> Unit
         ): MeasureResult {
@@ -923,7 +923,7 @@
                 override val height: Int
                     get() = height
 
-                override val alignmentLines: Map<AlignmentLine, Int>
+                override val alignmentLines: Map<out AlignmentLine, Int>
                     get() = alignmentLines
 
                 override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index ffc88f5..f967cd8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -99,48 +99,33 @@
     return null
 }
 
-internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
-    // TODO(lmr): we might want to add some safety wheels to prevent this from being called
-    //  while one of the chains is being diffed / updated.
-    checkPrecondition(node.isAttached) { "visitSubtree called on an unattached node" }
-    var node: Modifier.Node? = node.child
-    var layout: LayoutNode? = requireLayoutNode()
-    // we use this bespoke data structure here specifically for traversing children. In the
-    // depth first traversal you would typically do a `stack.addAll(node.children)` type
-    // call, but to avoid enumerating the vector and moving into our stack, we simply keep
-    // a stack of vectors and keep track of where we are in each
-    val nodes = NestedVectorStack<LayoutNode>()
-    while (layout != null) {
-        // NOTE: the ?: is important here for the starting condition, since we are starting
-        // at THIS node, and not the head of this node chain.
-        node = node ?: layout.nodes.head
-        if (node.aggregateChildKindSet and mask != 0) {
-            while (node != null) {
-                if (node.kindSet and mask != 0) {
-                    block(node)
-                }
-                node = node.child
-            }
-        }
-        node = null
-        nodes.push(layout._children)
-        layout = if (nodes.isNotEmpty()) nodes.pop() else null
+private fun LayoutNode.getChildren(zOrder: Boolean) =
+    if (zOrder) {
+        zSortedChildren
+    } else {
+        _children
     }
+
+private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(
+    node: Modifier.Node,
+    zOrder: Boolean,
+) {
+    node.requireLayoutNode().getChildren(zOrder).forEachReversed { add(it.nodes.head) }
 }
 
-private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(node: Modifier.Node) {
-    node.requireLayoutNode()._children.forEachReversed { add(it.nodes.head) }
-}
-
-internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
+internal inline fun DelegatableNode.visitChildren(
+    mask: Int,
+    zOrder: Boolean,
+    block: (Modifier.Node) -> Unit
+) {
     check(node.isAttached) { "visitChildren called on an unattached node" }
     val branches = mutableVectorOf<Modifier.Node>()
     val child = node.child
-    if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+    if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
     while (branches.isNotEmpty()) {
         val branch = branches.removeAt(branches.lastIndex)
         if (branch.aggregateChildKindSet and mask == 0) {
-            branches.addLayoutNodeChildren(branch)
+            branches.addLayoutNodeChildren(branch, zOrder)
             // none of these nodes match the mask, so don't bother traversing them
             continue
         }
@@ -159,11 +144,15 @@
  * visit the shallow tree of children of a given mask, but if block returns true, we will continue
  * traversing below it
  */
-internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
+internal inline fun DelegatableNode.visitSubtreeIf(
+    mask: Int,
+    zOrder: Boolean,
+    block: (Modifier.Node) -> Boolean
+) {
     checkPrecondition(node.isAttached) { "visitSubtreeIf called on an unattached node" }
     val branches = mutableVectorOf<Modifier.Node>()
     val child = node.child
-    if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+    if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
     outer@ while (branches.isNotEmpty()) {
         val branch = branches.removeAt(branches.size - 1)
         if (branch.aggregateChildKindSet and mask != 0) {
@@ -176,7 +165,7 @@
                 node = node.child
             }
         }
-        branches.addLayoutNodeChildren(branch)
+        branches.addLayoutNodeChildren(branch, zOrder)
     }
 }
 
@@ -264,33 +253,41 @@
     return null
 }
 
-internal inline fun <reified T> DelegatableNode.visitSubtree(
-    type: NodeKind<T>,
-    block: (T) -> Unit
-) = visitSubtree(type.mask) { it.dispatchForKind(type, block) }
-
 internal inline fun <reified T> DelegatableNode.visitChildren(
     type: NodeKind<T>,
+    zOrder: Boolean = false,
     block: (T) -> Unit
-) = visitChildren(type.mask) { it.dispatchForKind(type, block) }
+) = visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
 
 internal inline fun <reified T> DelegatableNode.visitSelfAndChildren(
     type: NodeKind<T>,
+    zOrder: Boolean = false,
     block: (T) -> Unit
 ) {
     node.dispatchForKind(type, block)
-    visitChildren(type.mask) { it.dispatchForKind(type, block) }
+    visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
 }
 
 internal inline fun <reified T> DelegatableNode.visitSubtreeIf(
     type: NodeKind<T>,
+    zOrder: Boolean = false,
     block: (T) -> Boolean
 ) =
-    visitSubtreeIf(type.mask) foo@{ node ->
+    visitSubtreeIf(type.mask, zOrder) foo@{ node ->
         node.dispatchForKind(type) { if (!block(it)) return@foo false }
         true
     }
 
+internal inline fun <reified T> DelegatableNode.visitSubtree(
+    type: NodeKind<T>,
+    zOrder: Boolean = false,
+    block: (T) -> Unit
+) =
+    visitSubtreeIf(type.mask, zOrder) {
+        it.dispatchForKind(type, block)
+        true
+    }
+
 internal fun DelegatableNode.has(type: NodeKind<*>): Boolean =
     node.aggregateChildKindSet and type.mask != 0
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
index a4d667fb..fe22dff 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -94,9 +94,9 @@
     /** The alignment lines of this layout, inherited + intrinsic */
     private val alignmentLineMap: MutableMap<AlignmentLine, Int> = hashMapOf()
 
-    fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLineMap
+    fun getLastCalculation(): Map<out AlignmentLine, Int> = alignmentLineMap
 
-    protected abstract val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+    protected abstract val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
 
     protected abstract fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int
 
@@ -201,7 +201,7 @@
 internal class LayoutNodeAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
     AlignmentLines(alignmentLinesOwner) {
 
-    override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+    override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
         get() = measureResult.alignmentLines
 
     override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
@@ -215,7 +215,7 @@
 internal class LookaheadAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
     AlignmentLines(alignmentLinesOwner) {
 
-    override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+    override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
         get() = lookaheadDelegate!!.measureResult.alignmentLines
 
     override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 3df4e68..bc03b51 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -950,7 +950,7 @@
             return true
         }
 
-        override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+        override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
             if (!duringAlignmentLinesQuery) {
                 // Mark alignments used by modifier
                 if (layoutState == LayoutState.Measuring) {
@@ -1278,7 +1278,7 @@
             }
         }
 
-        override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+        override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
             if (!duringAlignmentLinesQuery) {
                 if (layoutState == LayoutState.LookaheadMeasuring) {
                     // Mark alignments used by modifier
@@ -1894,7 +1894,7 @@
     fun layoutChildren()
 
     /** Recalculate the alignment lines if dirty, and layout children as needed. */
-    fun calculateAlignmentLines(): Map<AlignmentLine, Int>
+    fun calculateAlignmentLines(): Map<out AlignmentLine, Int>
 
     /**
      * Parent [AlignmentLinesOwner]. This will be the AlignmentLinesOwner for the same pass but for
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index f5e4439..8bf8d59 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -203,7 +203,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: PlacementScope.() -> Unit
     ): MeasureResult {
@@ -215,7 +215,7 @@
             override val height: Int
                 get() = height
 
-            override val alignmentLines: Map<AlignmentLine, Int>
+            override val alignmentLines: Map<out AlignmentLine, Int>
                 get() = alignmentLines
 
             override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
deleted file mode 100644
index 7f93d07..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
+++ /dev/null
@@ -1,58 +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.compose.ui.node
-
-import androidx.compose.runtime.collection.MutableVector
-
-internal class NestedVectorStack<T> {
-    // number of vectors in the stack
-    private var size = 0
-    // holds the current "top" index for each vector
-    private var currentIndexes = IntArray(16)
-    private var vectors = arrayOfNulls<MutableVector<T>>(16)
-
-    fun isNotEmpty(): Boolean {
-        return size > 0 && currentIndexes[size - 1] >= 0
-    }
-
-    fun pop(): T {
-        check(size > 0) { "Cannot call pop() on an empty stack. Guard with a call to isNotEmpty()" }
-        val indexOfVector = size - 1
-        val indexOfItem = currentIndexes[indexOfVector]
-        val vector = vectors[indexOfVector]!!
-        if (indexOfItem > 0) currentIndexes[indexOfVector]--
-        else if (indexOfItem == 0) {
-            vectors[indexOfVector] = null
-            size--
-        }
-        return vector[indexOfItem]
-    }
-
-    fun push(vector: MutableVector<T>) {
-        // if the vector is empty there is no reason for us to add it
-        if (vector.isEmpty()) return
-        val nextIndex = size
-        // check to see that we have capacity to add another vector
-        if (nextIndex >= currentIndexes.size) {
-            currentIndexes = currentIndexes.copyOf(currentIndexes.size * 2)
-            vectors = vectors.copyOf(vectors.size * 2)
-        }
-        currentIndexes[nextIndex] = vector.size - 1
-        vectors[nextIndex] = vector
-        size++
-    }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index b522a01..9a1ba69 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -1407,7 +1407,7 @@
 @Suppress("PrimitiveInCollection")
 private fun compareEquals(
     a: MutableObjectIntMap<AlignmentLine>?,
-    b: Map<AlignmentLine, Int>
+    b: Map<out AlignmentLine, Int>
 ): Boolean {
     if (a == null) return false
     if (a.size != b.size) return false
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index ed0ed4d..007491b 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -37,6 +37,7 @@
 import androidx.test.filters.MediumTest;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -365,6 +366,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     public void testEnqueueOne() throws Throwable {
         initStatics();
 
@@ -386,6 +388,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     public void testEnqueueMultiple() throws Throwable {
         initStatics();
 
@@ -410,6 +413,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     public void testEnqueueSubWork() throws Throwable {
         initStatics();
 
@@ -439,6 +443,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     @RequiresApi(26)
     public void testStopWhileWorking() throws Throwable {
         if (Build.VERSION.SDK_INT < 26) {
diff --git a/development/update_studio.sh b/development/update_studio.sh
index cdd888e..3fa7ebb 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -48,7 +48,7 @@
 sed -i "s/androidStudio = .*/androidStudio = \"$STUDIO_VERSION\"/g" gradle/libs.versions.toml
 
 # update settings.gradle
-sed -i "s/com.android.settings:com.android.settings.gradle.plugin:.*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
+sed -i "s/com.android.settings:com.android.settings.gradle.plugin:[0-9a-z\.\-]*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
 
 # Pull all UTP artifacts for ADT version
 ADT_VERSION=${3:-$LINT_VERSION}
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -57,6 +57,16 @@
   public static final class BrushFamily.Companion {
   }
 
+  public final class BrushUtil {
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
   }
 
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -57,6 +57,16 @@
   public static final class BrushFamily.Companion {
   }
 
+  public final class BrushUtil {
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
   }
 
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 2483290..353b80f 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -50,9 +50,9 @@
     jvmAndroidMain {
       dependsOn(commonMain)
       dependencies {
-        implementation(libs.kotlinStdlib)
         api(libs.androidx.annotation)
-        implementation(project(":collection:collection"))
+        implementation(libs.kotlinStdlib)
+        implementation("androidx.collection:collection:1.4.3")
         implementation(project(":ink:ink-geometry"))
         implementation(project(":ink:ink-nativeloader"))
       }
@@ -100,5 +100,4 @@
     type = LibraryType.PUBLISHED_LIBRARY
     inceptionYear = "2024"
     description = "Define brushes for freehand input."
-    metalavaK2UastEnabled = false
 }
diff --git a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
index b23e689..1a277fe 100644
--- a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
+++ b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
@@ -41,7 +41,7 @@
     private val testFamily = BrushFamily(uri = "/brush-family:pencil")
 
     @Test
-    fun brushGetAndroidColor_getsCorrectColor() {
+    fun brushCreateAndroidColor_getsCorrectColor() {
         val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
 
         // Note that expectedColor is not necessarily the same as testColor, because of precision
@@ -50,7 +50,7 @@
         // the
         // color internally as a ColorLong anyway).
         val expectedColor = AndroidColor.valueOf(testColorLong)
-        assertThat(brush.getAndroidColor()).isEqualTo(expectedColor)
+        assertThat(brush.createAndroidColor()).isEqualTo(expectedColor)
     }
 
     @Test
@@ -97,7 +97,7 @@
     }
 
     @Test
-    fun brushBuilderAndroidColor_setsColor() {
+    fun brushBuilderSetAndroidColor_setsColor() {
         val brush =
             Brush.builder()
                 .setFamily(testFamily)
@@ -110,7 +110,7 @@
     }
 
     @Test
-    fun brushBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+    fun brushBuilderSetAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
         val brush =
             Brush.builder()
@@ -126,13 +126,13 @@
     }
 
     @Test
-    fun brushWithAndroidColor_createsBrushWithColor() {
+    fun brushCreateWithAndroidColor_createsBrushWithColor() {
         val brush = Brush.createWithAndroidColor(testFamily, testColor, 1f, 1f)
         assertThat(brush.colorLong).isEqualTo(testColorLong)
     }
 
     @Test
-    fun brushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+    fun brushCreateWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
         val brush = Brush.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
 
@@ -142,21 +142,10 @@
     }
 
     @Test
-    fun brushUtilGetAndroidColor_getsCorrectColor() {
-        val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
-
-        // Note that expectedColor is not necessarily the same as testColor, because of precision
-        // loss
-        // when converting from testColor to testColorLong.
-        val expectedColor = AndroidColor.valueOf(testColorLong)
-        assertThat(BrushUtil.getAndroidColor(brush)).isEqualTo(expectedColor)
-    }
-
-    @Test
-    fun brushUtilToBuilderWithAndroidColor_setsColor() {
+    fun brushToBuilderWithAndroidColor_setsColor() {
         val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
 
-        val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, testColor).build()
+        val newBrush = brush.toBuilderWithAndroidColor(testColor).build()
 
         assertThat(newBrush.colorLong).isEqualTo(testColorLong)
         assertThat(brush.family).isEqualTo(testFamily)
@@ -165,11 +154,11 @@
     }
 
     @Test
-    fun brushUtilToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+    fun brushToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
         val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
 
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
-        val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, unsupportedColor).build()
+        val newBrush = brush.toBuilderWithAndroidColor(unsupportedColor).build()
 
         // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
         val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
@@ -181,9 +170,9 @@
     }
 
     @Test
-    fun brushUtilMakeBuilderWithAndroidColor_setsColor() {
+    fun createBrushBuilderWithAndroidColor_setsColor() {
         val brush =
-            BrushUtil.createBuilderWithAndroidColor(testColor)
+            createBrushBuilderWithAndroidColor(testColor)
                 .setFamily(testFamily)
                 .setSize(2f)
                 .setEpsilon(0.2f)
@@ -196,10 +185,10 @@
     }
 
     @Test
-    fun brushUtilMakeBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+    fun createBrushBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
         val brush =
-            BrushUtil.createBuilderWithAndroidColor(unsupportedColor)
+            createBrushBuilderWithAndroidColor(unsupportedColor)
                 .setFamily(testFamily)
                 .setSize(2f)
                 .setEpsilon(0.2f)
@@ -209,20 +198,4 @@
         val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
         assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
     }
-
-    @Test
-    fun brushUtilMakeBrushWithAndroidColor_createsBrushWithColor() {
-        val brush = BrushUtil.createWithAndroidColor(testFamily, testColor, 1f, 1f)
-        assertThat(brush.colorLong).isEqualTo(testColorLong)
-    }
-
-    @Test
-    fun brushUtilMakeBrushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
-        val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
-        val brush = BrushUtil.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
-
-        // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
-        val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
-        assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
-    }
 }
diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
index a1a5bdc..ff89d66 100644
--- a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
+++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@file:JvmName("BrushUtil")
 
 package androidx.ink.brush
 
@@ -22,17 +22,20 @@
 import android.os.Build
 import androidx.annotation.CheckResult
 import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
 
 /**
  * The brush color as an [android.graphics.Color] instance, which can express colors in several
  * different color spaces. sRGB and Display P3 are supported; a color in any other color space will
  * be converted to Display P3.
+ *
+ * Unless an instance of [android.graphics.Color] is actually needed, prefer to use
+ * [Brush.colorLong] to get the color without causing an allocation, especially in
+ * performance-sensitive code. [Brush.colorLong] is fully compatible with the [Long] representation
+ * of [android.graphics.Color].
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
-public fun Brush.getAndroidColor(): AndroidColor = BrushUtil.getAndroidColor(this)
+public fun Brush.createAndroidColor(): AndroidColor = AndroidColor.valueOf(colorLong)
 
 /**
  * Creates a copy of `this` [Brush] and allows named properties to be altered while keeping the rest
@@ -40,7 +43,6 @@
  * several different color spaces. sRGB and Display P3 are supported; a color in any other color
  * space will be converted to Display P3.
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
 public fun Brush.copyWithAndroidColor(
@@ -53,19 +55,53 @@
 /**
  * Set the color on a [Brush.Builder] as an [android.graphics.Color] instance. sRGB and Display P3
  * are supported; a color in any other color space will be converted to Display P3.
+ *
+ * Java callers should prefer [toBuilderWithAndroidColor] or [createBrushBuilderWithAndroidColor] as
+ * a more fluent API.
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
 public fun Brush.Builder.setAndroidColor(color: AndroidColor): Brush.Builder =
     setColorLong(color.pack())
 
 /**
+ * Returns a [Brush.Builder] with values set equivalent to the [Brush] and the color specified by an
+ * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
+ * Display P3 are supported; a color in any other color space will be converted to Display P3. Java
+ * developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
+ * [copyWithAndroidColor] method.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.toBuilder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.toBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+    toBuilder().setAndroidColor(color)
+
+/**
+ * Returns a new, blank [Brush.Builder] with the color specified by an [android.graphics.Color]
+ * instance, which can encode several different color spaces. sRGB and Display P3 are supported; a
+ * color in any other color space will be converted to Display P3.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.builder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@JvmName("createBuilderWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+    Brush.Builder().setAndroidColor(color)
+
+/**
  * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
  * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
  * color space will be converted to Display P3.
+ *
+ * Java callers should prefer `BrushUtil.createWithAndroidColor` ([createBrushWithAndroidColor]).
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
 public fun Brush.Companion.createWithAndroidColor(
@@ -73,57 +109,21 @@
     color: AndroidColor,
     size: Float,
     epsilon: Float,
-): Brush = BrushUtil.createWithAndroidColor(family, color, size, epsilon)
+): Brush = createWithColorLong(family, color.pack(), size, epsilon)
 
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public object BrushUtil {
-
-    /**
-     * The brush color as an [android.graphics.Color] instance, which can express colors in several
-     * different color spaces. sRGB and Display P3 are supported; a color in any other color space
-     * will be converted to Display P3.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun getAndroidColor(brush: Brush): AndroidColor = AndroidColor.valueOf(brush.colorLong)
-
-    /**
-     * Returns a [Brush.Builder] with values set equivalent to [brush] and the color specified by an
-     * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
-     * Display P3 are supported; a color in any other color space will be converted to Display P3.
-     * Java developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
-     * [copyWithAndroidColor] method.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun toBuilderWithAndroidColor(brush: Brush, color: AndroidColor): Brush.Builder =
-        brush.toBuilder().setAndroidColor(color)
-
-    /**
-     * Returns a new [Brush.Builder] with the color specified by an [android.graphics.Color]
-     * instance, which can encode several different color spaces. sRGB and Display P3 are supported;
-     * a color in any other color space will be converted to Display P3.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun createBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
-        Brush.Builder().setAndroidColor(color)
-
-    /**
-     * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which
-     * can encode several different color spaces. sRGB and Display P3 are supported; a color in any
-     * other color space will be converted to Display P3.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun createWithAndroidColor(
-        family: BrushFamily,
-        color: AndroidColor,
-        size: Float,
-        epsilon: Float,
-    ): Brush = Brush.createWithColorLong(family, color.pack(), size, epsilon)
-}
+/**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
+ * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
+ * color space will be converted to Display P3.
+ *
+ * Kotlin callers should prefer [Brush.Companion.createWithAndroidColor].
+ */
+@JvmName("createWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushWithAndroidColor(
+    family: BrushFamily,
+    color: AndroidColor,
+    size: Float,
+    epsilon: Float,
+): Brush = Brush.createWithAndroidColor(family, color, size, epsilon)
diff --git a/ink/ink-geometry/api/current.txt b/ink/ink-geometry/api/current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/current.txt
+++ b/ink/ink-geometry/api/current.txt
@@ -65,6 +65,7 @@
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+    method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
   public final class Intersection {
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+    method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
   public static final class Parallelogram.Companion {
   }
 
+  public final class PartitionedMesh {
+    method public androidx.ink.geometry.Box? computeBoundingBox();
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method protected void finalize();
+    method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+    method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+    method @IntRange(from=0L) public int getRenderGroupCount();
+    method public void initializeSpatialIndex();
+    method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+    field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+  }
+
+  public static final class PartitionedMesh.Companion {
+  }
+
   public abstract class Segment {
     method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
     method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/api/restricted_current.txt b/ink/ink-geometry/api/restricted_current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/restricted_current.txt
+++ b/ink/ink-geometry/api/restricted_current.txt
@@ -65,6 +65,7 @@
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+    method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
   public final class Intersection {
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+    method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
   public static final class Parallelogram.Companion {
   }
 
+  public final class PartitionedMesh {
+    method public androidx.ink.geometry.Box? computeBoundingBox();
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method protected void finalize();
+    method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+    method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+    method @IntRange(from=0L) public int getRenderGroupCount();
+    method public void initializeSpatialIndex();
+    method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+    field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+  }
+
+  public static final class PartitionedMesh.Companion {
+  }
+
   public abstract class Segment {
     method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
     method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
index 79501d8..dde4d23 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
@@ -227,8 +227,7 @@
      *
      * @return `this`
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-    public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.bounds)
+    public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.computeBoundingBox())
 
     /**
      * Compares this [BoxAccumulator] with [other], and returns true if either: Both this and
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
index 1ee275d..fcdf2f5 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
@@ -16,7 +16,6 @@
 
 package androidx.ink.geometry
 
-import androidx.annotation.RestrictTo
 import androidx.ink.nativeloader.NativeLoader
 
 /**
@@ -116,7 +115,6 @@
      * intersection of the point in [mesh]’s object coordinates.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Vec.intersects(mesh: PartitionedMesh, meshToPoint: AffineTransform): Boolean {
         return nativeMeshVecIntersects(
             nativeMeshAddress = mesh.getNativeAddress(),
@@ -218,7 +216,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Segment.intersects(mesh: PartitionedMesh, meshToSegment: AffineTransform): Boolean {
         return nativeMeshSegmentIntersects(
             nativeMeshAddress = mesh.getNativeAddress(),
@@ -311,7 +308,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Triangle.intersects(
         mesh: PartitionedMesh,
         meshToTriangle: AffineTransform
@@ -382,7 +378,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Box.intersects(mesh: PartitionedMesh, meshToBox: AffineTransform): Boolean {
         return nativeMeshBoxIntersects(
             nativeMeshAddress = mesh.getNativeAddress(),
@@ -436,7 +431,6 @@
      * checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Parallelogram.intersects(
         mesh: PartitionedMesh,
         meshToParallelogram: AffineTransform,
@@ -467,7 +461,6 @@
      * coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         other: PartitionedMesh,
         thisToCommonTransForm: AffineTransform,
@@ -566,7 +559,6 @@
      * intersection of the point in [mesh]’s object coordinates.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
         point.intersects(this, meshToPoint)
 
@@ -578,7 +570,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         segment: Segment,
         meshToSegment: AffineTransform
@@ -592,7 +583,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         triangle: Triangle,
         meshToTriangle: AffineTransform,
@@ -606,7 +596,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(box: Box, meshToBox: AffineTransform): Boolean =
         box.intersects(this, meshToBox)
 
@@ -619,7 +608,6 @@
      * checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         parallelogram: Parallelogram,
         meshToParallelogram: AffineTransform,
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
index d279937..6f186f6 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
@@ -25,23 +25,22 @@
 import androidx.ink.nativeloader.NativeLoader
 
 /**
- * An immutable† complex shape expressed as a set of triangles. This is used to represent the shape
- * of a stroke or other complex objects see [MeshCreation]. The mesh may be divided into multiple
- * partitions, which enables certain brush effects (e.g. "multi-coat"), and allows ink to create
- * strokes requiring greater than 216 triangles (which must be rendered in multiple passes).
+ * An immutable** complex shape expressed as a set of triangles. This is used to represent the shape
+ * of a stroke or other complex objects. The mesh may be divided into multiple partitions, which
+ * enables certain brush effects (e.g. "multi-coat"), and allows strokes to be created using greater
+ * than 2^16 triangles (which must be rendered in multiple passes).
  *
- * A PartitionedMesh may optionally have one or more "outlines", which are polylines that traverse
+ * A [PartitionedMesh] may optionally have one or more "outlines", which are polylines that traverse
  * some or all of the vertices in the mesh; these are used for path-based rendering of strokes. This
  * supports disjoint meshes such as dashed lines.
  *
- * PartitionedMesh provides fast intersection and coverage testing by use of an internal spatial
+ * [PartitionedMesh] provides fast intersection and coverage testing by use of an internal spatial
  * index.
  *
- * † PartitionedMesh is technically not immutable, as the spatial index is lazily instantiated;
+ * ** [PartitionedMesh] is technically not immutable, as the spatial index is lazily instantiated;
  * however, from the perspective of a caller, its properties do not change over the course of its
  * lifetime. The entire object is thread-safe.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
 @Suppress("NotCloseable") // Finalize is only used to free the native peer.
 public class PartitionedMesh
 /** Only for use within the ink library. Constructs a [PartitionedMesh] from native pointer. */
@@ -73,42 +72,53 @@
     @VisibleForTesting internal constructor() : this(ModeledShapeNative.alloc())
 
     /**
-     * The number of render groups in this mesh. Each outline in the [PartitionedMesh] belongs to
-     * exactly one render group, which are numbered in z-order: the group with index zero should be
-     * rendered on bottom; the group with the highest index should be rendered on top.
+     * Returns the number of render groups in this mesh. Each outline in the [PartitionedMesh]
+     * belongs to exactly one render group, which are numbered in z-order: the group with index zero
+     * should be rendered on bottom; the group with the highest index should be rendered on top.
      */
     @IntRange(from = 0)
-    public val renderGroupCount: Int =
+    public fun getRenderGroupCount(): Int =
         ModeledShapeNative.getRenderGroupCount(nativeAddress).also { check(it >= 0) }
 
     /** The [Mesh] objects that make up this shape. */
     private val meshesByGroup: List<List<Mesh>> = buildList {
-        for (groupIndex in 0 until renderGroupCount) {
+        for (groupIndex in 0 until getRenderGroupCount()) {
             val nativeAddressesOfMeshes =
                 ModeledShapeNative.getNativeAddressesOfMeshes(nativeAddress, groupIndex)
             add(nativeAddressesOfMeshes.map(::Mesh))
         }
     }
 
+    private var _bounds: Box? = null
+
     /**
-     * The minimum bounding box of the [PartitionedMesh]. This will be null if the [PartitionedMesh]
-     * is empty.
+     * Returns the minimum bounding box of the [PartitionedMesh]. This will be null if the
+     * [PartitionedMesh] is empty.
      */
-    public val bounds: Box? = run {
+    public fun computeBoundingBox(): Box? {
+        // If we've already computed the bounding box, re-use it -- it won't change over the
+        // lifetime of
+        // this object.
+        if (_bounds != null) return _bounds
+
+        // If we have no meshes, then the bounding box is null.
+        if (meshesByGroup.isEmpty()) return null
+
         val envelope = BoxAccumulator()
         for (meshes in meshesByGroup) {
             for (mesh in meshes) {
                 envelope.add(mesh.bounds)
             }
         }
-        envelope.box
+        _bounds = envelope.box
+        return envelope.box
     }
 
     /** Returns the [MeshFormat] used for each [Mesh] in the specified render group. */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
     public fun renderGroupFormat(@IntRange(from = 0) groupIndex: Int): MeshFormat {
-        require(groupIndex >= 0 && groupIndex < renderGroupCount) {
-            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+        require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
         }
         return MeshFormat(ModeledShapeNative.getRenderGroupFormat(nativeAddress, groupIndex))
     }
@@ -119,51 +129,51 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
     public fun renderGroupMeshes(@IntRange(from = 0) groupIndex: Int): List<Mesh> {
-        require(groupIndex >= 0 && groupIndex < renderGroupCount) {
-            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+        require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
         }
         return meshesByGroup[groupIndex]
     }
 
-    /** The number of outlines that comprise this shape. */
+    /** Returns the number of outlines that comprise this shape. */
     @IntRange(from = 0)
-    public fun outlineCount(@IntRange(from = 0) groupIndex: Int): Int {
-        require(groupIndex >= 0 && groupIndex < renderGroupCount) {
-            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+    public fun getOutlineCount(@IntRange(from = 0) groupIndex: Int): Int {
+        require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
         }
         return ModeledShapeNative.getOutlineCount(nativeAddress, groupIndex).also { check(it >= 0) }
     }
 
     /**
-     * The number of vertices that are in the outline at index [outlineIndex], and within the render
-     * group at [groupIndex].
+     * Returns the number of vertices that are in the outline at index [outlineIndex], and within
+     * the render group at [groupIndex].
      */
     @IntRange(from = 0)
-    public fun outlineVertexCount(
+    public fun getOutlineVertexCount(
         @IntRange(from = 0) groupIndex: Int,
         @IntRange(from = 0) outlineIndex: Int,
     ): Int {
-        require(outlineIndex >= 0 && outlineIndex < outlineCount(groupIndex)) {
-            "outlineIndex=$outlineIndex must be between 0 and outlineCount=${outlineCount(groupIndex)}"
+        require(outlineIndex >= 0 && outlineIndex < getOutlineCount(groupIndex)) {
+            "outlineIndex=$outlineIndex must be between 0 and outlineCount=${getOutlineCount(groupIndex)}"
         }
         return ModeledShapeNative.getOutlineVertexCount(nativeAddress, groupIndex, outlineIndex)
             .also { check(it >= 0) }
     }
 
     /**
-     * Retrieve the outline vertex position from the outline at index [outlineIndex] (which can be
-     * up to, but not including, [outlineCount]), and the vertex from within that outline at index
-     * [outlineVertexIndex] (which can be up to, but not including, the result of calling
-     * [outlineVertexCount] with [outlineIndex]). The resulting x/y position of that outline vertex
-     * will be put into [outPosition], which can be pre-allocated and reused to avoid allocations.
+     * Populates [outPosition] with the position of the outline vertex at [outlineVertexIndex] in
+     * the outline at [outlineIndex] in the render group at [groupIndex], and returns [outPosition].
+     * [groupIndex] must be less than [getRenderGroupCount], [outlineIndex] must be less
+     * [getOutlineVertexCount] for [groupIndex], and [outlineVertexIndex] must be less than
+     * [getOutlineVertexCount] for [groupIndex] and [outlineIndex].
      */
     public fun populateOutlinePosition(
         @IntRange(from = 0) groupIndex: Int,
         @IntRange(from = 0) outlineIndex: Int,
         @IntRange(from = 0) outlineVertexIndex: Int,
         outPosition: MutableVec,
-    ) {
-        val outlineVertexCount = outlineVertexCount(groupIndex, outlineIndex)
+    ): MutableVec {
+        val outlineVertexCount = getOutlineVertexCount(groupIndex, outlineIndex)
         require(outlineVertexIndex >= 0 && outlineVertexIndex < outlineVertexCount) {
             "outlineVertexIndex=$outlineVertexIndex must be between 0 and " +
                 "outlineVertexCount($outlineVertexIndex)=$outlineVertexCount"
@@ -178,6 +188,7 @@
         val (meshIndex, meshVertexIndex) = scratchIntArray
         val mesh = meshesByGroup[groupIndex][meshIndex]
         mesh.fillPosition(meshVertexIndex, outPosition)
+        return outPosition
     }
 
     /**
@@ -187,18 +198,18 @@
      * triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
      * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
      * loops back over itself) are counted individually. Note that, if any triangles have negative
-     * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
-     * value of their area will be used instead.
+     * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+     * will be used instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(
+    public fun computeCoverage(
         triangle: Triangle,
         triangleToThis: AffineTransform = AffineTransform.IDENTITY,
     ): Float =
@@ -225,17 +236,20 @@
      * [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space. Triangles in the
      * [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that loops back over
      * itself) are counted individually. Note that, if any triangles have negative area (due to
-     * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
-     * area will be used instead.
+     * winding, see [Triangle.computeSignedArea]), the absolute value of their area will be used
+     * instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
-     * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+     * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(box: Box, boxToThis: AffineTransform = AffineTransform.IDENTITY): Float =
+    public fun computeCoverage(
+        box: Box,
+        boxToThis: AffineTransform = AffineTransform.IDENTITY
+    ): Float =
         ModeledShapeNative.modeledShapeBoxCoverage(
             nativeAddress = nativeAddress,
             boxXMin = box.xMin,
@@ -257,18 +271,18 @@
      * of all triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
      * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
      * loops back over itself) are counted individually. Note that, if any triangles have negative
-     * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
-     * value of their area will be used instead.
+     * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+     * will be used instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [parallelogramToThis] contains the transform that maps from
      * [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
-     * defaults to the [IDENTITY].
+     * defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(
+    public fun computeCoverage(
         parallelogram: Parallelogram,
         parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
     ): Float =
@@ -295,18 +309,18 @@
      * triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
      * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
      * loops back over itself) are counted individually. Note that, if any triangles have negative
-     * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
-     * value of their area will be used instead.t
+     * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+     * will be used instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(
+    public fun computeCoverage(
         other: PartitionedMesh,
         otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
     ): Float =
@@ -327,7 +341,7 @@
      *
      * This is equivalent to:
      * ```
-     * this.coverage(triangle, triangleToThis) > coverageThreshold
+     * computeCoverage(triangle, triangleToThis) > coverageThreshold
      * ```
      *
      * but may be faster.
@@ -335,11 +349,11 @@
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         triangle: Triangle,
         coverageThreshold: Float,
         triangleToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -375,10 +389,10 @@
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
-     * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+     * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         box: Box,
         coverageThreshold: Float,
         boxToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -413,10 +427,10 @@
      *
      * Optional argument [parallelogramToThis] contains the transform that maps from
      * [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
-     * defaults to the [IDENTITY].
+     * defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         parallelogram: Parallelogram,
         coverageThreshold: Float,
         parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -452,11 +466,11 @@
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         other: PartitionedMesh,
         coverageThreshold: Float,
         otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -488,7 +502,7 @@
 
     override fun toString(): String {
         val address = java.lang.Long.toHexString(nativeAddress)
-        return "PartitionedMesh(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
+        return "PartitionedMesh(bounds=${computeBoundingBox()}, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
     }
 
     protected fun finalize() {
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
index 10bf22b..50106fe 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
@@ -31,35 +31,35 @@
 class PartitionedMeshTest {
 
     @Test
-    fun bounds_shouldBeEmpty() {
+    fun computeBoundingBox_shouldBeEmpty() {
         val partitionedMesh = PartitionedMesh()
 
-        assertThat(partitionedMesh.bounds).isNull()
+        assertThat(partitionedMesh.computeBoundingBox()).isNull()
     }
 
     @Test
-    fun renderGroupCount_whenEmptyShape_shouldBeZero() {
+    fun getRenderGroupCount_whenEmptyShape_shouldBeZero() {
         val partitionedMesh = PartitionedMesh()
 
-        assertThat(partitionedMesh.renderGroupCount).isEqualTo(0)
+        assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(0)
     }
 
     @Test
-    fun outlineCount_whenEmptyShape_shouldThrow() {
+    fun getOutlineCount_whenEmptyShape_shouldThrow() {
         val partitionedMesh = PartitionedMesh()
 
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(-1) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(0) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(1) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(-1) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(1) }
     }
 
     @Test
-    fun outlineVertexCount_whenEmptyShape_shouldThrow() {
+    fun getOutlineVertexCount_whenEmptyShape_shouldThrow() {
         val partitionedMesh = PartitionedMesh()
 
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(-1, 0) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(0, 0) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(1, 0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(-1, 0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(0, 0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(1, 0) }
     }
 
     @Test
@@ -92,14 +92,14 @@
     fun populateOutlinePosition_withStrokeShape_shouldBeWithinBounds() {
         val shape = buildTestStrokeShape()
 
-        assertThat(shape.renderGroupCount).isEqualTo(1)
-        assertThat(shape.outlineCount(0)).isEqualTo(1)
-        assertThat(shape.outlineVertexCount(0, 0)).isGreaterThan(2)
+        assertThat(shape.getRenderGroupCount()).isEqualTo(1)
+        assertThat(shape.getOutlineCount(0)).isEqualTo(1)
+        assertThat(shape.getOutlineVertexCount(0, 0)).isGreaterThan(2)
 
-        val bounds = assertNotNull(shape.bounds)
+        val bounds = assertNotNull(shape.computeBoundingBox())
 
         val p = MutableVec()
-        for (outlineVertexIndex in 0 until shape.outlineVertexCount(0, 0)) {
+        for (outlineVertexIndex in 0 until shape.getOutlineVertexCount(0, 0)) {
             shape.populateOutlinePosition(groupIndex = 0, outlineIndex = 0, outlineVertexIndex, p)
             assertThat(p.x).isAtLeast(bounds.xMin)
             assertThat(p.y).isAtLeast(bounds.yMin)
@@ -124,7 +124,7 @@
     @Test
     fun meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMesh() {
         val partitionedMesh = buildTestStrokeShape()
-        assertThat(partitionedMesh.renderGroupCount).isEqualTo(1)
+        assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(1)
         val shapeFormat = partitionedMesh.renderGroupFormat(0)
         val meshes = partitionedMesh.renderGroupMeshes(0)
         assertThat(meshes).isNotEmpty()
@@ -152,9 +152,9 @@
                 p2 = ImmutableVec(100f, 700f),
             )
 
-        assertThat(partitionedMesh.coverage(intersectingTriangle)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalTriangle)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingTriangle)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalTriangle)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
     }
 
     /**
@@ -169,9 +169,9 @@
         val externalBox =
             ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
 
-        assertThat(partitionedMesh.coverage(intersectingBox)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalBox)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingBox)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalBox)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
     }
 
     /**
@@ -196,9 +196,10 @@
                 shearFactor = 2f,
             )
 
-        assertThat(partitionedMesh.coverage(intersectingParallelogram)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalParallelogram)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalParallelogram, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingParallelogram)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalParallelogram)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalParallelogram, SCALE_TRANSFORM))
+            .isEqualTo(0f)
     }
 
     /**
@@ -221,9 +222,9 @@
                 )
                 .shape
 
-        assertThat(partitionedMesh.coverage(intersectingShape)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalShape)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingShape)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalShape)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
     }
 
     /**
@@ -246,9 +247,11 @@
                 p2 = ImmutableVec(100f, 700f),
             )
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f)).isFalse()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM))
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f)).isFalse()
+        assertThat(
+                partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM)
+            )
             .isFalse()
     }
 
@@ -270,9 +273,9 @@
         val externalBox =
             ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingBox, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f)).isFalse()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingBox, 0f)).isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f)).isFalse()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
             .isFalse()
     }
 
@@ -298,10 +301,16 @@
                 shearFactor = 2f,
             )
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingParallelogram, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f)).isFalse()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingParallelogram, 0f))
+            .isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalParallelogram, 0f))
+            .isFalse()
         assertThat(
-                partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f, SCALE_TRANSFORM)
+                partitionedMesh.computeCoverageIsGreaterThan(
+                    externalParallelogram,
+                    0f,
+                    SCALE_TRANSFORM
+                )
             )
             .isFalse()
     }
@@ -333,9 +342,9 @@
                 )
                 .shape
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingShape, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f)).isFalse()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingShape, 0f)).isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f)).isFalse()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
             .isFalse()
     }
 
@@ -360,7 +369,7 @@
             )
         assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
 
-        assertThat(partitionedMesh.coverage(triangle)).isNotNaN()
+        assertThat(partitionedMesh.computeCoverage(triangle)).isNotNaN()
 
         assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
     }
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
index 050e442..5b262d7 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
@@ -125,8 +125,8 @@
      */
     internal constructor(nativeAddress: Long, brush: Brush) {
         val shape = PartitionedMesh(StrokeJni.allocShallowCopyOfShape(nativeAddress))
-        require(shape.renderGroupCount == brush.family.coats.size) {
-            "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+        require(shape.getRenderGroupCount() == brush.family.coats.size) {
+            "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
         }
         this.nativeAddress = nativeAddress
         this.brush = brush
@@ -142,8 +142,8 @@
      * [PartitionedMesh] is being stored in addition to the [Brush] and [StrokeInputBatch].
      */
     public constructor(brush: Brush, inputs: StrokeInputBatch, shape: PartitionedMesh) {
-        require(shape.renderGroupCount == brush.family.coats.size) {
-            "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+        require(shape.getRenderGroupCount() == brush.family.coats.size) {
+            "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
         }
         this.brush = brush
         this.shape = shape
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
index c730120..2155aff 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
@@ -68,7 +68,7 @@
         // Create a [ModeledShape] with render group.
         val inputs = makeTestInputs()
         val shape = Stroke(buildTestBrush(), inputs).shape
-        assertThat(shape.renderGroupCount).isEqualTo(1)
+        assertThat(shape.getRenderGroupCount()).isEqualTo(1)
 
         // Create a brush with two brush coats.
         val coat = BrushCoat(BrushTip(), BrushPaint())
diff --git a/libraryversions.toml b/libraryversions.toml
index c73bef8..75a598d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -89,9 +89,9 @@
 LEANBACK_TAB = "1.1.0-beta01"
 LEGACY = "1.1.0-alpha01"
 LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.9.0-alpha01"
+LIFECYCLE = "2.9.0-alpha02"
 LIFECYCLE_EXTENSIONS = "2.2.0"
-LINT = "1.0.0-alpha01"
+LINT = "1.0.0-alpha02"
 LOADER = "1.2.0-alpha01"
 MEDIA = "1.7.0-rc01"
 MEDIAROUTER = "1.8.0-alpha01"
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
index 897a60c..10b89a5 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
@@ -68,7 +68,7 @@
         destination.id = 1
         assertThat(destination.route).isEqualTo("route")
         assertThat(destination.id).isEqualTo(1)
-        assertThat(destination.hasDeepLink(createRoute("route").toUri())).isTrue()
+        assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
 
         destination.route = null
         assertThat(destination.route).isNull()
@@ -682,4 +682,44 @@
 
         assertThat(destination.hasRoute<TestClass>()).isFalse()
     }
+
+    @Test
+    fun routeNotAddedToDeepLink() {
+        val destination = NoOpNavigator().createDestination()
+        assertThat(destination.route).isNull()
+
+        destination.route = "route"
+        assertThat(destination.route).isEqualTo("route")
+        assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
+    }
+
+    @Test
+    fun matchRoute() {
+        val destination = NoOpNavigator().createDestination()
+
+        destination.route = "route"
+        assertThat(destination.route).isEqualTo("route")
+
+        val match = destination.matchRoute("route")
+        assertThat(match).isNotNull()
+        assertThat(match!!.destination).isEqualTo(destination)
+    }
+
+    @Test
+    fun matchRouteAfterSetNewRoute() {
+        val destination = NoOpNavigator().createDestination()
+
+        destination.route = "route"
+        assertThat(destination.route).isEqualTo("route")
+
+        val match = destination.matchRoute("route")
+        assertThat(match).isNotNull()
+        assertThat(match!!.destination).isEqualTo(destination)
+
+        destination.route = "newRoute"
+        assertThat(destination.route).isEqualTo("newRoute")
+        val match2 = destination.matchRoute("newRoute")
+        assertThat(match2).isNotNull()
+        assertThat(match2!!.destination).isEqualTo(destination)
+    }
 }
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
index 5e1c141..64768f7 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -168,9 +168,7 @@
             }
         assertThat(expected.message)
             .isEqualTo(
-                "Deep link android-app://androidx.navigation/route can't be used to " +
-                    "open destination NavDestination(0xa2bd82dc).\n" +
-                    "Following required arguments are missing: [intArg]"
+                "Cannot set route \"route\" for destination NavDestination(0x0). Following required arguments are missing: [intArg]"
             )
     }
 
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index de01d95..4225f41 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -223,14 +223,36 @@
                 id = 0
             } else {
                 require(route.isNotBlank()) { "Cannot have an empty route" }
-                val internalRoute = createRoute(route)
-                id = internalRoute.hashCode()
-                addDeepLink(internalRoute)
+
+                // make sure the route contains all required arguments
+                val tempRoute = createRoute(route)
+                val tempDeepLink = NavDeepLink.Builder().setUriPattern(tempRoute).build()
+                val missingRequiredArguments =
+                    _arguments.missingRequiredArguments { key ->
+                        key !in tempDeepLink.argumentsNames
+                    }
+                require(missingRequiredArguments.isEmpty()) {
+                    "Cannot set route \"$route\" for destination $this. " +
+                        "Following required arguments are missing: $missingRequiredArguments"
+                }
+
+                routeDeepLink = lazy { NavDeepLink.Builder().setUriPattern(tempRoute).build() }
+                id = tempRoute.hashCode()
             }
-            deepLinks.remove(deepLinks.firstOrNull { it.uriPattern == createRoute(field) })
             field = route
         }
 
+    /**
+     * This destination's unique route as a NavDeepLink.
+     *
+     * This deeplink must be kept private and segregated from the explicitly added public deeplinks
+     * to ensure that external users cannot deeplink into this destination with this routeDeepLink.
+     *
+     * This value is reassigned a new lazy value every time [route] is updated to ensure that any
+     * initialized lazy value is overwritten with the latest value.
+     */
+    private var routeDeepLink: Lazy<NavDeepLink>? = null
+
     public open val displayName: String
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get() = idName ?: id.toString()
 
@@ -346,26 +368,28 @@
     }
 
     /**
-     * Determines if this NavDestination has a deep link of this route.
+     * Determines if this NavDestination's route matches the requested route.
      *
      * @param [route] The route to match against this [NavDestination.route]
      * @return The matching [DeepLinkMatch], or null if no match was found.
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public fun matchDeepLink(route: String): DeepLinkMatch? {
-        val request = NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build()
-        val matchingDeepLink =
-            if (this is NavGraph) {
-                matchDeepLinkComprehensive(
-                    request,
-                    searchChildren = false,
-                    searchParent = false,
-                    lastVisited = this
-                )
-            } else {
-                matchDeepLink(request)
-            }
-        return matchingDeepLink
+    public fun matchRoute(route: String): DeepLinkMatch? {
+        val routeDeepLink = this.routeDeepLink?.value ?: return null
+
+        val uri = createRoute(route).toUri()
+
+        // includes matching args for path, query, and fragment
+        val matchingArguments = routeDeepLink.getMatchingArguments(uri, _arguments) ?: return null
+        val matchingPathSegments = routeDeepLink.calculateMatchingPathSegments(uri)
+        return DeepLinkMatch(
+            this,
+            matchingArguments,
+            routeDeepLink.isExactDeepLink,
+            matchingPathSegments,
+            false,
+            -1
+        )
     }
 
     /**
@@ -481,7 +505,7 @@
 
         // if no match based on routePattern, this means route contains filled in args or query
         // params
-        val matchingDeepLink = matchDeepLink(route)
+        val matchingDeepLink = matchRoute(route)
 
         // if no matchingDeepLink or mismatching destination, return false directly
         if (this != matchingDeepLink?.destination) return false
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index 3a34696..5fbe69d 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -65,6 +65,50 @@
     }
 
     /**
+     * Matches route with all children and parents recursively.
+     *
+     * Does not revisit graphs (whether it's a child or parent) if it has already been visited.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun matchRouteComprehensive(
+        route: String,
+        searchChildren: Boolean,
+        searchParent: Boolean,
+        lastVisited: NavDestination
+    ): DeepLinkMatch? {
+        // First try to match with this graph's route
+        val bestMatch = matchRoute(route)
+        // If searchChildren is true, search through all child destinations for a matching route
+        val bestChildMatch =
+            if (searchChildren) {
+                mapNotNull { child ->
+                        when (child) {
+                            lastVisited -> null
+                            is NavGraph ->
+                                child.matchRouteComprehensive(
+                                    route,
+                                    searchChildren = true,
+                                    searchParent = false,
+                                    lastVisited = this
+                                )
+                            else -> child.matchRoute(route)
+                        }
+                    }
+                    .maxOrNull()
+            } else null
+
+        // If searchParent is true, search through all parents (and their children) destinations
+        // for a matching route
+        val bestParentMatch =
+            parent?.let {
+                if (searchParent && it != lastVisited)
+                    it.matchRouteComprehensive(route, searchChildren, true, this)
+                else null
+            }
+        return listOfNotNull(bestMatch, bestChildMatch, bestParentMatch).maxOrNull()
+    }
+
+    /**
      * Matches deeplink with all children and parents recursively.
      *
      * Does not revisit graphs (whether it's a child or parent) if it has already been visited.
@@ -262,7 +306,7 @@
             nodes.valueIterator().asSequence().firstOrNull {
                 // first try matching with routePattern
                 // if not found with routePattern, try matching with route args
-                it.route.equals(route) || it.matchDeepLink(route) != null
+                it.route.equals(route) || it.matchRoute(route) != null
             }
 
         // Search the parent for the NavDestination if it is not a child of this navigation graph
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
index 79035974..a8b4a4f 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
@@ -84,7 +84,7 @@
             )
         }
         if (startRoute != null && startRoute != startDestination.route) {
-            val matchingArgs = startDestination.matchDeepLink(startRoute)?.matchingArgs
+            val matchingArgs = startDestination.matchRoute(startRoute)?.matchingArgs
             if (matchingArgs != null && !matchingArgs.isEmpty) {
                 val bundle = Bundle()
                 // we need to add args from startRoute, but it should not override existing args
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b672a68..eb7498d 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -57,10 +57,6 @@
     kotlinTarget = KotlinTarget.KOTLIN_2_0
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 android {
     compileSdk 35
     namespace "androidx.navigation.compose.samples"
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
index 838929a6..bfb1c33 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
@@ -103,65 +103,77 @@
     SharedTransitionLayout {
         val selectFirst = mutableStateOf(true)
         NavHost(navController, startDestination = RedBox) {
-            composable<RedBox> { RedBox(this, selectFirst) { navController.navigate(BlueBox) } }
-            composable<BlueBox> { BlueBox(this, selectFirst) { navController.popBackStack() } }
+            composable<RedBox> {
+                RedBox(this@SharedTransitionLayout, this, selectFirst) {
+                    navController.navigate(BlueBox)
+                }
+            }
+            composable<BlueBox> {
+                BlueBox(this@SharedTransitionLayout, this, selectFirst) {
+                    navController.popBackStack()
+                }
+            }
         }
     }
 }
 
-context(SharedTransitionScope)
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun RedBox(
+    sharedScope: SharedTransitionScope,
     scope: AnimatedContentScope,
     selectFirst: MutableState<Boolean>,
     onNavigate: () -> Unit
 ) {
-    Box(
-        Modifier.sharedBounds(
-                rememberSharedContentState("name"),
-                scope,
-                renderInOverlayDuringTransition = selectFirst.value
-            )
-            .clickable(
-                onClick = {
-                    selectFirst.value = !selectFirst.value
-                    onNavigate()
-                }
-            )
-            .background(Color.Red)
-            .size(100.dp)
-    ) {
-        Text("start", color = Color.White)
+    with(sharedScope) {
+        Box(
+            Modifier.sharedBounds(
+                    rememberSharedContentState("name"),
+                    scope,
+                    renderInOverlayDuringTransition = selectFirst.value
+                )
+                .clickable(
+                    onClick = {
+                        selectFirst.value = !selectFirst.value
+                        onNavigate()
+                    }
+                )
+                .background(Color.Red)
+                .size(100.dp)
+        ) {
+            Text("start", color = Color.White)
+        }
     }
 }
 
-context(SharedTransitionScope)
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun BlueBox(
+    sharedScope: SharedTransitionScope,
     scope: AnimatedContentScope,
     selectFirst: MutableState<Boolean>,
     onPopBack: () -> Unit
 ) {
-    Box(
-        Modifier.offset(180.dp, 180.dp)
-            .sharedBounds(
-                rememberSharedContentState("name"),
-                scope,
-                renderInOverlayDuringTransition = !selectFirst.value
-            )
-            .clickable(
-                onClick = {
-                    selectFirst.value = !selectFirst.value
-                    onPopBack()
-                }
-            )
-            .alpha(0.5f)
-            .background(Color.Blue)
-            .size(180.dp)
-    ) {
-        Text("finish", color = Color.White)
+    with(sharedScope) {
+        Box(
+            Modifier.offset(180.dp, 180.dp)
+                .sharedBounds(
+                    rememberSharedContentState("name"),
+                    scope,
+                    renderInOverlayDuringTransition = !selectFirst.value
+                )
+                .clickable(
+                    onClick = {
+                        selectFirst.value = !selectFirst.value
+                        onPopBack()
+                    }
+                )
+                .alpha(0.5f)
+                .background(Color.Blue)
+                .size(180.dp)
+        ) {
+            Text("finish", color = Color.White)
+        }
     }
 }
 
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 986c95e..c05732dc 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -29,6 +29,7 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelStore
 import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT
 import androidx.navigation.NavDestination.Companion.createRoute
 import androidx.navigation.NavDestination.Companion.hasRoute
 import androidx.navigation.serialization.generateHashCode
@@ -814,6 +815,30 @@
 
     @UiThreadTest
     @Test
+    fun testNavigateContainsIntent() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(startDestination = "start") {
+                test("start")
+                test("second")
+            }
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navController.currentDestination?.route).isEqualTo("start")
+        assertThat(navigator.backStack.size).isEqualTo(1)
+
+        navController.navigate("second")
+        assertThat(navController.currentDestination?.route).isEqualTo("second")
+        assertThat(navigator.backStack.size).isEqualTo(2)
+        val intent: Intent? =
+            @Suppress("DEPRECATION")
+            navController.currentBackStackEntry?.arguments?.getParcelable(KEY_DEEP_LINK_INTENT)
+        assertThat(intent).isNotNull()
+        assertThat(intent!!.data).isEqualTo(Uri.parse("android-app://androidx.navigation/second"))
+    }
+
+    @UiThreadTest
+    @Test
     fun testNavigateNestedSharedDestination() {
         val navController = createNavController()
         navController.graph =
@@ -1524,7 +1549,6 @@
                 test<TestClass>()
             }
         assertThat(navController.currentDestination?.route).isEqualTo("start")
-
         // passed in arg
         navController.navigate(TestClass(TestTopLevelEnum.TWO))
         assertThat(navController.currentDestination?.hasRoute(TestClass::class)).isTrue()
@@ -1725,12 +1749,10 @@
         navController.graph = nav_singleArg_graph
 
         // first nav with arg filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
         // second nav with arg filled in
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13", "second_test/18"]
@@ -1761,12 +1783,10 @@
             }
 
         // fist nested graph
-        val deepLink = Uri.parse("android-app://androidx.navigation/graph2/13")
-        navController.navigate(deepLink)
+        navController.navigate("graph2/13")
 
         // second nested graph
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/graph3/18")
-        navController.navigate(deepLink2)
+        navController.navigate("graph3/18")
 
         val navigator = navController.navigatorProvider.getNavigator(NavGraphNavigator::class.java)
         // ["graph", "graph2/13", "graph3/18"]
@@ -1786,8 +1806,7 @@
         navController.graph = nav_multiArg_graph
 
         // navigate with both args filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/18"]
@@ -1804,8 +1823,7 @@
         navController.graph = nav_multiArg_graph
 
         // navigate with args partially filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/{arg2}")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/{arg2}"]
@@ -1902,8 +1920,7 @@
             }
 
         // navigate with query param
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?opt=13")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -1937,8 +1954,7 @@
             }
 
         // navigate with query params
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null&opt2=13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?opt=null&opt2=13")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -1998,8 +2014,7 @@
         navController.graph = nav_singleArg_graph
 
         // navigate with arg filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13"]
@@ -2024,8 +2039,7 @@
         navController.graph = nav_multiArg_graph
 
         // navigate with args partially filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/18"]
@@ -2049,8 +2063,7 @@
         navController.graph = nav_multiArg_graph
 
         // navigate with args partially filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/{arg2}")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/{arg2}"]
@@ -2073,8 +2086,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/18"]
@@ -2446,12 +2458,10 @@
         navController.graph = nav_singleArg_graph
 
         // first nav with arg filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
         // second nav with arg filled in
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13", "second_test/18"]
@@ -2470,12 +2480,10 @@
         navController.graph = nav_singleArg_graph
 
         // first nav with arg filed in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
         // second nav with arg filled in
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13", "second_test/18"]
@@ -2492,8 +2500,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/18"]
@@ -2510,8 +2517,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/{arg2}")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/{arg2}"]
@@ -2539,8 +2545,7 @@
             }
 
         // navigate without filling query param
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?{arg}")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -2569,8 +2574,7 @@
             }
 
         // navigate without query param
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?opt={arg}")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -2596,8 +2600,7 @@
             }
 
         // navigate without query param
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?opt=null")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -2612,8 +2615,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/18"]
@@ -2630,8 +2632,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/{arg2}")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/{arg2}"]
@@ -2648,8 +2649,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/18"]
@@ -2666,8 +2666,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         // ["start_test", "second_test/13/18"]
@@ -2989,11 +2988,9 @@
         val navController = createNavController()
         navController.graph = nav_singleArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3012,11 +3009,9 @@
         val navController = createNavController()
         navController.graph = nav_singleArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3036,11 +3031,9 @@
         val navController = createNavController()
         navController.graph = nav_singleArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3090,11 +3083,9 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/14")
 
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18/19")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3111,11 +3102,9 @@
         val navController = createNavController()
         navController.graph = nav_singleArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
-        val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink3)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3133,11 +3122,9 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/14")
 
-        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
-        navController.navigate(deepLink2)
+        navController.navigate("second_test/18/19")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3155,11 +3142,9 @@
         val navController = createNavController()
         navController.graph = nav_singleArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
-        val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
-        navController.navigate(deepLink3)
+        navController.navigate("second_test/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3179,8 +3164,7 @@
         val navController = createNavController()
         navController.graph = nav_singleArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3201,8 +3185,7 @@
         navController.graph = nav_multiArg_graph
 
         // navigate with partial args filled in
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/{arg2}")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3229,8 +3212,7 @@
             }
 
         // navigate without filling query param
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?{arg}")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -3258,8 +3240,7 @@
             }
 
         // navigate without filling query param
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?opt={arg}")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -3287,8 +3268,7 @@
             }
 
         // navigate without filling query param
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
-        navController.navigate(deepLink)
+        navController.navigate("second_test?opt=null")
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
 
@@ -3305,8 +3285,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/{arg2}")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3323,8 +3302,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3341,8 +3319,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3359,8 +3336,7 @@
         val navController = createNavController()
         navController.graph = nav_multiArg_graph
 
-        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
-        navController.navigate(deepLink)
+        navController.navigate("second_test/13/18")
 
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
         assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3846,6 +3822,71 @@
         assertThat(navigator.backStack.size).isEqualTo(1)
     }
 
+    @UiThreadTest
+    @Test
+    fun testNavigateViaUriOnlyIfDeepLinkExplicitlyAdded() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(startDestination = "start") {
+                test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+            }
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navController.currentDestination?.route).isEqualTo("start")
+        assertThat(navigator.backStack.size).isEqualTo(1)
+
+        val deepLink = Uri.parse(createRoute("explicit_start_deeplink"))
+
+        navController.navigate(deepLink)
+        assertThat(navController.currentDestination?.route).isEqualTo("start")
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        // ensure can't deep link with destination's public route
+        val deepLink2 = Uri.parse(createRoute("start"))
+
+        val exception =
+            assertFailsWith<IllegalArgumentException> { navController.navigate(deepLink2) }
+        assertThat(exception.message)
+            .isEqualTo(
+                "Navigation destination that matches request " +
+                    "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+                    "cannot be found in the navigation graph NavGraph(0x0) " +
+                    "startDestination={Destination(0xa2cd94f5) route=start}"
+            )
+    }
+
+    @UiThreadTest
+    @Test
+    fun testNavigateViaRequestOnlyIfDeepLinkExplicitlyAdded() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(startDestination = "start") {
+                test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+            }
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navController.currentDestination?.route).isEqualTo("start")
+        assertThat(navigator.backStack.size).isEqualTo(1)
+
+        val request =
+            NavDeepLinkRequest(Uri.parse(createRoute("explicit_start_deeplink")), null, null)
+
+        navController.navigate(request)
+        assertThat(navController.currentDestination?.route).isEqualTo("start")
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        // ensure can't deep link with destination's public route
+        val request2 = NavDeepLinkRequest(Uri.parse(createRoute("start")), null, null)
+
+        val exception =
+            assertFailsWith<IllegalArgumentException> { navController.navigate(request2) }
+        assertThat(exception.message)
+            .isEqualTo(
+                "Navigation destination that matches request " +
+                    "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+                    "cannot be found in the navigation graph NavGraph(0x0) " +
+                    "startDestination={Destination(0xa2cd94f5) route=start}"
+            )
+    }
+
     @LargeTest
     @Test
     fun testNavigateViaImplicitDeepLink() {
@@ -5125,6 +5166,56 @@
 
     @UiThreadTest
     @Test
+    fun testHandleDeepLinkFromRouteOnlyIfExplicitlyAdded() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(startDestination = "start") {
+                test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+            }
+        val collectedDestinationRoutes = mutableListOf<String?>()
+        navController.addOnDestinationChangedListener { _, destination, _ ->
+            collectedDestinationRoutes.add(destination.route)
+        }
+
+        assertThat(collectedDestinationRoutes).containsExactly("start")
+
+        val intent =
+            Intent(
+                Intent.ACTION_VIEW,
+                Uri.parse(createRoute("explicit_start_deeplink")),
+                ApplicationProvider.getApplicationContext() as Context,
+                TestActivity::class.java
+            )
+
+        assertWithMessage("handleDeepLink should return true with valid deep link")
+            .that(navController.handleDeepLink(intent))
+            .isTrue()
+
+        assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+            .that(collectedDestinationRoutes)
+            .hasSize(2)
+        assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+
+        val intent2 =
+            Intent(
+                Intent.ACTION_VIEW,
+                Uri.parse(createRoute("start")),
+                ApplicationProvider.getApplicationContext() as Context,
+                TestActivity::class.java
+            )
+
+        assertWithMessage("handleDeepLink should return false with invalid deep link")
+            .that(navController.handleDeepLink(intent2))
+            .isFalse()
+
+        assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+            .that(collectedDestinationRoutes)
+            .hasSize(2)
+        assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+    }
+
+    @UiThreadTest
+    @Test
     fun testHandleDeepLinkToRootInvalid() {
         val navController = createNavController()
         navController.graph = nav_simple_route_graph
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index ae11abb..bcf2373 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -313,9 +313,9 @@
             }
         assertThat(expected.message)
             .isEqualTo(
-                "Deep link android-app://androidx.navigation/graph can't be used to open destination " +
-                    "NavGraph(0xa22391e1) startDestination=0x0.\n" +
-                    "Following required arguments are missing: [intArg]"
+                "Cannot set route \"graph\" for destination " +
+                    "NavGraph(0x0) startDestination=0x0. Following required " +
+                    "arguments are missing: [intArg]"
             )
     }
 
@@ -339,9 +339,9 @@
             }
         assertThat(expected.message)
             .isEqualTo(
-                "Deep link android-app://androidx.navigation/graph/{intArg} can't be used to " +
-                    "open destination NavGraph(0xf9423909) startDestination=0x0.\n" +
-                    "Following required arguments are missing: [longArg]"
+                "Cannot set route \"graph/{intArg}\" for destination " +
+                    "NavGraph(0x0) startDestination=0x0. Following required " +
+                    "arguments are missing: [longArg]"
             )
     }
 
@@ -365,9 +365,9 @@
             }
         assertThat(expected.message)
             .isEqualTo(
-                "Deep link android-app://androidx.navigation/graph can't be used to open " +
-                    "destination NavGraph(0xa22391e1) startDestination=0x0.\n" +
-                    "Following required arguments are missing: [intArg, longArg]"
+                "Cannot set route \"graph\" for destination NavGraph(0x0) " +
+                    "startDestination=0x0. Following required arguments " +
+                    "are missing: [intArg, longArg]"
             )
     }
 
@@ -389,9 +389,8 @@
             }
         assertThat(expected.message)
             .isEqualTo(
-                "Deep link android-app://androidx.navigation/dest1 can't be used to open " +
-                    "destination Destination(0xa1f3a662).\n" +
-                    "Following required arguments are missing: [intArg]"
+                "Cannot set route \"dest1\" for destination " +
+                    "Destination(0x0). Following required arguments are missing: [intArg]"
             )
     }
 
@@ -420,9 +419,9 @@
             }
         assertThat(expected.message)
             .isEqualTo(
-                "Deep link android-app://androidx.navigation/dest1/{intArg} can't be used to " +
-                    "open destination Destination(0x994aa5a8).\n" +
-                    "Following required arguments are missing: [longArg]"
+                "Cannot set route \"dest1/{intArg}\" for " +
+                    "destination Destination(0x0). Following required " +
+                    "arguments are missing: [longArg]"
             )
     }
 
@@ -448,9 +447,7 @@
             }
         assertThat(expected.message)
             .isEqualTo(
-                "Deep link android-app://androidx.navigation/dest can't be used to open " +
-                    "destination Destination(0x78d64faf).\n" +
-                    "Following required arguments are missing: [intArg, longArg]"
+                "Cannot set route \"dest\" for destination Destination(0x0). Following required arguments are missing: [intArg, longArg]"
             )
     }
 
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 0075458..1d6d518 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -1683,7 +1683,7 @@
             return null
         }
         // if not matched by routePattern, try matching with route args
-        if (_graph!!.route == route || _graph!!.matchDeepLink(route) != null) {
+        if (_graph!!.route == route || _graph!!.matchRoute(route) != null) {
             return _graph
         }
         return backQueue.getTopGraph().findNode(route)
@@ -2150,7 +2150,7 @@
             backStackMap.values.removeAll { it == backStackId }
             val backStackState = backStackStates.remove(backStackId)
 
-            val matchingDeepLink = matchingDestination.matchDeepLink(route)
+            val matchingDeepLink = matchingDestination.matchRoute(route)
             // check if the topmost NavBackStackEntryState contains the arguments in this
             // matchingDeepLink. If not, we didn't find the correct stack.
             val isCorrectStack =
@@ -2424,11 +2424,35 @@
         navOptions: NavOptions? = null,
         navigatorExtras: Navigator.Extras? = null
     ) {
-        navigate(
-            NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build(),
-            navOptions,
-            navigatorExtras
-        )
+        requireNotNull(_graph) {
+            "Cannot navigate to $route. Navigation graph has not been set for " +
+                "NavController $this."
+        }
+        val currGraph = backQueue.getTopGraph()
+        val deepLinkMatch =
+            currGraph.matchRouteComprehensive(
+                route,
+                searchChildren = true,
+                searchParent = true,
+                lastVisited = currGraph
+            )
+        if (deepLinkMatch != null) {
+            val destination = deepLinkMatch.destination
+            val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
+            val node = deepLinkMatch.destination
+            val intent =
+                Intent().apply {
+                    setDataAndType(createRoute(destination.route).toUri(), null)
+                    action = null
+                }
+            args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
+            navigate(node, args, navOptions, navigatorExtras)
+        } else {
+            throw IllegalArgumentException(
+                "Navigation destination that matches route $route cannot be found in the " +
+                    "navigation graph $_graph"
+            )
+        }
     }
 
     /**
@@ -2470,12 +2494,7 @@
         navOptions: NavOptions? = null,
         navigatorExtras: Navigator.Extras? = null
     ) {
-        val finalRoute = generateRouteFilled(route)
-        navigate(
-            NavDeepLinkRequest.Builder.fromUri(createRoute(finalRoute).toUri()).build(),
-            navOptions,
-            navigatorExtras
-        )
+        navigate(generateRouteFilled(route), navOptions, navigatorExtras)
     }
 
     /**
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index d31fa8e..63fe0d1 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -18,11 +18,13 @@
 
 import android.app.Activity
 import android.content.ContentResolver
+import android.content.Context
 import android.net.Uri
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.view.WindowManager
 import android.widget.FrameLayout
 import androidx.core.os.BundleCompat
 import androidx.core.view.WindowInsetsCompat
@@ -66,6 +68,7 @@
 import androidx.pdf.widget.ZoomView
 import androidx.pdf.widget.ZoomView.ZoomScroll
 import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.IOException
 import kotlinx.coroutines.launch
 
 /**
@@ -235,7 +238,6 @@
     ): View? {
         super.onCreateView(inflater, container, savedInstanceState)
         this.container = container
-
         if (!hasContents && delayedContentsAvailable == null) {
             if (savedInstanceState != null) {
                 restoreContents(savedInstanceState)
@@ -298,7 +300,7 @@
                         }
                     }
                 },
-                onDocumentLoadFailure = { thrown -> onLoadDocumentError(thrown) }
+                onDocumentLoadFailure = { thrown -> showLoadingErrorView(thrown) }
             )
 
         setUpEditFab()
@@ -362,20 +364,25 @@
 
     /** Adjusts the [FindInFileView] to be displayed on top of the keyboard. */
     private fun adjustInsetsForSearchMenu(findInFileView: FindInFileView, activity: Activity) {
-        val screenHeight = activity.resources.displayMetrics.heightPixels
+        val containerLocation = IntArray(2)
+        container!!.getLocationInWindow(containerLocation)
+
+        val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        val screenHeight = windowManager.currentWindowMetrics.bounds.height()
+
         val imeInsets =
             activity.window.decorView.rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime())
 
-        var menuMargin = 0
         val keyboardTop = screenHeight - imeInsets.bottom
-        if (container!!.bottom >= keyboardTop) {
-            menuMargin = container!!.bottom - keyboardTop
-        }
+        val absoluteContainerBottom = container!!.height + containerLocation[1]
 
+        var menuMargin = 0
+        if (absoluteContainerBottom >= keyboardTop) {
+            menuMargin = absoluteContainerBottom - keyboardTop
+        }
         findInFileView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
             bottomMargin = menuMargin
         }
-
         isSearchMenuAdjusted = true
     }
 
@@ -494,8 +501,11 @@
         savedState?.let { state ->
             if (isFileRestoring) {
                 state.containsKey(KEY_LAYOUT_REACH).let {
-                    val layoutReach = state.getInt(KEY_LAYOUT_REACH)
-                    layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+                    val layoutReach = state.getInt(KEY_LAYOUT_REACH, -1)
+                    if (layoutReach != -1) {
+                        layoutHandler?.pageLayoutReach = layoutReach
+                        layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+                    }
                 }
 
                 // Restore page selection from saved state if it exists
@@ -581,6 +591,7 @@
         pdfLoaderCallbacks?.pdfLoader = pdfLoader
 
         layoutHandler = LayoutHandler(pdfLoader)
+        paginatedView?.model?.size?.let { layoutHandler!!.pageLayoutReach = it }
 
         val updatedSelectionModel = PdfSelectionModel(pdfLoader)
         updateSelectionModel(updatedSelectionModel)
@@ -630,7 +641,7 @@
                 // app that owns it has been killed by the system. We will still recover,
                 // but log this.
                 viewState.set(ViewState.ERROR)
-                onLoadDocumentError(e)
+                showLoadingErrorView(e)
             }
         }
     }
@@ -654,9 +665,7 @@
     }
 
     private fun destroyContentModel() {
-
         pdfLoader?.cancelAll()
-
         paginationModel = null
 
         selectionHandles?.destroy()
@@ -737,6 +746,13 @@
         )
     }
 
+    private fun showLoadingErrorView(error: Throwable) {
+        context?.resources?.getString(R.string.error_cannot_open_pdf)?.let {
+            loadingView?.showErrorView(it)
+        }
+        onLoadDocumentError(error)
+    }
+
     private fun loadFile(fileUri: Uri) {
         Preconditions.checkNotNull(fileUri)
         Preconditions.checkArgument(
@@ -759,8 +775,13 @@
         try {
             validateFileUri(fileUri)
             fetchFile(fileUri)
-        } catch (e: SecurityException) {
-            onLoadDocumentError(e)
+        } catch (error: Exception) {
+            when (error) {
+                is IOException,
+                is SecurityException,
+                is NullPointerException -> showLoadingErrorView(error)
+                else -> throw error
+            }
         }
         if (localUri != null && localUri != fileUri) {
             annotationButton?.hide()
@@ -787,7 +808,7 @@
                 }
 
                 override fun failed(thrown: Throwable) {
-                    onLoadDocumentError(thrown)
+                    showLoadingErrorView(thrown)
                 }
 
                 override fun progress(progress: Float) {}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 3cdd35d..ea847f0 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -18,6 +18,8 @@
 
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.view.View;
@@ -27,6 +29,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.ParcelCompat;
 import androidx.pdf.ViewState;
 import androidx.pdf.data.Range;
 import androidx.pdf.util.PaginationUtils;
@@ -168,6 +171,22 @@
         }
     }
 
+    @Nullable
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        return new SavedState(superState, mModel);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        SavedState savedState = (SavedState) state;
+        super.onRestoreInstanceState(((SavedState) state).getSuperState());
+        mModel = savedState.mModel;
+        mPageRangeHandler = new PageRangeHandler(mModel);
+        requestLayout();
+    }
+
     /**
      * Returns the current viewport in content coordinates
      */
@@ -522,4 +541,24 @@
     public boolean isConfigurationChanged() {
         return mIsConfigurationChanged;
     }
+
+    static class SavedState extends View.BaseSavedState {
+        final PaginationModel mModel;
+
+        SavedState(Parcelable superState, PaginationModel model) {
+            super(superState);
+            mModel = model;
+        }
+
+        SavedState(Parcel source, ClassLoader loader) {
+            super(source);
+            mModel = ParcelCompat.readParcelable(source, loader, PaginationModel.class);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeParcelable(mModel, flags);
+        }
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index 8407a743..3279dfe 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -16,8 +16,11 @@
 
 package androidx.pdf.viewer;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
@@ -57,7 +60,8 @@
  * pages are added
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PaginationModel {
+@SuppressLint("BanParcelableUsage")
+public class PaginationModel implements Parcelable {
     /**
      * The spacing added before and after each page (the actual space between 2 consecutive pages is
      * twice this distance), in pixels.
@@ -87,6 +91,28 @@
         mPageSpacingPx = PaginationUtils.getPageSpacingInPixels(context);
     }
 
+    protected PaginationModel(@NonNull Parcel in) {
+        mPageSpacingPx = in.readInt();
+        mMaxPages = in.readInt();
+        mPages = in.createTypedArray(Dimensions.CREATOR);
+        mPageStops = in.createIntArray();
+        mSize = in.readInt();
+        mEstimatedPageHeight = in.readFloat();
+        mAccumulatedPageSize = in.readInt();
+    }
+
+    public static final Creator<PaginationModel> CREATOR = new Creator<PaginationModel>() {
+        @Override
+        public PaginationModel createFromParcel(Parcel in) {
+            return new PaginationModel(in);
+        }
+
+        @Override
+        public PaginationModel[] newArray(int size) {
+            return new PaginationModel[size];
+        }
+    };
+
     /**
      * Initializes the model.
      *
@@ -264,7 +290,6 @@
     }
 
 
-
     /**
      * Returns the location of the page in the model.
      *
@@ -277,7 +302,7 @@
      *       maximizes the portion of that view that is visible on the screen
      * </ul>
      *
-     * @param pageNum - index of requested page
+     * @param pageNum  - index of requested page
      * @param viewArea - the current viewport in content coordinates
      * @return - coordinates of the page within this model
      */
@@ -391,4 +416,20 @@
         mObservers.clear();
         super.finalize();
     }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mPageSpacingPx);
+        dest.writeInt(mMaxPages);
+        dest.writeTypedArray(mPages, flags);
+        dest.writeIntArray(mPageStops);
+        dest.writeInt(mSize);
+        dest.writeFloat(mEstimatedPageHeight);
+        dest.writeInt(mAccumulatedPageSize);
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index b5fbe27..963a8cd 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -228,9 +228,6 @@
                         "Document not loaded but status " + status.number
                     )
                 PdfStatus.PDF_ERROR -> {
-                    loadingView.showErrorView(
-                        context.resources.getString(R.string.error_cannot_open_pdf)
-                    )
                     handleError(status)
                 }
                 PdfStatus.FILE_ERROR,
@@ -259,9 +256,7 @@
 
     override fun setPageDimensions(pageNum: Int, dimensions: Dimensions) {
         if (viewState.get() != ViewState.NO_VIEW) {
-
             paginatedView.model.addPage(pageNum, dimensions)
-
             layoutHandler!!.pageLayoutReach = paginatedView.model.size
 
             if (
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 86fed41..79fdec1 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -17,8 +17,11 @@
 package androidx.privacysandbox.ui.integration.testapp
 
 import android.app.Activity
+import android.graphics.Color
+import android.graphics.Typeface
 import android.os.Bundle
 import android.util.Log
+import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
 import androidx.fragment.app.Fragment
@@ -59,6 +62,10 @@
         }
     }
 
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        getSandboxedSdkViews().forEach { it.addStateChangedListener() }
+    }
+
     /** Returns a handle to the already loaded SDK. */
     fun getSdkApi(): ISdkApi {
         return sdkApi
@@ -119,6 +126,8 @@
                 val parent = view.parent as ViewGroup
                 val index = parent.indexOfChild(view)
                 val textView = TextView(requireActivity())
+                textView.setTypeface(null, Typeface.BOLD_ITALIC)
+                textView.setTextColor(Color.RED)
                 textView.text = state.throwable.message
 
                 requireActivity().runOnUiThread {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index 4a9ccf7..d3eba3b 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -104,6 +104,7 @@
                 } catch (e: Exception) {
                     Log.w(TAG, "Ad not loaded $e")
                 }
+                childSandboxedSdkView.addStateChangedListener()
                 sandboxedSdkViewSet.add(childSandboxedSdkView)
             }
         }
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
index 3a3f32b..166d109 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
@@ -67,7 +67,6 @@
     }
 
     private fun loadResizableBannerAd() {
-        resizableBannerView.addStateChangedListener()
         loadBannerAd(
             currentAdType,
             currentMediationOption,
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
index e49be51..2d38fe7 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
@@ -61,7 +61,6 @@
     }
 
     private fun loadBottomBannerAd() {
-        bottomBannerView.addStateChangedListener()
         bottomBannerView.layoutParams =
             inflatedView.findViewById<LinearLayout>(R.id.bottom_banner_container).layoutParams
         requireActivity().runOnUiThread {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
index 5115d6f..0deb550 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
@@ -22,8 +22,8 @@
 import androidx.sqlite.SQLiteConnection
 import java.util.concurrent.Callable
 import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 
 /**
  * A LiveData implementation that closely works with [InvalidationTracker] to implement database
@@ -53,6 +53,17 @@
     private val computing = AtomicBoolean(false)
     private val registeredObserver = AtomicBoolean(false)
 
+    private val launchContext =
+        if (database.inCompatibilityMode()) {
+            if (inTransaction) {
+                database.getTransactionContext()
+            } else {
+                database.getQueryContext()
+            }
+        } else {
+            EmptyCoroutineContext
+        }
+
     private suspend fun refresh() {
         if (registeredObserver.compareAndSet(false, true)) {
             database.invalidationTracker.subscribe(
@@ -105,7 +116,7 @@
         val isActive = hasActiveObservers()
         if (invalid.compareAndSet(false, true)) {
             if (isActive) {
-                database.getCoroutineScope().launch { refresh() }
+                database.getCoroutineScope().launch(launchContext) { refresh() }
             }
         }
     }
@@ -115,7 +126,7 @@
     override fun onActive() {
         super.onActive()
         container.onActive(this)
-        database.getCoroutineScope().launch { refresh() }
+        database.getCoroutineScope().launch(launchContext) { refresh() }
     }
 
     override fun onInactive() {
@@ -132,13 +143,7 @@
     private val callableFunction: Callable<T?>
 ) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
     override suspend fun compute(): T? {
-        val queryContext =
-            if (inTransaction) {
-                database.getTransactionContext()
-            } else {
-                database.getQueryContext()
-            }
-        return withContext(queryContext) { callableFunction.call() }
+        return callableFunction.call()
     }
 }
 
diff --git a/settings.gradle b/settings.gradle
index 988cca5..ca5d8f6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -29,7 +29,12 @@
         classpath("com.gradle:develocity-gradle-plugin:3.18")
         classpath("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
         classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-beta10")
-        classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+        def agpOverride = System.getenv("GRADLE_PLUGIN_VERSION")
+        if (agpOverride != null) {
+            classpath("com.android.settings:com.android.settings.gradle.plugin:$agpOverride")
+        } else {
+            classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+        }
     }
 }
 
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -388,8 +388,10 @@
 
   public sealed interface LazyColumnLayoutInfo {
     method public int getTotalItemsCount();
+    method public long getViewportSize();
     method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
     property public abstract int totalItemsCount;
+    property public abstract long viewportSize;
     property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
   }
 
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -388,8 +388,10 @@
 
   public sealed interface LazyColumnLayoutInfo {
     method public int getTotalItemsCount();
+    method public long getViewportSize();
     method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
     property public abstract int totalItemsCount;
+    property public abstract long viewportSize;
     property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
   }
 
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
new file mode 100644
index 0000000..f07a6b7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyColumnLayoutInfoTest {
+    @get:Rule val rule = createComposeRule()
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) { itemSizeDp = itemSizePx.toDp() }
+    }
+
+    @Test
+    fun visibleItemsAreCorrect() {
+        lateinit var state: LazyColumnState
+
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyColumnState().also { state = it },
+                // Viewport take 4 items, item 0 is exactly above the center and there is space for
+                // two more items below the center line.
+                modifier = Modifier.requiredSize(itemSizeDp * 4f),
+                verticalArrangement = Arrangement.spacedBy(0.dp)
+            ) {
+                items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+            // Start offset compensates for the layout where the first item is exactly above the
+            // center line.
+            state.layoutInfo.assertVisibleItems(count = 3, startOffset = itemSizePx)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectWithSpacing() {
+        lateinit var state: LazyColumnState
+
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyColumnState().also { state = it },
+                // Viewport take 4 items, item 0 is exactly above the center and there is space for
+                // two more items below the center line.
+                modifier = Modifier.requiredSize(itemSizeDp * 4f),
+                verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+            ) {
+                items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+            // Start offset compensates for the layout where the first item is exactly above the
+            // center line.
+            state.layoutInfo.assertVisibleItems(
+                count = 2,
+                spacing = itemSizePx,
+                startOffset = itemSizePx
+            )
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenResize() {
+        lateinit var state: LazyColumnState
+        var size by mutableStateOf(itemSizeDp * 2)
+        var currentInfo: LazyColumnLayoutInfo? = null
+        @Composable
+        fun observingFun() {
+            currentInfo = state.layoutInfo
+        }
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyColumnState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 4f)
+            ) {
+                item { Box(Modifier.requiredSize(size)) }
+            }
+            observingFun()
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+            currentInfo = null
+            size = itemSizeDp
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+        }
+    }
+
+    @Test
+    fun totalCountIsCorrect() {
+        var count by mutableStateOf(10)
+        lateinit var state: LazyColumnState
+        rule.setContent {
+            LazyColumn(state = rememberLazyColumnState().also { state = it }) {
+                items((0 until count).toList()) { Box(Modifier.requiredSize(10.dp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+            count = 20
+        }
+
+        rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrect() {
+        val sizePx = 45
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        lateinit var state: LazyColumnState
+        rule.setContent {
+            LazyColumn(
+                Modifier.height(sizeDp).width(sizeDp * 2),
+                state = rememberLazyColumnState().also { state = it }
+            ) {
+                items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+        }
+    }
+
+    private fun LazyColumnLayoutInfo.assertVisibleItems(
+        count: Int,
+        startIndex: Int = 0,
+        startOffset: Int = 0,
+        expectedSize: Int = itemSizePx,
+        spacing: Int = 0
+    ) {
+        assertThat(visibleItems.size).isEqualTo(count)
+        var currentIndex = startIndex
+        var currentOffset = startOffset
+        visibleItems.forEach {
+            assertThat(it.index).isEqualTo(currentIndex)
+            assertWithMessage("Offset of item $currentIndex")
+                .that(it.offset)
+                .isEqualTo(currentOffset)
+            assertThat(it.height).isEqualTo(expectedSize)
+            currentIndex++
+            currentOffset += it.height + spacing
+        }
+    }
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
index c5002a0..14fc4c4 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
@@ -16,6 +16,8 @@
 
 package androidx.wear.compose.foundation.lazy
 
+import androidx.compose.ui.unit.IntSize
+
 /**
  * Scroll progress of an item in a [LazyColumn] before any modifications to the item's height are
  * applied (using [LazyColumnItemScope.transformedHeight] modifier).
@@ -44,21 +46,26 @@
 sealed interface LazyColumnVisibleItemInfo {
     /** The index of the item in the underlying data source. */
     val index: Int
+
     /** The offset of the item from the start of the visible area. */
     val offset: Int
+
     /** The height of the item after applying any height changes. */
     val height: Int
+
     /** The scroll progress of the item, indicating its position within the visible area. */
     val scrollProgress: LazyColumnItemScrollProgress
 }
 
 /** Holds the layout information for a [LazyColumn]. */
 sealed interface LazyColumnLayoutInfo {
+
     /** A list of [LazyColumnVisibleItemInfo] objects representing the visible items in the list. */
     val visibleItems: List<LazyColumnVisibleItemInfo>
 
     /** The total count of items passed to [LazyColumn]. */
     val totalItemsCount: Int
 
-    // TODO: b/352686661 - Expose more properties related to layout.
+    /** The size of the viewport in pixels. */
+    val viewportSize: IntSize
 }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
index 3f316f2..1bd6f3e 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
@@ -17,6 +17,7 @@
 package androidx.wear.compose.foundation.lazy
 
 import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
 
 /** The result of the measure pass of the [LazyColumn]. */
 internal class LazyColumnMeasureResult(
@@ -32,4 +33,8 @@
     override val visibleItems: List<LazyColumnVisibleItemInfo>,
     /** see [LazyColumnLayoutInfo.totalItemsCount] */
     override val totalItemsCount: Int,
-) : LazyColumnLayoutInfo, MeasureResult by measureResult
+) : LazyColumnLayoutInfo, MeasureResult by measureResult {
+    /** see [LazyColumnLayoutInfo.viewportSize] */
+    override val viewportSize: IntSize
+        get() = IntSize(width = width, height = height)
+}
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
index bd54d7f..9be2bf8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
@@ -385,32 +385,6 @@
         rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
         rule.onNodeWithText(dismissedText).assertExists()
     }
-
-    @Test
-    fun calls_ondismissrequest_when_dialog_becomes_hidden() {
-        val show = mutableStateOf(true)
-        var dismissed = false
-        rule.setContentWithTheme {
-            Box {
-                Dialog(
-                    showDialog = show.value,
-                    onDismissRequest = { dismissed = true },
-                ) {
-                    Alert(
-                        icon = {},
-                        title = {},
-                        message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
-                        content = {},
-                    )
-                }
-            }
-        }
-        rule.waitForIdle()
-        show.value = false
-
-        rule.waitForIdle()
-        assert(dismissed)
-    }
 }
 
 class DialogContentSizeAndPositionTest {
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
index 697583e..b07f490 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
@@ -233,17 +233,18 @@
                     transitionState.targetState = DialogVisibility.Hide
                 }
             }
-        }
-    }
-    LaunchedEffect(transitionState.currentState) {
-        if (
-            pendingOnDismissCall &&
-                transitionState.currentState == DialogVisibility.Hide &&
-                transitionState.isIdle
-        ) {
-            // After the outro animation, leave the dialog & reset alpha/scale transitions.
-            onDismissRequest()
-            pendingOnDismissCall = false
+
+            LaunchedEffect(transitionState.currentState) {
+                if (
+                    pendingOnDismissCall &&
+                        transitionState.currentState == DialogVisibility.Hide &&
+                        transitionState.isIdle
+                ) {
+                    // After the outro animation, leave the dialog & reset alpha/scale transitions.
+                    onDismissRequest()
+                    pendingOnDismissCall = false
+                }
+            }
         }
     }
 }
diff --git a/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
new file mode 100644
index 0000000..e9e624a
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.MaterialTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ProgressIndicatorBenchmark {
+    @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+    private val testCaseFactory = { ProgressIndicatorTestCase() }
+
+    @Test
+    fun first_pixel() {
+        benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+    }
+}
+
+internal class ProgressIndicatorTestCase : LayeredComposeTestCase() {
+    @Composable
+    override fun MeasuredContent() {
+        CircularProgressIndicator(progress = { 0.5f })
+    }
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        MaterialTheme { content() }
+    }
+}
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index bb35fa2..1e61bdbc 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -45,7 +45,6 @@
 
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-    androidTestImplementation(project(":navigation:navigation-common"))
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(project(":wear:compose:compose-material"))
     androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
diff --git a/wear/wear_sdk/README.txt b/wear/wear_sdk/README.txt
index 26122f9..224156b 100644
--- a/wear/wear_sdk/README.txt
+++ b/wear/wear_sdk/README.txt
@@ -5,5 +5,5 @@
     "preinstalled on WearOS devices."
 gerrit source: "vendor/google_clockwork/sdk/lib"
 API version: 35.1
-Build ID: 12239970
-Last updated: Fri Aug 16 06:57:41 PM UTC 2024
+Build ID: 12295646
+Last updated: Thu Aug 29 08:37:30 PM UTC 2024
diff --git a/wear/wear_sdk/wear-sdk.jar b/wear/wear_sdk/wear-sdk.jar
index 36f61d4..43b434d 100644
--- a/wear/wear_sdk/wear-sdk.jar
+++ b/wear/wear_sdk/wear-sdk.jar
Binary files differ