Merge "Use WindowMetrics.density instead of Context density" into androidx-platform-dev
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
index 9235137..7892f10 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
@@ -129,6 +129,9 @@
     }
 
     private fun getSupportedProfiles(cameraSelector: CameraSelector): List<CamcorderProfileProxy> {
+        if (!CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!)) {
+            return emptyList()
+        }
         val cameraInfo = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector).cameraInfo
         val videoCapabilities = VideoCapabilities.from(cameraInfo)
         return videoCapabilities.supportedQualities
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 380f24b..29c53eb 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -20,6 +20,7 @@
 import android.Manifest
 import android.annotation.SuppressLint
 import android.app.AppOpsManager
+import android.app.AppOpsManager.OnOpNotedCallback
 import android.app.AsyncNotedAppOp
 import android.app.SyncNotedAppOp
 import android.content.ContentResolver
@@ -41,8 +42,9 @@
 import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.ImageFormatConstants
-import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.impl.Observable.Observer
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.AudioUtil
 import androidx.camera.testing.CameraPipeConfigTestRule
@@ -55,18 +57,27 @@
 import androidx.camera.testing.mocks.MockConsumer
 import androidx.camera.testing.mocks.helpers.CallTimes
 import androidx.camera.testing.mocks.helpers.CallTimesAtLeast
+import androidx.camera.video.VideoOutput.SourceState.ACTIVE_NON_STREAMING
+import androidx.camera.video.VideoOutput.SourceState.ACTIVE_STREAMING
+import androidx.camera.video.VideoOutput.SourceState.INACTIVE
+import androidx.camera.video.VideoRecordEvent.Finalize
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INVALID_OUTPUT_OPTIONS
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NO_VALID_DATA
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_RECORDER_ERROR
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.camera.video.VideoRecordEvent.Pause
+import androidx.camera.video.VideoRecordEvent.Resume
+import androidx.camera.video.VideoRecordEvent.Start
+import androidx.camera.video.VideoRecordEvent.Status
 import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks
 import androidx.camera.video.internal.compat.quirk.ExtraSupportedResolutionQuirk
 import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite
+import androidx.camera.video.internal.encoder.EncoderFactory
 import androidx.camera.video.internal.encoder.InvalidConfigException
-import androidx.core.util.Consumer
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -77,9 +88,7 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.io.File
-import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
-import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CompletableDeferred
@@ -104,10 +113,14 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.timeout
 
+private const val DEFAULT_STATUS_COUNT = 5
 private const val GENERAL_TIMEOUT = 5000L
 private const val STATUS_TIMEOUT = 15000L
 private const val TEST_ATTRIBUTION_TAG = "testAttribution"
-private const val BITRATE_AUTO = 0
+// For the file size is small, the final file length possibly exceeds the file size limit
+// after adding the file header. We still add the buffer for the tolerance of comparing the
+// file length and file size limit.
+private const val FILE_SIZE_LIMIT_BUFFER = 50 * 1024 // 50k threshold buffer
 
 @LargeTest
 @RunWith(Parameterized::class)
@@ -156,12 +169,11 @@
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context: Context = ApplicationProvider.getApplicationContext()
     private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+    private val recordingsToStop = mutableListOf<RecordingProcess>()
 
     private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-    private lateinit var recorder: Recorder
     private lateinit var preview: Preview
     private lateinit var surfaceTexturePreview: Preview
-    private lateinit var mockVideoRecordEventConsumer: MockConsumer<VideoRecordEvent>
 
     @Before
     fun setUp() {
@@ -249,19 +261,18 @@
             surfaceTexturePreview,
             preview
         )
-
-        mockVideoRecordEventConsumer = MockConsumer<VideoRecordEvent>()
     }
 
     @After
     fun tearDown() {
+        for (recording in recordingsToStop) {
+            recording.stop()
+        }
+
         if (this::cameraUseCaseAdapter.isInitialized) {
             instrumentation.runOnMainSync {
                 cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
             }
-            if (this::recorder.isInitialized) {
-                recorder.onSourceStateChanged(VideoOutput.SourceState.INACTIVE)
-            }
         }
 
         CameraXUtil.shutdown().get(10, TimeUnit.SECONDS)
@@ -269,316 +280,207 @@
 
     @Test
     fun canRecordToFile() {
-        testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(BITRATE_AUTO, enableAudio = true)
+        // Arrange.
+        val outputOptions = createFileOutputOptions()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
+
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            val uri = finalize.outputResults.outputUri
+            assertThat(uri).isEqualTo(Uri.fromFile(outputOptions.file))
+            checkFileHasAudioAndVideo(uri)
+        }
     }
 
     @Test
     fun recordingWithSetTargetVideoEncodingBitRate() {
         testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(6_000_000)
-        verifyConfiguredVideoBitrate()
     }
 
     @Test
     fun recordingWithSetTargetVideoEncodingBitRateOutOfRange() {
         testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(1000_000_000)
-        verifyConfiguredVideoBitrate()
     }
 
     @Test
     fun recordingWithNegativeBitRate() {
-        initializeRecorder()
         assertThrows(IllegalArgumentException::class.java) {
-            Recorder.Builder().setTargetVideoEncodingBitRate(-5).build()
+            createRecorder(targetBitrate = -5)
         }
     }
 
     @Test
     fun canRecordToMediaStore() {
-        initializeRecorder()
         assumeTrue(
             "Ignore the test since the MediaStore.Video has compatibility issues.",
             DeviceQuirks.get(MediaStoreVideoCannotWrite::class.java) == null
         )
-        invokeSurfaceRequest()
-        val statusSemaphore = Semaphore(0)
-        val finalizeSemaphore = Semaphore(0)
-        val context: Context = ApplicationProvider.getApplicationContext()
+
+        // Arrange.
         val contentResolver: ContentResolver = context.contentResolver
         val contentValues = ContentValues().apply {
             put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
         }
-
         val outputOptions = MediaStoreOutputOptions.Builder(
             contentResolver,
             MediaStore.Video.Media.EXTERNAL_CONTENT_URI
         ).setContentValues(contentValues).build()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        var uri: Uri = Uri.EMPTY
-        val recording = recorder.prepareRecording(context, outputOptions)
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor()) {
-                if (it is VideoRecordEvent.Status) {
-                    statusSemaphore.release()
-                }
-                if (it is VideoRecordEvent.Finalize) {
-                    uri = it.outputResults.outputUri
-                    finalizeSemaphore.release()
-                }
-            }
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            val uri = finalize.outputResults.outputUri
+            checkFileHasAudioAndVideo(uri)
 
-        assertThat(statusSemaphore.tryAcquire(5, 15000L, TimeUnit.MILLISECONDS)).isTrue()
-
-        recording.stopSafely()
-
-        // Wait for the recording to complete.
-        assertThat(finalizeSemaphore.tryAcquire(GENERAL_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
-        assertThat(uri).isNotEqualTo(Uri.EMPTY)
-
-        checkFileHasAudioAndVideo(uri)
-
-        contentResolver.delete(uri, null, null)
+            // Clean-up.
+            contentResolver.delete(uri, null, null)
+        }
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 26)
     fun canRecordToFileDescriptor() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val pfd = ParcelFileDescriptor.open(
-            file,
-            ParcelFileDescriptor.MODE_READ_WRITE
-        )
-        val recording = recorder
-            .prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Arrange.
+        val file = createTempFile()
+        val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
+        val outputOptions = FileDescriptorOutputOptions.Builder(pfd).build()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
+        // Act.
+        recording.startAndVerify()
         // ParcelFileDescriptor should be safe to close after PendingRecording#start.
         pfd.close()
+        recording.stopAndVerify()
 
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true,
-            GENERAL_TIMEOUT
-        )
-
+        // Assert.
         checkFileHasAudioAndVideo(Uri.fromFile(file))
-
-        file.delete()
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 26)
     fun recordToFileDescriptor_withClosedFileDescriptor_receiveError() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val file = createTempFile()
         val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
-
         pfd.close()
+        val outputOptions = FileDescriptorOutputOptions.Builder(pfd).build()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        recorder.prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        // Check the output Uri from the finalize event match the Uri from the given file.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Act.
+        recording.start()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_INVALID_OUTPUT_OPTIONS)
         }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_INVALID_OUTPUT_OPTIONS)
-
-        file.delete()
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 25)
     @SuppressLint("NewApi") // Intentionally testing behavior of calling from invalid API level
     fun prepareRecordingWithFileDescriptor_throwsExceptionBeforeApi26() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder()
+        val file = createTempFile()
         ParcelFileDescriptor.open(
             file,
             ParcelFileDescriptor.MODE_READ_WRITE
         ).use { pfd ->
+            // Assert.
             assertThrows(UnsupportedOperationException::class.java) {
+                // Act.
                 recorder.prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
             }
         }
-
-        file.delete()
     }
 
     @Test
     fun canPauseResume() {
-        initializeRecorder()
-        invokeSurfaceRequest()
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                pause()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                resume()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Resume::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                // Check there are data being encoded after resuming.
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Status::class.java,
-                    true, STATUS_TIMEOUT, CallTimesAtLeast(5)
-                )
-
-                stopSafely()
-            }
-
-        // Wait for the recording to be finalized.
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT)
-
-        checkFileHasAudioAndVideo(Uri.fromFile(file))
-
-        file.delete()
+        // Act.
+        recording.startAndVerify()
+        recording.pauseAndVerify()
+        recording.resumeAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_NONE)
+        }
     }
 
     @Test
     fun canStartRecordingPaused_whenRecorderInitializing() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder(sendSurfaceRequest = false)
+        val recording = createRecordingProcess(recorder = recorder)
 
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                pause()
+        // Act.
+        recording.start()
+        recording.pause()
+        // Only invoke surface request after pause() has been called
+        recorder.sendSurfaceRequest()
 
-                // Only invoke surface request after pause() has been called
-                invokeSurfaceRequest()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Start::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                stopSafely()
-            }
-
-        file.delete()
+        // Assert.
+        recording.verifyStart()
+        recording.verifyPause()
     }
 
     @Test
     fun canReceiveRecordingStats() {
-        initializeRecorder()
-        invokeSurfaceRequest()
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Act.
+        recording.startAndVerify()
+        recording.pauseAndVerify()
+        recording.resumeAndVerify()
+        recording.stopAndVerify()
 
-        // Start
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-                pause()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                resume()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Resume::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Status::class.java,
-                    true, STATUS_TIMEOUT, CallTimesAtLeast(5)
-                )
-
-                stopSafely()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Finalize::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-            }
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
+        // Assert.
+        val events = recording.getAllEvents()
+        assertThat(events.size).isAtLeast(
+            1 /* Start */ +
+                5 /* Status */ +
+                1 /* Pause */ +
+                1 /* Resume */ +
+                5 /* Status */ +
+                1 /* Stop */
         )
 
-        captor.allValues.run {
-            assertThat(size).isAtLeast(
-                (
-                    1 /* Start */ +
-                        5 /* Status */ +
-                        1 /* Pause */ +
-                        1 /* Resume */ +
-                        5 /* Status */ +
-                        1 /* Stop */
-                    )
-            )
-
-            // Ensure duration and bytes are increasing
-            take(size - 1).mapIndexed { index, _ ->
-                Pair(get(index).recordingStats, get(index + 1).recordingStats)
-            }.forEach { (former: RecordingStats, latter: RecordingStats) ->
-                assertThat(former.numBytesRecorded).isAtMost(latter.numBytesRecorded)
-                assertThat(former.recordedDurationNanos).isAtMost((latter.recordedDurationNanos))
-            }
-
-            // Ensure they are not all zero by checking last stats
-            last().recordingStats.also {
-                assertThat(it.numBytesRecorded).isGreaterThan(0L)
-                assertThat(it.recordedDurationNanos).isGreaterThan(0L)
-            }
+        // Assert: Ensure duration and bytes are increasing.
+        List(events.size - 1) { index ->
+            Pair(events[index].recordingStats, events[index + 1].recordingStats)
+        }.forEach { (former: RecordingStats, latter: RecordingStats) ->
+            assertThat(former.numBytesRecorded).isAtMost(latter.numBytesRecorded)
+            assertThat(former.recordedDurationNanos).isAtMost((latter.recordedDurationNanos))
         }
 
-        file.delete()
+        // Assert: Ensure they are not all zero by checking the last stats.
+        events.last().recordingStats.also {
+            assertThat(it.numBytesRecorded).isGreaterThan(0L)
+            assertThat(it.recordedDurationNanos).isGreaterThan(0L)
+        }
     }
 
     @Test
     fun setFileSizeLimit() {
-        initializeRecorder()
+        // Arrange.
         val fileSizeLimit = 500L * 1024L // 500 KB
-        runFileSizeLimitTest(fileSizeLimit)
+        val outputOptions = createFileOutputOptions(fileSizeLimit = fileSizeLimit)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
+
+        // Act.
+        recording.startAndVerify()
+        recording.verifyFinalize(timeoutMs = 60_000L) { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
+            assertThat(outputOptions.file.length())
+                .isLessThan(fileSizeLimit + FILE_SIZE_LIMIT_BUFFER)
+        }
     }
 
     // Sets the file size limit to 1 byte, which will be lower than the initial data sent from
@@ -586,800 +488,429 @@
     // written to it.
     @Test
     fun setFileSizeLimitLowerThanInitialDataSize() {
-        initializeRecorder()
+        // Arrange.
         val fileSizeLimit = 1L // 1 byte
-        runFileSizeLimitTest(fileSizeLimit)
+        val outputOptions = createFileOutputOptions(fileSizeLimit = fileSizeLimit)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
+
+        // Act.
+        recording.start()
+        recording.verifyFinalize { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
+        }
     }
 
     @Test
     fun setLocation() {
-        initializeRecorder()
         runLocationTest(createLocation(25.033267462243586, 121.56454121737946))
     }
 
     @Test
     fun setNegativeLocation() {
-        initializeRecorder()
         runLocationTest(createLocation(-27.14394722411734, -109.33053675296067))
     }
 
     @Test
     fun stop_withErrorWhenDurationLimitReached() {
-        initializeRecorder()
-        val videoRecordEventListener = MockConsumer<VideoRecordEvent>()
-        invokeSurfaceRequest()
+        // Arrange.
         val durationLimitMs = 3000L
-        val durationTolerance = 50L
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file)
-            .setDurationLimitMillis(durationLimitMs)
-            .build()
+        val durationToleranceMs = 50L
+        val outputOptions = createFileOutputOptions(durationLimitMillis = durationLimitMs)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        val recording = recorder
-            .prepareRecording(context, outputOptions)
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
+        // Act.
+        recording.start()
 
-        // The recording should be finalized after the specified duration limit plus some time
-        // for processing it.
-        videoRecordEventListener.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            false,
-            durationLimitMs + 2000L
-        )
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
-                argument -> VideoRecordEvent::class.java.isInstance(argument)
+        // Assert.
+        recording.verifyFinalize(timeoutMs = durationLimitMs + 2000L) { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_DURATION_LIMIT_REACHED)
+            assertThat(finalize.recordingStats.recordedDurationNanos)
+                .isAtMost(TimeUnit.MILLISECONDS.toNanos(durationLimitMs + durationToleranceMs))
+            checkDurationAtMost(
+                Uri.fromFile(outputOptions.file),
+                durationLimitMs + durationToleranceMs
+            )
         }
-        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java,
-            false, CallTimesAtLeast(1), captor)
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_DURATION_LIMIT_REACHED)
-        assertThat(finalize.recordingStats.recordedDurationNanos)
-            .isAtMost(TimeUnit.MILLISECONDS.toNanos(durationLimitMs + durationTolerance))
-        checkDurationAtMost(Uri.fromFile(file), durationLimitMs)
-
-        recording.stopSafely()
-        file.delete()
     }
 
     @Test
     fun checkStreamState() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
+        // Arrange.
+        val recorder = createRecorder()
         @Suppress("UNCHECKED_CAST")
-        val streamInfoObserver =
-            mock(Observable.Observer::class.java) as Observable.Observer<StreamInfo>
+        val streamInfoObserver = mock(Observer::class.java) as Observer<StreamInfo>
         val inOrder = inOrder(streamInfoObserver)
-        recorder.streamInfo.addObserver(CameraXExecutors.directExecutor(), streamInfoObserver)
+        recorder.streamInfo.addObserver(directExecutor(), streamInfoObserver)
 
-        // Recorder should start in INACTIVE stream state before any recordings
-        inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
-            argThat {
-                it!!.streamState == StreamInfo.StreamState.INACTIVE
-            }
-        )
-
-        // Start
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-        // Starting recording should move Recorder to ACTIVE stream state
-        inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
-            argThat {
-                it!!.streamState == StreamInfo.StreamState.ACTIVE
-            }
-        )
-
-        recording.stopSafely()
-
-        // Stopping recording should eventually move to INACTIVE stream state
+        // Assert: Recorder should start in INACTIVE stream state before any recordings
         inOrder.verify(streamInfoObserver, timeout(GENERAL_TIMEOUT)).onNewData(
             argThat {
                 it!!.streamState == StreamInfo.StreamState.INACTIVE
             }
         )
+        val recording = createRecordingProcess(recorder = recorder)
 
-        file.delete()
+        // Act.
+        recording.start()
+
+        // Assert: Starting recording should move Recorder to ACTIVE stream state
+        inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
+            argThat { it!!.streamState == StreamInfo.StreamState.ACTIVE }
+        )
+
+        // Act.
+        recording.stop()
+
+        // Assert: Stopping recording should eventually move to INACTIVE stream state
+        inOrder.verify(streamInfoObserver, timeout(GENERAL_TIMEOUT)).onNewData(
+            argThat {
+                it!!.streamState == StreamInfo.StreamState.INACTIVE
+            }
+        )
     }
 
     @Test
     fun start_throwsExceptionWhenActive() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file).build()
+        // Arrange.
+        val recorder = createRecorder()
+        val recording = createRecordingProcess(recorder = recorder)
 
-        val recording = recorder.prepareRecording(context, outputOptions).start(
-            CameraXExecutors.directExecutor()
-        ) {}
+        // Act: 1st start.
+        recording.start()
 
-        val pendingRecording = recorder.prepareRecording(context, outputOptions)
+        // Assert.
         assertThrows(java.lang.IllegalStateException::class.java) {
-            pendingRecording.start(CameraXExecutors.directExecutor()) {}
+            // Act: 2nd start.
+            val recording2 = createRecordingProcess(recorder = recorder)
+            recording2.start()
         }
-
-        recording.close()
-        file.delete()
     }
 
     @Test
     fun start_whenSourceActiveNonStreaming() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder(initSourceState = ACTIVE_NON_STREAMING)
+        val recording = createRecordingProcess(recorder = recorder)
 
-        recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        invokeSurfaceRequest()
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-        // Wait for the recording to be finalized.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        file.delete()
+        // Act.
+        recording.start()
+        recorder.onSourceStateChanged(ACTIVE_STREAMING)
+        recording.verifyStart()
+        recording.verifyStatus()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_NONE)
+        }
     }
 
     @Test
     fun start_finalizeImmediatelyWhenSourceInactive() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val videoCaptureMonitor = VideoCaptureMonitor()
-        recorder.startVideoRecording(temporaryFolder.newFile(), videoCaptureMonitor).use {
-            // Ensure the Recorder is initialized before start test.
-            videoCaptureMonitor.waitForVideoCaptureStatus()
+        // Arrange.
+        val recorder = createRecorder(initSourceState = INACTIVE)
+        val recording = createRecordingProcess(recorder = recorder)
+
+        // Act.
+        recording.start()
+
+        // Assert.
+        recording.verifyFinalize { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
         }
-
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        recorder.onSourceStateChanged(VideoOutput.SourceState.INACTIVE)
-
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            false, GENERAL_TIMEOUT
-        )
-        mockVideoRecordEventConsumer.verifyNoMoreAcceptCalls(false)
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
-
-        recording.stopSafely()
-
-        file.delete()
     }
 
     @Test
     fun pause_whenSourceActiveNonStreaming() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                pause()
-
-                invokeSurfaceRequest()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Start::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                stopSafely()
-            }
-
-        // Wait for the recording to be finalized.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
+        // Arrange.
+        val recorder = createRecorder(
+            sendSurfaceRequest = false,
+            initSourceState = ACTIVE_NON_STREAMING
         )
+        val recording = createRecordingProcess(recorder = recorder)
 
-        // If the recording is paused immediately after being started, the recording should be
-        // finalized with ERROR_NO_VALID_DATA.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Act.
+        recording.start()
+        recording.pause()
+        recorder.sendSurfaceRequest()
+
+        // Assert.
+        recording.verifyStart()
+        recording.verifyPause()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
         }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
-
-        file.delete()
     }
 
     @Test
     fun pause_noOpWhenAlreadyPaused() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+        // Act.
+        recording.startAndVerify()
+        recording.pauseAndVerify()
+        recording.pause()
 
-                pause()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                pause()
-
-                stopSafely()
-            }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        // As described in b/197416199, there might be encoded data in flight which will trigger
-        // Status event after pausing. So here it checks there's only one Pause event.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        assertThat(captor.allValues.count { it is VideoRecordEvent.Pause }).isAtMost(1)
-
-        file.delete()
+        // Assert: One Pause event.
+        val events = recording.getAllEvents()
+        val pauseEvents = events.filterIsInstance<Pause>()
+        assertThat(pauseEvents.size).isAtMost(1)
     }
 
     @Test
     fun pause_throwsExceptionWhenStopping() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify()
 
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
+        // Assert.
         assertThrows(IllegalStateException::class.java) {
             recording.pause()
         }
-
-        file.delete()
     }
 
     @Test
     fun resume_noOpWhenNotPaused() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        // Calling resume shouldn't affect the stream of status events finally followed
-        // by a finalize event. There shouldn't be another resume event generated.
+        // Act.
+        recording.startAndVerify()
         recording.resume()
+        recording.stopAndVerify()
 
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Status::class.java,
-            true,
-            STATUS_TIMEOUT,
-            CallTimesAtLeast(5)
-        )
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        // Ensure no resume events were ever sent.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Resume::class.java,
-            false,
-            GENERAL_TIMEOUT,
-            CallTimes(0)
-        )
-
-        file.delete()
+        // Assert: No Resume event.
+        val events = recording.getAllEvents()
+        val resumeEvents = events.filterIsInstance<Resume>()
+        assertThat(resumeEvents).isEmpty()
     }
 
     @Test
     fun resume_throwsExceptionWhenStopping() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Act.
+        recording.startAndVerify()
+        recording.stop()
 
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
+        // Assert.
         assertThrows(IllegalStateException::class.java) {
-            recording.resume()
+            recording.resumeAndVerify()
         }
-
-        file.delete()
     }
 
     @Test
     fun stop_beforeSurfaceRequested() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder(sendSurfaceRequest = false)
+        val recording = createRecordingProcess(recorder = recorder)
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Act.
+        recording.start()
+        recording.stop()
+        recorder.sendSurfaceRequest()
 
-        recording.pause()
-
-        recording.stopSafely()
-
-        invokeSurfaceRequest()
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Assert.
+        recording.verifyFinalize { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
         }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
-
-        file.delete()
-    }
-
-    @Test
-    fun stop_fromAutoCloseable() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        // Recording will be stopped by AutoCloseable.close() upon exiting use{} block
-        val pendingRecording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-        pendingRecording.start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-            .use {
-                invokeSurfaceRequest()
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-            }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        file.delete()
     }
 
     @Test
     fun stop_WhenUseCaseDetached() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
+        // Act.
+        recording.startAndVerify()
         instrumentation.runOnMainSync {
             cameraUseCaseAdapter.removeUseCases(listOf(preview))
         }
 
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        recording.stopSafely()
-        file.delete()
+        // Assert.
+        recording.verifyFinalize { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
+        }
     }
 
-    @Suppress("UNUSED_VALUE", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
     @Test
     fun stop_whenRecordingIsGarbageCollected() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        var recording: RecordingProcess? = createRecordingProcess()
+        val listener = recording!!.listener
 
-        var recording: Recording? = recorder
-            .prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        // First ensure the recording gets some status events
-        invokeSurfaceRequest()
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
+        // Act.
+        recording.startAndVerify()
         // Remove reference to recording and run GC. The recording should be stopped once
         // the Recording's finalizer runs.
+        recordingsToStop.remove(recording)
+        @Suppress("UNUSED_VALUE")
         recording = null
         GarbageCollectionUtil.runFinalization()
 
-        // Ensure the event listener gets a finalize event. Note: the word "finalize" is very
-        // overloaded here. This event means the recording has finished, but does not relate to the
-        // finalizer that runs during garbage collection. However, that is what causes the
+        // Assert: Ensure the event listener gets a finalize event. Note: the word "finalize" is
+        // very overloaded here. This event means the recording has finished, but does not relate
+        // to the finalizer that runs during garbage collection. However, that is what causes the
         // recording to finish.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        file.delete()
+        listener.verifyFinalize()
     }
 
     @Test
     fun stop_noOpWhenStopping() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify()
+        recording.stop()
 
-                stopSafely()
-                stopSafely()
-            }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-        mockVideoRecordEventConsumer.verifyNoMoreAcceptCalls(true)
-
-        file.delete()
+        // Assert.
+        recording.verifyNoMoreEvent()
     }
 
     @Test
     fun optionsOverridesDefaults() {
-        initializeRecorder()
         val qualitySelector = QualitySelector.from(Quality.HIGHEST)
-        val recorder = Recorder.Builder()
-            .setQualitySelector(qualitySelector)
-            .build()
+        val recorder = createRecorder(qualitySelector = qualitySelector)
 
         assertThat(recorder.qualitySelector).isEqualTo(qualitySelector)
     }
 
     @Test
     fun canRetrieveProvidedExecutorFromRecorder() {
-        initializeRecorder()
         val myExecutor = Executor { command -> command?.run() }
-        val recorder = Recorder.Builder()
-            .setExecutor(myExecutor)
-            .build()
+        val recorder = createRecorder(executor = myExecutor)
 
         assertThat(recorder.executor).isSameInstanceAs(myExecutor)
     }
 
     @Test
     fun cannotRetrieveExecutorWhenExecutorNotProvided() {
-        initializeRecorder()
-        val recorder = Recorder.Builder().build()
+        val recorder = createRecorder()
 
         assertThat(recorder.executor).isNull()
     }
 
     @Test
     fun canRecordWithoutAudio() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess(withAudio = false)
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        // Check the audio information reports state as disabled.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            val uri = finalize.outputResults.outputUri
+            checkFileHasAudioAndVideo(uri, hasAudio = false)
         }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent::class.java,
-            false, CallTimesAtLeast(1), captor)
-
-        assertThat(captor.value).isInstanceOf(VideoRecordEvent.Status::class.java)
-        val status = captor.value as VideoRecordEvent.Status
-        assertThat(status.recordingStats.audioStats.audioState)
-            .isEqualTo(AudioStats.AUDIO_STATE_DISABLED)
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
-            false, GENERAL_TIMEOUT)
-
-        checkFileAudio(Uri.fromFile(file), false)
-        checkFileVideo(Uri.fromFile(file), true)
-
-        file.delete()
     }
 
     @Test
     fun cannotStartMultiplePendingRecordingsWhileInitializing() {
-        initializeRecorder()
-        val file1 = File.createTempFile("CameraX1", ".tmp").apply { deleteOnExit() }
-        val file2 = File.createTempFile("CameraX2", ".tmp").apply { deleteOnExit() }
-        try {
-            // We explicitly do not invoke the surface request so the recorder is initializing.
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file1).build())
-                .start(CameraXExecutors.directExecutor()) {}
-                .apply {
-                    assertThrows<IllegalStateException> {
-                        recorder.prepareRecording(context, FileOutputOptions.Builder(file2).build())
-                            .start(CameraXExecutors.directExecutor()) {}
-                    }
-                    stopSafely()
-                }
-        } finally {
-            file1.delete()
-            file2.delete()
+        // Arrange: Prepare 1st recording and start.
+        val recorder = createRecorder(sendSurfaceRequest = false)
+        val recording = createRecordingProcess(recorder = recorder)
+        recording.start()
+
+        // Assert.
+        assertThrows<IllegalStateException> {
+            // Act: Prepare 2nd recording and start.
+            createRecordingProcess(recorder = recorder).start()
         }
     }
 
     @Test
     fun canRecoverFromErrorState(): Unit = runBlocking {
-        initializeRecorder()
+        // Arrange.
         // Create a video encoder factory that will fail on first 2 create encoder requests.
-        // Recorder initialization should fail by 1st encoder creation fail.
-        // 1st recording request should fail by 2nd encoder creation fail.
-        // 2nd recording request should be successful.
         var createEncoderRequestCount = 0
-        val recorder = Recorder.Builder()
-            .setVideoEncoderFactory { executor, config ->
-                if (createEncoderRequestCount < 2) {
-                    createEncoderRequestCount++
-                    throw InvalidConfigException("Create video encoder fail on purpose.")
-                } else {
-                    Recorder.DEFAULT_ENCODER_FACTORY.createEncoder(executor, config)
-                }
-            }.build().apply { onSourceStateChanged(VideoOutput.SourceState.INACTIVE) }
-
-        invokeSurfaceRequest(recorder)
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
+        val recorder = createRecorder(
+            videoEncoderFactory = { executor, config ->
+            if (createEncoderRequestCount < 2) {
+                createEncoderRequestCount++
+                throw InvalidConfigException("Create video encoder fail on purpose.")
+            } else {
+                Recorder.DEFAULT_ENCODER_FACTORY.createEncoder(executor, config)
+            }
+        })
+        // Recorder initialization should fail by 1st encoder creation fail.
         // Wait STREAM_ID_ERROR which indicates Recorder enter the error state.
         withTimeoutOrNull(3000) {
             recorder.streamInfo.asFlow().dropWhile { it!!.id != StreamInfo.STREAM_ID_ERROR }.first()
         } ?: fail("Do not observe STREAM_ID_ERROR from StreamInfo observer.")
 
-        // 1st recording request
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).let {
-                val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-                    VideoRecordEvent::class.java.isInstance(
-                        argument
-                    )
-                }
+        // Act: 1st recording request should fail by 2nd encoder creation fail.
+        var recording = createRecordingProcess(recorder = recorder)
+        recording.start()
+        recording.verifyFinalize { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_RECORDER_ERROR)
+        }
 
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent::class.java,
-                    false,
-                    3000L,
-                    CallTimesAtLeast(1),
-                    captor
-                )
-
-                val finalize = captor.value as VideoRecordEvent.Finalize
-                assertThat(finalize.error).isEqualTo(ERROR_RECORDER_ERROR)
-            }
-
-        // 2nd recording request
-        mockVideoRecordEventConsumer.clearAcceptCalls()
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).let {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-                it.stopSafely()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Finalize::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-            }
-
-        file.delete()
+        // Act: 2nd recording request should be successful.
+        recording = createRecordingProcess(recorder = recorder)
+        recording.startAndVerify()
+        recording.stopAndVerify()
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 31)
     fun audioRecordIsAttributed() = runBlocking {
-        initializeRecorder()
+        // Arrange.
         val notedTag = CompletableDeferred<String>()
         val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
-        appOps.setOnOpNotedCallback(
-            Dispatchers.Main.asExecutor(),
-            object : AppOpsManager.OnOpNotedCallback() {
-                override fun onNoted(p0: SyncNotedAppOp) {
-                    // no-op. record_audio should be async.
-                }
+        appOps.setOnOpNotedCallback(Dispatchers.Main.asExecutor(), object : OnOpNotedCallback() {
+            override fun onNoted(p0: SyncNotedAppOp) {
+                // no-op. record_audio should be async.
+            }
 
-                override fun onSelfNoted(p0: SyncNotedAppOp) {
-                    // no-op. record_audio should be async.
-                }
+            override fun onSelfNoted(p0: SyncNotedAppOp) {
+                // no-op. record_audio should be async.
+            }
 
-                override fun onAsyncNoted(noted: AsyncNotedAppOp) {
-                    if (AppOpsManager.OPSTR_RECORD_AUDIO == noted.op &&
-                        TEST_ATTRIBUTION_TAG == noted.attributionTag
-                    ) {
-                        notedTag.complete(noted.attributionTag!!)
-                    }
+            override fun onAsyncNoted(noted: AsyncNotedAppOp) {
+                if (AppOpsManager.OPSTR_RECORD_AUDIO == noted.op &&
+                    TEST_ATTRIBUTION_TAG == noted.attributionTag
+                ) {
+                    notedTag.complete(noted.attributionTag!!)
                 }
-            })
+            }
+        })
+        val attributionContext = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
+        val recording = createRecordingProcess(context = attributionContext)
 
-        var recording: Recording? = null
+        // Act.
+        recording.start()
         try {
-            val attributionContext = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
-            invokeSurfaceRequest()
-            val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-            recording =
-                recorder.prepareRecording(
-                    attributionContext, FileOutputOptions.Builder(file).build()
-                )
-                    .withAudioEnabled()
-                    .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
             val timeoutDuration = 5.seconds
             withTimeoutOrNull(timeoutDuration) {
+                // Assert.
                 assertThat(notedTag.await()).isEqualTo(TEST_ATTRIBUTION_TAG)
             } ?: fail("Timed out waiting for attribution tag. Waited $timeoutDuration.")
         } finally {
             appOps.setOnOpNotedCallback(null, null)
-            recording?.stopSafely()
         }
     }
 
-    private fun invokeSurfaceRequest() {
-        invokeSurfaceRequest(recorder)
-    }
+    private fun testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(targetBitrate: Int) {
+        // Arrange.
+        val recorder = createRecorder(targetBitrate = targetBitrate)
+        val recording = createRecordingProcess(recorder = recorder, withAudio = false)
 
-    private fun invokeSurfaceRequest(recorder: Recorder) {
-        instrumentation.runOnMainSync {
-            preview.setSurfaceProvider { request: SurfaceRequest ->
-                recorder.onSurfaceRequested(request)
-            }
-            recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_STREAMING)
-        }
-    }
-
-    private fun initializeRecorder(bitrate: Int = BITRATE_AUTO) {
-        recorder = Recorder.Builder().apply {
-            if (bitrate != BITRATE_AUTO) {
-                setTargetVideoEncodingBitRate(bitrate)
-            }
-        }.build()
-        recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-    }
-
-    private fun testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(
-        bitrate: Int,
-        enableAudio: Boolean = false
-    ) {
-        initializeRecorder(bitrate)
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .apply { if (enableAudio) withAudioEnabled() }
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true,
-            GENERAL_TIMEOUT
-        )
-
-        val uri = Uri.fromFile(file)
-        if (enableAudio) {
-            checkFileHasAudioAndVideo(uri)
-        } else {
-            checkFileVideo(uri, true)
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_NONE)
         }
 
-        // Check the output Uri from the finalize event match the Uri from the given file.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
-
-        file.delete()
-    }
-
-    private fun verifyConfiguredVideoBitrate() {
+        // Assert.
         assertThat(recorder.mFirstRecordingVideoBitrate).isIn(
             com.google.common.collect.Range.closed(
                 recorder.mVideoEncoderBitrateRange.lower,
@@ -1388,96 +919,224 @@
         )
     }
 
-    private fun checkFileHasAudioAndVideo(uri: Uri) {
-        checkFileAudio(uri, true)
-        checkFileVideo(uri, true)
-    }
-
-    private fun checkFileAudio(uri: Uri, hasAudio: Boolean) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
-
-                assertThat(value).isEqualTo(
-                    if (hasAudio) {
-                        "yes"
-                    } else {
-                        null
-                    }
-                )
-            } finally {
-                release()
+    private fun Recorder.sendSurfaceRequest() {
+        instrumentation.runOnMainSync {
+            preview.setSurfaceProvider { request: SurfaceRequest ->
+                onSurfaceRequested(request)
             }
         }
     }
 
-    private fun checkFileVideo(uri: Uri, hasVideo: Boolean) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+    private fun createTempFile() = temporaryFolder.newFile()
 
-                assertThat(value).isEqualTo(
-                    if (hasVideo) {
-                        "yes"
-                    } else {
-                        null
-                    }
-                )
-            } finally {
-                release()
+    private fun createRecorder(
+        sendSurfaceRequest: Boolean = true,
+        initSourceState: VideoOutput.SourceState = ACTIVE_STREAMING,
+        qualitySelector: QualitySelector? = null,
+        executor: Executor? = null,
+        videoEncoderFactory: EncoderFactory? = null,
+        audioEncoderFactory: EncoderFactory? = null,
+        targetBitrate: Int? = null,
+    ): Recorder {
+        val recorder = Recorder.Builder().apply {
+            qualitySelector?.let { setQualitySelector(it) }
+            executor?.let { setExecutor(it) }
+            videoEncoderFactory?.let { setVideoEncoderFactory(it) }
+            audioEncoderFactory?.let { setAudioEncoderFactory(it) }
+            targetBitrate?.let { setTargetVideoEncodingBitRate(targetBitrate) }
+        }.build()
+        if (sendSurfaceRequest) {
+            recorder.sendSurfaceRequest()
+        }
+        recorder.onSourceStateChanged(initSourceState)
+        return recorder
+    }
+
+    private fun createFileOutputOptions(
+        file: File = createTempFile(),
+        fileSizeLimit: Long? = null,
+        durationLimitMillis: Long? = null,
+        location: Location? = null,
+    ): FileOutputOptions = FileOutputOptions.Builder(file).apply {
+        fileSizeLimit?.let { setFileSizeLimit(it) }
+        durationLimitMillis?.let { setDurationLimitMillis(it) }
+        location?.let { setLocation(it) }
+    }.build()
+
+    private fun createRecordingProcess(
+        recorder: Recorder = createRecorder(),
+        context: Context = ApplicationProvider.getApplicationContext(),
+        outputOptions: OutputOptions = createFileOutputOptions(),
+        withAudio: Boolean = true
+    ) = RecordingProcess(
+        recorder,
+        context,
+        outputOptions,
+        withAudio
+    )
+
+    inner class RecordingProcess(
+        private val recorder: Recorder,
+        context: Context,
+        outputOptions: OutputOptions,
+        withAudio: Boolean
+    ) {
+        private val pendingRecording: PendingRecording =
+            PendingRecording(context, recorder, outputOptions).apply {
+                if (withAudio) {
+                    withAudioEnabled()
+                }
             }
+        val listener = MockConsumer<VideoRecordEvent>()
+        private lateinit var recording: Recording
+
+        fun startAndVerify(
+            statusCount: Int = DEFAULT_STATUS_COUNT,
+            onStatus: ((List<Status>) -> Unit)? = null,
+        ) = startInternal(verify = true, statusCount = statusCount, onStatus = onStatus)
+
+        fun start() = startInternal(verify = false)
+
+        private fun startInternal(
+            verify: Boolean = false,
+            statusCount: Int = DEFAULT_STATUS_COUNT,
+            onStatus: ((List<Status>) -> Unit)? = null
+        ) {
+            recording = pendingRecording.start(mainThreadExecutor(), listener)
+            recordingsToStop.add(this)
+            if (verify) {
+                verifyStart()
+                verifyStatus(statusCount = statusCount, onStatus = onStatus)
+            }
+        }
+
+        fun verifyStart() {
+            listener.verifyStart()
+        }
+
+        fun verifyStatus(
+            statusCount: Int = DEFAULT_STATUS_COUNT,
+            onStatus: ((List<Status>) -> Unit)? = null,
+        ) {
+            listener.verifyStatus(eventCount = statusCount, onEvent = onStatus)
+        }
+
+        fun stopAndVerify(onFinalize: ((Finalize) -> Unit)? = null) =
+            stopInternal(verify = true, onFinalize)
+
+        fun stop() = stopInternal(verify = false)
+
+        private fun stopInternal(
+            verify: Boolean = false,
+            onFinalize: ((Finalize) -> Unit)? = null
+        ) {
+            recording.stopSafely(recorder)
+            if (verify) {
+                verifyFinalize(onFinalize = onFinalize)
+            }
+        }
+
+        fun verifyFinalize(
+            timeoutMs: Long = GENERAL_TIMEOUT,
+            onFinalize: ((Finalize) -> Unit)? = null
+        ) = listener.verifyFinalize(timeoutMs = timeoutMs, onFinalize = onFinalize)
+
+        fun pauseAndVerify() = pauseInternal(verify = true)
+
+        fun pause() = pauseInternal(verify = false)
+
+        private fun pauseInternal(verify: Boolean = false) {
+            recording.pause()
+            if (verify) {
+                verifyPause()
+            }
+        }
+
+        fun verifyPause() = listener.verifyPause()
+
+        fun resumeAndVerify() = resumeInternal(verify = true)
+
+        fun resume() = resumeInternal(verify = false)
+
+        private fun resumeInternal(verify: Boolean = false) {
+            recording.resume()
+            if (verify) {
+                verifyResume()
+            }
+        }
+
+        private fun verifyResume() {
+            listener.verifyResume()
+            listener.verifyStatus()
+        }
+
+        fun getAllEvents(): List<VideoRecordEvent> {
+            lateinit var events: List<VideoRecordEvent>
+            listener.verifyEvent(
+                VideoRecordEvent::class.java,
+                CallTimesAtLeast(1),
+                onEvent = {
+                    events = it
+                }
+            )
+            return events
+        }
+
+        fun verifyNoMoreEvent() = listener.verifyNoMoreAcceptCalls(/*inOrder=*/true)
+    }
+
+    private fun checkFileHasAudioAndVideo(
+        uri: Uri,
+        hasAudio: Boolean = true,
+    ) {
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, uri)
+            assertThat(it.hasVideo()).isEqualTo(true)
+            assertThat(it.hasAudio()).isEqualTo(hasAudio)
         }
     }
 
+    @Suppress("SameParameterValue")
     private fun checkLocation(uri: Uri, location: Location) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                // Only test on mp4 output format, others will be ignored.
-                val mime = extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
-                assumeTrue("Unsupported mime = $mime",
-                    "video/mp4".equals(mime, ignoreCase = true))
-                val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
-                assertThat(value).isNotNull()
-                // ex: (90, 180) => "+90.0000+180.0000/" (ISO-6709 standard)
-                val matchGroup =
-                    "([\\+-]?[0-9]+(\\.[0-9]+)?)([\\+-]?[0-9]+(\\.[0-9]+)?)".toRegex()
-                        .find(value!!) ?: fail("Fail on checking location metadata: $value")
-                val lat = matchGroup.groupValues[1].toDouble()
-                val lon = matchGroup.groupValues[3].toDouble()
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, uri)
+            // Only test on mp4 output format, others will be ignored.
+            val mime = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
+            assumeTrue("Unsupported mime = $mime",
+                "video/mp4".equals(mime, ignoreCase = true))
+            val value = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
+            assertThat(value).isNotNull()
+            // ex: (90, 180) => "+90.0000+180.0000/" (ISO-6709 standard)
+            val matchGroup =
+                "([+-]?[0-9]+(\\.[0-9]+)?)([+-]?[0-9]+(\\.[0-9]+)?)".toRegex()
+                    .find(value!!) ?: fail("Fail on checking location metadata: $value")
+            val lat = matchGroup.groupValues[1].toDouble()
+            val lon = matchGroup.groupValues[3].toDouble()
 
-                // MediaMuxer.setLocation rounds the value to 4 decimal places
-                val tolerance = 0.0001
-                assertWithMessage("Fail on latitude. $lat($value) vs ${location.latitude}")
-                    .that(lat).isWithin(tolerance).of(location.latitude)
-                assertWithMessage("Fail on longitude. $lon($value) vs ${location.longitude}")
-                    .that(lon).isWithin(tolerance).of(location.longitude)
-            } finally {
-                release()
-            }
+            // MediaMuxer.setLocation rounds the value to 4 decimal places
+            val tolerance = 0.0001
+            assertWithMessage("Fail on latitude. $lat($value) vs ${location.latitude}")
+                .that(lat).isWithin(tolerance).of(location.latitude)
+            assertWithMessage("Fail on longitude. $lon($value) vs ${location.longitude}")
+                .that(lon).isWithin(tolerance).of(location.longitude)
         }
     }
 
+    @Suppress("SameParameterValue")
     private fun checkDurationAtMost(uri: Uri, duration: Long) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                val durationFromFile = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, uri)
+            val durationFromFile = it.getDuration()
 
-                assertThat(durationFromFile).isNotNull()
-                assertThat(durationFromFile!!.toLong()).isAtMost(duration)
-            } finally {
-                release()
-            }
+            assertThat(durationFromFile).isNotNull()
+            assertThat(durationFromFile!!).isAtMost(duration)
         }
     }
 
     // It fails on devices with certain chipset if the codec is stopped when the camera is still
     // producing frames to the provided surface. This method first stop the camera from
     // producing frames then stops the recording safely on the problematic devices.
-    private fun Recording.stopSafely() {
+    private fun Recording.stopSafely(recorder: Recorder) {
         val deactivateSurfaceBeforeStop =
             DeviceQuirks.get(DeactivateEncoderSurfaceBeforeStopEncoderQuirk::class.java) != null
         if (deactivateSurfaceBeforeStop) {
@@ -1487,70 +1146,21 @@
         }
         stop()
         if (deactivateSurfaceBeforeStop && Build.VERSION.SDK_INT >= 23) {
-            invokeSurfaceRequest()
+            recorder.sendSurfaceRequest()
         }
     }
 
-    private fun runFileSizeLimitTest(fileSizeLimit: Long) {
-        // For the file size is small, the final file length possibly exceeds the file size limit
-        // after adding the file header. We still add the buffer for the tolerance of comparing the
-        // file length and file size limit.
-        val sizeLimitBuffer = 50 * 1024 // 50k threshold buffer
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file)
-            .setFileSizeLimit(fileSizeLimit)
-            .build()
-
-        val recording = recorder
-            .prepareRecording(context, outputOptions)
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            false, 60000L
-        )
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
-                argument -> VideoRecordEvent::class.java.isInstance(argument)
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent::class.java,
-            false, CallTimesAtLeast(1), captor)
-
-        assertThat(captor.value).isInstanceOf(VideoRecordEvent.Finalize::class.java)
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
-        assertThat(file.length()).isLessThan(fileSizeLimit + sizeLimitBuffer)
-
-        recording.stopSafely()
-
-        file.delete()
-    }
-
     private fun runLocationTest(location: Location) {
-        invokeSurfaceRequest(recorder)
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file)
-            .setLocation(location)
-            .build()
+        // Arrange.
+        val outputOptions = createFileOutputOptions(location = location)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        val recording = recorder
-            .prepareRecording(context, outputOptions)
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        checkLocation(Uri.fromFile(file), location)
-
-        file.delete()
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            checkLocation(finalize.outputResults.outputUri, location)
+        }
     }
 
     private fun createLocation(
@@ -1563,50 +1173,85 @@
             this.longitude = longitude
         }
 
-    private fun MockConsumer<VideoRecordEvent>.verifyRecordingStartSuccessfully() {
-        verifyAcceptCall(
-            VideoRecordEvent.Start::class.java,
-            true,
-            GENERAL_TIMEOUT
-        )
-        verifyAcceptCall(
-            VideoRecordEvent.Status::class.java,
-            true,
-            STATUS_TIMEOUT,
-            CallTimesAtLeast(5)
+    private fun MockConsumer<VideoRecordEvent>.verifyStart(
+        inOrder: Boolean = true,
+        onEvent: ((Start) -> Unit)? = null
+    ) {
+        verifyEvent(Start::class.java, inOrder = inOrder, onEvent = onEvent)
+    }
+
+    private fun MockConsumer<VideoRecordEvent>.verifyFinalize(
+        inOrder: Boolean = true,
+        timeoutMs: Long = GENERAL_TIMEOUT,
+        onFinalize: ((Finalize) -> Unit)? = null
+    ) {
+        verifyEvent(
+            Finalize::class.java,
+            inOrder = inOrder,
+            timeoutMs = timeoutMs,
+            onEvent = onFinalize
         )
     }
-    class VideoCaptureMonitor : Consumer<VideoRecordEvent> {
-        private var countDown: CountDownLatch? = null
 
-        fun waitForVideoCaptureStatus(
-            count: Int = 10,
-            timeoutMillis: Long = TimeUnit.SECONDS.toMillis(10)
-        ) {
-            assertWithMessage("Video recording doesn't start").that(synchronized(this) {
-                countDown = CountDownLatch(count)
-                countDown
-            }!!.await(timeoutMillis, TimeUnit.MILLISECONDS)).isTrue()
+    private fun MockConsumer<VideoRecordEvent>.verifyStatus(
+        eventCount: Int = DEFAULT_STATUS_COUNT,
+        inOrder: Boolean = true,
+        onEvent: ((List<Status>) -> Unit)? = null,
+    ) {
+        verifyEvent(
+            Status::class.java,
+            CallTimesAtLeast(eventCount),
+            inOrder = inOrder,
+            timeoutMs = STATUS_TIMEOUT,
+            onEvent = onEvent
+        )
+    }
+
+    private fun MockConsumer<VideoRecordEvent>.verifyPause(
+        inOrder: Boolean = true,
+        onEvent: ((Pause) -> Unit)? = null
+    ) {
+        verifyEvent(Pause::class.java, inOrder = inOrder, onEvent = onEvent)
+    }
+
+    private fun MockConsumer<VideoRecordEvent>.verifyResume(
+        inOrder: Boolean = true,
+        onEvent: ((Resume) -> Unit)? = null,
+    ) {
+        verifyEvent(Resume::class.java, inOrder = inOrder, onEvent = onEvent)
+    }
+
+    private fun <T : VideoRecordEvent> MockConsumer<VideoRecordEvent>.verifyEvent(
+        eventType: Class<T>,
+        inOrder: Boolean = false,
+        timeoutMs: Long = GENERAL_TIMEOUT,
+        onEvent: ((T) -> Unit)? = null,
+    ) {
+        verifyEvent(
+            eventType,
+            callTimes = CallTimes(1),
+            inOrder = inOrder,
+            timeoutMs = timeoutMs
+        ) { events ->
+            onEvent?.invoke(events.last())
         }
+    }
 
-        override fun accept(event: VideoRecordEvent?) {
-            when (event) {
-                is VideoRecordEvent.Status -> {
-                    synchronized(this) {
-                        countDown?.countDown()
-                    }
-                }
-                else -> {
-                    // Ignore other events.
-                }
+    private fun <T : VideoRecordEvent> MockConsumer<VideoRecordEvent>.verifyEvent(
+        eventType: Class<T>,
+        callTimes: CallTimes,
+        inOrder: Boolean = false,
+        timeoutMs: Long = GENERAL_TIMEOUT,
+        onEvent: ((List<T>) -> Unit)? = null,
+    ) {
+        verifyAcceptCall(eventType, inOrder, timeoutMs, callTimes)
+        if (onEvent != null) {
+            val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+                eventType.isInstance(argument)
             }
+            verifyAcceptCall(eventType, false, callTimes, captor)
+            @Suppress("UNCHECKED_CAST")
+            onEvent.invoke(captor.allValues as List<T>)
         }
     }
-
-    private fun Recorder.startVideoRecording(
-        file: File,
-        eventListener: Consumer<VideoRecordEvent>
-    ): Recording = prepareRecording(
-        context, FileOutputOptions.Builder(file).build()
-    ).start(Dispatchers.Main.asExecutor(), eventListener)
 }
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 3b00ca9..912c57a 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -448,7 +448,7 @@
         instrumentation.runOnMainSync {
             cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture)
         }
-        val videoCaptureMonitor = RecorderTest.VideoCaptureMonitor()
+        val videoCaptureMonitor = VideoCaptureMonitor()
         videoCapture.startVideoRecording(temporaryFolder.newFile(), videoCaptureMonitor).use {
             // Ensure the Recorder is initialized before start test.
             videoCaptureMonitor.waitForVideoCaptureStatus()
@@ -1080,10 +1080,46 @@
         )
 }
 
-private fun MediaMetadataRetriever.useAndRelease(block: (MediaMetadataRetriever) -> Unit) {
+private class VideoCaptureMonitor : Consumer<VideoRecordEvent> {
+    private var countDown: CountDownLatch? = null
+
+    fun waitForVideoCaptureStatus(
+        count: Int = 10,
+        timeoutMillis: Long = TimeUnit.SECONDS.toMillis(10)
+    ) {
+        assertWithMessage("Video recording doesn't start").that(synchronized(this) {
+            countDown = CountDownLatch(count)
+            countDown
+        }!!.await(timeoutMillis, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    override fun accept(event: VideoRecordEvent?) {
+        when (event) {
+            is VideoRecordEvent.Status -> {
+                synchronized(this) {
+                    countDown?.countDown()
+                }
+            }
+            else -> {
+                // Ignore other events.
+            }
+        }
+    }
+}
+
+internal fun MediaMetadataRetriever.useAndRelease(block: (MediaMetadataRetriever) -> Unit) {
     try {
         block(this)
     } finally {
         release()
     }
 }
+
+internal fun MediaMetadataRetriever.hasAudio(): Boolean =
+    extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == "yes"
+
+internal fun MediaMetadataRetriever.hasVideo(): Boolean =
+    extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) == "yes"
+
+internal fun MediaMetadataRetriever.getDuration(): Long? =
+    extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.ignore b/compose/ui/ui-graphics/api/current.ignore
new file mode 100644
index 0000000..ac8aa37
--- /dev/null
+++ b/compose/ui/ui-graphics/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getPosition(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getPosition(float)
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getTangent(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getTangent(float)
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index f1c4192..8f387eca 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -115,7 +115,9 @@
 
   public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public float length;
   }
@@ -666,7 +668,9 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public abstract float length;
   }
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 90bb660..f5a56d4 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -115,7 +115,9 @@
 
   public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public float length;
   }
@@ -669,7 +671,9 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public abstract float length;
   }
diff --git a/compose/ui/ui-graphics/api/restricted_current.ignore b/compose/ui/ui-graphics/api/restricted_current.ignore
new file mode 100644
index 0000000..ac8aa37
--- /dev/null
+++ b/compose/ui/ui-graphics/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getPosition(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getPosition(float)
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getTangent(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getTangent(float)
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index adad1a5..a7f88e5 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -145,7 +145,9 @@
 
   public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public float length;
   }
@@ -698,7 +700,9 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public abstract float length;
   }
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
new file mode 100644
index 0000000..afacfe3
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.graphics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PathMeasureTest {
+
+    @Test
+    fun testGetPositionAndTangent() {
+        val width = 100f
+        val height = 100f
+        val path = Path().apply {
+            lineTo(width, height)
+        }
+        val pathMeasure = PathMeasure()
+
+        pathMeasure.setPath(path, false)
+        val distance = pathMeasure.length
+        val position = pathMeasure.getPosition(distance * 0.5f)
+
+        val tangent = pathMeasure.getTangent(distance * 0.5f)
+
+        Assert.assertEquals(50f, position.x)
+        Assert.assertEquals(50f, position.y)
+        Assert.assertEquals(0.707106f, tangent.x, 0.00001f)
+        Assert.assertEquals(0.707106f, tangent.y, 0.00001f)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
index d9996fa..358b3af 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.ui.geometry.Offset
+
 actual fun PathMeasure(): PathMeasure = AndroidPathMeasure(android.graphics.PathMeasure())
 
 class AndroidPathMeasure internal constructor(
@@ -25,6 +27,10 @@
     override val length: Float
         get() = internalPathMeasure.length
 
+    private var positionArray: FloatArray? = null
+
+    private var tangentArray: FloatArray? = null
+
     override fun getSegment(
         startDistance: Float,
         stopDistance: Float,
@@ -42,4 +48,38 @@
     override fun setPath(path: Path?, forceClosed: Boolean) {
         internalPathMeasure.setPath(path?.asAndroidPath(), forceClosed)
     }
+
+    override fun getPosition(
+        distance: Float
+    ): Offset {
+        if (positionArray == null) {
+            positionArray = FloatArray(2)
+        }
+        if (tangentArray == null) {
+            tangentArray = FloatArray(2)
+        }
+        val result = internalPathMeasure.getPosTan(distance, positionArray, tangentArray)
+        return if (result) {
+            Offset(positionArray!![0], positionArray!![1])
+        } else {
+            Offset.Unspecified
+        }
+    }
+
+    override fun getTangent(
+        distance: Float
+    ): Offset {
+        if (positionArray == null) {
+            positionArray = FloatArray(2)
+        }
+        if (tangentArray == null) {
+            tangentArray = FloatArray(2)
+        }
+        val result = internalPathMeasure.getPosTan(distance, positionArray, tangentArray)
+        return if (result) {
+            Offset(tangentArray!![0], tangentArray!![1])
+        } else {
+            Offset.Unspecified
+        }
+    }
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
index 9e168cb..d2ee71f 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.internal.JvmDefaultWithCompatibility
 
 /**
@@ -53,4 +54,22 @@
      * Assign a new path, or null to have none.
      */
     fun setPath(path: Path?, forceClosed: Boolean)
+
+    /**
+     * Pins distance to 0 <= distance <= getLength(), and then computes the corresponding position
+     *
+     * @param distance The distance along the current contour to sample
+     *
+     * @return [Offset.Unspecified] if there is no path set
+     */
+    fun getPosition(distance: Float): Offset
+
+    /**
+     * Pins distance to 0 <= distance <= getLength(), and then computes the corresponding tangent
+     *
+     * @param distance The distance along the current contour to sample
+     *
+     * @return [Offset.Unspecified] if there is no path set
+     */
+    fun getTangent(distance: Float): Offset
 }
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
index 7658c68..810a409 100644
--- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.ui.geometry.Offset
 /**
  * Convert the [org.jetbrains.skia.PathMeasure] instance into a Compose-compatible PathMeasure
  */
@@ -49,6 +50,28 @@
 
     override val length: Float
         get() = skia.length
+
+    override fun getPosition(
+        distance: Float
+    ): Offset {
+        val result = skia.getPosition(distance)
+        return if (result != null) {
+            Offset(result.x, result.y)
+        } else {
+            Offset.Unspecified
+        }
+    }
+
+    override fun getTangent(
+        distance: Float
+    ): Offset {
+        val result = skia.getTangent(distance)
+        return if (result != null) {
+            Offset(result.x, result.y)
+        } else {
+            Offset.Unspecified
+        }
+    }
 }
 
 actual fun PathMeasure(): PathMeasure =
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
index ca445ce..071f8bd 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
@@ -21,7 +21,6 @@
 import android.os.OutcomeReceiver
 import android.service.credentials.BeginCreateCredentialRequest
 import android.service.credentials.BeginCreateCredentialResponse
-import android.service.credentials.BeginGetCredentialOption
 import android.service.credentials.BeginGetCredentialRequest
 import android.service.credentials.BeginGetCredentialResponse
 import android.service.credentials.CredentialProviderService
@@ -48,21 +47,7 @@
         cancellationSignal: CancellationSignal,
         callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
     ) {
-        val beginGetCredentialOptions: MutableList<BeginGetCredentialOption> =
-            mutableListOf()
-        request.beginGetCredentialOptions.forEach {
-            val structuredOption = BeginGetCredentialUtil
-                .convertRequestOption(
-                    it.type,
-                    it.candidateQueryData)
-            if (structuredOption != null) {
-                beginGetCredentialOptions.add(structuredOption)
-            }
-        }
-        val structuredRequest =
-            BeginGetCredentialRequest.Builder(request.callingAppInfo)
-                .setBeginGetCredentialOptions(beginGetCredentialOptions)
-                .build()
+        val structuredRequest = BeginGetCredentialUtil.convertToStructuredRequest(request)
         val outcome = object : OutcomeReceiver<BeginGetCredentialResponse,
             androidx.credentials.exceptions.GetCredentialException> {
             override fun onResult(response: BeginGetCredentialResponse?) {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
index c2ae7cb..64277fc 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
@@ -26,6 +26,7 @@
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.credentials.GetCredentialResponse
+import androidx.credentials.provider.utils.BeginGetCredentialUtil
 
 /**
  * PendingIntentHandler to be used by credential providers to extract requests from
@@ -79,10 +80,11 @@
          */
         @JvmStatic
         fun getBeginGetCredentialRequest(intent: Intent): BeginGetCredentialRequest? {
-            return intent.getParcelableExtra(
+            val request = intent.getParcelableExtra(
                 "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST",
                 BeginGetCredentialRequest::class.java
             )
+            return request?.let { BeginGetCredentialUtil.convertToStructuredRequest(it) }
         }
 
         /**
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
index 8e85125..fef103b 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
@@ -35,7 +35,7 @@
 class BeginCreateCredentialUtil {
     companion object {
         @JvmStatic
-        fun convertToStructuredRequest(request: BeginCreateCredentialRequest):
+        internal fun convertToStructuredRequest(request: BeginCreateCredentialRequest):
             BeginCreateCredentialRequest {
             return try {
                 when (request.type) {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
index bed5c11..c743c49 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
@@ -18,12 +18,12 @@
 
 import android.os.Bundle
 import android.service.credentials.BeginGetCredentialOption
+import android.service.credentials.BeginGetCredentialRequest
 import androidx.annotation.RequiresApi
 import androidx.credentials.GetPublicKeyCredentialOption
 import androidx.credentials.GetPublicKeyCredentialOptionPrivileged
 import androidx.credentials.PasswordCredential
 import androidx.credentials.PublicKeyCredential
-import androidx.credentials.internal.FrameworkClassParsingException
 import androidx.credentials.provider.BeginGetCustomCredentialOption
 import androidx.credentials.provider.BeginGetPasswordOption
 import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
@@ -36,37 +36,49 @@
 class BeginGetCredentialUtil {
     companion object {
         @JvmStatic
-        fun convertRequestOption(type: String, candidateQueryData: Bundle):
-            BeginGetCredentialOption? {
-            return try {
-                when (type) {
-                    PasswordCredential.TYPE_PASSWORD_CREDENTIAL -> {
-                        BeginGetPasswordOption(candidateQueryData)
-                    }
-                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> {
-                        when (candidateQueryData.getString(
-                            PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
-                            GetPublicKeyCredentialOption
-                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION -> {
-                                BeginGetPublicKeyCredentialOption.createFrom(candidateQueryData)
-                            }
-                            GetPublicKeyCredentialOptionPrivileged
-                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
-                            -> {
-                                BeginGetPublicKeyCredentialOptionPrivileged
-                                    .createFrom(candidateQueryData)
-                            }
-                            else -> {
-                                throw FrameworkClassParsingException()
-                            }
+        internal fun convertToStructuredRequest(request: BeginGetCredentialRequest):
+            BeginGetCredentialRequest {
+            val beginGetCredentialOptions: MutableList<BeginGetCredentialOption> =
+                mutableListOf()
+            request.beginGetCredentialOptions.forEach {
+                beginGetCredentialOptions.add(convertRequestOption(
+                    it.type,
+                    it.candidateQueryData)
+                )
+            }
+            return BeginGetCredentialRequest.Builder(request.callingAppInfo)
+                .setBeginGetCredentialOptions(beginGetCredentialOptions)
+                .build()
+        }
+        @JvmStatic
+        internal fun convertRequestOption(type: String, candidateQueryData: Bundle):
+            BeginGetCredentialOption {
+            return when (type) {
+                PasswordCredential.TYPE_PASSWORD_CREDENTIAL -> {
+                    BeginGetPasswordOption(candidateQueryData)
+                }
+                PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> {
+                    when (candidateQueryData.getString(
+                        PublicKeyCredential.BUNDLE_KEY_SUBTYPE
+                    )) {
+                        GetPublicKeyCredentialOption
+                            .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION -> {
+                            BeginGetPublicKeyCredentialOption.createFrom(candidateQueryData)
+                        }
+                        GetPublicKeyCredentialOptionPrivileged
+                            .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
+                        -> {
+                            BeginGetPublicKeyCredentialOptionPrivileged
+                                .createFrom(candidateQueryData)
+                        }
+                        else -> {
+                            BeginGetCustomCredentialOption(type, candidateQueryData)
                         }
                     }
-                    else -> {
-                        BeginGetCustomCredentialOption(type, candidateQueryData)
-                    }
                 }
-            } catch (e: FrameworkClassParsingException) {
-                null
+                else -> {
+                    BeginGetCustomCredentialOption(type, candidateQueryData)
+                }
             }
         }
     }
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
index 141611d..a155641 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
@@ -15,12 +15,11 @@
   limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <uses-sdk android:minSdkVersion="21"/>
     <application>
         <activity android:name=".MainActivity" android:exported="true"
-            android:theme="@style/Theme.AppCompat.DayNight">
+            android:theme="@style/MyPinkTheme">
             <!-- Handle Google app icon launch. -->
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
new file mode 100644
index 0000000..35b4a46
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
@@ -0,0 +1,31 @@
+
+<!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+
+    <style name="MyPinkTheme" parent="Theme.AppCompat.DayNight" >
+        <!-- The color for selected headers -->
+        <item name="colorAccent">#ffffff</item>
+        <!-- The color for unselected headers -->
+        <item name="colorControlNormal">#FF69B4</item>
+        <!-- The color for all text -->
+        <item name="android:textColorPrimary">#ffffff</item>
+        <!-- The color for variant popup background -->
+        <item name="colorButtonNormal">#FFC0CB</item>
+    </style>
+
+</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
index e27fe2e..a471ae6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
@@ -19,7 +19,7 @@
     android:height="24dp"
     android:viewportWidth="24.0"
     android:viewportHeight="24.0"
-    android:tint="?attr/colorControlNormal">
+    android:tint="?attr/colorButtonNormal">
     <path
         android:fillColor="@android:color/white"
         android:pathData="M2,22h20V2L2,22z"/>
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index d5a9334..675a185 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -18,6 +18,8 @@
 
 import android.content.Context
 import android.os.Build
+import android.os.StrictMode
+import android.os.StrictMode.ThreadPolicy
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.arch.core.executor.TaskExecutor
 import androidx.room.Room
@@ -33,6 +35,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.io.IOException
@@ -153,6 +156,31 @@
     }
 
     @Test
+    fun allBookSuspend_autoClose() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        context.deleteDatabase("autoClose.db")
+        val db = Room.databaseBuilder(
+            context = context,
+            klass = TestDatabase::class.java,
+            name = "test.db"
+        ).setAutoCloseTimeout(10, TimeUnit.MILLISECONDS).build()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            StrictMode.setThreadPolicy(
+                ThreadPolicy.Builder()
+                    .detectDiskReads()
+                    .detectDiskWrites()
+                    .penaltyDeath()
+                    .build()
+            )
+            runBlocking {
+                db.booksDao().getBooksSuspend()
+                delay(100) // let db auto-close
+                db.booksDao().getBooksSuspend()
+            }
+        }
+    }
+
+    @Test
     @Suppress("DEPRECATION")
     fun suspendingBlock_beginEndTransaction() {
         runBlocking {
diff --git a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index 1495051e..69c458d 100644
--- a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -53,7 +53,7 @@
             inTransaction: Boolean,
             callable: Callable<R>
         ): R {
-            if (db.isOpen && db.inTransaction()) {
+            if (db.isOpenInternal && db.inTransaction()) {
                 return callable.call()
             }
 
@@ -74,7 +74,7 @@
             cancellationSignal: CancellationSignal?,
             callable: Callable<R>
         ): R {
-            if (db.isOpen && db.inTransaction()) {
+            if (db.isOpenInternal && db.inTransaction()) {
                 return callable.call()
             }
 
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
index 251b82a..2088242 100644
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
@@ -162,11 +162,11 @@
         }
     }
 
-    // TODO: Close CleanupStatement
-    internal fun onAutoCloseCallback() {
+    private fun onAutoCloseCallback() {
         synchronized(trackerLock) {
             initialized = false
             observedTableTracker.resetTriggerState()
+            cleanupStatement?.close()
         }
     }
 
@@ -329,7 +329,7 @@
     }
 
     internal fun ensureInitialization(): Boolean {
-        if (!database.isOpen) {
+        if (!database.isOpenInternal) {
             return false
         }
         if (!initialized) {
@@ -526,7 +526,7 @@
      * This api should eventually be public.
      */
     internal fun syncTriggers() {
-        if (!database.isOpen) {
+        if (!database.isOpenInternal) {
             return
         }
         syncTriggers(database.openHelper.writableDatabase)
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
index 599d9d3..a455992 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
@@ -394,13 +394,22 @@
     /**
      * True if database connection is open and initialized.
      *
+     * When Room is configured with [RoomDatabase.Builder.setAutoCloseTimeout] the database
+     * is considered open even if internally the connection has been closed, unless manually closed.
+     *
      * @return true if the database connection is open, false otherwise.
      */
-    @Suppress("Deprecation")
+    @Suppress("Deprecation") // Due to usage of `mDatabase`
     open val isOpen: Boolean
-        get() {
-            return (autoCloser?.isActive ?: mDatabase?.isOpen) == true
-        }
+        get() = (autoCloser?.isActive ?: mDatabase?.isOpen) == true
+
+    /**
+     * True if the actual database connection is open, regardless of auto-close.
+     */
+    @Suppress("Deprecation") // Due to usage of `mDatabase`
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    val isOpenInternal: Boolean
+        get() = mDatabase?.isOpen == true
 
     /**
      * Closes the database if it is already open.
@@ -1193,7 +1202,7 @@
 
         /**
          * Enables auto-closing for the database to free up unused resources. The underlying
-         * database will be closed after it's last use after the specified `autoCloseTimeout` has
+         * database will be closed after it's last use after the specified [autoCloseTimeout] has
          * elapsed since its last usage. The database will be automatically
          * re-opened the next time it is accessed.
          *
@@ -1210,7 +1219,7 @@
          *
          * The auto-closing database operation runs on the query executor.
          *
-         * The database will not be reopened if the RoomDatabase or the
+         * The database will not be re-opened if the RoomDatabase or the
          * SupportSqliteOpenHelper is closed manually (by calling
          * [RoomDatabase.close] or [SupportSQLiteOpenHelper.close]. If the
          * database is closed manually, you must create a new database using
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
index 5f2c9aa..060110e 100644
--- a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
@@ -77,7 +77,7 @@
         doReturn(statement).whenever(mSqliteDb)
             .compileStatement(eq(InvalidationTracker.RESET_UPDATED_TABLES_SQL))
         doReturn(mSqliteDb).whenever(mOpenHelper).writableDatabase
-        doReturn(true).whenever(mRoomDatabase).isOpen
+        doReturn(true).whenever(mRoomDatabase).isOpenInternal
         doReturn(ArchTaskExecutor.getIOThreadExecutor()).whenever(mRoomDatabase).queryExecutor
         val closeLock = ReentrantLock()
         doReturn(closeLock).whenever(mRoomDatabase).getCloseLock()
@@ -247,7 +247,7 @@
 
     @Test
     fun closedDb() {
-        doReturn(false).whenever(mRoomDatabase).isOpen
+        doReturn(false).whenever(mRoomDatabase).isOpenInternal
         doThrow(IllegalStateException("foo")).whenever(mOpenHelper).writableDatabase
         mTracker.addObserver(LatchObserver(1, "a", "b"))
         mTracker.refreshRunnable.run()
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
index 1ae07c2..1569ff6 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 import android.webkit.WebView;
+import android.webkit.WebViewClient;
 
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
@@ -33,6 +34,7 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_image_drag);
         WebView demoWebview = findViewById(R.id.image_webview);
+        demoWebview.setWebViewClient(new WebViewClient()); // Open links in this WebView.
 
         demoWebview.loadUrl("www.google.com");
     }
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -5,6 +5,16 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
   }
 
+  public final class DropDataContentProvider extends android.content.ContentProvider {
+    ctor public DropDataContentProvider();
+    method public int delete(android.net.Uri, String?, String![]?);
+    method public String? getType(android.net.Uri);
+    method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public boolean onCreate();
+    method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public abstract class JavaScriptReplyProxy {
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -5,6 +5,16 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
   }
 
+  public final class DropDataContentProvider extends android.content.ContentProvider {
+    ctor public DropDataContentProvider();
+    method public int delete(android.net.Uri, String?, String![]?);
+    method public String? getType(android.net.Uri);
+    method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public boolean onCreate();
+    method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public abstract class JavaScriptReplyProxy {
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -5,6 +5,16 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
   }
 
+  public final class DropDataContentProvider extends android.content.ContentProvider {
+    ctor public DropDataContentProvider();
+    method public int delete(android.net.Uri, String?, String![]?);
+    method public String? getType(android.net.Uri);
+    method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public boolean onCreate();
+    method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public abstract class JavaScriptReplyProxy {
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
index 6c38760..6af699c 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
@@ -25,7 +25,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import androidx.webkit.internal.WebViewGlueCommunicator;
 
 import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
@@ -33,13 +32,23 @@
 import java.io.FileNotFoundException;
 
 /**
- * TODO(1353048): Un-hide this after finishing the feature.
+ * WebView provides partial support for Android
+ * <a href="https://developer.android.com/develop/ui/views/touch-and-input/drag-drop">
+ * Drag and Drop</a> allowing images, text and links to be dragged out of a WebView.
  *
- * @hide
- * This should be added to the manifest in order to enable dragging images out.
+ * The content provider is required to make the images drag work, to enable, you should add this
+ * class to your manifest, for example:
+ *
+ * <pre class="prettyprint">
+ *  &lt;provider
+ *             android:authorities="{{your package}}.DropDataProvider"
+ *             android:name="androidx.webkit.DropDataContentProvider"
+ *             android:exported="false"
+ *             android:grantUriPermissions="true"/&gt;
+ * </pre>
+ *
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class DropDataContentProvider extends ContentProvider {
+public final class DropDataContentProvider extends ContentProvider {
     DropDataContentProviderBoundaryInterface mImpl;
 
     @Override
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index d008345..ead684f 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -37,7 +37,7 @@
     method public void clearSplitInfoCallback();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
-    method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
+    method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
     method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
     method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
   }
@@ -106,17 +106,13 @@
     method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
   }
 
-  public interface SplitAttributesCalculator {
-    method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
-  }
-
-  public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+  public class SplitAttributesCalculatorParams {
+    method public boolean areDefaultConstraintsSatisfied();
     method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
     method public android.content.res.Configuration getParentConfiguration();
     method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
     method public android.view.WindowMetrics getParentWindowMetrics();
     method public String? getSplitRuleTag();
-    method public boolean isDefaultMinSizeSatisfied();
   }
 
   public class SplitInfo {
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index d008345..ead684f 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -37,7 +37,7 @@
     method public void clearSplitInfoCallback();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
-    method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
+    method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
     method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
     method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
   }
@@ -106,17 +106,13 @@
     method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
   }
 
-  public interface SplitAttributesCalculator {
-    method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
-  }
-
-  public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+  public class SplitAttributesCalculatorParams {
+    method public boolean areDefaultConstraintsSatisfied();
     method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
     method public android.content.res.Configuration getParentConfiguration();
     method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
     method public android.view.WindowMetrics getParentWindowMetrics();
     method public String? getSplitRuleTag();
-    method public boolean isDefaultMinSizeSatisfied();
   }
 
   public class SplitInfo {
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index d008345..ead684f 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -37,7 +37,7 @@
     method public void clearSplitInfoCallback();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
-    method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
+    method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
     method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
     method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
   }
@@ -106,17 +106,13 @@
     method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
   }
 
-  public interface SplitAttributesCalculator {
-    method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
-  }
-
-  public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+  public class SplitAttributesCalculatorParams {
+    method public boolean areDefaultConstraintsSatisfied();
     method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
     method public android.content.res.Configuration getParentConfiguration();
     method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
     method public android.view.WindowMetrics getParentWindowMetrics();
     method public String? getSplitRuleTag();
-    method public boolean isDefaultMinSizeSatisfied();
   }
 
   public class SplitInfo {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index f8707a9..df1fa32 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -73,7 +73,8 @@
      * <ul>
      *     <li>{@link SplitPlaceholderRule.Builder#setFinishPrimaryWithPlaceholder(int)}</li>
      *     <li>{@link androidx.window.extensions.embedding.SplitAttributes} APIs</li>
-     *     <li>{@link androidx.window.extensions.embedding.SplitAttributesCalculator} APIs</li>
+     *     <li>{@link ActivityEmbeddingComponent#setSplitAttributesCalculator(
+     *         androidx.window.extensions.core.util.function.Function)}</li>
      * </ul>
      * </p>
      * @hide
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index fd99cab..9e5ef39 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
@@ -17,10 +17,12 @@
 package androidx.window.extensions.embedding;
 
 import android.app.Activity;
+import android.view.WindowMetrics;
 
 import androidx.annotation.NonNull;
 import androidx.window.extensions.WindowExtensions;
 import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.core.util.function.Function;
 
 import java.util.List;
 import java.util.Set;
@@ -80,18 +82,24 @@
     boolean isActivityEmbedded(@NonNull Activity activity);
 
     /**
-     * Sets a {@link SplitAttributesCalculator}.
+     * Sets a callback to compute the {@link SplitAttributes} for the {@link SplitRule} and current
+     * window state provided in {@link SplitAttributesCalculatorParams}. This method can be used
+     * to dynamically configure the split layout properties when new activities are launched or
+     * window properties change. If set, {@link SplitRule#getDefaultSplitAttributes() the default
+     * split properties} and {@link SplitRule#checkParentMetrics(WindowMetrics) restrictions}
+     * will be ignored, and the callback will be invoked for every change.
      *
-     * @param calculator the calculator to set. It will replace the previously set
-     * {@link SplitAttributesCalculator} if it exists.
+     * @param calculator the callback to set. It will replace the previously set callback if it
+     *                  exists.
      * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
      */
-    void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator);
+    void setSplitAttributesCalculator(
+            @NonNull Function<SplitAttributesCalculatorParams, SplitAttributes> calculator);
 
     /**
-     * Clears the previously set {@link SplitAttributesCalculator}.
+     * Clears the previously callback set in {@link #setSplitAttributesCalculator(Function)}.
      *
-     * @see #setSplitAttributesCalculator(SplitAttributesCalculator)
+     * @see #setSplitAttributesCalculator(Function)
      * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
      */
     void clearSplitAttributesCalculator();
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
index 179afb4..6c9577d 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
@@ -17,6 +17,7 @@
 package androidx.window.extensions.embedding;
 
 import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Function;
 
 import java.util.Objects;
 
@@ -32,13 +33,13 @@
         mTag = tag;
     }
 
-    // TODO(b/240912390): refer to the real API in later CLs.
     /**
      * A unique string to identify this {@link EmbeddingRule}.
      * The suggested usage is to set the tag in the corresponding rule builder to be able to
-     * differentiate between different rules in the callbacks. For example, it can be used to
-     * compute the right {@link SplitAttributes} for the right split rule in
-     * {@code SplitAttributesCalculator#computeSplitAttributesForState}.
+     * differentiate between different rules in {@link SplitAttributes} calculator function. For
+     * example, it can be used to compute the {@link SplitAttributes} for the specific
+     * {@link SplitRule} in the {@link Function} set with
+     * {@link ActivityEmbeddingComponent#setSplitAttributesCalculator(Function)}.
      *
      * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
      */
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
index 992d934..d724e1b 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
@@ -29,7 +29,7 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.window.extensions.layout.FoldingFeature;
+import androidx.window.extensions.core.util.function.Function;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -56,10 +56,9 @@
  *     <li>Setting the default {@code SplitAttributes} using
  *         {@link SplitPairRule.Builder#setDefaultSplitAttributes} or
  *         {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes}.</li>
- *     <li>Using
- *         {@link SplitAttributesCalculator#computeSplitAttributesForParams}
- *         to customize the {@code SplitAttributes} for a given device and
- *         window state.</li>
+ *     <li>Using {@link ActivityEmbeddingComponent#setSplitAttributesCalculator(Function)} to set
+ *         the callback to customize the {@code SplitAttributes} for a given device and window
+ *         state.</li>
  * </ul>
  *
  * @see SplitAttributes.SplitType
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
deleted file mode 100644
index ef69323..0000000
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
+++ /dev/null
@@ -1,156 +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.window.extensions.embedding;
-
-import android.content.res.Configuration;
-import android.os.Build;
-import android.view.WindowMetrics;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.window.extensions.layout.WindowLayoutInfo;
-
-/**
- * A developer-defined {@link SplitAttributes} calculator to compute the current split layout with
- * the current device and window state.
- *
- * @see ActivityEmbeddingComponent#setSplitAttributesCalculator(SplitAttributesCalculator)
- * @see ActivityEmbeddingComponent#clearSplitAttributesCalculator()
- * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
- */
-public interface SplitAttributesCalculator {
-    /**
-     * Computes the {@link SplitAttributes} with the current device state.
-     *
-     * @param params See {@link SplitAttributesCalculatorParams}
-     */
-    @NonNull
-    SplitAttributes computeSplitAttributesForParams(
-            @NonNull SplitAttributesCalculatorParams params
-    );
-
-    /** The container of {@link SplitAttributesCalculator} parameters */
-    class SplitAttributesCalculatorParams {
-        @NonNull
-        private final WindowMetrics mParentWindowMetrics;
-        @NonNull
-        private final Configuration mParentConfiguration;
-        @NonNull
-        private final SplitAttributes mDefaultSplitAttributes;
-        private final boolean mIsDefaultMinSizeSatisfied;
-        @NonNull
-        private final WindowLayoutInfo mParentWindowLayoutInfo;
-        @Nullable
-        private final String mSplitRuleTag;
-
-        /** Returns the parent container's {@link WindowMetrics} */
-        @NonNull
-        public WindowMetrics getParentWindowMetrics() {
-            return mParentWindowMetrics;
-        }
-
-        /** Returns the parent container's {@link Configuration} */
-        @NonNull
-        public Configuration getParentConfiguration() {
-            return new Configuration(mParentConfiguration);
-        }
-
-        /**
-         * Returns the {@link SplitRule#getDefaultSplitAttributes()}. It could be from
-         * {@link SplitRule} Builder APIs
-         * ({@link SplitPairRule.Builder#setDefaultSplitAttributes(SplitAttributes)} or
-         * {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes(SplitAttributes)}) or from
-         * the {@code splitRatio} and {@code splitLayoutDirection} attributes from static rule
-         * definitions.
-         */
-        @NonNull
-        public SplitAttributes getDefaultSplitAttributes() {
-            return mDefaultSplitAttributes;
-        }
-
-        /**
-         * Returns whether the {@link #getParentWindowMetrics()} satisfies
-         * {@link SplitRule#checkParentMetrics(WindowMetrics)} with the minimal size requirement
-         * specified in the {@link SplitRule} Builder constructors.
-         *
-         * @see SplitPairRule.Builder
-         * @see SplitPlaceholderRule.Builder
-         */
-        public boolean isDefaultMinSizeSatisfied() {
-            return mIsDefaultMinSizeSatisfied;
-        }
-
-        /** Returns the parent container's {@link WindowLayoutInfo} */
-        @NonNull
-        public WindowLayoutInfo getParentWindowLayoutInfo() {
-            return mParentWindowLayoutInfo;
-        }
-
-        /** Returns {@link SplitRule#getTag()} to apply the {@link SplitAttributes} result. */
-        @Nullable
-        public String getSplitRuleTag() {
-            return mSplitRuleTag;
-        }
-
-        SplitAttributesCalculatorParams(
-                @NonNull WindowMetrics parentWindowMetrics,
-                @NonNull Configuration parentConfiguration,
-                @NonNull SplitAttributes defaultSplitAttributes,
-                boolean isDefaultMinSizeSatisfied,
-                @NonNull WindowLayoutInfo parentWindowLayoutInfo,
-                @Nullable String splitRuleTag
-        ) {
-            mParentWindowMetrics = parentWindowMetrics;
-            mParentConfiguration = parentConfiguration;
-            mDefaultSplitAttributes = defaultSplitAttributes;
-            mIsDefaultMinSizeSatisfied = isDefaultMinSizeSatisfied;
-            mParentWindowLayoutInfo = parentWindowLayoutInfo;
-            mSplitRuleTag = splitRuleTag;
-        }
-
-        @NonNull
-        @Override
-        public String toString() {
-            return getClass().getSimpleName() + ":{"
-                    + "windowMetrics=" + windowMetricsToString(mParentWindowMetrics)
-                    + ", configuration=" + mParentConfiguration
-                    + ", windowLayoutInfo=" + mParentWindowLayoutInfo
-                    + ", defaultSplitAttributes=" + mDefaultSplitAttributes
-                    + ", isDefaultMinSizeSatisfied=" + mIsDefaultMinSizeSatisfied
-                    + ", tag=" + mSplitRuleTag + "}";
-        }
-
-        private static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
-            // TODO(b/187712731): Use WindowMetrics#toString after it's implemented in U.
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-                return Api30Impl.windowMetricsToString(windowMetrics);
-            }
-            throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");
-        }
-
-        @RequiresApi(30)
-        private static final class Api30Impl {
-            static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
-                return WindowMetrics.class.getSimpleName() + ":{"
-                        + "bounds=" + windowMetrics.getBounds()
-                        + ", windowInsets=" + windowMetrics.getWindowInsets()
-                        + "}";
-            }
-        }
-    }
-}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java
new file mode 100644
index 0000000..e26e496
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java
@@ -0,0 +1,149 @@
+/*
+ * 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.window.extensions.embedding;
+
+import android.content.res.Configuration;
+import android.os.Build;
+import android.view.WindowMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.window.extensions.layout.WindowLayoutInfo;
+
+/**
+ * The parameter container used to report the current device and window state in
+ * {@link ActivityEmbeddingComponent#setSplitAttributesCalculator(
+ * androidx.window.extensions.core.util.function.Function)} and references the corresponding
+ * {@link SplitRule} by {@link #getSplitRuleTag()} if {@link SplitRule#getTag()} is specified.
+ *
+ * @see ActivityEmbeddingComponent#clearSplitAttributesCalculator()
+ * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+ */
+public class SplitAttributesCalculatorParams {
+    @NonNull
+    private final WindowMetrics mParentWindowMetrics;
+    @NonNull
+    private final Configuration mParentConfiguration;
+    @NonNull
+    private final WindowLayoutInfo mParentWindowLayoutInfo;
+    @NonNull
+    private final SplitAttributes mDefaultSplitAttributes;
+    private final boolean mAreDefaultConstraintsSatisfied;
+    @Nullable
+    private final String mSplitRuleTag;
+
+    /** Returns the parent container's {@link WindowMetrics} */
+    @NonNull
+    public WindowMetrics getParentWindowMetrics() {
+        return mParentWindowMetrics;
+    }
+
+    /** Returns the parent container's {@link Configuration} */
+    @NonNull
+    public Configuration getParentConfiguration() {
+        return new Configuration(mParentConfiguration);
+    }
+
+    /**
+     * Returns the {@link SplitRule#getDefaultSplitAttributes()}. It could be from
+     * {@link SplitRule} Builder APIs
+     * ({@link SplitPairRule.Builder#setDefaultSplitAttributes(SplitAttributes)} or
+     * {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes(SplitAttributes)}) or from
+     * the {@code splitRatio} and {@code splitLayoutDirection} attributes from static rule
+     * definitions.
+     */
+    @NonNull
+    public SplitAttributes getDefaultSplitAttributes() {
+        return mDefaultSplitAttributes;
+    }
+
+    /**
+     * Returns whether the {@link #getParentWindowMetrics()} satisfies the dimensions and aspect
+     * ratios requirements specified in the {@link androidx.window.embedding.SplitRule}, which
+     * are:
+     *  - {@link androidx.window.embedding.SplitRule#minWidthDp}
+     *  - {@link androidx.window.embedding.SplitRule#minHeightDp}
+     *  - {@link androidx.window.embedding.SplitRule#minSmallestWidthDp}
+     *  - {@link androidx.window.embedding.SplitRule#maxAspectRatioInPortrait}
+     *  - {@link androidx.window.embedding.SplitRule#maxAspectRatioInLandscape}
+     */
+    public boolean areDefaultConstraintsSatisfied() {
+        return mAreDefaultConstraintsSatisfied;
+    }
+
+    /** Returns the parent container's {@link WindowLayoutInfo} */
+    @NonNull
+    public WindowLayoutInfo getParentWindowLayoutInfo() {
+        return mParentWindowLayoutInfo;
+    }
+
+    /**
+     * Returns {@link SplitRule#getTag()} to apply the {@link SplitAttributes} result if it was
+     * set.
+     */
+    @Nullable
+    public String getSplitRuleTag() {
+        return mSplitRuleTag;
+    }
+
+    SplitAttributesCalculatorParams(
+            @NonNull WindowMetrics parentWindowMetrics,
+            @NonNull Configuration parentConfiguration,
+            @NonNull WindowLayoutInfo parentWindowLayoutInfo,
+            @NonNull SplitAttributes defaultSplitAttributes,
+            boolean areDefaultConstraintsSatisfied,
+            @Nullable String splitRuleTag
+    ) {
+        mParentWindowMetrics = parentWindowMetrics;
+        mParentConfiguration = parentConfiguration;
+        mParentWindowLayoutInfo = parentWindowLayoutInfo;
+        mDefaultSplitAttributes = defaultSplitAttributes;
+        mAreDefaultConstraintsSatisfied = areDefaultConstraintsSatisfied;
+        mSplitRuleTag = splitRuleTag;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + ":{"
+                + "windowMetrics=" + windowMetricsToString(mParentWindowMetrics)
+                + ", configuration=" + mParentConfiguration
+                + ", windowLayoutInfo=" + mParentWindowLayoutInfo
+                + ", defaultSplitAttributes=" + mDefaultSplitAttributes
+                + ", areDefaultConstraintsSatisfied=" + mAreDefaultConstraintsSatisfied
+                + ", tag=" + mSplitRuleTag + "}";
+    }
+
+    private static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
+        // TODO(b/187712731): Use WindowMetrics#toString after it's implemented in U.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            return Api30Impl.windowMetricsToString(windowMetrics);
+        }
+        throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");
+    }
+
+    @RequiresApi(30)
+    private static final class Api30Impl {
+        static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
+            return WindowMetrics.class.getSimpleName() + ":{"
+                    + "bounds=" + windowMetrics.getBounds()
+                    + ", windowInsets=" + windowMetrics.getWindowInsets()
+                    + "}";
+        }
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index b975fe1..10dac95 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -163,8 +163,8 @@
          * @param activityIntentPredicate the {@link Predicate} to verify if an ({@link Activity},
          *                              {@link Intent}) pair matches this rule
          * @param parentWindowMetricsPredicate the {@link Predicate} to verify if the matched split
-         *                               pair is allowed to show side-by-side with the given
-         *                               parent {@link WindowMetrics}
+         *                               pair is allowed to show adjacent to each other with the
+         *                               given parent {@link WindowMetrics}
          * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
          */
         public Builder(@NonNull Predicate<Pair<Activity, Activity>> activityPairPredicate,
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index 1e6d044..a1be65d 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -35,7 +35,7 @@
 /**
  * Split configuration rules for activities that are launched to side in a split. Define when an
  * activity that was launched in a side container from another activity should be shown
- * side-by-side or on top of it, as well as the visual properties of the split. Can be applied to
+ * adjacent or on top of it, as well as the visual properties of the split. Can be applied to
  * new activities started from the same process automatically by the embedding implementation on
  * the device.
  */
@@ -57,9 +57,8 @@
      */
     public static final int FINISH_ALWAYS = 1;
     /**
-     * Only finish the associated container when displayed side-by-side/adjacent to the one
-     * being finished. Does not finish the associated one when containers are stacked on top of
-     * each other.
+     * Only finish the associated container when displayed adjacent to the one being finished. Does
+     * not finish the associated one when containers are stacked on top of each other.
      * @see SplitFinishBehavior
      */
     public static final int FINISH_ADJACENT = 2;
@@ -70,7 +69,7 @@
      * <p>
      * For example, given that {@link SplitPairRule#getFinishPrimaryWithSecondary()} is
      * {@link #FINISH_ADJACENT} and secondary container finishes. The primary associated
-     * container is finished if it's side-by-side with secondary container. The primary
+     * container is finished if it's shown adjacent to the secondary container. The primary
      * associated container is not finished if it occupies entire task bounds.</p>
      *
      * @see SplitPairRule#getFinishPrimaryWithSecondary()
@@ -92,6 +91,18 @@
         mDefaultSplitAttributes = defaultSplitAttributes;
     }
 
+    /**
+     * Checks whether the parent window satisfied the dimensions and aspect ratios requirements
+     * specified in the {@link androidx.window.embedding.SplitRule}, which are
+     * {@link androidx.window.embedding.SplitRule#minWidthDp},
+     * {@link androidx.window.embedding.SplitRule#minHeightDp},
+     * {@link androidx.window.embedding.SplitRule#minSmallestWidthDp},
+     * {@link androidx.window.embedding.SplitRule#maxAspectRatioInPortrait} and
+     * {@link androidx.window.embedding.SplitRule#maxAspectRatioInLandscape}.
+     *
+     * @param parentMetrics the {@link WindowMetrics} of the parent window.
+     * @return whether the parent window satisfied the {@link SplitRule} requirements.
+     */
     @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
     @RequiresApi(api = Build.VERSION_CODES.N)
     public boolean checkParentMetrics(@NonNull WindowMetrics parentMetrics) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
index d7d4b1f..8665740 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
@@ -33,7 +33,7 @@
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
-import androidx.window.embedding.SplitAttributesCalculator
+import androidx.window.embedding.SplitAttributesCalculatorParams
 import androidx.window.embedding.SplitController
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowLayoutInfo
@@ -43,10 +43,12 @@
  * Initializes SplitController with a set of statically defined rules.
  */
 class ExampleWindowInitializer : Initializer<RuleController> {
+    private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
     override fun create(context: Context): RuleController {
         SplitController.getInstance(context).apply {
             if (isSplitAttributesCalculatorSupported()) {
-                setSplitAttributesCalculator(SampleSplitAttributesCalculator())
+                setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
             }
         }
         return RuleController.getInstance(context).apply {
@@ -57,86 +59,132 @@
     }
 
     /**
-     * A sample [SplitAttributesCalculator] to demonstrate how to change the [SplitAttributes] with
-     * the current device and window state and
-     * [SplitAttributesCalculator.SplitAttributesCalculatorParams.splitRuleTag].
+     * A sample callback set in [SplitController.setSplitAttributesCalculator] to demonstrate how to
+     * change the [SplitAttributes] with the current device and window state and
+     * [SplitAttributesCalculatorParams.splitRuleTag].
      */
-    private class SampleSplitAttributesCalculator : SplitAttributesCalculator {
-
-        private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
-
-        override fun computeSplitAttributesForParams(
-            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
-        ): SplitAttributes {
-            val isPortrait = params.parentWindowMetrics.isPortrait()
-            val windowLayoutInfo = params.parentWindowLayoutInfo
-            val isTabletop = windowLayoutInfo.isTabletop()
-            val isBookMode = windowLayoutInfo.isBookMode()
-            val config = params.parentConfiguration
-            // The SplitAttributes to occupy the whole task bounds
-            val expandContainersAttrs = SplitAttributes.Builder()
-                .setSplitType(SplitAttributes.SplitType.expandContainers())
-                .build()
-            val tag = params.splitRuleTag
-            val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
-            // Make a copy of the default splitAttributes, but replace the animation background
-            // color to what is configured in the Demo app.
-            val backgroundColor = mDemoActivityEmbeddingController.animationBackgroundColor
-            val defaultSplitAttributes = SplitAttributes.Builder()
-                .setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
-                .setSplitType(params.defaultSplitAttributes.splitType)
-                .setAnimationBackgroundColor(backgroundColor)
-                .build()
-            when (tag?.substringBefore(SUFFIX_REVERSED)) {
-                TAG_USE_DEFAULT_SPLIT_ATTRIBUTES, null -> {
-                    return if (params.isDefaultMinSizeSatisfied) {
-                        defaultSplitAttributes
-                    } else {
-                        expandContainersAttrs
-                    }
+    private fun sampleSplitAttributesCalculator(
+        params: SplitAttributesCalculatorParams
+    ): SplitAttributes {
+        val isPortrait = params.parentWindowMetrics.isPortrait()
+        val windowLayoutInfo = params.parentWindowLayoutInfo
+        val isTabletop = windowLayoutInfo.isTabletop()
+        val isBookMode = windowLayoutInfo.isBookMode()
+        val config = params.parentConfiguration
+        // The SplitAttributes to occupy the whole task bounds
+        val expandContainersAttrs = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.expandContainers())
+            .build()
+        val tag = params.splitRuleTag
+        val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
+        // Make a copy of the default splitAttributes, but replace the animation background
+        // color to what is configured in the Demo app.
+        val backgroundColor = mDemoActivityEmbeddingController.animationBackgroundColor
+        val defaultSplitAttributes = SplitAttributes.Builder()
+            .setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
+            .setSplitType(params.defaultSplitAttributes.splitType)
+            .setAnimationBackgroundColor(backgroundColor)
+            .build()
+        when (tag?.substringBefore(SUFFIX_REVERSED)) {
+            TAG_USE_DEFAULT_SPLIT_ATTRIBUTES, null -> {
+                return if (params.areDefaultConstraintsSatisfied) {
+                    defaultSplitAttributes
+                } else {
+                    expandContainersAttrs
                 }
-                TAG_SHOW_FULLSCREEN_IN_PORTRAIT -> {
-                    if (isPortrait) {
-                        return expandContainersAttrs
-                    }
+            }
+            TAG_SHOW_FULLSCREEN_IN_PORTRAIT -> {
+                if (isPortrait) {
+                    return expandContainersAttrs
                 }
-                TAG_SHOW_FULLSCREEN_IN_PORTRAIT + SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
-                    if (isTabletop) {
-                        return SplitAttributes.Builder()
-                            .setSplitType(SplitAttributes.SplitType.splitByHinge())
-                            .setLayoutDirection(
-                                if (shouldReversed) {
-                                    BOTTOM_TO_TOP
-                                } else {
-                                    TOP_TO_BOTTOM
-                                }
-                            )
-                            .setAnimationBackgroundColor(backgroundColor)
-                            .build()
-                    } else if (isPortrait) {
-                        return expandContainersAttrs
-                    }
-                }
-                TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
-                    if (isTabletop) {
-                        return SplitAttributes.Builder()
-                            .setSplitType(SplitAttributes.SplitType.splitByHinge())
-                            .setLayoutDirection(
-                                if (shouldReversed) {
-                                    BOTTOM_TO_TOP
-                                } else {
-                                    TOP_TO_BOTTOM
-                                }
-                            )
-                            .setAnimationBackgroundColor(backgroundColor)
-                            .build()
-                    }
-                }
-                TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE -> {
+            }
+            TAG_SHOW_FULLSCREEN_IN_PORTRAIT + SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
+                if (isTabletop) {
                     return SplitAttributes.Builder()
+                        .setSplitType(SplitAttributes.SplitType.splitByHinge())
+                        .setLayoutDirection(
+                            if (shouldReversed) {
+                                BOTTOM_TO_TOP
+                            } else {
+                                TOP_TO_BOTTOM
+                            }
+                        )
+                        .setAnimationBackgroundColor(backgroundColor)
+                        .build()
+                } else if (isPortrait) {
+                    return expandContainersAttrs
+                }
+            }
+            TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
+                if (isTabletop) {
+                    return SplitAttributes.Builder()
+                        .setSplitType(SplitAttributes.SplitType.splitByHinge())
+                        .setLayoutDirection(
+                            if (shouldReversed) {
+                                BOTTOM_TO_TOP
+                            } else {
+                                TOP_TO_BOTTOM
+                            }
+                        )
+                        .setAnimationBackgroundColor(backgroundColor)
+                        .build()
+                }
+            }
+            TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE -> {
+                return SplitAttributes.Builder()
+                    .setSplitType(SplitAttributes.SplitType.splitByHinge())
+                    .setLayoutDirection(
+                        if (shouldReversed) {
+                            BOTTOM_TO_TOP
+                        } else {
+                            TOP_TO_BOTTOM
+                        }
+                    ).build()
+            }
+            TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE + SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE -> {
+                return if (isBookMode) {
+                    expandContainersAttrs
+                } else if (config.screenWidthDp <= 600) {
+                    SplitAttributes.Builder()
                         .setSplitType(SplitAttributes.SplitType.splitEqually())
                         .setLayoutDirection(
-                            if (config.screenWidthDp <= 600) {
+                            if (shouldReversed) {
+                                BOTTOM_TO_TOP
+                            } else {
+                                TOP_TO_BOTTOM
+                            }
+                        )
+                        .setAnimationBackgroundColor(backgroundColor)
+                        .build()
+                } else {
+                    SplitAttributes.Builder()
+                        .setSplitType(SplitAttributes.SplitType.splitEqually())
+                        .setLayoutDirection(
+                            if (shouldReversed) {
+                                RIGHT_TO_LEFT
+                            } else {
+                                LEFT_TO_RIGHT
+                            }
+                        )
+                        .setAnimationBackgroundColor(backgroundColor)
+                        .build()
+                }
+            }
+            TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING -> {
+                val foldingState = windowLayoutInfo.getFoldingFeature()
+                if (foldingState != null) {
+                    return SplitAttributes.Builder()
+                        .setSplitType(
+                            if (foldingState.isSeparating) {
+                                SplitAttributes.SplitType.splitByHinge()
+                            } else {
+                                SplitAttributes.SplitType.ratio(0.3f)
+                            }
+                        ).setLayoutDirection(
+                            if (
+                                foldingState.orientation
+                                    == FoldingFeature.Orientation.HORIZONTAL
+                            ) {
                                 if (shouldReversed) BOTTOM_TO_TOP else TOP_TO_BOTTOM
                             } else {
                                 if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
@@ -145,86 +193,33 @@
                         .setAnimationBackgroundColor(backgroundColor)
                         .build()
                 }
-                TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE + SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE -> {
-                    return if (isBookMode) {
-                        expandContainersAttrs
-                    } else if (config.screenWidthDp <= 600) {
-                        SplitAttributes.Builder()
-                            .setSplitType(SplitAttributes.SplitType.splitEqually())
-                            .setLayoutDirection(
-                                if (shouldReversed) {
-                                    BOTTOM_TO_TOP
-                                } else {
-                                    TOP_TO_BOTTOM
-                                }
-                            )
-                            .setAnimationBackgroundColor(backgroundColor)
-                            .build()
-                    } else {
-                        SplitAttributes.Builder()
-                            .setSplitType(SplitAttributes.SplitType.splitEqually())
-                            .setLayoutDirection(
-                                if (shouldReversed) {
-                                    RIGHT_TO_LEFT
-                                } else {
-                                    LEFT_TO_RIGHT
-                                }
-                            )
-                            .setAnimationBackgroundColor(backgroundColor)
-                            .build()
-                    }
-                }
-                TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING -> {
-                    val foldingState = windowLayoutInfo.getFoldingFeature()
-                    if (foldingState != null) {
-                        return SplitAttributes.Builder()
-                            .setSplitType(
-                                if (foldingState.isSeparating) {
-                                    SplitAttributes.SplitType.splitByHinge()
-                                } else {
-                                    SplitAttributes.SplitType.ratio(0.3f)
-                                }
-                            ).setLayoutDirection(
-                                if (
-                                    foldingState.orientation
-                                        == FoldingFeature.Orientation.HORIZONTAL
-                                ) {
-                                    if (shouldReversed) BOTTOM_TO_TOP else TOP_TO_BOTTOM
-                                } else {
-                                    if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
-                                }
-                            )
-                            .setAnimationBackgroundColor(backgroundColor)
-                            .build()
-                    }
-                }
             }
-            return defaultSplitAttributes
         }
+        return defaultSplitAttributes
+    }
 
-        private fun WindowMetrics.isPortrait(): Boolean =
-            bounds.height() > bounds.width()
+    private fun WindowMetrics.isPortrait(): Boolean =
+        bounds.height() > bounds.width()
 
-        private fun WindowLayoutInfo.isTabletop(): Boolean {
-            val foldingFeature = getFoldingFeature()
-            return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
-                foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
-        }
+    private fun WindowLayoutInfo.isTabletop(): Boolean {
+        val foldingFeature = getFoldingFeature()
+        return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
+            foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
+    }
 
-        private fun WindowLayoutInfo.isBookMode(): Boolean {
-            val foldingFeature = getFoldingFeature()
-            return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
-                foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
-        }
+    private fun WindowLayoutInfo.isBookMode(): Boolean {
+        val foldingFeature = getFoldingFeature()
+        return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
+            foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
+    }
 
-        /**
-         * Returns the [FoldingFeature] if it is exactly the only [FoldingFeature] in
-         * [WindowLayoutInfo]. Otherwise, returns `null`.
-         */
-        private fun WindowLayoutInfo.getFoldingFeature(): FoldingFeature? {
-            val foldingFeatures = displayFeatures.filterIsInstance<FoldingFeature>()
-            return if (foldingFeatures.size == 1) foldingFeatures[0] else null
-        }
+    /**
+     * Returns the [FoldingFeature] if it is exactly the only [FoldingFeature] in
+     * [WindowLayoutInfo]. Otherwise, returns `null`.
+     */
+    private fun WindowLayoutInfo.getFoldingFeature(): FoldingFeature? {
+        val foldingFeatures = displayFeatures.filterIsInstance<FoldingFeature>()
+        return if (foldingFeatures.size == 1) foldingFeatures[0] else null
     }
 
     override fun dependencies(): List<Class<out Initializer<*>>> {
diff --git a/window/window-rxjava2/api/api_lint.ignore b/window/window-rxjava2/api/api_lint.ignore
new file mode 100644
index 0000000..636cfcc
--- /dev/null
+++ b/window/window-rxjava2/api/api_lint.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+ContextFirst: androidx.window.rxjava2.layout.WindowInfoTrackerRx#windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+    Context is distinct, so it must be the first argument (method `windowLayoutInfoFlowable`)
+ContextFirst: androidx.window.rxjava2.layout.WindowInfoTrackerRx#windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+    Context is distinct, so it must be the first argument (method `windowLayoutInfoObservable`)
diff --git a/window/window-rxjava2/api/current.txt b/window/window-rxjava2/api/current.txt
index 8135cee..5250696 100644
--- a/window/window-rxjava2/api/current.txt
+++ b/window/window-rxjava2/api/current.txt
@@ -3,7 +3,9 @@
 
   public final class WindowInfoTrackerRx {
     method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
     method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
   }
 
 }
diff --git a/window/window-rxjava2/api/public_plus_experimental_current.txt b/window/window-rxjava2/api/public_plus_experimental_current.txt
index 8135cee..5250696 100644
--- a/window/window-rxjava2/api/public_plus_experimental_current.txt
+++ b/window/window-rxjava2/api/public_plus_experimental_current.txt
@@ -3,7 +3,9 @@
 
   public final class WindowInfoTrackerRx {
     method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
     method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
   }
 
 }
diff --git a/window/window-rxjava2/api/restricted_current.txt b/window/window-rxjava2/api/restricted_current.txt
index 8135cee..5250696 100644
--- a/window/window-rxjava2/api/restricted_current.txt
+++ b/window/window-rxjava2/api/restricted_current.txt
@@ -3,7 +3,9 @@
 
   public final class WindowInfoTrackerRx {
     method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
     method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
   }
 
 }
diff --git a/window/window-rxjava2/build.gradle b/window/window-rxjava2/build.gradle
index 389a75c..dee2b16 100644
--- a/window/window-rxjava2/build.gradle
+++ b/window/window-rxjava2/build.gradle
@@ -37,6 +37,7 @@
     api(libs.kotlinCoroutinesRx2)
     api(libs.rxjava2)
     api(project(":window:window"))
+    implementation 'androidx.annotation:annotation:1.5.0'
 
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
diff --git a/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt b/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt
index a22df89..418725d 100644
--- a/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt
+++ b/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt
@@ -17,6 +17,7 @@
 package androidx.window.rxjava2.layout
 
 import android.app.Activity
+import android.content.Context
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowInfoTracker
 import androidx.window.layout.WindowLayoutInfo
@@ -28,10 +29,10 @@
 /**
  * Tests for the RxJava 2 adapters.
  */
-public class WindowInfoTrackerRxTest {
+class WindowInfoTrackerRxTest {
 
     @Test
-    public fun testWindowLayoutInfoObservable() {
+    fun testWindowLayoutInfoObservable() {
         val activity = mock<Activity>()
         val feature = mock<FoldingFeature>()
         val expected = WindowLayoutInfo(listOf(feature))
@@ -44,7 +45,7 @@
     }
 
     @Test
-    public fun testWindowLayoutInfoFlowable() {
+    fun testWindowLayoutInfoFlowable() {
         val activity = mock<Activity>()
         val feature = mock<FoldingFeature>()
         val expected = WindowLayoutInfo(listOf(feature))
@@ -55,4 +56,29 @@
 
         testSubscriber.assertValue(expected)
     }
+    @Test
+    fun testWindowLayoutInfoObservable_context() {
+        val activity = mock<Context>()
+        val feature = mock<FoldingFeature>()
+        val expected = WindowLayoutInfo(listOf(feature))
+        val mockTracker = mock<WindowInfoTracker>()
+        whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+        val testSubscriber = mockTracker.windowLayoutInfoObservable(activity).test()
+
+        testSubscriber.assertValue(expected)
+    }
+
+    @Test
+    fun testWindowLayoutInfoFlowable_context() {
+        val activity = mock<Context>()
+        val feature = mock<FoldingFeature>()
+        val expected = WindowLayoutInfo(listOf(feature))
+        val mockTracker = mock<WindowInfoTracker>()
+        whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+        val testSubscriber = mockTracker.windowLayoutInfoFlowable(activity).test()
+
+        testSubscriber.assertValue(expected)
+    }
 }
diff --git a/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt b/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt
index 2a82d8c..d41cc80 100644
--- a/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt
+++ b/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt
@@ -18,6 +18,8 @@
 package androidx.window.rxjava2.layout
 
 import android.app.Activity
+import android.content.Context
+import androidx.annotation.UiContext
 import androidx.window.layout.WindowInfoTracker
 import androidx.window.layout.WindowLayoutInfo
 import io.reactivex.Flowable
@@ -29,7 +31,7 @@
  * Return an [Observable] stream of [WindowLayoutInfo].
  * @see WindowInfoTracker.windowLayoutInfo
  */
-public fun WindowInfoTracker.windowLayoutInfoObservable(
+fun WindowInfoTracker.windowLayoutInfoObservable(
     activity: Activity
 ): Observable<WindowLayoutInfo> {
     return windowLayoutInfo(activity).asObservable()
@@ -39,8 +41,28 @@
  * Return a [Flowable] stream of [WindowLayoutInfo].
  * @see WindowInfoTracker.windowLayoutInfo
  */
-public fun WindowInfoTracker.windowLayoutInfoFlowable(
+fun WindowInfoTracker.windowLayoutInfoFlowable(
     activity: Activity
 ): Flowable<WindowLayoutInfo> {
     return windowLayoutInfo(activity).asFlowable()
 }
+
+/**
+ * Return an [Observable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoObservable(
+    @UiContext context: Context
+): Observable<WindowLayoutInfo> {
+    return windowLayoutInfo(context).asObservable()
+}
+
+/**
+ * Return a [Flowable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoFlowable(
+    @UiContext context: Context
+): Flowable<WindowLayoutInfo> {
+    return windowLayoutInfo(context).asFlowable()
+}
diff --git a/window/window-rxjava3/api/api_lint.ignore b/window/window-rxjava3/api/api_lint.ignore
new file mode 100644
index 0000000..8c55c33
--- /dev/null
+++ b/window/window-rxjava3/api/api_lint.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+ContextFirst: androidx.window.rxjava3.layout.WindowInfoTrackerRx#windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+    Context is distinct, so it must be the first argument (method `windowLayoutInfoFlowable`)
+ContextFirst: androidx.window.rxjava3.layout.WindowInfoTrackerRx#windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+    Context is distinct, so it must be the first argument (method `windowLayoutInfoObservable`)
diff --git a/window/window-rxjava3/api/current.txt b/window/window-rxjava3/api/current.txt
index 5700dd3..23510cc 100644
--- a/window/window-rxjava3/api/current.txt
+++ b/window/window-rxjava3/api/current.txt
@@ -3,7 +3,9 @@
 
   public final class WindowInfoTrackerRx {
     method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
     method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
   }
 
 }
diff --git a/window/window-rxjava3/api/public_plus_experimental_current.txt b/window/window-rxjava3/api/public_plus_experimental_current.txt
index 5700dd3..23510cc 100644
--- a/window/window-rxjava3/api/public_plus_experimental_current.txt
+++ b/window/window-rxjava3/api/public_plus_experimental_current.txt
@@ -3,7 +3,9 @@
 
   public final class WindowInfoTrackerRx {
     method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
     method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
   }
 
 }
diff --git a/window/window-rxjava3/api/restricted_current.txt b/window/window-rxjava3/api/restricted_current.txt
index 5700dd3..23510cc 100644
--- a/window/window-rxjava3/api/restricted_current.txt
+++ b/window/window-rxjava3/api/restricted_current.txt
@@ -3,7 +3,9 @@
 
   public final class WindowInfoTrackerRx {
     method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
     method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+    method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
   }
 
 }
diff --git a/window/window-rxjava3/build.gradle b/window/window-rxjava3/build.gradle
index 6874507..32804c4 100644
--- a/window/window-rxjava3/build.gradle
+++ b/window/window-rxjava3/build.gradle
@@ -36,6 +36,7 @@
     api(libs.kotlinCoroutinesRx3)
     api(libs.rxjava3)
     api(project(":window:window"))
+    implementation 'androidx.annotation:annotation:1.5.0'
 
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
diff --git a/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt b/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt
index 04c68fc..f4a4c4c 100644
--- a/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt
+++ b/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt
@@ -17,6 +17,7 @@
 package androidx.window.rxjava3.layout
 
 import android.app.Activity
+import android.content.Context
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowInfoTracker
 import androidx.window.layout.WindowLayoutInfo
@@ -30,10 +31,10 @@
  * [io.reactivex.rxjava3.core.Flowable] and ensure that data is forwarded appropriately.
  * @see WindowInfoTracker
  */
-public class WindowInfoTrackerRxTest {
+class WindowInfoTrackerRxTest {
 
     @Test
-    public fun testWindowLayoutInfoObservable() {
+    fun testWindowLayoutInfoObservable() {
         val activity = mock<Activity>()
         val feature = mock<FoldingFeature>()
         val expected = WindowLayoutInfo(listOf(feature))
@@ -46,7 +47,7 @@
     }
 
     @Test
-    public fun testWindowLayoutInfoFlowable() {
+    fun testWindowLayoutInfoFlowable() {
         val activity = mock<Activity>()
         val feature = mock<FoldingFeature>()
         val expected = WindowLayoutInfo(listOf(feature))
@@ -57,4 +58,30 @@
 
         testSubscriber.assertValue(expected)
     }
+
+    @Test
+    fun testWindowLayoutInfoObservable_context() {
+        val activity = mock<Context>()
+        val feature = mock<FoldingFeature>()
+        val expected = WindowLayoutInfo(listOf(feature))
+        val mockTracker = mock<WindowInfoTracker>()
+        whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+        val testSubscriber = mockTracker.windowLayoutInfoObservable(activity).test()
+
+        testSubscriber.assertValue(expected)
+    }
+
+    @Test
+    fun testWindowLayoutInfoFlowable_context() {
+        val activity = mock<Context>()
+        val feature = mock<FoldingFeature>()
+        val expected = WindowLayoutInfo(listOf(feature))
+        val mockTracker = mock<WindowInfoTracker>()
+        whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+        val testSubscriber = mockTracker.windowLayoutInfoFlowable(activity).test()
+
+        testSubscriber.assertValue(expected)
+    }
 }
diff --git a/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt b/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt
index 374bbbb..084292b 100644
--- a/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt
+++ b/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt
@@ -18,6 +18,8 @@
 package androidx.window.rxjava3.layout
 
 import android.app.Activity
+import android.content.Context
+import androidx.annotation.UiContext
 import androidx.window.layout.WindowInfoTracker
 import androidx.window.layout.WindowLayoutInfo
 import io.reactivex.rxjava3.core.Flowable
@@ -29,7 +31,7 @@
  * Return an [Observable] stream of [WindowLayoutInfo].
  * @see WindowInfoTracker.windowLayoutInfo
  */
-public fun WindowInfoTracker.windowLayoutInfoObservable(
+fun WindowInfoTracker.windowLayoutInfoObservable(
     activity: Activity
 ): Observable<WindowLayoutInfo> {
     return windowLayoutInfo(activity).asObservable()
@@ -39,8 +41,28 @@
  * Return a [Flowable] stream of [WindowLayoutInfo].
  * @see WindowInfoTracker.windowLayoutInfo
  */
-public fun WindowInfoTracker.windowLayoutInfoFlowable(
+fun WindowInfoTracker.windowLayoutInfoFlowable(
     activity: Activity
 ): Flowable<WindowLayoutInfo> {
     return windowLayoutInfo(activity).asFlowable()
 }
+
+/**
+ * Return an [Observable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoObservable(
+    @UiContext context: Context
+): Observable<WindowLayoutInfo> {
+    return windowLayoutInfo(context).asObservable()
+}
+
+/**
+ * Return a [Flowable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoFlowable(
+    @UiContext context: Context
+): Flowable<WindowLayoutInfo> {
+    return windowLayoutInfo(context).asFlowable()
+}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 58ca5a5..ee2f53d 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -147,19 +147,15 @@
     property public final float ratio;
   }
 
-  public interface SplitAttributesCalculator {
-    method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
-  }
-
-  public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+  public final class SplitAttributesCalculatorParams {
+    method public boolean getAreDefaultConstraintsSatisfied();
     method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
     method public android.content.res.Configuration getParentConfiguration();
     method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
     method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
     method public String? getSplitRuleTag();
-    method public boolean isDefaultMinSizeSatisfied();
+    property public final boolean areDefaultConstraintsSatisfied;
     property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
-    property public final boolean isDefaultMinSizeSatisfied;
     property public final android.content.res.Configuration parentConfiguration;
     property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
     property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
@@ -170,11 +166,10 @@
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
     method public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
-    method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
     method public boolean isSplitAttributesCalculatorSupported();
     method public boolean isSplitSupported();
     method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
-    method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
+    method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
 
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 81ab30f..8d4cb95 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -186,19 +186,15 @@
     property public final float ratio;
   }
 
-  public interface SplitAttributesCalculator {
-    method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
-  }
-
-  public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+  public final class SplitAttributesCalculatorParams {
+    method public boolean getAreDefaultConstraintsSatisfied();
     method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
     method public android.content.res.Configuration getParentConfiguration();
     method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
     method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
     method public String? getSplitRuleTag();
-    method public boolean isDefaultMinSizeSatisfied();
+    property public final boolean areDefaultConstraintsSatisfied;
     property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
-    property public final boolean isDefaultMinSizeSatisfied;
     property public final android.content.res.Configuration parentConfiguration;
     property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
     property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
@@ -209,11 +205,10 @@
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
     method public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
-    method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
     method public boolean isSplitAttributesCalculatorSupported();
     method public boolean isSplitSupported();
     method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
-    method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
+    method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
 
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 58ca5a5..ee2f53d 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -147,19 +147,15 @@
     property public final float ratio;
   }
 
-  public interface SplitAttributesCalculator {
-    method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
-  }
-
-  public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+  public final class SplitAttributesCalculatorParams {
+    method public boolean getAreDefaultConstraintsSatisfied();
     method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
     method public android.content.res.Configuration getParentConfiguration();
     method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
     method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
     method public String? getSplitRuleTag();
-    method public boolean isDefaultMinSizeSatisfied();
+    property public final boolean areDefaultConstraintsSatisfied;
     property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
-    property public final boolean isDefaultMinSizeSatisfied;
     property public final android.content.res.Configuration parentConfiguration;
     property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
     property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
@@ -170,11 +166,10 @@
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
     method public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
-    method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
     method public boolean isSplitAttributesCalculatorSupported();
     method public boolean isSplitSupported();
     method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
-    method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
+    method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
 
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
index b5e4534..ddbbf0d 100644
--- a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
@@ -20,18 +20,13 @@
 import android.graphics.Color
 import androidx.annotation.Sampled
 import androidx.window.embedding.SplitAttributes
-import androidx.window.embedding.SplitAttributesCalculator
 import androidx.window.embedding.SplitController
 import androidx.window.layout.FoldingFeature
 
 @Sampled
 fun splitAttributesCalculatorSample() {
     SplitController.getInstance(context)
-        .setSplitAttributesCalculator(
-            object : SplitAttributesCalculator {
-        override fun computeSplitAttributesForParams(
-            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
-        ): SplitAttributes {
+        .setSplitAttributesCalculator { params ->
             val tag = params.splitRuleTag
             val parentWindowMetrics = params.parentWindowMetrics
             val parentConfig = params.parentConfiguration
@@ -39,8 +34,8 @@
                 .filterIsInstance<FoldingFeature>()
             val foldingState = if (foldingFeatures.size == 1) foldingFeatures[0] else null
             // Tag can be used to filter the SplitRule to apply the SplitAttributes
-            if (TAG_SPLIT_RULE_MAIN != tag && params.isDefaultMinSizeSatisfied) {
-                return params.defaultSplitAttributes
+            if (TAG_SPLIT_RULE_MAIN != tag && params.areDefaultConstraintsSatisfied) {
+                return@setSplitAttributesCalculator params.defaultSplitAttributes
             }
 
             // This sample will make the app show a layout to
@@ -52,7 +47,7 @@
             if (foldingState?.isSeparating == true) {
                 // Split the parent container that followed by the hinge if the hinge separates the
                 // parent window.
-                return SplitAttributes.Builder()
+                return@setSplitAttributesCalculator SplitAttributes.Builder()
                     .setSplitType(SplitAttributes.SplitType.splitByHinge())
                     .setLayoutDirection(
                         if (foldingState.orientation == FoldingFeature.Orientation.HORIZONTAL) {
@@ -65,7 +60,9 @@
                     .setAnimationBackgroundColor(Color.GRAY)
                     .build()
             }
-            return if (parentConfig.screenWidthDp >= 600 && bounds.width() >= bounds.height()) {
+            return@setSplitAttributesCalculator if (
+                parentConfig.screenWidthDp >= 600 && bounds.width() >= bounds.height()
+            ) {
                 // Split the parent container equally and vertically if the device is in landscape.
                 SplitAttributes.Builder()
                     .setSplitType(SplitAttributes.SplitType.splitEqually())
@@ -79,22 +76,17 @@
                     .build()
             }
         }
-    })
 }
 
 @Sampled
 fun splitWithOrientations() {
     SplitController.getInstance(context)
-        .setSplitAttributesCalculator(
-            object : SplitAttributesCalculator {
-        override fun computeSplitAttributesForParams(
-            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
-        ): SplitAttributes {
+        .setSplitAttributesCalculator { params ->
             // A sample to split with the dimension that larger than 600 DP. If there's no dimension
             // larger than 600 DP, show the presentation to fill the task bounds.
             val parentConfiguration = params.parentConfiguration
             val builder = SplitAttributes.Builder()
-            return if (parentConfiguration.screenWidthDp >= 600) {
+            return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 600) {
                 builder
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                     // Set the color to use when switching between vertical and horizontal
@@ -113,39 +105,33 @@
                     .build()
             }
         }
-    })
 }
 
 @Sampled
 fun expandContainersInPortrait() {
     SplitController.getInstance(context)
-        .setSplitAttributesCalculator(
-            object : SplitAttributesCalculator {
-        override fun computeSplitAttributesForParams(
-            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
-        ): SplitAttributes {
+        .setSplitAttributesCalculator { params ->
             // A sample to always fill task bounds when the device is in portrait.
             val tag = params.splitRuleTag
             val bounds = params.parentWindowMetrics.bounds
             val defaultSplitAttributes = params.defaultSplitAttributes
-            val isDefaultMinSizeSatisfied = params.isDefaultMinSizeSatisfied
+            val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied
 
             val expandContainersAttrs = SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.expandContainers())
                 .build()
-            if (!isDefaultMinSizeSatisfied) {
-                return expandContainersAttrs
+            if (!areDefaultConstraintsSatisfied) {
+                return@setSplitAttributesCalculator expandContainersAttrs
             }
             // Always expand containers for the splitRule tagged as
             // TAG_SPLIT_RULE_EXPAND_IN_PORTRAIT if the device is in portrait
-            // even if [isDefaultMinSizeSatisfied] reports true.
+            // even if [areDefaultConstraintsSatisfied] reports true.
             if (bounds.height() > bounds.width() && TAG_SPLIT_RULE_EXPAND_IN_PORTRAIT.equals(tag)) {
-                return expandContainersAttrs
+                return@setSplitAttributesCalculator expandContainersAttrs
             }
             // Otherwise, use the default splitAttributes.
-            return defaultSplitAttributes
+            return@setSplitAttributesCalculator defaultSplitAttributes
         }
-    })
 }
 
 /** Assume it's a valid [Application]... */
diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
index b1db6b7..156652f 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -30,9 +30,10 @@
      *
      * If `true`, the system is permitted to override the app's windowing
      * behavior and implement activity embedding split rules, such as displaying
-     * activities side by side. A system override informs the app that the
-     * activity embedding APIs are disabled so the app will not provide its own
-     * activity embedding rules, which would conflict with the system's rules.
+     * activities adjacent to each other. A system override informs the app that
+     * the activity embedding APIs are disabled so the app will not provide its
+     * own activity embedding rules, which would conflict with the system's
+     * rules.
      *
      * If `false`, the system is not permitted to override the windowing
      * behavior of the app. Set the property to `false` if the app provides its
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index a060969..c96432f 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -22,8 +22,7 @@
 import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
 import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
 import androidx.window.extensions.embedding.SplitAttributes.SplitType as OEMSplitType
-import androidx.window.extensions.embedding.SplitAttributesCalculator as OEMSplitAttributesCalculator
-import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams as OEMSplitAttributesCalculatorParams
+import androidx.window.extensions.embedding.SplitAttributesCalculatorParams as OEMSplitAttributesCalculatorParams
 import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
 import androidx.window.extensions.embedding.SplitPairRule as OEMSplitPairRule
 import androidx.window.extensions.embedding.SplitPairRule.Builder as SplitPairRuleBuilder
@@ -43,8 +42,8 @@
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
 import androidx.window.embedding.SplitAttributes.SplitType
-import androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams
 import androidx.window.extensions.WindowExtensions
+import androidx.window.extensions.core.util.function.Function
 import androidx.window.extensions.core.util.function.Predicate
 import androidx.window.extensions.embedding.SplitPairRule.FINISH_ADJACENT
 import androidx.window.extensions.embedding.SplitPairRule.FINISH_ALWAYS
@@ -136,14 +135,9 @@
         SplitType.ratio(splitRatio.ratio)
 
     fun translateSplitAttributesCalculator(
-        calculator: SplitAttributesCalculator
-    ): OEMSplitAttributesCalculator =
-        OEMSplitAttributesCalculator { oemSplitAttributesCalculatorParams ->
-            translateSplitAttributes(
-                calculator.computeSplitAttributesForParams(
-                    translate(oemSplitAttributesCalculatorParams)
-                )
-            )
+        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+    ): Function<OEMSplitAttributesCalculatorParams, OEMSplitAttributes> = Function { oemParams ->
+            translateSplitAttributes(calculator.invoke(translate(oemParams)))
         }
 
     @SuppressLint("NewApi")
@@ -152,18 +146,18 @@
     ): SplitAttributesCalculatorParams = let {
         val taskWindowMetrics = params.parentWindowMetrics
         val taskConfiguration = params.parentConfiguration
-        val defaultSplitAttributes = params.defaultSplitAttributes
-        val isDefaultMinSizeSatisfied = params.isDefaultMinSizeSatisfied
         val windowLayoutInfo = params.parentWindowLayoutInfo
+        val defaultSplitAttributes = params.defaultSplitAttributes
+        val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
         val splitRuleTag = params.splitRuleTag
         val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics)
 
         SplitAttributesCalculatorParams(
             windowMetrics,
             taskConfiguration,
-            translate(defaultSplitAttributes),
-            isDefaultMinSizeSatisfied,
             ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
+            translate(defaultSplitAttributes),
+            areDefaultConstraintsSatisfied,
             splitRuleTag,
         )
     }
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
index b54fbe9..77ed5ef 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -44,11 +44,11 @@
 
     fun isActivityEmbedded(activity: Activity): Boolean
 
-    fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator)
+    fun setSplitAttributesCalculator(
+        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+    )
 
     fun clearSplitAttributesCalculator()
 
-    fun getSplitAttributesCalculator(): SplitAttributesCalculator?
-
     fun isSplitAttributesCalculatorSupported(): Boolean
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
index 46ef801..0109ecf 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -83,7 +83,9 @@
         return embeddingExtension.isActivityEmbedded(activity)
     }
 
-    override fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+    override fun setSplitAttributesCalculator(
+        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+    ) {
         if (!isSplitAttributesCalculatorSupported()) {
             throw UnsupportedOperationException("#setSplitAttributesCalculator is not supported " +
                 "on the device.")
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
index 2317536..8c74aec 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -35,7 +35,9 @@
 
     fun isActivityEmbedded(activity: Activity): Boolean
 
-    fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator)
+    fun setSplitAttributesCalculator(
+        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+    )
 
     fun clearSplitAttributesCalculator()
 
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
index 0f44fcc..987360f 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
@@ -25,8 +25,8 @@
      * A unique string to identify this [EmbeddingRule], which defaults to `null`.
      * The suggested usage is to set the tag in the corresponding rule builder to be able to
      * differentiate between different rules in the callbacks. For example, it can be used to
-     * compute the right [SplitAttributes] for the right split rule in
-     * [SplitAttributesCalculator.computeSplitAttributesForParams].
+     * compute the right [SplitAttributes] for the right split rule in callback set in
+     * [SplitController.setSplitAttributesCalculator].
      *
      * @see androidx.window.embedding.RuleController.addRule
      */
diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
index dc1d5a4..666b488 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -41,7 +41,6 @@
     @VisibleForTesting
     val splitChangeCallbacks: CopyOnWriteArrayList<SplitListenerWrapper>
     private val splitInfoEmbeddingCallback = EmbeddingCallbackImpl()
-    private var splitAttributesCalculator: SplitAttributesCalculator? = null
 
     init {
         splitChangeCallbacks = CopyOnWriteArrayList<SplitListenerWrapper>()
@@ -304,23 +303,20 @@
         return embeddingExtension?.isActivityEmbedded(activity) ?: false
     }
 
-    override fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+    override fun setSplitAttributesCalculator(
+        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+    ) {
         globalLock.withLock {
-            splitAttributesCalculator = calculator
             embeddingExtension?.setSplitAttributesCalculator(calculator)
         }
     }
 
     override fun clearSplitAttributesCalculator() {
         globalLock.withLock {
-            splitAttributesCalculator = null
             embeddingExtension?.clearSplitAttributesCalculator()
         }
     }
 
-    override fun getSplitAttributesCalculator(): SplitAttributesCalculator? =
-        globalLock.withLock { splitAttributesCalculator }
-
     override fun isSplitAttributesCalculatorSupported(): Boolean =
         embeddingExtension?.isSplitAttributesCalculatorSupported() ?: false
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
index a05dd47..d83ce52 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -218,10 +218,10 @@
              * containers each expand to fill the parent window; the secondary
              * container overlays the primary container.
              *
-             * Use this method with [SplitAttributesCalculator] to expand the
-             * activity containers in some device states. The following sample
-             * shows how to always fill the parent bounds if the device is in
-             * portrait orientation:
+             * Use this method with the function set in
+             * [SplitController.setSplitAttributesCalculator] to expand the activity containers in
+             * some device states. The following sample shows how to always fill the parent bounds
+             * if the device is in portrait orientation:
              *
              * @sample androidx.window.samples.embedding.expandContainersInPortrait
              *
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt
deleted file mode 100644
index 77720a8..0000000
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt
+++ /dev/null
@@ -1,113 +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.window.embedding
-
-import android.content.res.Configuration
-import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.WindowMetrics
-
-/**
- * A developer-defined [SplitAttributes] calculator to compute the current [SplitAttributes] with
- * the current device and window state if it is registered via
- * [SplitController.setSplitAttributesCalculator]. Then
- * [computeSplitAttributesForParams] will be called when there's
- * - An activity is started and matches a registered [SplitRule].
- * - There's a parent configuration update and there's an existing split pair.
- *
- * By default, [SplitRule.defaultSplitAttributes] are applied if the parent container's
- * [WindowMetrics] satisfies the [SplitRule]'s minimum dimensions requirements, which are
- * [SplitRule.minWidthDp], [SplitRule.minHeightDp] and [SplitRule.minSmallestWidthDp].
- * The [SplitRule.defaultSplitAttributes] can be set by
- * - [SplitRule] Builder APIs, which are
- *   [SplitPairRule.Builder.setDefaultSplitAttributes] and
- *   [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
- * - Specifying with `splitRatio` and `splitLayoutDirection` attributes in `<SplitPairRule>` or
- * `<SplitPlaceHolderRule>` tags in XML files.
- *
- * However, developers may want to apply different [SplitAttributes] for different device or window
- * states. For example, on foldable devices, developers may want to split the screen vertically if
- * the device is in landscape, fill the screen if the device is in portrait and split the screen
- * horizontally if the device is in
- * [tabletop posture](https://developer.android.com/guide/topics/ui/foldables#postures).
- * In this case, the [SplitAttributes] can be customized by this callback, which takes effects after
- * calling [SplitController.setSplitAttributesCalculator]. Developers can also
- * clear the callback by [SplitController.clearSplitAttributesCalculator].
- * Then, developers could implement [computeSplitAttributesForParams] as the sample linked below
- * shows.
- *
- * **Note** that [SplitController.setSplitAttributesCalculator] and
- * [SplitController.clearSplitAttributesCalculator] are only supported if
- * [SplitController.isSplitAttributesCalculatorSupported] reports `true`. It's
- * callers' responsibility to check if [SplitAttributesCalculator] is supported by
- * [SplitController.isSplitAttributesCalculatorSupported] before using the
- * [SplitAttributesCalculator] feature. It is suggested to always set meaningful
- * [SplitRule.defaultSplitAttributes] in case [SplitAttributesCalculator] is not supported on
- * some devices.
- *
- * @sample androidx.window.samples.embedding.splitAttributesCalculatorSample
- *
- * @see androidx.window.embedding.SplitRule.defaultSplitAttributes
- * @see androidx.window.embedding.SplitController.getSplitAttributesCalculator
- */
-interface SplitAttributesCalculator {
-    /**
-     * Computes the [SplitAttributes] with the current device and window states.
-     * @param params See [SplitAttributesCalculatorParams]
-     */
-    fun computeSplitAttributesForParams(params: SplitAttributesCalculatorParams): SplitAttributes
-
-    /** The container of [SplitAttributesCalculator] parameters */
-    class SplitAttributesCalculatorParams internal constructor(
-        /** The parent container's [WindowMetrics] */
-        val parentWindowMetrics: WindowMetrics,
-        /** The parent container's [Configuration] */
-        val parentConfiguration: Configuration,
-        /**
-         * The [SplitRule.defaultSplitAttributes]. It could be from [SplitRule] Builder APIs
-         * ([SplitPairRule.Builder.setDefaultSplitAttributes] or
-         * [SplitPlaceholderRule.Builder.setDefaultSplitAttributes]) or from the `splitRatio` and
-         * `splitLayoutDirection` attributes from static rule definitions.
-         */
-        val defaultSplitAttributes: SplitAttributes,
-        /**
-         * Whether the [parentWindowMetrics] are larger than [SplitRule]'s minimum size criteria,
-         * which are [SplitRule.minWidthDp], [SplitRule.minHeightDp] and
-         * [SplitRule.minSmallestWidthDp]
-         */
-        @get: JvmName("isDefaultMinSizeSatisfied")
-        val isDefaultMinSizeSatisfied: Boolean,
-        /** The parent container's [WindowLayoutInfo] */
-        val parentWindowLayoutInfo: WindowLayoutInfo,
-        /**
-         * The [tag of `SplitRule`][SplitRule.tag] to apply this [SplitAttributes], which is `null`
-         * if the tag is not set.
-         *
-         * @see SplitPairRule.Builder.setTag
-         * @see SplitPlaceholderRule.Builder.setTag
-         */
-        val splitRuleTag: String?,
-    ) {
-        override fun toString(): String =
-            "${SplitAttributesCalculatorParams::class.java.simpleName}:{" +
-                "windowMetrics=$parentWindowMetrics" +
-                ", configuration=$parentConfiguration" +
-                ", windowLayoutInfo=$parentWindowLayoutInfo" +
-                ", defaultSplitAttributes=$defaultSplitAttributes" +
-                ", isDefaultMinSizeSatisfied=$isDefaultMinSizeSatisfied" +
-                ", tag=$splitRuleTag}"
-    }
-}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt
new file mode 100644
index 0000000..c099362
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.window.embedding
+
+import android.content.res.Configuration
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+
+/**
+ * The parameter container used to report the current device and window state in
+ * [SplitController.setSplitAttributesCalculator] and references the corresponding [SplitRule] by
+ * [splitRuleTag] if [SplitPairRule.tag] is specified.
+ */
+class SplitAttributesCalculatorParams internal constructor(
+    /** The parent container's [WindowMetrics] */
+    val parentWindowMetrics: WindowMetrics,
+    /** The parent container's [Configuration] */
+    val parentConfiguration: Configuration,
+    /** The parent container's [WindowLayoutInfo] */
+    val parentWindowLayoutInfo: WindowLayoutInfo,
+    /**
+     * The [SplitRule.defaultSplitAttributes]. It could be from [SplitRule] Builder APIs
+     * ([SplitPairRule.Builder.setDefaultSplitAttributes] or
+     * [SplitPlaceholderRule.Builder.setDefaultSplitAttributes]) or from the `splitRatio` and
+     * `splitLayoutDirection` attributes from static rule definitions.
+     */
+    val defaultSplitAttributes: SplitAttributes,
+    /**
+     * Whether the [parentWindowMetrics] satisfies the dimensions and aspect
+     * ratios requirements specified in the [SplitRule], which are:
+     *  - [SplitRule.minWidthDp]
+     *  - [SplitRule.minHeightDp]
+     *  - [SplitRule.minSmallestWidthDp]
+     *  - [SplitRule.maxAspectRatioInPortrait]
+     *  - [SplitRule.maxAspectRatioInLandscape]
+     */
+    @get: JvmName("areDefaultConstraintsSatisfied")
+    val areDefaultConstraintsSatisfied: Boolean,
+    /**
+     * The [tag of `SplitRule`][SplitRule.tag] to apply this [SplitAttributes], which is `null`
+     * if the tag is not set.
+     *
+     * @see SplitPairRule.Builder.setTag
+     * @see SplitPlaceholderRule.Builder.setTag
+     */
+    val splitRuleTag: String?,
+) {
+    override fun toString(): String =
+        "${SplitAttributesCalculatorParams::class.java.simpleName}:{" +
+            "windowMetrics=$parentWindowMetrics" +
+            ", configuration=$parentConfiguration" +
+            ", windowLayoutInfo=$parentWindowLayoutInfo" +
+            ", defaultSplitAttributes=$defaultSplitAttributes" +
+            ", areDefaultConstraintsSatisfied=$areDefaultConstraintsSatisfied" +
+            ", tag=$splitRuleTag}"
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index efb4f9f..221d0d1 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -26,6 +26,7 @@
 import androidx.core.util.Consumer
 import androidx.window.WindowProperties
 import androidx.window.embedding.SplitController.Api31Impl.isSplitPropertyEnabled
+import androidx.window.layout.WindowMetrics
 import java.util.concurrent.Executor
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
@@ -112,26 +113,59 @@
     }
 
     /**
-     * Sets or updates the previously registered [SplitAttributesCalculator].
+     * Sets or replaces the previously registered [SplitAttributes] calculator.
      *
-     * **Note** that if the [SplitAttributesCalculator] is replaced, the existing split pairs will
-     * be updated after there's a window or device state change.
-     * The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
+     * **Note** that it's callers' responsibility to check if this API is supported by calling
+     * [isSplitAttributesCalculatorSupported] before using the this API. It is suggested to always
+     * set meaningful [SplitRule.defaultSplitAttributes] in case this API is not supported on some
+     * devices.
      *
-     * @param calculator the calculator to set. It will replace the previously set
-     * [SplitAttributesCalculator] if it exists.
+     * Also, replacing the calculator will only update existing split pairs after a change
+     * in the window or device state, such as orientation changes or folding state changes.
+     *
+     * The [SplitAttributes] calculator is a function to compute the current [SplitAttributes] for
+     * the given [SplitRule] with the current device and window state. Then The calculator will be
+     * invoked if either:
+     * - An activity is started and matches a registered [SplitRule].
+     * - A parent configuration is updated and there's an existing split pair.
+     *
+     * By default, [SplitRule.defaultSplitAttributes] are applied if the parent container's
+     * [WindowMetrics] satisfies the [SplitRule]'s dimensions requirements, which are
+     * [SplitRule.minWidthDp], [SplitRule.minHeightDp] and [SplitRule.minSmallestWidthDp].
+     * The [SplitRule.defaultSplitAttributes] can be set by
+     * - [SplitRule] Builder APIs, which are
+     *   [SplitPairRule.Builder.setDefaultSplitAttributes] and
+     *   [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
+     * - Specifying with `splitRatio` and `splitLayoutDirection` attributes in `<SplitPairRule>` or
+     * `<SplitPlaceHolderRule>` tags in XML files.
+     *
+     * Developers may want to apply different [SplitAttributes] for different device or window
+     * states. For example, on foldable devices, developers may want to split the screen vertically
+     * if the device is in landscape, fill the screen if the device is in portrait and split
+     * the screen horizontally if the device is in
+     * [tabletop posture](https://developer.android.com/guide/topics/ui/foldables#postures).
+     * In this case, the [SplitAttributes] can be customized by the [SplitAttributes] calculator,
+     * which takes effects after calling this API. Developers can also clear the calculator
+     * by [clearSplitAttributesCalculator].
+     * Then, developers could implement the [SplitAttributes] calculator as the sample linked below
+     * shows.
+     *
+     * @sample androidx.window.samples.embedding.splitAttributesCalculatorSample
+     * @param calculator the function to calculate [SplitAttributes] based on the
+     * [SplitAttributesCalculatorParams]. It will replace the previously set if it exists.
      * @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
      * `false`
      */
-    fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+    fun setSplitAttributesCalculator(
+        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+    ) {
         embeddingBackend.setSplitAttributesCalculator(calculator)
     }
 
     /**
-     * Clears the previously set [SplitAttributesCalculator].
+     * Clears the callback previously set by [setSplitAttributesCalculator].
      * The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
      *
-     * @see androidx.window.embedding.SplitController.setSplitAttributesCalculator
      * @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
      * `false`
      */
@@ -139,11 +173,7 @@
         embeddingBackend.clearSplitAttributesCalculator()
     }
 
-    /** Returns the current set [SplitAttributesCalculator]. */
-    fun getSplitAttributesCalculator(): SplitAttributesCalculator? =
-        embeddingBackend.getSplitAttributesCalculator()
-
-    /** Returns whether [SplitAttributesCalculator] is supported or not. */
+    /** Returns whether [setSplitAttributesCalculator] is supported or not. */
     fun isSplitAttributesCalculatorSupported(): Boolean =
         embeddingBackend.isSplitAttributesCalculatorSupported()
 
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
index 38e0927..ad966ab 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -21,8 +21,8 @@
 import androidx.window.embedding.SplitRule.FinishBehavior.Companion.NEVER
 
 /**
- * Split configuration rules for activity pairs. Define when activities that were launched on top of
- * each other should be shown side-by-side, and the visual properties of such splits. Can be set
+ * Split configuration rules for activity pairs. Define when activities that were launched on top
+ * should be placed adjacent to the one below, and the visual properties of such splits. Can be set
  * either by [RuleController.setRules] or [RuleController.addRule]. The rules are always
  * applied only to activities that will be started after the rules were set.
  */
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
index 05428b6..7cdfefe 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -41,15 +41,13 @@
  * started after the rules were set.
  *
  * Note that regardless of whether the minimal requirements ([minWidthDp], [minHeightDp] and
- * [minSmallestWidthDp]) are met or not, [SplitAttributesCalculator.computeSplitAttributesForParams]
- * will still be called for the rule if the calculator is registered via
- * [SplitController.setSplitAttributesCalculator]. Whether this [SplitRule]'s
- * minimum requirements are satisfied is dispatched in
- * [SplitAttributesCalculator.SplitAttributesCalculatorParams.isDefaultMinSizeSatisfied] instead.
- * The width and height could be verified in
- * [SplitAttributesCalculator.computeSplitAttributesForParams] as the sample linked below shows.
- * It is useful if this rule is supported to split the parent container in different directions
- * with different device states.
+ * [minSmallestWidthDp]) are met or not, the callback set in
+ * [SplitController.setSplitAttributesCalculator] will still be called for the rule if the
+ * calculator is registered via [SplitController.setSplitAttributesCalculator].
+ * Whether this [SplitRule]'s minimum requirements are satisfied is dispatched in
+ * [SplitAttributesCalculatorParams.areDefaultConstraintsSatisfied] instead.
+ * The width and height could be verified in the [SplitAttributes] calculator callback
+ * as the sample linked below shows.
  *
  * It is useful if this [SplitRule] is supported to split the parent container in different
  * directions with different device states.
@@ -65,8 +63,8 @@
      * When the window size is smaller than requested here, activities in the secondary container
      * will be stacked on top of the activities in the primary one, completely overlapping them.
      *
-     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
-     * `0` means to always allow split.
+     * Uses `0` to always allow split regardless of the parent task width.
+     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT].
      */
     @IntRange(from = 0)
     val minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
@@ -77,8 +75,8 @@
      * will be stacked on top of the activities in the primary one, completely overlapping them.
      * It is useful if it's necessary to split the parent window horizontally for this [SplitRule].
      *
-     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
-     * `0` means to always allow split.
+     * Uses `0` to always allow split regardless of the parent task height.
+     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT].
      *
      * @see SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
      * @see SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
@@ -92,8 +90,8 @@
      * activities in the secondary container will be stacked on top of the activities in the primary
      * one, completely overlapping them.
      *
-     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
-     * `0` means to always allow split.
+     * Uses `0` to always allow split regardless of the parent task smallest width.
+     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT].
      */
     @IntRange(from = 0)
     val minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
@@ -106,8 +104,8 @@
      *
      * This value is only used when the parent window is in portrait (height >= width).
      *
-     * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT] if the app doesn't set, which is the
-     * recommend value to only allow split when the parent window is not too stretched in portrait.
+     * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT], which is the recommend value to
+     * only allow split when the parent window is not too stretched in portrait.
      *
      * @see EmbeddingAspectRatio.ratio
      * @see EmbeddingAspectRatio.alwaysAllow
@@ -123,8 +121,8 @@
      *
      * This value is only used when the parent window is in landscape (width > height).
      *
-     * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT] if the app doesn't set, which is
-     * the recommend value to always allow split when the parent window is in landscape.
+     * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT], which is the recommend value to
+     * always allow split when the parent window is in landscape.
      *
      * @see EmbeddingAspectRatio.ratio
      * @see EmbeddingAspectRatio.alwaysAllow
@@ -183,7 +181,7 @@
      *
      * For example, given that [SplitPairRule.finishPrimaryWithSecondary] is [ADJACENT] and
      * secondary container finishes. The primary associated container is finished if it's
-     * side-by-side with secondary container. The primary associated container is not finished
+     * adjacent to the secondary container. The primary associated container is not finished
      * if it occupies entire task bounds.
      *
      * @see SplitPairRule.finishPrimaryWithSecondary
@@ -208,9 +206,9 @@
             @JvmField
             val ALWAYS = FinishBehavior("ALWAYS", 1)
             /**
-             * Only finish the associated container when displayed side-by-side/adjacent to the one
-             * being finished. Does not finish the associated one when containers are stacked on top
-             * of each other.
+             * Only finish the associated container when displayed adjacent to the one being
+             * finished. Does not finish the associated one when containers are stacked on top of
+             * each other.
              */
             @JvmField
             val ADJACENT = FinishBehavior("ADJACENT", 2)
diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
index df33e9a..d4af572 100644
--- a/window/window/src/main/res/values/attrs.xml
+++ b/window/window/src/main/res/values/attrs.xml
@@ -15,96 +15,125 @@
   limitations under the License.
   -->
 <resources xmlns:xs="http://schemas.android.com/apk/res/android">
-    <!-- Defines what activity container should be given to the primary part of the task
-     bounds. Values in range (0.0, 1.0) define the size of the primary container of the split
-     relative to the corresponding task dimension size. 0.0 means the secondary activity container
-     fills the full Task bounds, and occludes the primary activity container, which is also expanded
-     to fill the full Task bounds. 0.5 means the primary and secondary container shares an equal
-     split. Ratio larger than `0.5` means the primary container takes more partition. Otherwise,
-     the secondary container takes more partition. -->
+    <!-- Defines what part of the Task bounds should be given to the primary container in split.
+         Values in range `(0.0, 1.0)` define the size of the primary container of the split relative
+         to the corresponding parent Task bounds.
+         The default is `0.5`, which is to split with equal width. -->
     <attr name="splitRatio" format="float" />
-    <!-- The smallest value of width of the parent window when the split should be used. -->
+    <!-- The smallest value of width of the parent Task bounds when the split should be used, in DP.
+         Uses `0` to always allow split regardless of the parent Task width.
+         The default is `600`. -->
     <attr name="splitMinWidthDp" format="integer" />
-    <!-- The smallest value of height of the parent window when the split should be used. -->
+    <!-- The smallest value of height of the parent Task bounds when the split should be used, in
+         DP.
+         Uses `0` to always allow split regardless of the parent Task height.
+         The default is `600`. -->
     <attr name="splitMinHeightDp" format="integer" />
-    <!-- The smallest value of the smallest-width (sw) of the parent window in any rotation when
-    the split should be used. -->
+    <!-- The smallest value of the smallest possible width of the parent Task bounds in any rotation
+         when the split should be used, in DP.
+         Uses `0` to always allow split regardless of the parent Task smallest width.
+         The default is `600`. -->
     <attr name="splitMinSmallestWidthDp" format="integer" />
     <!-- The largest value of the aspect ratio, expressed as (height / width) in decimal form, of
-    the parent window bounds in portrait when the split should be used.
-    `0` or `alwaysAllow` means to always allow split in portrait.
-    `-1` or `alwaysDisallow` means to always disallow split in portrait.
-    Any other values less than 1 are invalid. -->
+         the parent Task bounds in portrait when the split should be used.
+         Uses `0` or `alwaysAllow` to always allow split in portrait.
+         Uses `-1` or `alwaysDisallow` to always disallow split in portrait.
+         Any other values less than `1` are invalid.
+         The default is `1.4`. -->
     <attr name="splitMaxAspectRatioInPortrait"  format="float">
-        <!-- Special value to always allow split in portrait. -->
+        <!-- Special value to always allow split in portrait regardless of the aspect ratio. -->
         <enum name="alwaysAllow" value="0" />
-        <!-- Special value to always disallow split in portrait. -->
+        <!-- Special value to always disallow split in portrait regardless of the aspect ratio. -->
         <enum name="alwaysDisallow" value="-1" />
     </attr>
     <!-- The largest value of the aspect ratio, expressed as (width / height) in decimal form, of
-    the parent window bounds in landscape when the split should be used.
-    `0` or `alwaysAllow` means to always allow split in landscape.
-    `-1` or `alwaysDisallow` means to always disallow split in landscape.
-    Any other values less than 1 are invalid. -->
+         the parent Task bounds in landscape when the split should be used.
+         Uses `0` or `alwaysAllow` to always allow split in landscape.
+         Uses `-1` or `alwaysDisallow` to always disallow split in landscape.
+         Any other values less than `1` are invalid.
+         The default is `alwaysAllow`. -->
     <attr name="splitMaxAspectRatioInLandscape"  format="float">
-        <!-- Special value to always allow split in landscape. -->
+        <!-- Special value to always allow split in landscape regardless of the aspect ratio. -->
         <enum name="alwaysAllow" value="0" />
-        <!-- Special value to always disallow split in landscape. -->
+        <!-- Special value to always disallow split in landscape regardless of the aspect ratio. -->
         <enum name="alwaysDisallow" value="-1" />
     </attr>
+    <!-- The layout direction for the split. -->
     <attr name="splitLayoutDirection" format="enum">
         <!-- A layout direction that splits the task bounds vertically, and the direction is deduced
-        from the language script of locale. The direction can be either rtl or ltr -->
+             from the language script of locale. The direction can be either `rtl` or `ltr` -->
         <enum name="locale" value="0" />
         <!-- The primary container is placed on the left, and the secondary is on the right hand
-        side. -->
+             side. -->
         <enum name="ltr" value="1" />
         <!-- The primary container is placed on the right, and the secondary is on the left hand
-        side. -->
+             side. -->
         <enum name="rtl" value="2" />
         <!-- The primary container is placed on top, and the secondary is at the bottom. -->
         <enum name="topToBottom" value="3" />
         <!-- The primary container is placed on bottom, and the secondary is at the top. -->
         <enum name="bottomToTop" value="4" />
     </attr>
+    <!-- Determines what happens with the primary container when all activities are finished in
+         the associated secondary container.
+         The default is `never`. -->
     <attr name="finishPrimaryWithSecondary" format="enum">
+        <!-- Never finish the primary container. -->
         <enum name="never" value="0" />
+        <!-- Always finish the primary container. -->
         <enum name="always" value="1" />
+        <!-- Only finish the primary container when the primary container is displayed adjacent to
+             the secondary container.
+             Does not finish the primary one when the secondary container is stacked on top of
+             the primary. -->
         <enum name="adjacent" value="2" />
     </attr>
-    <attr name="finishPrimaryWithPlaceholder" format="enum">
-        <enum name="always" value="1" />
-        <enum name="adjacent" value="2" />
-    </attr>
+    <!-- Determines what happens with the secondary container when all activities are finished
+         in the associated primary container.
+         The default is `always`. -->
     <attr name="finishSecondaryWithPrimary" format="enum">
+        <!-- Never finish the secondary container. -->
         <enum name="never" value="0" />
+        <!-- Always finish the secondary container. -->
         <enum name="always" value="1" />
+        <!-- Only finish the secondary container when the primary container is displayed adjacent to
+             the secondary container.
+             Does not finish the secondary one when the secondary container is stacked on top of
+             the primary. -->
         <enum name="adjacent" value="2" />
     </attr>
-    <!-- An optional but unique string to identify a SplitPairRule, SplitPlaceholderRule or
-    ActivityRule. The suggested usage is to set the tag to be able to differentiate between
-    different rules in the callbacks. For example, it can be used to compute the right
-    SplitAttributes for the right split rule in
-    SplitAttributesCalculator#computeSplitAttributesForState.-->
+    <!-- Determines what happens with the primary container when the associated placeholder
+         activity is being finished.
+         The default is `always`. -->
+    <attr name="finishPrimaryWithPlaceholder" format="enum">
+        <!-- Always finish the primary container. -->
+        <enum name="always" value="1" />
+        <!-- Only finish the primary container when the primary container is displayed adjacent to
+             the placeholder container.
+             Does not finish the primary one when the placeholder container is stacked on top of
+             the primary. -->
+        <enum name="adjacent" value="2" />
+    </attr>
+    <!-- An optional but unique string to identify a `SplitPairRule`, `SplitPlaceholderRule` or
+         `ActivityRule`. The suggested usage is to set the tag to be able to differentiate between
+         different rules in the callbacks.
+         For example, it can be used to compute the right `SplitAttributes` for the given split rule
+         in `SplitAttributesCalculator.computeSplitAttributesForParams`.-->
     <attr name="tag" format="string" />
-    <!-- Background color of animation if the animation requires a background. Defaults to the
-     theme's windowBackground. -->
+    <!-- Background color of animation if the animation requires a background.
+         The default is to use the theme's windowBackground. -->
     <attr name="animationBackgroundColor" format="color" />
 
-    <!-- Split configuration rules for activity pairs. Must contain at least one SplitPairFilter.
-    See androidx.window.embedding.SplitPairRule for complete documentation. -->
+    <!-- Split configuration rules for activity pairs. Must contain at least one `SplitPairFilter`.
+         -->
     <declare-styleable name="SplitPairRule">
-        <!-- When all activities are finished in the secondary container, the activity in the
-         primary container that created the split should also be finished. Defaults to "never". -->
-        <attr name="finishPrimaryWithSecondary" />
-        <!-- When all activities are finished in the primary container, the activities in the
-         secondary container in the split should also be finished. Defaults to "always". -->
-        <attr name="finishSecondaryWithPrimary" />
         <!-- If there is an existing split with the same primary container, indicates whether the
-        existing secondary container on top and all activities in it should be destroyed when a new
-        split is created using this rule. Otherwise the new secondary will appear on top. Defaults
-         to "false". -->
+             existing secondary container on top and all activities in it should be destroyed when a
+             new split is created using this rule. Otherwise the new secondary will appear on top.
+             The default is 'false'. -->
         <attr name="clearTop" format="boolean" />
+        <attr name="finishPrimaryWithSecondary" />
+        <attr name="finishSecondaryWithPrimary" />
         <attr name="splitRatio"/>
         <attr name="splitMinWidthDp"/>
         <attr name="splitMinHeightDp"/>
@@ -116,17 +145,16 @@
         <attr name="animationBackgroundColor"/>
     </declare-styleable>
 
-    <!-- Configuration rules for split placeholders. Must contain at least one ActivityFilter for
-    the primary activity for which the rule should be applied.
-    See androidx.window.embedding.SplitPlaceholderRule for complete documentation. -->
+    <!-- Configuration rules for split placeholders. Must contain at least one `ActivityFilter` for
+         the primary activity for which the rule should be applied.  -->
     <declare-styleable name="SplitPlaceholderRule">
-        <!-- Component name of the placeholder activity in the split. Must be non-empty. -->
+        <!-- Component name of the placeholder activity to launch in the split.
+             Must be non-empty. -->
         <attr name="placeholderActivityName" format="string" />
-        <!-- Determines whether the placeholder will show on top in a smaller window size after
-        it first appeared in a split with sufficient minimum width. -->
+        <!-- Determines whether the placeholder will show on top if Task window constraints are not
+             satisfied after it first appeared in a split with sufficient Task window constraints.
+             The default is `false`. -->
         <attr name="stickyPlaceholder" format="boolean" />
-        <!-- When all activities are finished in the secondary container, the activity in the
-         primary container that created the split should also be finished. Defaults to "always". -->
         <attr name="finishPrimaryWithPlaceholder"/>
         <attr name="splitRatio"/>
         <attr name="splitMinWidthDp"/>
@@ -139,35 +167,38 @@
         <attr name="animationBackgroundColor"/>
     </declare-styleable>
 
-    <!-- Filter used to find if a pair of activities should be put in a split. -->
-    <declare-styleable name="SplitPairFilter">
-        <!-- Component name of the primary activity in the split. Must be non-empty. Can contain a
-         wildcard at the end or instead of package name and/or class name. -->
-        <attr name="primaryActivityName" format="string" />
-        <!-- Component name of the secondary activity in the split. Must be non-empty. Can contain a
-         wildcard at the end or instead of package name and/or class name. -->
-        <attr name="secondaryActivityName" format="string" />
-        <!-- Action used for secondary activity launch. May be empty. Must not contain wildcards.
-         -->
-        <attr name="secondaryActivityAction" format="string" />
-    </declare-styleable>
-
     <!-- Layout configuration rules for individual activities. Takes precedence over
-    SplitPairRule. Must contain at least one ActivityFilter.
-    See androidx.window.layout.ActivityRule for complete documentation. -->
+         `SplitPairRule`. Must contain at least one `ActivityFilter`. -->
     <declare-styleable name="ActivityRule">
-        <!-- Whether the activity should always be expanded on launch. -->
+        <!-- Whether the activity should always be expanded on launch.
+             The default is `false`. -->
         <attr name="alwaysExpand" format="boolean" />
         <attr name="tag"/>
     </declare-styleable>
 
-    <!-- Filter for ActivityRule. -->
-    <declare-styleable name="ActivityFilter">
+    <!-- Filter for `SplitPairRule` to find if a pair of activities should be put in a split. -->
+    <declare-styleable name="SplitPairFilter">
         <!-- Component name of the primary activity in the split. Must be non-empty. Can contain a
-         single wildcard at the end. -->
+             wildcard at the end or instead of package name and/or class name. -->
+        <attr name="primaryActivityName" format="string" />
+        <!-- Component name of the secondary activity in the split. Must be non-empty. Can contain a
+             wildcard at the end or instead of package name and/or class name. -->
+        <attr name="secondaryActivityName" format="string" />
+        <!-- Action used for the secondary activity launch. May be empty. Must not contain
+             wildcards.
+             When it is set, the filter only matches if the secondary activity is launched with an
+             intent with the given action. -->
+        <attr name="secondaryActivityAction" format="string" />
+    </declare-styleable>
+
+    <!-- Filter for `ActivityRule` and `SplitPlaceholderRule`. -->
+    <declare-styleable name="ActivityFilter">
+        <!-- Component name of the launched activity. Must be non-empty. Can contain a single
+             wildcard at the end. -->
         <attr name="activityName" format="string" />
-        <!-- Action used for activity launch. May be empty. Must not contain wildcards.
-         -->
+        <!-- Action used for the activity launch. May be empty. Must not contain wildcards.
+             When it is set, the filter only matches if the activity is launched with an intent with
+             the given action. -->
         <attr name="activityAction" format="string" />
     </declare-styleable>
 </resources>
\ No newline at end of file