Merge "Cancel graph state collection after UseCaseCamera is closed" into androidx-main
diff --git a/OWNERS b/OWNERS
index b6e6906..12e1923 100644
--- a/OWNERS
+++ b/OWNERS
@@ -28,3 +28,6 @@
 per-file *settings.gradle = set noparent
 per-file *settings.gradle = nickanthony@google.com, alanv@google.com, aurimas@google.com
 per-file *libraryversions.toml = jnichol@google.com
+
+# Copybara can self-approve CLs within synced docs.
+per-file docs/... = copybara-worker-blackhole@google.com
\ No newline at end of file
diff --git a/arch/core/core-common/api/restricted_current.txt b/arch/core/core-common/api/restricted_current.txt
index 7f1c354..4fbc435 100644
--- a/arch/core/core-common/api/restricted_current.txt
+++ b/arch/core/core-common/api/restricted_current.txt
@@ -20,11 +20,15 @@
     method public int size();
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions implements java.util.Iterator<java.util.Map.Entry<K,V>> {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove<K,V> implements java.util.Iterator<java.util.Map.Entry<K,V>> {
     method public boolean hasNext();
     method public java.util.Map.Entry<K!,V!>! next();
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class SafeIterableMap.SupportRemove<K, V> {
+    ctor public SafeIterableMap.SupportRemove();
+  }
+
 }
 
 package androidx.arch.core.util {
diff --git a/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java b/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
index 08cce2f..bdaeca8 100644
--- a/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
+++ b/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
@@ -358,7 +358,14 @@
         }
     }
 
-    abstract static class SupportRemove<K, V> {
+    /**
+     * @hide
+     *
+     * @param <K>
+     * @param <V>
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public abstract static class SupportRemove<K, V> {
         abstract void supportRemove(@NonNull Entry<K, V> entry);
     }
 
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
index 680dd62..0f0b95f 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -476,6 +476,15 @@
     fun pathExists(absoluteFilePath: String): Boolean {
         return ShellImpl.executeCommandUnsafe("ls $absoluteFilePath").trim() == absoluteFilePath
     }
+
+    @RequiresApi(21)
+    fun amBroadcast(broadcastArguments: String): Int? {
+        // unsafe here for perf, since we validate the return value so we don't need to check stderr
+        return ShellImpl.executeCommandUnsafe("am broadcast $broadcastArguments")
+            .substringAfter("Broadcast completed: result=")
+            .trim()
+            .toIntOrNull()
+    }
 }
 
 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index aeec388..685451c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -270,4 +270,13 @@
     fun dropShaderCachePublicApi() = validateDropShaderCacheWithRoot {
         dropShaderCache()
     }
+
+    @Test
+    fun dropKernelPageCache() {
+        val scope = MacrobenchmarkScope(
+            Packages.TARGET,
+            launchWithClearTask = false
+        )
+        scope.dropKernelPageCache() // shouldn't crash
+    }
 }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
index 0da5134..055ccfd 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -17,16 +17,23 @@
 package androidx.benchmark.macro
 
 import android.os.Build
+import androidx.benchmark.junit4.PerfettoTraceRule
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import kotlin.test.assertNull
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class ProfileInstallBroadcastTest {
+    @OptIn(ExperimentalPerfettoCaptureApi::class)
+    @get:Rule
+    val perfettoTraceRule = PerfettoTraceRule()
+
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
     @Test
     fun installProfile() {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
index ff6b62a..088d93a 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
@@ -22,6 +22,7 @@
 import androidx.benchmark.perfetto.PerfettoHelper
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOWEST_BUNDLED_VERSION_SUPPORTED
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.testutils.verifyWithPolling
@@ -60,10 +61,12 @@
         PerfettoHelper.stopAllPerfettoProcesses()
     }
 
+    @FlakyTest(bugId = 258216025)
     @SdkSuppress(minSdkVersion = LOWEST_BUNDLED_VERSION_SUPPORTED)
     @Test
     fun captureAndValidateTrace_bundled() = captureAndValidateTrace(unbundled = false)
 
+    @FlakyTest(bugId = 258216025)
     @Test
     fun captureAndValidateTrace_unbundled() = captureAndValidateTrace(unbundled = true)
 
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 296d682..0d3280b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -294,11 +294,11 @@
         if (Build.VERSION.SDK_INT >= 31) {
             dropKernelPageCacheSetProp()
         } else {
-            val result = Shell.executeScriptCaptureStdout(
+            val result = Shell.executeScriptCaptureStdoutStderr(
                 "echo 3 > /proc/sys/vm/drop_caches && echo Success || echo Failure"
-            ).trim()
+            )
             // Older user builds don't allow drop caches, should investigate workaround
-            if (result != "Success") {
+            if (result.stdout.trim() != "Success") {
                 if (DeviceInfo.isRooted && !Shell.isSessionRooted()) {
                     throw IllegalStateException("Failed to drop caches - run `adb root`")
                 }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index dc8d53f..f81127b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -16,6 +16,7 @@
 
 package androidx.benchmark.macro
 
+import android.os.Build
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.benchmark.Shell
@@ -26,7 +27,8 @@
     private val receiverName = ProfileInstallReceiver::class.java.name
 
     /**
-     * Returns null on success, or an error string otherwise.
+     * Returns null on success, error string on suppress-able error, or throws if profileinstaller
+     * not up to date.
      *
      * Returned error strings aren't thrown, to let the calling function decide strictness.
      */
@@ -36,13 +38,7 @@
         // installed synchronously
         val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
         // Use an explicit broadcast given the app was force-stopped.
-        val result = Shell.executeScriptCaptureStdout(
-            "am broadcast -a $action $packageName/$receiverName"
-        )
-            .substringAfter("Broadcast completed: result=")
-            .trim()
-            .toIntOrNull()
-        when (result) {
+        when (val result = Shell.amBroadcast("-a $action $packageName/$receiverName")) {
             null,
                 // 0 is returned by the platform by default, and also if no broadcast receiver
                 // receives the broadcast.
@@ -63,12 +59,25 @@
                 )
             }
             ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
+                val sdkInt = Build.VERSION.SDK_INT
                 throw RuntimeException(
-                    "Baseline profiles aren't supported on this device version"
+                    if (sdkInt <= 23) {
+                        "Baseline profiles aren't supported on this device version," +
+                            " as all apps are fully ahead-of-time compiled."
+                    } else {
+                        "The device SDK version ($sdkInt) isn't supported" +
+                            " by the target app's copy of profileinstaller." +
+                            if (sdkInt in 31..33) {
+                                " Please use profileinstaller `1.2.1`" +
+                                    " or newer for API 31-33 support"
+                            } else {
+                                ""
+                            }
+                    }
                 )
             }
             ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
-                return "No baseline profile was found in the target apk."
+                    return "No baseline profile was found in the target apk."
             }
             ProfileInstaller.RESULT_NOT_WRITABLE,
             ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
@@ -103,12 +112,7 @@
         val action = "androidx.profileinstaller.action.SKIP_FILE"
         val operationKey = "EXTRA_SKIP_FILE_OPERATION"
         val extras = "$operationKey $operation"
-        val result = Shell.executeScriptCaptureStdout(
-            "am broadcast -a $action -e $extras $packageName/$receiverName"
-        )
-            .substringAfter("Broadcast completed: result=")
-            .trim()
-            .toIntOrNull()
+        val result = Shell.amBroadcast("-a $action -e $extras $packageName/$receiverName")
         return when {
             result == null || result == 0 -> {
                 // 0 is returned by the platform by default, and also if no broadcast receiver
@@ -143,13 +147,7 @@
     fun saveProfile(packageName: String): String? {
         Log.d(TAG, "Profile Installer - Save Profile")
         val action = "androidx.profileinstaller.action.SAVE_PROFILE"
-        val result = Shell.executeScriptCaptureStdout(
-            "am broadcast -a $action $packageName/$receiverName"
-        )
-            .substringAfter("Broadcast completed: result=")
-            .trim()
-            .toIntOrNull()
-        return when (result) {
+        return when (val result = Shell.amBroadcast("-a $action $packageName/$receiverName")) {
             null, 0 -> {
                 // 0 is returned by the platform by default, and also if no broadcast receiver
                 // receives the broadcast.
@@ -176,20 +174,19 @@
         }
     }
 
-    private fun benchmarkOperation(packageName: String, operation: String): String? {
+    private fun benchmarkOperation(
+        packageName: String,
+        @Suppress("SameParameterValue") operation: String
+    ): String? {
         Log.d(TAG, "Profile Installer - Benchmark Operation: $operation")
         // Redefining constants here, because these are only defined in the latest alpha for
         // ProfileInstaller.
         // Use an explicit broadcast given the app was force-stopped.
         val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
         val operationKey = "EXTRA_BENCHMARK_OPERATION"
-        val extras = "$operationKey $operation"
-        val result = Shell.executeScriptCaptureStdout(
-            "am broadcast -a $action -e $extras $packageName/$receiverName"
+        val result = Shell.amBroadcast(
+            "-a $action -e $operationKey $operation $packageName/$receiverName"
         )
-            .substringAfter("Broadcast completed: result=")
-            .trim()
-            .toIntOrNull()
         return when (result) {
             null, 0, 16 /* BENCHMARK_OPERATION_UNKNOWN */ -> {
                 // 0 is returned by the platform by default, and also if no broadcast receiver
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt
index 8b88add..5095f08 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt
@@ -18,6 +18,7 @@
 
 import android.bluetooth.le.AdvertiseSettings as FwkAdvertiseSettings
 
+@JvmDefaultWithCompatibility
 /**
  * Bluetooth LE advertising callbacks, used to deliver advertising operation status.
  *
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt
index 4b8f628..4f77e63 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt
@@ -19,6 +19,7 @@
 import android.bluetooth.le.AdvertisingSetCallback as FwkAdvertisingSetCallback
 import android.bluetooth.le.AdvertisingSet as FwkAdvertisingSet
 
+@JvmDefaultWithCompatibility
 /**
  * Bluetooth LE advertising set callbacks, used to deliver advertising operation
  * status.
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt
index 6b4e653..cc28712 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt
@@ -957,6 +957,7 @@
         return impl.toBundle()
     }
 
+    @JvmDefaultWithCompatibility
     interface BluetoothDeviceImpl : Bundleable {
         fun connectGatt(
             context: Context,
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt
index adba929..2133332 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt
@@ -21,6 +21,7 @@
 
 import androidx.annotation.IntDef
 
+@JvmDefaultWithCompatibility
 /**
  * Bluetooth LE scan callbacks. Scan results are reported using these callbacks.
  *
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 91d7c75..98ad678 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -298,9 +298,14 @@
                 } else {
                     task.kotlinOptions.jvmTarget = "1.8"
                 }
-                task.kotlinOptions.freeCompilerArgs += listOf(
-                    "-Xskip-metadata-version-check"
+                val kotlinCompilerArgs = mutableListOf(
+                    "-Xskip-metadata-version-check",
                 )
+                // TODO (b/259578592): enable -Xjvm-default=all for camera-camera2-pipe projects
+                if (!project.name.contains("camera-camera2-pipe")) {
+                    kotlinCompilerArgs += "-Xjvm-default=all"
+                }
+                task.kotlinOptions.freeCompilerArgs += kotlinCompilerArgs
             }
 
             // If no one else is going to register a source jar, then we should.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 7021369..0e99e30 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -16,6 +16,8 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
+import android.media.MediaCodec
+import android.os.Build
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.core.Log
@@ -26,8 +28,9 @@
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
-import androidx.camera.core.ImageCapture
 import androidx.camera.core.UseCase
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.SessionConfig.ValidatingBuilder
 import javax.inject.Inject
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.joinAll
@@ -259,10 +262,14 @@
 
     @GuardedBy("lock")
     private fun shouldAddRepeatingUseCase(runningUseCases: Set<UseCase>): Boolean {
-        return !attachedUseCases.contains(meteringRepeating) &&
-            runningUseCases.only { it is ImageCapture }
-    }
+        val meteringRepeatingEnabled = attachedUseCases.contains(meteringRepeating)
 
+        val coreLibraryUseCases = runningUseCases.filterNot { it is MeteringRepeating }
+        val onlyVideoCapture = coreLibraryUseCases.onlyVideoCapture()
+        val requireMeteringRepeating = coreLibraryUseCases.requireMeteringRepeating()
+
+        return !meteringRepeatingEnabled && (onlyVideoCapture || requireMeteringRepeating)
+    }
     @GuardedBy("lock")
     private fun addRepeatingUseCase() {
         meteringRepeating.setupSession()
@@ -272,11 +279,13 @@
 
     @GuardedBy("lock")
     private fun shouldRemoveRepeatingUseCase(runningUseCases: Set<UseCase>): Boolean {
-        val onlyMeteringRepeatingEnabled = runningUseCases.only { it is MeteringRepeating }
-        val meteringRepeatingAndNonImageCaptureEnabled =
-            runningUseCases.any { it is MeteringRepeating } &&
-                runningUseCases.any { it !is MeteringRepeating && it !is ImageCapture }
-        return onlyMeteringRepeatingEnabled || meteringRepeatingAndNonImageCaptureEnabled
+        val meteringRepeatingEnabled = runningUseCases.contains(meteringRepeating)
+
+        val coreLibraryUseCases = runningUseCases.filterNot { it is MeteringRepeating }
+        val onlyVideoCapture = coreLibraryUseCases.onlyVideoCapture()
+        val requireMeteringRepeating = coreLibraryUseCases.requireMeteringRepeating()
+
+        return meteringRepeatingEnabled && !onlyVideoCapture && !requireMeteringRepeating
     }
 
     @GuardedBy("lock")
@@ -286,11 +295,31 @@
         meteringRepeating.onDetached()
     }
 
-    /**
-     * Returns true when the collection only has elements (1 or more) that verify the predicate,
-     * false otherwise.
-     */
-    private fun <T> Collection<T>.only(predicate: (T) -> Boolean): Boolean {
-        return isNotEmpty() && all(predicate)
+    private fun Collection<UseCase>.onlyVideoCapture(): Boolean {
+        return isNotEmpty() && checkSurfaces { _, sessionSurfaces ->
+            sessionSurfaces.isNotEmpty() && sessionSurfaces.all {
+                it.containerClass == MediaCodec::class.java
+            }
+        }
+    }
+
+    private fun Collection<UseCase>.requireMeteringRepeating(): Boolean {
+        return isNotEmpty() && checkSurfaces { repeatingSurfaces, sessionSurfaces ->
+            // There is no repeating UseCases
+            sessionSurfaces.isNotEmpty() && repeatingSurfaces.isEmpty()
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun Collection<UseCase>.checkSurfaces(
+        predicate: (
+            repeatingSurfaces: List<DeferrableSurface>,
+            sessionSurfaces: List<DeferrableSurface>
+        ) -> Boolean
+    ): Boolean = ValidatingBuilder().let { validatingBuilder ->
+        forEach { useCase -> validatingBuilder.add(useCase.sessionConfig) }
+        val sessionConfig = validatingBuilder.build()
+        val captureConfig = sessionConfig.repeatingCaptureConfig
+        return predicate(captureConfig.surfaces, sessionConfig.surfaces)
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 2249cca..da13be6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -714,39 +714,6 @@
     }
 
     @Test
-    fun legacyVideo_suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
-        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
-            mockCamcorderProfileAdapter
-        )
-        val imageCapture = ImageCapture.Builder()
-            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-            .build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder()
-            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-            .build()
-        val preview = Preview.Builder()
-            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-            .build()
-        val useCases: MutableList<UseCase> = ArrayList()
-        useCases.add(imageCapture)
-        useCases.add(videoCapture)
-        useCases.add(preview)
-        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
-            useCases,
-            useCaseConfigFactory
-        )
-        assertThrows(IllegalArgumentException::class.java) {
-            supportedSurfaceCombination.getSuggestedResolutions(
-                emptyList(),
-                ArrayList(useCaseToConfigMap.values)
-            )
-        }
-    }
-
-    @Test
     fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
@@ -778,46 +745,6 @@
     }
 
     @Test
-    fun legacyVideo_suggestedResForCustomizeResolutionsNotSupportedInLegacyDevice() {
-        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
-            mockCamcorderProfileAdapter
-        )
-
-        // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
-        val videoResolutionsPairs = listOf(
-            Pair.create(ImageFormat.PRIVATE, arrayOf(recordSize))
-        )
-        val previewResolutionsPairs = listOf(
-            Pair.create(ImageFormat.PRIVATE, arrayOf(previewSize))
-        )
-        // Override the default max resolution in VideoCapture
-        val videoCapture =
-            androidx.camera.core.VideoCapture.Builder()
-                .setMaxResolution(recordSize)
-                .setSupportedResolutions(videoResolutionsPairs)
-                .build()
-        val preview = Preview.Builder()
-            .setSupportedResolutions(previewResolutionsPairs)
-            .build()
-        val useCases: MutableList<UseCase> = ArrayList()
-        useCases.add(videoCapture)
-        useCases.add(preview)
-        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
-            useCases,
-            useCaseConfigFactory
-        )
-        assertThrows(IllegalArgumentException::class.java) {
-            supportedSurfaceCombination.getSuggestedResolutions(
-                emptyList(),
-                ArrayList(useCaseToConfigMap.values)
-            )
-        }
-    }
-
-    @Test
     fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
@@ -850,52 +777,6 @@
         }
     }
 
-    @Test
-    fun legacyVideo_getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
-        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
-            mockCamcorderProfileAdapter
-        )
-        val imageCapture = ImageCapture.Builder()
-            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-            .build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder()
-            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-            .build()
-        val preview = Preview.Builder()
-            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-            .build()
-        val useCases: MutableList<UseCase> = ArrayList()
-        useCases.add(imageCapture)
-        useCases.add(videoCapture)
-        useCases.add(preview)
-        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
-            useCases,
-            useCaseConfigFactory
-        )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
-                emptyList(),
-                ArrayList(useCaseToConfigMap.values)
-            )
-
-        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
-            useCaseToConfigMap[imageCapture],
-            recordSize
-        )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
-            useCaseToConfigMap[videoCapture],
-            legacyVideoMaximumVideoSize
-        )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
-            useCaseToConfigMap[preview],
-            previewSize
-        )
-    }
-
     // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
     @Test
     fun suggestedResolutionsForMixedUseCaseInLimitedDevice() {
@@ -1129,60 +1010,6 @@
     }
 
     @Test
-    fun legacyVideo_getSuggestedResolutionsForCustomizedSupportedResolutions() {
-        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
-            mockCamcorderProfileAdapter
-        )
-        val formatResolutionsPairList: MutableList<Pair<Int, Array<Size>>> = ArrayList()
-        formatResolutionsPairList.add(Pair.create(ImageFormat.JPEG, arrayOf(vgaSize)))
-        formatResolutionsPairList.add(
-            Pair.create(ImageFormat.YUV_420_888, arrayOf(vgaSize))
-        )
-        formatResolutionsPairList.add(Pair.create(ImageFormat.PRIVATE, arrayOf(vgaSize)))
-
-        // Sets use cases customized supported resolutions to 640x480 only.
-        val imageCapture = ImageCapture.Builder()
-            .setSupportedResolutions(formatResolutionsPairList)
-            .build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder()
-            .setSupportedResolutions(formatResolutionsPairList)
-            .build()
-        val preview = Preview.Builder()
-            .setSupportedResolutions(formatResolutionsPairList)
-            .build()
-        val useCases: MutableList<UseCase> = ArrayList()
-        useCases.add(imageCapture)
-        useCases.add(videoCapture)
-        useCases.add(preview)
-        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
-            useCases,
-            useCaseConfigFactory
-        )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
-                emptyList(),
-                ArrayList(useCaseToConfigMap.values)
-            )
-
-        // Checks all suggested resolutions will become 640x480.
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
-            useCaseToConfigMap[imageCapture],
-            vgaSize
-        )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
-            useCaseToConfigMap[videoCapture],
-            vgaSize
-        )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
-            useCaseToConfigMap[preview],
-            vgaSize
-        )
-    }
-
-    @Test
     fun suggestedResolutionsForCustomizedSupportedResolutions() {
 
         // Checks all suggested resolutions will become 640x480.
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index a98af2c..5f4d5d2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -17,6 +17,7 @@
 package androidx.camera.camera2.pipe.integration.impl
 
 import android.os.Build
+import android.util.Size
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
@@ -28,12 +29,18 @@
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraComponentBuilder
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeCamera
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
@@ -41,6 +48,8 @@
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class UseCaseManagerTest {
+    private val useCaseManagerList = mutableListOf<UseCaseManager>()
+    private val useCaseList = mutableListOf<UseCase>()
     private val useCaseThreads by lazy {
         val dispatcher = Dispatchers.Default
         val cameraScope = CoroutineScope(
@@ -55,11 +64,17 @@
         )
     }
 
+    @After
+    fun tearDown() = runBlocking {
+        useCaseManagerList.forEach { it.close() }
+        useCaseList.forEach { it.onDetached() }
+    }
+
     @Test
     fun enabledUseCasesEmpty_whenUseCaseAttachedOnly() {
         // Arrange
         val useCaseManager = createUseCaseManager()
-        val useCase = Preview.Builder().build()
+        val useCase = createPreview()
 
         // Act
         useCaseManager.attach(listOf(useCase))
@@ -73,7 +88,7 @@
     fun enabledUseCasesNotEmpty_whenUseCaseEnabled() {
         // Arrange
         val useCaseManager = createUseCaseManager()
-        val useCase = Preview.Builder().build()
+        val useCase = createPreview()
         useCaseManager.attach(listOf(useCase))
 
         // Act
@@ -88,8 +103,8 @@
     fun meteringRepeatingNotEnabled_whenPreviewEnabled() {
         // Arrange
         val useCaseManager = createUseCaseManager()
-        val preview = Preview.Builder().build()
-        val imageCapture = ImageCapture.Builder().build()
+        val preview = createPreview()
+        val imageCapture = createImageCapture()
         useCaseManager.attach(listOf(preview, imageCapture))
 
         // Act
@@ -105,7 +120,7 @@
     fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() {
         // Arrange
         val useCaseManager = createUseCaseManager()
-        val imageCapture = ImageCapture.Builder().build()
+        val imageCapture = createImageCapture()
         useCaseManager.attach(listOf(imageCapture))
 
         // Act
@@ -123,12 +138,12 @@
     fun meteringRepeatingDisabled_whenPreviewBecomesEnabled() {
         // Arrange
         val useCaseManager = createUseCaseManager()
-        val imageCapture = ImageCapture.Builder().build()
+        val imageCapture = createImageCapture()
         useCaseManager.attach(listOf(imageCapture))
         useCaseManager.activate(imageCapture)
 
         // Act
-        val preview = Preview.Builder().build()
+        val preview = createPreview()
         useCaseManager.attach(listOf(preview))
         useCaseManager.activate(preview)
 
@@ -141,8 +156,8 @@
     fun meteringRepeatingEnabled_afterAllUseCasesButImageCaptureDisabled() {
         // Arrange
         val useCaseManager = createUseCaseManager()
-        val preview = Preview.Builder().build()
-        val imageCapture = ImageCapture.Builder().build()
+        val preview = createPreview()
+        val imageCapture = createImageCapture()
         useCaseManager.attach(listOf(preview, imageCapture))
         useCaseManager.activate(preview)
         useCaseManager.activate(imageCapture)
@@ -162,7 +177,7 @@
     fun meteringRepeatingDisabled_whenAllUseCasesDisabled() {
         // Arrange
         val useCaseManager = createUseCaseManager()
-        val imageCapture = ImageCapture.Builder().build()
+        val imageCapture = createImageCapture()
         useCaseManager.attach(listOf(imageCapture))
         useCaseManager.activate(imageCapture)
 
@@ -187,6 +202,36 @@
             ComboRequestListener()
         ),
         cameraStateAdapter = CameraStateAdapter(),
-        displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext()),
-    )
+        displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+    ).also {
+        useCaseManagerList.add(it)
+    }
+
+    private fun createImageCapture(): ImageCapture =
+        ImageCapture.Builder()
+            .setCaptureOptionUnpacker { _, _ -> }
+            .setSessionOptionUnpacker() { _, _ -> }
+            .build().also {
+                it.simulateActivation()
+                useCaseList.add(it)
+            }
+
+    private fun createPreview(): Preview =
+        Preview.Builder()
+            .setCaptureOptionUnpacker { _, _ -> }
+            .setSessionOptionUnpacker() { _, _ -> }
+            .build().apply {
+                setSurfaceProvider(
+                    CameraXExecutors.mainThreadExecutor(),
+                    SurfaceTextureProvider.createSurfaceTextureProvider()
+                )
+            }.also {
+                it.simulateActivation()
+                useCaseList.add(it)
+            }
+
+    private fun UseCase.simulateActivation() {
+        onAttach(FakeCamera("0"), null, null)
+        updateSuggestedResolution(Size(640, 480))
+    }
 }
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
deleted file mode 100644
index b8b5535..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2
-
-import android.Manifest
-import android.content.ContentResolver
-import android.content.ContentValues
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.media.MediaRecorder
-import android.os.Build
-import android.os.ParcelFileDescriptor
-import android.provider.MediaStore
-import android.util.Size
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Logger
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
-import androidx.camera.testing.AudioUtil
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
-import androidx.camera.testing.SurfaceTextureProvider.createSurfaceTextureProvider
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.GrantPermissionRule
-import androidx.testutils.assertThrows
-import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@Suppress("DEPRECATION")
-@SdkSuppress(minSdkVersion = 21)
-class VideoCaptureTest {
-    companion object {
-        private const val TAG = "VideoCaptureTest"
-    }
-
-    @get:Rule
-    val useRecordingResource = CameraUtil.checkVideoRecordingResource()
-
-    @get:Rule
-    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        PreTestCameraIdList(Camera2Config.defaultConfig())
-    )
-
-    @get:Rule
-    val permissionRule: GrantPermissionRule =
-        GrantPermissionRule.grant(
-            Manifest.permission.WRITE_EXTERNAL_STORAGE,
-            Manifest.permission.RECORD_AUDIO
-        )
-
-    private val instrumentation = InstrumentationRegistry.getInstrumentation()
-
-    private val context = ApplicationProvider.getApplicationContext<Context>()
-
-    private lateinit var cameraSelector: CameraSelector
-
-    private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-
-    private lateinit var contentResolver: ContentResolver
-
-    @Before
-    fun setUp() {
-        // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
-        assumeFalse(
-            "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
-            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
-        )
-
-        assumeTrue(CameraUtil.deviceHasCamera())
-        assumeTrue(AudioUtil.canStartAudioRecord(MediaRecorder.AudioSource.CAMCORDER))
-
-        cameraSelector = if (CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
-            CameraSelector.DEFAULT_BACK_CAMERA
-        } else {
-            CameraSelector.DEFAULT_FRONT_CAMERA
-        }
-
-        CameraXUtil.initialize(
-            context,
-            Camera2Config.defaultConfig()
-        ).get()
-        cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-
-        contentResolver = context.contentResolver
-    }
-
-    @After
-    fun tearDown() {
-        instrumentation.runOnMainSync {
-            if (this::cameraUseCaseAdapter.isInitialized) {
-                cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
-            }
-        }
-
-        CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 25)
-    fun buildFileOutputOptionsWithFileDescriptor_throwExceptionWhenAPILevelSmallerThan26() {
-        val file = File.createTempFile("CameraX", ".tmp").apply {
-            deleteOnExit()
-        }
-
-        val fileDescriptor =
-            ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE).fileDescriptor
-
-        assertThrows<IllegalArgumentException> {
-            androidx.camera.core.VideoCapture.OutputFileOptions.Builder(fileDescriptor).build()
-        }
-
-        file.delete()
-    }
-
-    @Test(timeout = 30000)
-    @SdkSuppress(minSdkVersion = 26)
-    fun startRecordingWithFileDescriptor_whenAPILevelLargerThan26() {
-        val file = File.createTempFile("CameraX", ".tmp").apply {
-            deleteOnExit()
-        }
-
-        // It's needed to have a variable here to hold the parcel file descriptor reference which
-        // returned from ParcelFileDescriptor.open(), the returned parcel descriptor reference might
-        // be garbage collected unexpectedly. That will caused an "invalid file descriptor" issue.
-        val parcelFileDescriptor =
-            ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
-        val fileDescriptor = parcelFileDescriptor.fileDescriptor
-
-        val preview = Preview.Builder().build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
-        assumeTrue(
-            "This combination (videoCapture, preview) is not supported.",
-            cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
-        )
-
-        instrumentation.runOnMainSync {
-            preview.setSurfaceProvider(
-                CameraXExecutors.mainThreadExecutor(),
-                getSurfaceProvider()
-            )
-            // b/168187087 if there is only VideoCapture , VideoCapture will failed when setting the
-            // repeating request with the surface, the workaround is binding one more useCase
-            // Preview.
-            cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
-        }
-
-        val outputFileOptions =
-            androidx.camera.core.VideoCapture.OutputFileOptions.Builder(fileDescriptor).build()
-
-        val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-
-        // Start recording with FileDescriptor
-        videoCapture.startRecording(
-            outputFileOptions,
-            CameraXExecutors.mainThreadExecutor(),
-            callback
-        )
-
-        // Recording for seconds
-        recordingUntilKeyFrameArrived(videoCapture)
-
-        // Stop recording
-        videoCapture.stopRecording()
-
-        verify(callback, timeout(10000)).onVideoSaved(any())
-        parcelFileDescriptor.close()
-        file.delete()
-    }
-
-    @FlakyTest // b/182165222
-    @Test(timeout = 30000)
-    fun unbind_shouldStopRecording() {
-        val file = File.createTempFile("CameraX", ".tmp").apply {
-            deleteOnExit()
-        }
-
-        val preview = Preview.Builder().build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
-        assumeTrue(
-            "This combination (videoCapture, preview) is not supported.",
-            cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
-        )
-        instrumentation.runOnMainSync {
-            preview.setSurfaceProvider(
-                CameraXExecutors.mainThreadExecutor(),
-                getSurfaceProvider()
-            )
-            cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
-        }
-
-        val outputFileOptions =
-            androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build()
-
-        val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-
-        videoCapture.startRecording(
-            outputFileOptions,
-            CameraXExecutors.mainThreadExecutor(),
-            callback
-        )
-
-        recordingUntilKeyFrameArrived(videoCapture)
-
-        instrumentation.runOnMainSync {
-            cameraUseCaseAdapter.removeUseCases(listOf(videoCapture, preview))
-        }
-
-        verify(callback, timeout(10000)).onVideoSaved(any())
-        file.delete()
-    }
-
-    @Test(timeout = 30000)
-    @SdkSuppress(minSdkVersion = 26)
-    fun startRecordingWithUri_whenAPILevelLargerThan26() {
-        val preview = Preview.Builder().build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
-        assumeTrue(
-            "This combination (videoCapture, preview) is not supported.",
-            cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
-        )
-        instrumentation.runOnMainSync {
-            preview.setSurfaceProvider(
-                CameraXExecutors.mainThreadExecutor(),
-                getSurfaceProvider()
-            )
-            cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
-        }
-
-        val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-        videoCapture.startRecording(
-            getNewVideoOutputFileOptions(contentResolver),
-            CameraXExecutors.mainThreadExecutor(),
-            callback
-        )
-        recordingUntilKeyFrameArrived(videoCapture)
-
-        videoCapture.stopRecording()
-
-        // Assert: Wait for the signal that the image has been saved.
-        val outputFileResultsArgumentCaptor =
-            ArgumentCaptor.forClass(
-                androidx.camera.core.VideoCapture.OutputFileResults::class.java
-            )
-        verify(callback, timeout(10000)).onVideoSaved(outputFileResultsArgumentCaptor.capture())
-
-        // get file path to remove it
-        val saveLocationUri =
-            outputFileResultsArgumentCaptor.value.savedUri
-        assertThat(saveLocationUri).isNotNull()
-
-        // Remove temp test file
-        contentResolver.delete(saveLocationUri!!, null, null)
-    }
-
-    @Test(timeout = 30000)
-    fun videoCapture_saveResultToFile() {
-        val file = File.createTempFile("CameraX", ".tmp").apply {
-            deleteOnExit()
-        }
-
-        val preview = Preview.Builder().build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
-        assumeTrue(
-            "This combination (videoCapture, preview) is not supported.",
-            cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
-        )
-        instrumentation.runOnMainSync {
-            preview.setSurfaceProvider(
-                CameraXExecutors.mainThreadExecutor(),
-                getSurfaceProvider()
-            )
-            cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
-        }
-
-        val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-        videoCapture.startRecording(
-            androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build(),
-            CameraXExecutors.mainThreadExecutor(),
-            callback
-        )
-
-        recordingUntilKeyFrameArrived(videoCapture)
-        videoCapture.stopRecording()
-
-        // Wait for the signal that the video has been saved.
-        verify(callback, timeout(10000)).onVideoSaved(any())
-        file.delete()
-    }
-
-    /** Return a VideoOutputFileOption which is used to save a video.  */
-    private fun getNewVideoOutputFileOptions(
-        resolver: ContentResolver
-    ): androidx.camera.core.VideoCapture.OutputFileOptions {
-        val videoFileName = "video_" + System.currentTimeMillis()
-        val contentValues = ContentValues().apply {
-            put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
-            put(MediaStore.Video.Media.TITLE, videoFileName)
-            put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName)
-        }
-
-        return androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
-            resolver,
-            MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues
-        ).build()
-    }
-
-    private fun getSurfaceProvider(): Preview.SurfaceProvider {
-        return createSurfaceTextureProvider(object : SurfaceTextureCallback {
-            override fun onSurfaceTextureReady(surfaceTexture: SurfaceTexture, resolution: Size) {
-                // No-op
-            }
-
-            override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
-                surfaceTexture.release()
-            }
-        })
-    }
-
-    private fun recordingUntilKeyFrameArrived(videoCapture: androidx.camera.core.VideoCapture) {
-        Logger.i(TAG, "recordingUntilKeyFrameArrived begins: " + System.nanoTime() / 1000)
-        while (true) {
-            if (videoCapture.mIsFirstVideoKeyFrameWrite.get() && videoCapture
-                .mIsFirstAudioSampleWrite.get()
-            ) {
-                Logger.i(
-                    TAG,
-                    "Video Key Frame and audio frame Arrived: " + System.nanoTime() / 1000
-                )
-                break
-            }
-            Thread.sleep(100)
-        }
-        Logger.i(TAG, "recordingUntilKeyFrameArrived ends: " + System.nanoTime() / 1000)
-    }
-}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt
deleted file mode 100644
index 64a717c..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2
-
-import android.Manifest
-import android.content.ContentResolver
-import android.content.Context
-import android.content.pm.PackageManager
-import android.graphics.SurfaceTexture
-import android.media.MediaMetadataRetriever
-import android.net.Uri
-import android.os.Build
-import android.util.Size
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Logger
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.core.content.ContextCompat
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.GrantPermissionRule
-import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assume
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 21)
-class VideoCaptureTestWithoutAudioPermissionTest {
-    companion object {
-        const val TAG: String = "VideoCaptureTestWithoutAudioPermission"
-    }
-    @get:Rule
-    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        PreTestCameraIdList(Camera2Config.defaultConfig())
-    )
-
-    @get:Rule
-    val permissionRule: GrantPermissionRule =
-        GrantPermissionRule.grant(
-            Manifest.permission.WRITE_EXTERNAL_STORAGE
-            // Don't grant Manifest.permission.RECORD_AUDIO
-        )
-
-    private val instrumentation = InstrumentationRegistry.getInstrumentation()
-
-    private val context = ApplicationProvider.getApplicationContext<Context>()
-
-    private lateinit var cameraSelector: CameraSelector
-
-    private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-
-    private lateinit var contentResolver: ContentResolver
-
-    @Before
-    fun setUp() {
-        // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
-        Assume.assumeFalse(
-            "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
-            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
-        )
-
-        assumeTrue(CameraUtil.deviceHasCamera())
-
-        cameraSelector = if (CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
-            CameraSelector.DEFAULT_BACK_CAMERA
-        } else {
-            CameraSelector.DEFAULT_FRONT_CAMERA
-        }
-
-        CameraXUtil.initialize(
-            context,
-            Camera2Config.defaultConfig()
-        ).get()
-        cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-
-        contentResolver = context.contentResolver
-    }
-
-    @After
-    fun tearDown() {
-        instrumentation.runOnMainSync {
-            if (this::cameraUseCaseAdapter.isInitialized) {
-                cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
-            }
-        }
-
-        CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
-    }
-
-    /**
-     * This test intends to test recording features without audio permission (RECORD_AUDIO).
-     * Currently we cannot guarantee test cases' running sequence, the audio permission might be
-     * granted by previous tests.
-     * And if we revoke audio permission on the runtime it will cause the test crash.
-     * That makes it necessary to check if the audio permission is denied or not before the test.
-     * It's conceivable this test will be skipped because it's not the first case to test.
-     */
-    @Test
-    @Suppress("DEPRECATION")
-    fun videoCapture_saveResultToFileWithoutAudioPermission() {
-        val checkPermissionResult =
-            ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
-
-        Logger.i(TAG, "checkSelfPermission RECORD_AUDIO: $checkPermissionResult")
-
-        // This test is only for audio permission does not granted case.
-        assumeTrue(checkPermissionResult == PackageManager.PERMISSION_DENIED)
-
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        val preview = Preview.Builder().build()
-        val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
-        assumeTrue(
-            "This combination (videoCapture, preview) is not supported.",
-            cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
-        )
-        instrumentation.runOnMainSync {
-            preview.setSurfaceProvider(
-                CameraXExecutors.mainThreadExecutor(),
-                getSurfaceProvider()
-            )
-            cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
-        }
-
-        val callback =
-            Mockito.mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-        videoCapture.startRecording(
-            androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build(),
-            CameraXExecutors.mainThreadExecutor(),
-            callback
-        )
-
-        Thread.sleep(3000)
-
-        videoCapture.stopRecording()
-
-        // Wait for the signal that the video has been saved.
-        Mockito.verify(callback, Mockito.timeout(10000)).onVideoSaved(ArgumentMatchers.any())
-
-        val mediaRetriever = MediaMetadataRetriever()
-
-        mediaRetriever.apply {
-            setDataSource(context, Uri.fromFile(file))
-            val hasAudio = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
-            val numOfTracks = extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS)
-
-            // In most of case and test environment, the RECORD_AUDIO permission is granted.
-            // But if there is any audio permission denied cases, the recording should be keeps
-            // going and only video recorded.
-            assertThat(hasAudio).isNull()
-            assertThat(numOfTracks).isEqualTo("1")
-        }
-
-        file.delete()
-    }
-
-    private fun getSurfaceProvider(): Preview.SurfaceProvider {
-        return SurfaceTextureProvider.createSurfaceTextureProvider(object :
-                SurfaceTextureProvider.SurfaceTextureCallback {
-                override fun onSurfaceTextureReady(
-                    surfaceTexture: SurfaceTexture,
-                    resolution: Size,
-                ) {
-                    // No-op
-                }
-
-                override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
-                    surfaceTexture.release()
-                }
-            }
-        )
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
index 29cce4f..980b398 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
@@ -80,7 +80,6 @@
 import androidx.camera.core.impl.Quirks;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.VideoCaptureConfig;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
@@ -383,33 +382,6 @@
                 == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE);
     }
 
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseConfigsVideoCapture() {
-        Collection<UseCaseConfig<?>> useCaseConfigs = new ArrayList<>();
-        VideoCaptureConfig videoCaptureConfig =
-                new VideoCaptureConfig(MutableOptionsBundle.create());
-        useCaseConfigs.add(videoCaptureConfig);
-        assertTrue(StreamUseCaseUtil.getStreamUseCaseFromUseCaseConfigs(useCaseConfigs,
-                new ArrayList<>())
-                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD);
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseConfigsVideoAndImageCapture() {
-        Collection<UseCaseConfig<?>> useCaseConfigs = new ArrayList<>();
-        VideoCaptureConfig videoCaptureConfig =
-                new VideoCaptureConfig(MutableOptionsBundle.create());
-        useCaseConfigs.add(videoCaptureConfig);
-        ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig(
-                MutableOptionsBundle.create());
-        useCaseConfigs.add(imageCaptureConfig);
-        assertTrue(StreamUseCaseUtil.getStreamUseCaseFromUseCaseConfigs(useCaseConfigs,
-                new ArrayList<>())
-                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL);
-    }
-
     // Sharing surface of YUV format is supported since API 28
     @SdkSuppress(minSdkVersion = 28)
     @Test
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
index 602b86b..24d785a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
@@ -91,7 +91,7 @@
  */
 @LargeTest
 @RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = 23)
+@SdkSuppress(minSdkVersion = 28) // ImageWriter to PRIVATE format requires API 28
 class ProcessingCaptureSessionTest(
     private var lensFacing: Int,
     // The pair specifies (Output image format to Input image format). SessionProcessor will
@@ -111,7 +111,7 @@
                 CameraSelector.LENS_FACING_BACK, (YUV_420_888 to YUV_420_888), (JPEG to null)
             ),
             arrayOf(
-                CameraSelector.LENS_FACING_BACK, (PRIVATE to null), (YUV_420_888 to YUV_420_888)
+                CameraSelector.LENS_FACING_BACK, (PRIVATE to null), (JPEG to YUV_420_888)
             ),
             arrayOf(
                 CameraSelector.LENS_FACING_FRONT, (PRIVATE to null), (JPEG to null)
@@ -120,7 +120,7 @@
                 CameraSelector.LENS_FACING_FRONT, (YUV_420_888 to YUV_420_888), (JPEG to null)
             ),
             arrayOf(
-                CameraSelector.LENS_FACING_FRONT, (PRIVATE to null), (YUV_420_888 to YUV_420_888)
+                CameraSelector.LENS_FACING_FRONT, (PRIVATE to null), (JPEG to YUV_420_888)
             )
         )
     }
@@ -310,8 +310,10 @@
         )
 
         // Assert
+        sessionProcessor.assertStartCaptureInvoked()
         sessionConfigParameters.assertStillCaptureCompleted()
         sessionConfigParameters.assertCaptureImageReceived()
+
         val parametersConfig = sessionProcessor.getLatestParameters()
         assertThat(
             parametersConfig.isParameterSet(
@@ -323,6 +325,41 @@
         ).isTrue()
     }
 
+    @Test
+    fun canIssueAfTrigger(): Unit = runBlocking(Dispatchers.Main) {
+        assertCanIssueTriggerRequest(CaptureRequest.CONTROL_AF_TRIGGER,
+            CaptureRequest.CONTROL_AF_TRIGGER_START)
+    }
+
+    @Test
+    fun canIssueAePrecaptureTrigger(): Unit = runBlocking(Dispatchers.Main) {
+        assertCanIssueTriggerRequest(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+    }
+
+    private suspend fun <T : Any> assertCanIssueTriggerRequest(
+        testKey: CaptureRequest.Key<T>,
+        testValue: T
+    ) {
+        // Arrange
+        val cameraDevice = cameraDeviceHolder.get()!!
+        val captureSession = createProcessingCaptureSession()
+        captureSession.open(
+            sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
+            captureSessionOpenerBuilder.build()
+        ).awaitWithTimeout(3000)
+
+        // Act
+        captureSession.issueCaptureRequests(
+            listOf(sessionConfigParameters.getTriggerCaptureConfig(testKey, testValue))
+        )
+
+        // Assert
+        val triggerConfig = sessionProcessor.assertStartTriggerInvoked()
+        assertThat(triggerConfig.isParameterSet(testKey, testValue)).isTrue()
+        sessionConfigParameters.assertTriggerCompleted()
+    }
+
     private fun <T> Config.isParameterSet(key: CaptureRequest.Key<T>, objValue: T): Boolean {
         val options = CaptureRequestOptions.Builder.from(this).build()
         return Objects.equals(
@@ -396,41 +433,6 @@
     }
 
     @Test
-    fun willCancelRequests_whenIssueMultipleConfigs(): Unit = runBlocking(Dispatchers.Main) {
-        // Arrange
-        val cameraDevice = cameraDeviceHolder.get()!!
-        val captureSession = createProcessingCaptureSession()
-        captureSession.open(
-            sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
-            captureSessionOpenerBuilder.build()
-        ).awaitWithTimeout(3000)
-
-        val cancelCountLatch = CountDownLatch(2)
-        val captureConfig1 = CaptureConfig.Builder().apply {
-            templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
-            addCameraCaptureCallback(object : CameraCaptureCallback() {
-                override fun onCaptureCancelled() {
-                    cancelCountLatch.countDown()
-                }
-            })
-        }.build()
-        val captureConfig2 = CaptureConfig.Builder().apply {
-            templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
-            addCameraCaptureCallback(object : CameraCaptureCallback() {
-                override fun onCaptureCancelled() {
-                    cancelCountLatch.countDown()
-                }
-            })
-        }.build()
-
-        // Act
-        captureSession.issueCaptureRequests(listOf(captureConfig1, captureConfig2))
-
-        // Assert
-        assertThat(cancelCountLatch.await(3, TimeUnit.SECONDS)).isTrue()
-    }
-
-    @Test
     fun willCancelNonStillCaptureRequests(): Unit = runBlocking(Dispatchers.Main) {
         // Arrange
         val cameraDevice = cameraDeviceHolder.get()!!
@@ -457,37 +459,6 @@
     }
 
     @Test
-    fun willCancelRequests_whenPendingRequestNotFinished(): Unit = runBlocking(Dispatchers.Main) {
-        // Arrange
-        val cameraDevice = cameraDeviceHolder.get()!!
-        val captureSession = createProcessingCaptureSession()
-        captureSession.open(
-            sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
-            captureSessionOpenerBuilder.build()
-        ).awaitWithTimeout(3000)
-
-        // Act
-        captureSession.issueCaptureRequests(
-            listOf(sessionConfigParameters.getStillCaptureCaptureConfig())
-        )
-
-        val cancelCountLatch = CountDownLatch(1)
-        val captureConfig = CaptureConfig.Builder().apply {
-            templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
-            addCameraCaptureCallback(object : CameraCaptureCallback() {
-                override fun onCaptureCancelled() {
-                    cancelCountLatch.countDown()
-                }
-            })
-        }.build()
-        // send 2nd request immediately.
-        captureSession.issueCaptureRequests(listOf(captureConfig))
-
-        // Assert
-        assertThat(cancelCountLatch.await(3, TimeUnit.SECONDS)).isTrue()
-    }
-
-    @Test
     fun canExecuteStillCaptureOneByOne(): Unit = runBlocking(Dispatchers.Main) {
         // Arrange
         val cameraDevice = cameraDeviceHolder.get()!!
@@ -761,6 +732,7 @@
         private val previewImageReady = CompletableDeferred<Unit>()
         private val captureImageReady = CompletableDeferred<Unit>()
         private val stillCaptureCompleted = CompletableDeferred<Unit>()
+        private val triggerRequestCompleted = CompletableDeferred<Unit>()
         private val tagKey1 = "KEY1"
         private val tagKey2 = "KEY2"
         private val tagValue1 = "Value1"
@@ -890,6 +862,23 @@
             }.build()
         }
 
+        fun <T : Any> getTriggerCaptureConfig(
+            triggerKey: CaptureRequest.Key<T>,
+            triggerValue: T
+        ): CaptureConfig {
+            return CaptureConfig.Builder().apply {
+                templateType = CameraDevice.TEMPLATE_PREVIEW
+                implementationOptions = CaptureRequestOptions.Builder().apply {
+                    setCaptureRequestOption(triggerKey, triggerValue)
+                }.build()
+                addCameraCaptureCallback(object : CameraCaptureCallback() {
+                    override fun onCaptureCompleted(cameraCaptureResult: CameraCaptureResult) {
+                        triggerRequestCompleted.complete(Unit)
+                    }
+                })
+            }.build()
+        }
+
         fun closeOutputSurfaces() {
             previewOutputDeferrableSurface.close()
             captureOutputDeferrableSurface.close()
@@ -921,6 +910,10 @@
             captureImageReady.awaitWithTimeout(3000)
         }
 
+        suspend fun assertTriggerCompleted() {
+            triggerRequestCompleted.awaitWithTimeout(3000)
+        }
+
         fun tearDown() {
             closeOutputSurfaces()
         }
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
index aaef6b0..c7ef572 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.graphics.ImageFormat
-import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
 import android.os.Handler
 import android.os.Looper
@@ -36,10 +35,15 @@
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.ImageProxy
 import androidx.camera.core.Preview
+import androidx.camera.core.impl.CameraConfig
 import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.CaptureProcessor
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.Identifier
 import androidx.camera.core.impl.ImageProxyBundle
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.SessionProcessor
 import androidx.camera.core.impl.SurfaceCombination
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -48,6 +52,7 @@
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeSessionProcessor
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -120,6 +125,7 @@
         CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     fun successCaptureImage_whenExtraYuvPrivYuvConfigurationSupported() = runBlocking {
         var cameraSelector = createCameraSelectorById(cameraId)
@@ -152,14 +158,15 @@
 
         // Image analysis use a YUV stream by default
         var imageAnalysis = ImageAnalysis.Builder().build()
-
         // Preview use a PRIV stream by default
         var preview = Preview.Builder().build()
-
-        // Forces the image capture to use a YUV stream
-        var imageCapture =
-            ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888).build()
-
+        var imageCapture = ImageCapture.Builder().build()
+        // This will force ImageCapture to use YUV_420_888 to configure capture session.
+        val fakeSessionProcessor = FakeSessionProcessor(
+            inputFormatPreview = null,
+            inputFormatCapture = ImageFormat.YUV_420_888
+        )
+        enableSessionProcessor(cameraUseCaseAdapter, fakeSessionProcessor)
         withContext(Dispatchers.Main) {
             preview.setSurfaceProvider(getSurfaceProvider())
             cameraUseCaseAdapter.addUseCases(Arrays.asList(imageAnalysis, preview, imageCapture))
@@ -173,6 +180,7 @@
         callback.awaitCapturesAndAssert()
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     fun successCaptureImage_whenExtraYuvYuvYuvConfigurationSupported() = runBlocking {
         var cameraSelector = createCameraSelectorById(cameraId)
@@ -205,15 +213,15 @@
 
         // Image analysis use a YUV stream by default
         var imageAnalysis = ImageAnalysis.Builder().build()
+        var preview = Preview.Builder().build()
+        var imageCapture = ImageCapture.Builder().build()
 
-        // Sets a CaptureProcessor to make the preview use a YUV stream
-        var preview = Preview.Builder()
-            .setCaptureProcessor(FakePreviewCaptureProcessor()).build()
-
-        // Forces the image capture to use a YUV stream
-        var imageCapture =
-            ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888).build()
-
+        // This will force ImageCapture / Preview to use YUV_420_888 to configure capture session.
+        val fakeSessionProcessor = FakeSessionProcessor(
+            inputFormatPreview = ImageFormat.YUV_420_888,
+            inputFormatCapture = ImageFormat.YUV_420_888
+        )
+        enableSessionProcessor(cameraUseCaseAdapter, fakeSessionProcessor)
         withContext(Dispatchers.Main) {
             preview.setSurfaceProvider(getSurfaceProvider())
             cameraUseCaseAdapter.addUseCases(Arrays.asList(imageAnalysis, preview, imageCapture))
@@ -227,6 +235,31 @@
         callback.awaitCapturesAndAssert()
     }
 
+    private fun enableSessionProcessor(
+        cameraUseCaseAdapter: CameraUseCaseAdapter,
+        sessionProcessor: SessionProcessor
+    ) {
+        cameraUseCaseAdapter.setExtendedConfig(object : CameraConfig {
+            override fun getConfig(): Config {
+                return MutableOptionsBundle.create()
+            }
+
+            override fun getCompatibilityId(): Identifier {
+                return Identifier.create(0)
+            }
+
+            override fun getSessionProcessor(
+                valueIfMissing: SessionProcessor?
+            ): SessionProcessor? {
+                return sessionProcessor
+            }
+
+            override fun getSessionProcessor(): SessionProcessor {
+                return sessionProcessor
+            }
+        })
+    }
+
     private fun createCameraSelectorById(id: String): CameraSelector {
         var builder = CameraSelector.Builder()
 
@@ -247,19 +280,9 @@
     }
 
     private fun getSurfaceProvider(): Preview.SurfaceProvider {
-        return SurfaceTextureProvider.createSurfaceTextureProvider(object :
-                SurfaceTextureProvider.SurfaceTextureCallback {
-                override fun onSurfaceTextureReady(
-                    surfaceTexture: SurfaceTexture,
-                    resolution: Size
-                ) {
-                    // No-op
-                }
-
-                override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
-                    surfaceTexture.release()
-                }
-            })
+        // Must use auto draining SurfaceTexture which will close the Image. Otherwise it could
+        // block the imageWriter to cause problems.
+        return SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider()
     }
 
     /**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index 173093a..e530e40 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -100,7 +100,7 @@
     private ProcessorState mProcessorState;
     private static List<DeferrableSurface> sHeldProcessorSurfaces = new ArrayList<>();
     @Nullable
-    private volatile CaptureConfig mPendingCaptureConfig = null;
+    private volatile List<CaptureConfig> mPendingCaptureConfigs = null;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     volatile boolean mIsExecutingStillCaptureRequest = false;
     private final SessionProcessorCaptureCallback mSessionProcessorCaptureCallback;
@@ -280,33 +280,69 @@
         }
     }
 
-    private boolean isStillCapture(@NonNull List<CaptureConfig> captureConfigs) {
-        if (captureConfigs.isEmpty()) {
-            return false;
-        }
-        for (CaptureConfig captureConfig : captureConfigs) {
-            // Don't need to consider TEMPLATE_VIDEO_SNAPSHOT case since extensions does not
-            // support Video Capture yet
-            if (captureConfig.getTemplateType() != CameraDevice.TEMPLATE_STILL_CAPTURE) {
-                return false;
+    /**
+     * Send a trigger request. Currently only CONTROL_AF_TRIGGER and CONTROL_AE_PRECAPTURE_TRIGGER
+     * are supported.
+     */
+    void issueTriggerRequest(@NonNull CaptureConfig captureConfig) {
+        Logger.d(TAG, "issueTriggerRequest");
+        CaptureRequestOptions options =
+                CaptureRequestOptions.Builder.from(
+                        captureConfig.getImplementationOptions()).build();
+
+        boolean hasTriggerParameters = false;
+        for (Config.Option<?> option : options.listOptions()) {
+            @SuppressWarnings("unchecked")
+            CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+            if (key.equals(CaptureRequest.CONTROL_AF_TRIGGER)
+                    || key.equals(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)) {
+                hasTriggerParameters = true;
+                break;
             }
         }
-        return true;
+
+        if (!hasTriggerParameters) {
+            cancelRequests(Arrays.asList(captureConfig));
+            return;
+        }
+        mSessionProcessor.startTrigger(options, new SessionProcessor.CaptureCallback() {
+            @Override
+            public void onCaptureFailed(int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
+                                CameraCaptureFailure.Reason.ERROR));
+                    }
+                });
+            }
+
+            @Override
+            public void onCaptureSequenceCompleted(int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureCompleted(
+                                new CameraCaptureResult.EmptyCameraCaptureResult());
+                    }
+                });
+            }
+        });
     }
 
     /**
-     * Submit a still capture request via
-     * {@link SessionProcessor#startCapture(SessionProcessor.CaptureCallback)}.
+     * Submit a list of capture requests.
      *
-     * <p>The method is more restrictive than {@link CaptureSession#issueCaptureRequests(List)}.
-     * Only one @link CaptureConfig} with {@link CameraDevice#TEMPLATE_STILL_CAPTURE} template is
-     * allowed. If the captureConfigs contain multiple {@link CaptureConfig}s or the contained
-     * {@link CaptureConfig} does not use {@link CameraDevice#TEMPLATE_STILL_CAPTURE}, all
-     * captureConfigs will be cancelled immediately.
+     * <p>Capture requests using {@link CameraDevice#TEMPLATE_STILL_CAPTURE} are executed by.
+     * {@link SessionProcessor#startCapture(SessionProcessor.CaptureCallback)}. Other
+     * capture requests that trigger {@link CaptureRequest#CONTROL_AF_TRIGGER} or
+     * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER} are executed by
+     * {@link SessionProcessor#startTrigger(Config, SessionProcessor.CaptureCallback)}.
      *
-     * <p>Camera2 capture options in {@link CaptureConfig#getImplementationOptions()} will be
+     * <p>For still capture requests, Camera2 capture options in
+     * {@link CaptureConfig#getImplementationOptions()} will be
      * merged with the options in {@link SessionConfig#getImplementationOptions()} set by
-     * {@link #setSessionConfig(SessionConfig)}. The merged parameters set will be passed to
+     * {@link #setSessionConfig(SessionConfig)}. The merged parameters set is passed to
      * {@link SessionProcessor#setParameters(Config)} but it is up to the implementation of the
      * {@link SessionProcessor} to determine which options to apply.
      *
@@ -314,103 +350,30 @@
      * to invoke callbacks of {@link CaptureCallbackContainer} type due to lack of the access to
      * the camera2 {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback}.
      *
-     * <p>Still capture requests are expected to arrive one at a time sequentially by upper layer.
-     * Capture requests will be cancelled if previous request have not finished.
+     * <p>Although it allows concurrent capture requests to be submitted, the session processor
+     * might not support more than one capture request to execute at the same time. The session
+     * processor could fail the request immediately if it can't run multiple requests.
      */
     @Override
     public void issueCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
         if (captureConfigs.isEmpty()) {
             return;
         }
-        if (captureConfigs.size() > 1 || !isStillCapture(captureConfigs)) {
-            cancelRequests(captureConfigs);
-            return;
-        }
-        // Only allows one capture config at a time.
-        if (mPendingCaptureConfig != null || mIsExecutingStillCaptureRequest) {
-            cancelRequests(captureConfigs);
-            return;
-        }
-
-        // captureConfigs should contain exactly one CaptureConfig.
-        CaptureConfig captureConfig = captureConfigs.get(0);
 
         Logger.d(TAG, "issueCaptureRequests (id=" + mInstanceId + ") + state =" + mProcessorState);
-
         switch (mProcessorState) {
             case UNINITIALIZED:
             case SESSION_INITIALIZED:
-                mPendingCaptureConfig = captureConfig;
-
+                mPendingCaptureConfigs = captureConfigs;
                 break;
             case ON_CAPTURE_SESSION_STARTED:
-                mIsExecutingStillCaptureRequest = true;
-                CaptureRequestOptions.Builder builder =
-                        CaptureRequestOptions.Builder.from(
-                                captureConfig.getImplementationOptions());
-
-                if (captureConfig.getImplementationOptions().containsOption(
-                        CaptureConfig.OPTION_ROTATION)) {
-                    builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
-                            captureConfig.getImplementationOptions().retrieveOption(
-                                    CaptureConfig.OPTION_ROTATION));
+                for (CaptureConfig captureConfig : captureConfigs) {
+                    if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_STILL_CAPTURE) {
+                        issueStillCaptureRequest(captureConfig);
+                    } else {
+                        issueTriggerRequest(captureConfig);
+                    }
                 }
-
-                if (captureConfig.getImplementationOptions().containsOption(
-                        CaptureConfig.OPTION_JPEG_QUALITY)) {
-                    builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
-                            captureConfig.getImplementationOptions().retrieveOption(
-                                    CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
-                }
-
-                mStillCaptureOptions = builder.build();
-                updateParameters(mSessionOptions, mStillCaptureOptions);
-                mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
-                    @Override
-                    public void onCaptureStarted(
-                            int captureSequenceId, long timestamp) {
-                    }
-
-                    @Override
-                    public void onCaptureProcessStarted(
-                            int captureSequenceId) {
-                    }
-
-                    @Override
-                    public void onCaptureFailed(
-                            int captureSequenceId) {
-                        mExecutor.execute(() -> {
-                            for (CameraCaptureCallback cameraCaptureCallback :
-                                    captureConfig.getCameraCaptureCallbacks()) {
-                                cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
-                                        CameraCaptureFailure.Reason.ERROR));
-                            }
-                            mIsExecutingStillCaptureRequest = false;
-                        });
-                    }
-
-                    @Override
-                    public void onCaptureSequenceCompleted(int captureSequenceId) {
-                        mExecutor.execute(() -> {
-                            for (CameraCaptureCallback cameraCaptureCallback :
-                                    captureConfig.getCameraCaptureCallbacks()) {
-                                cameraCaptureCallback.onCaptureCompleted(
-                                        new CameraCaptureResult.EmptyCameraCaptureResult());
-                            }
-                            mIsExecutingStillCaptureRequest = false;
-                        });
-                    }
-
-                    @Override
-                    public void onCaptureSequenceAborted(int captureSequenceId) {
-                    }
-
-                    @Override
-                    public void onCaptureCompleted(long timestamp, int captureSequenceId,
-                            @NonNull Map<CaptureResult.Key, Object> result) {
-
-                    }
-                });
                 break;
             case ON_CAPTURE_SESSION_ENDED:
             case CLOSED:
@@ -420,6 +383,52 @@
                 break;
         }
     }
+    void issueStillCaptureRequest(@NonNull CaptureConfig captureConfig) {
+        CaptureRequestOptions.Builder builder =
+                CaptureRequestOptions.Builder.from(
+                        captureConfig.getImplementationOptions());
+
+        if (captureConfig.getImplementationOptions().containsOption(
+                CaptureConfig.OPTION_ROTATION)) {
+            builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
+                    captureConfig.getImplementationOptions().retrieveOption(
+                            CaptureConfig.OPTION_ROTATION));
+        }
+
+        if (captureConfig.getImplementationOptions().containsOption(
+                CaptureConfig.OPTION_JPEG_QUALITY)) {
+            builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
+                    captureConfig.getImplementationOptions().retrieveOption(
+                            CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
+        }
+
+        mStillCaptureOptions = builder.build();
+        updateParameters(mSessionOptions, mStillCaptureOptions);
+        mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
+            @Override
+            public void onCaptureFailed(
+                    int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
+                                CameraCaptureFailure.Reason.ERROR));
+                    }
+                });
+            }
+
+            @Override
+            public void onCaptureSequenceCompleted(int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureCompleted(
+                                new CameraCaptureResult.EmptyCameraCaptureResult());
+                    }
+                });
+            }
+        });
+    }
 
     /**
      * {@inheritDoc}
@@ -457,10 +466,9 @@
             setSessionConfig(mSessionConfig);
         }
 
-        if (mPendingCaptureConfig != null) {
-            List<CaptureConfig> pendingCaptureConfigList = Arrays.asList(mPendingCaptureConfig);
-            mPendingCaptureConfig = null;
-            issueCaptureRequests(pendingCaptureConfigList);
+        if (mPendingCaptureConfigs != null) {
+            issueCaptureRequests(mPendingCaptureConfigs);
+            mPendingCaptureConfigs = null;
         }
     }
 
@@ -479,8 +487,7 @@
     @NonNull
     @Override
     public List<CaptureConfig> getCaptureConfigs() {
-        return mPendingCaptureConfig != null ? Arrays.asList(mPendingCaptureConfig)
-                : Collections.emptyList();
+        return mPendingCaptureConfigs != null ? mPendingCaptureConfigs : Collections.emptyList();
     }
 
     /**
@@ -489,12 +496,14 @@
     @Override
     public void cancelIssuedCaptureRequests() {
         Logger.d(TAG, "cancelIssuedCaptureRequests (id=" + mInstanceId + ")");
-        if (mPendingCaptureConfig != null) {
-            for (CameraCaptureCallback cameraCaptureCallback :
-                    mPendingCaptureConfig.getCameraCaptureCallbacks()) {
-                cameraCaptureCallback.onCaptureCancelled();
+        if (mPendingCaptureConfigs != null) {
+            for (CaptureConfig captureConfig : mPendingCaptureConfigs) {
+                for (CameraCaptureCallback cameraCaptureCallback :
+                        captureConfig.getCameraCaptureCallbacks()) {
+                    cameraCaptureCallback.onCaptureCancelled();
+                }
             }
-            mPendingCaptureConfig = null;
+            mPendingCaptureConfigs = null;
         }
     }
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
index 17881be..7af6200 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
@@ -27,7 +27,6 @@
 import androidx.camera.core.impl.PreviewConfig;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.VideoCaptureConfig;
 
 import java.util.Collection;
 
@@ -89,15 +88,10 @@
 
                 }
 
-                if (useCaseConfig instanceof VideoCaptureConfig) {
-                    if (hasImageCapture) {
-                        // If has both image and video capture, return preview video still case.
-                        return CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL;
-                    }
-                    hasVideoCapture = true;
-                    continue;
-
-                }
+                // TODO: Need to handle "hasVideoCapture". The original statement was removed in
+                //  aosp/2299682 because it uses the legacy core.VideoCapture API, which means the
+                //  statement's content will never be run. The new video.VideoCapture API should
+                //  used.
             }
 
             if (hasImageCapture) {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index 665019a..d10baed 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -16,8 +16,6 @@
 
 package androidx.camera.camera2.internal;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -39,26 +37,20 @@
 
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
-import androidx.camera.core.AspectRatio;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.CameraX;
 import androidx.camera.core.CameraXConfig;
-import androidx.camera.core.ImageCapture;
 import androidx.camera.core.InitializationException;
-import androidx.camera.core.Preview;
-import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
 import androidx.camera.core.impl.ImageFormatConstants;
 import androidx.camera.core.impl.SurfaceCombination;
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.SurfaceConfig.ConfigSize;
 import androidx.camera.core.impl.SurfaceConfig.ConfigType;
-import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.CameraXUtil;
-import androidx.camera.testing.Configs;
 import androidx.camera.testing.fakes.FakeCamera;
 import androidx.camera.testing.fakes.FakeCameraFactory;
 import androidx.test.core.app.ApplicationProvider;
@@ -77,10 +69,7 @@
 import org.robolectric.shadows.ShadowCameraCharacteristics;
 import org.robolectric.shadows.ShadowCameraManager;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -360,74 +349,6 @@
         }
     }
 
-    @SuppressWarnings("deprecation")
-    @Test(expected = IllegalArgumentException.class)
-    public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
-        ImageCapture imageCapture = new ImageCapture.Builder()
-                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-                .build();
-        androidx.camera.core.VideoCapture videoCapture =
-                new androidx.camera.core.VideoCapture.Builder()
-                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-                .build();
-        Preview preview = new Preview.Builder()
-                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-                .build();
-
-        List<UseCase> useCases = new ArrayList<>();
-        useCases.add(imageCapture);
-        useCases.add(videoCapture);
-        useCases.add(preview);
-
-        Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-                        mCameraFactory.getCamera(LEGACY_CAMERA_ID).getCameraInfoInternal(),
-                        useCases,
-                        mUseCaseConfigFactory);
-        // A legacy level camera device can't support JPEG (ImageCapture) + PRIV (VideoCapture) +
-        // PRIV (Preview) combination. An IllegalArgumentException will be thrown when trying to
-        // bind these use cases at the same time.
-        mSurfaceManager.getSuggestedResolutions(LEGACY_CAMERA_ID, Collections.emptyList(),
-                new ArrayList<>(useCaseToConfigMap.values()));
-    }
-
-    @SuppressWarnings("deprecation")
-    @Test
-    public void getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
-        ImageCapture imageCapture = new ImageCapture.Builder()
-                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-                .build();
-        androidx.camera.core.VideoCapture videoCapture =
-                new androidx.camera.core.VideoCapture.Builder()
-                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-                .build();
-        Preview preview = new Preview.Builder()
-                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
-                .build();
-
-        List<UseCase> useCases = new ArrayList<>();
-        useCases.add(imageCapture);
-        useCases.add(videoCapture);
-        useCases.add(preview);
-
-        Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-                        mCameraFactory.getCamera(LIMITED_CAMERA_ID).getCameraInfoInternal(),
-                        useCases,
-                        mUseCaseConfigFactory);
-        Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
-                mSurfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, Collections.emptyList(),
-                        new ArrayList<>(useCaseToConfigMap.values()));
-
-        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
-        assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(imageCapture),
-                mRecordSize);
-        assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(videoCapture),
-                mMaximumVideoSize);
-        assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(preview),
-                mPreviewSize);
-    }
-
     @Test
     public void transformSurfaceConfigWithYUVAnalysisSize() {
         SurfaceConfig surfaceConfig = mSurfaceManager.transformSurfaceConfig(
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 1d8366d5..c4a3072 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -2786,11 +2786,11 @@
     }
 
     /**
-     * Creates [Preview], [ImageCapture], [ImageAnalysis], [androidx.camera.core.VideoCapture] or
-     * FakeUseCase according to the specified settings.
+     * Creates [Preview], [ImageCapture], [ImageAnalysis] or FakeUseCase according to the specified
+     * settings.
      *
-     * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis],
-     * [androidx.camera.core.VideoCapture] and FakeUseCase should be created.
+     * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis] and FakeUseCase should
+     * be created.
      * @param targetRotation the target rotation setting. Default is UNKNOWN_ROTATION and no target
      * rotation will be set to the created use case.
      * @param targetAspectRatio the target aspect ratio setting. Default is UNKNOWN_ASPECT_RATIO
@@ -2802,7 +2802,6 @@
      * @param defaultResolution the default resolution setting. Default is null.
      * @param supportedResolutions the customized supported resolutions. Default is null.
      */
-    @Suppress("DEPRECATION")
     private fun createUseCaseByLegacyApi(
         useCaseType: Int,
         targetRotation: Int = UNKNOWN_ROTATION,
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 4eb4cb2..401ff46 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -23,6 +23,15 @@
   public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
   }
 
+  @RequiresApi(21) public abstract class CameraEffect {
+    ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+    method public int getTargets();
+    field public static final int IMAGE_CAPTURE = 4; // 0x4
+    field public static final int PREVIEW = 1; // 0x1
+  }
+
   @RequiresApi(21) public interface CameraFilter {
     method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
   }
@@ -349,6 +358,10 @@
     method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
   }
 
+  public class ProcessingException extends java.lang.Exception {
+    ctor public ProcessingException();
+  }
+
   @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
     method public abstract android.graphics.Rect getCropRect();
     method public abstract android.util.Size getResolution();
@@ -360,6 +373,26 @@
     ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
   }
 
+  public interface SurfaceOutput {
+    method public void close();
+    method public android.util.Size getSize();
+    method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+    method public int getTargets();
+    method public void updateTransformMatrix(float[], float[]);
+  }
+
+  @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+    ctor public SurfaceOutput.Event();
+    method public abstract int getEventCode();
+    method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+    field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+  }
+
+  public interface SurfaceProcessor {
+    method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+    method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+  }
+
   @RequiresApi(21) public final class SurfaceRequest {
     method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
     method public void clearTransformationInfoListener();
@@ -397,12 +430,14 @@
   }
 
   @RequiresApi(21) public final class UseCaseGroup {
+    method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
     method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
     method public androidx.camera.core.ViewPort? getViewPort();
   }
 
   public static final class UseCaseGroup.Builder {
     ctor public UseCaseGroup.Builder();
+    method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
     method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
     method public androidx.camera.core.UseCaseGroup build();
     method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index 139d818..41025e7 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -23,6 +23,15 @@
   public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
   }
 
+  @RequiresApi(21) public abstract class CameraEffect {
+    ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+    method public int getTargets();
+    field public static final int IMAGE_CAPTURE = 4; // 0x4
+    field public static final int PREVIEW = 1; // 0x1
+  }
+
   @RequiresApi(21) public interface CameraFilter {
     method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
   }
@@ -362,6 +371,10 @@
     method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
   }
 
+  public class ProcessingException extends java.lang.Exception {
+    ctor public ProcessingException();
+  }
+
   @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
     method public abstract android.graphics.Rect getCropRect();
     method public abstract android.util.Size getResolution();
@@ -373,6 +386,26 @@
     ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
   }
 
+  public interface SurfaceOutput {
+    method public void close();
+    method public android.util.Size getSize();
+    method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+    method public int getTargets();
+    method public void updateTransformMatrix(float[], float[]);
+  }
+
+  @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+    ctor public SurfaceOutput.Event();
+    method public abstract int getEventCode();
+    method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+    field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+  }
+
+  public interface SurfaceProcessor {
+    method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+    method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+  }
+
   @RequiresApi(21) public final class SurfaceRequest {
     method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
     method public void clearTransformationInfoListener();
@@ -410,12 +443,14 @@
   }
 
   @RequiresApi(21) public final class UseCaseGroup {
+    method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
     method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
     method public androidx.camera.core.ViewPort? getViewPort();
   }
 
   public static final class UseCaseGroup.Builder {
     ctor public UseCaseGroup.Builder();
+    method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
     method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
     method public androidx.camera.core.UseCaseGroup build();
     method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 4eb4cb2..401ff46 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -23,6 +23,15 @@
   public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
   }
 
+  @RequiresApi(21) public abstract class CameraEffect {
+    ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+    method public int getTargets();
+    field public static final int IMAGE_CAPTURE = 4; // 0x4
+    field public static final int PREVIEW = 1; // 0x1
+  }
+
   @RequiresApi(21) public interface CameraFilter {
     method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
   }
@@ -349,6 +358,10 @@
     method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
   }
 
+  public class ProcessingException extends java.lang.Exception {
+    ctor public ProcessingException();
+  }
+
   @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
     method public abstract android.graphics.Rect getCropRect();
     method public abstract android.util.Size getResolution();
@@ -360,6 +373,26 @@
     ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
   }
 
+  public interface SurfaceOutput {
+    method public void close();
+    method public android.util.Size getSize();
+    method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+    method public int getTargets();
+    method public void updateTransformMatrix(float[], float[]);
+  }
+
+  @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+    ctor public SurfaceOutput.Event();
+    method public abstract int getEventCode();
+    method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+    field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+  }
+
+  public interface SurfaceProcessor {
+    method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+    method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+  }
+
   @RequiresApi(21) public final class SurfaceRequest {
     method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
     method public void clearTransformationInfoListener();
@@ -397,12 +430,14 @@
   }
 
   @RequiresApi(21) public final class UseCaseGroup {
+    method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
     method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
     method public androidx.camera.core.ViewPort? getViewPort();
   }
 
   public static final class UseCaseGroup.Builder {
     ctor public UseCaseGroup.Builder();
+    method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
     method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
     method public androidx.camera.core.UseCaseGroup build();
     method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
index 1ce7421..d1bcd62 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.ImageProcessingUtil.convertJpegBytesToImage;
 import static androidx.camera.core.ImageProcessingUtil.rotateYUV;
+import static androidx.camera.core.ImageProcessingUtil.writeJpegBytesToSurface;
 import static androidx.camera.testing.ImageProxyUtil.createYUV420ImagePlanes;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -33,6 +34,7 @@
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.camera.core.impl.utils.Exif;
 import androidx.camera.testing.fakes.FakeImageInfo;
 import androidx.camera.testing.fakes.FakeImageProxy;
 import androidx.core.math.MathUtils;
@@ -47,6 +49,7 @@
 import org.junit.runner.RunWith;
 
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.IntBuffer;
 
@@ -151,6 +154,61 @@
         assertBitmapColor(bitmap, Color.RED, JPEG_ENCODE_ERROR_TOLERANCE);
     }
 
+    @Test
+    public void writeJpegToSurface_returnsTheSameImage() {
+        // Arrange: create a JPEG image with solid color.
+        byte[] inputBytes = createJpegBytesWithSolidColor(Color.RED);
+
+        // Act: acquire image and get the bytes.
+        writeJpegBytesToSurface(mJpegImageReaderProxy.getSurface(), inputBytes);
+
+        final ImageProxy imageProxy = mJpegImageReaderProxy.acquireLatestImage();
+        assertThat(imageProxy).isNotNull();
+        ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
+        byteBuffer.rewind();
+        byte[] outputBytes = new byte[byteBuffer.capacity()];
+        byteBuffer.get(outputBytes);
+
+        // Assert: the color and the dimension of the restored image.
+        Bitmap bitmap = BitmapFactory.decodeByteArray(outputBytes, 0, outputBytes.length);
+        assertThat(bitmap.getWidth()).isEqualTo(WIDTH);
+        assertThat(bitmap.getHeight()).isEqualTo(HEIGHT);
+        assertBitmapColor(bitmap, Color.RED, JPEG_ENCODE_ERROR_TOLERANCE);
+    }
+
+    @Test
+    public void convertYuvToJpegBytesIntoSurface_sizeAndRotationAreCorrect() throws IOException {
+        final int expectedRotation = 270;
+        // Arrange: create a YUV_420_888 image
+        mYUVImageProxy.setPlanes(createYUV420ImagePlanes(
+                WIDTH,
+                HEIGHT,
+                PIXEL_STRIDE_Y,
+                PIXEL_STRIDE_UV,
+                /*flipUV=*/false,
+                /*incrementValue=*/false));
+
+        // Act: convert it into JPEG and write into the surface.
+        ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(mYUVImageProxy,
+                100, expectedRotation, mJpegImageReaderProxy.getSurface());
+
+        final ImageProxy imageProxy = mJpegImageReaderProxy.acquireLatestImage();
+        assertThat(imageProxy).isNotNull();
+        ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
+        byteBuffer.rewind();
+        byte[] outputBytes = new byte[byteBuffer.capacity()];
+        byteBuffer.get(outputBytes);
+
+        // Assert: the format is JPEG and can decode,  the size is correct, the rotation in Exif
+        // is correct.
+        assertThat(imageProxy.getFormat()).isEqualTo(ImageFormat.JPEG);
+        Bitmap bitmap = BitmapFactory.decodeByteArray(outputBytes, 0, outputBytes.length);
+        assertThat(bitmap.getWidth()).isEqualTo(WIDTH);
+        assertThat(bitmap.getHeight()).isEqualTo(HEIGHT);
+        Exif exif = Exif.createFromImageProxy(imageProxy);
+        assertThat(exif.getRotation()).isEqualTo(expectedRotation);
+    }
+
     /**
      * Returns JPEG bytes of a image with the given color.
      */
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
deleted file mode 100644
index 878f0c5..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.graphics.ImageFormat
-import android.graphics.Matrix
-import android.media.ImageReader
-import android.media.ImageWriter
-import android.os.Handler
-import androidx.camera.core.impl.ImageReaderProxy
-import androidx.camera.core.impl.MutableTagBundle
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 23) // This test uses ImageWriter which is supported from api 23.
-class ModifiableImageReaderProxyTest {
-    private lateinit var imageReader: ImageReader
-    private lateinit var imageReaderProxy: ModifiableImageReaderProxy
-    private var imageWriter: ImageWriter? = null
-
-    @Before
-    fun setUp() {
-        imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2)
-        imageReaderProxy = ModifiableImageReaderProxy(imageReader)
-    }
-
-    @After
-    fun tearDown() {
-        imageReaderProxy.close()
-        imageWriter?.close()
-    }
-
-    @Test
-    fun canModifyImageTagBundle_acquireNext() {
-        generateImage(imageReader)
-
-        val tagBundle = MutableTagBundle.create()
-        imageReaderProxy.setImageTagBundle(tagBundle)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
-    }
-
-    @Test
-    fun canModifyImageTagBundle_acquireLatest() {
-        generateImage(imageReader)
-
-        val tagBundle = MutableTagBundle.create()
-        imageReaderProxy.setImageTagBundle(tagBundle)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageTimestamp_acquireNext() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageTimeStamp(1000)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageTimestamp_acquireLatest() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageTimeStamp(1000)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageRotationDegrees_acquireNext() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageRotationDegrees(90)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageRotationDegress_acquireLatest() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageRotationDegrees(90)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageMatrix_acquireNext() {
-        generateImage(imageReader)
-
-        val matrix = Matrix()
-        imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageMatrix_acquireLatest() {
-        generateImage(imageReader)
-
-        val matrix = Matrix()
-        imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
-        imageProxy.close()
-    }
-
-    private fun generateImage(imageReader: ImageReader) {
-        imageWriter = ImageWriter.newInstance(imageReader.surface, 2)
-        val image = imageWriter!!.dequeueInputImage()
-        imageWriter!!.queueInputImage(image)
-    }
-
-    @Test
-    fun parametersMatchesInnerImageReader() {
-        assertThat(imageReaderProxy.width).isEqualTo(640)
-        assertThat(imageReaderProxy.height).isEqualTo(480)
-        assertThat(imageReaderProxy.imageFormat).isEqualTo(ImageFormat.YUV_420_888)
-        assertThat(imageReaderProxy.maxImages).isEqualTo(2)
-        assertThat(imageReaderProxy.surface).isEqualTo(imageReader.surface)
-    }
-
-    @Test
-    fun setOnImageAvailableListener_innerReaderIsInvoked() {
-        val imageReader = Mockito.mock(
-            ImageReader::class.java
-        )
-        val imageReaderProxy = ModifiableImageReaderProxy(imageReader)
-
-        val listener = Mockito.mock(
-            ImageReaderProxy.OnImageAvailableListener::class.java
-        )
-        imageReaderProxy.setOnImageAvailableListener(
-            listener,
-            CameraXExecutors.directExecutor()
-        )
-
-        val transformedListenerCaptor = ArgumentCaptor.forClass(
-            ImageReader.OnImageAvailableListener::class.java
-        )
-        val handlerCaptor = ArgumentCaptor.forClass(
-            Handler::class.java
-        )
-        Mockito.verify(imageReader, Mockito.times(1))
-            .setOnImageAvailableListener(
-                transformedListenerCaptor.capture(), handlerCaptor.capture()
-            )
-
-        transformedListenerCaptor.value.onImageAvailable(imageReader)
-        Mockito.verify(listener, Mockito.times(1)).onImageAvailable(imageReaderProxy)
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt
deleted file mode 100644
index 1b55484..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.camera.core
-
-import android.graphics.ImageFormat
-import android.media.ImageWriter
-import android.os.Handler
-import android.os.HandlerThread
-import android.util.Pair
-import android.util.Size
-import android.view.Surface
-import androidx.annotation.RequiresApi
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.CaptureStage
-import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.ImageProxyBundle
-import androidx.camera.core.impl.ImmediateSurface
-import androidx.camera.core.impl.TagBundle
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.impl.utils.futures.Futures
-import androidx.camera.testing.fakes.FakeCameraCaptureResult
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.util.concurrent.ListenableFuture
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.Executors
-import java.util.concurrent.Semaphore
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 23) // This test uses ImageWriter which is supported from api 23.
-class ProcessingSurfaceTest {
-    private var backgroundThread: HandlerThread? = null
-    private var backgroundHandler: Handler? = null
-    private val captureStage: CaptureStage = CaptureStage.DefaultCaptureStage()
-    private val processingSurfaces: MutableList<ProcessingSurface> = ArrayList()
-    private val captureProcessor: FakeCaptureProcessor = FakeCaptureProcessor(captureStage.id)
-
-    @Before
-    fun setup() {
-        backgroundThread = HandlerThread("CallbackThread")
-        backgroundThread!!.start()
-        backgroundHandler = Handler(backgroundThread!!.looper)
-    }
-
-    @After
-    fun tearDown() {
-        for (processingSurface in processingSurfaces) {
-            processingSurface.close()
-        }
-        processingSurfaces.clear()
-        backgroundThread!!.looper.quitSafely()
-    }
-
-    @Test
-    fun validInputSurface() {
-        val processingSurface = createProcessingSurface(
-            newImmediateSurfaceDeferrableSurface()
-        )
-        val surface = processingSurface.surface.get()
-        assertThat(surface).isNotNull()
-    }
-
-    @Test
-    fun writeToInputSurface_userOutputSurfaceReceivesFrame() {
-        // Arrange.
-        val frameReceivedSemaphore = Semaphore(0)
-        val imageReaderProxy = ImageReaderProxys.createIsolatedReader(
-            RESOLUTION.width, RESOLUTION.height,
-            ImageFormat.YUV_420_888, 2
-        )
-        imageReaderProxy.setOnImageAvailableListener(
-            { frameReceivedSemaphore.release() },
-            CameraXExecutors.directExecutor()
-        )
-
-        // Create ProcessingSurface with user Surface.
-        val processingSurface = createProcessingSurface(
-            object : DeferrableSurface() {
-                override fun provideSurface(): ListenableFuture<Surface> {
-                    return Futures.immediateFuture(imageReaderProxy.surface)
-                }
-            })
-
-        // Act: Send one frame to processingSurface.
-        triggerImage(processingSurface, 1)
-
-        // Assert: verify that the frame has been received or time-out after 3 second.
-        assertThat(frameReceivedSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
-    }
-
-    // Exception should be thrown here
-    @Test
-    fun getSurfaceThrowsExceptionWhenClosed() {
-        val processingSurface = createProcessingSurface(newImmediateSurfaceDeferrableSurface())
-        processingSurface.close()
-
-        // Exception should be thrown here
-        val futureSurface = processingSurface.surface
-        var cause: Throwable? = null
-        try {
-            futureSurface.get()
-        } catch (e: ExecutionException) {
-            cause = e.cause
-        } catch (e: InterruptedException) {
-            cause = e.cause
-        }
-        assertThat(cause).isInstanceOf(DeferrableSurface.SurfaceClosedException::class.java)
-    }
-
-    // Exception should be thrown here
-    @Test(expected = IllegalStateException::class)
-    fun getCameraCaptureCallbackThrowsExceptionWhenReleased() {
-        val processingSurface = createProcessingSurface(newImmediateSurfaceDeferrableSurface())
-        processingSurface.close()
-
-        // Exception should be thrown here
-        processingSurface.cameraCaptureCallback
-    }
-
-    @Test
-    fun completeTerminationFutureAfterProcessIsFinished() {
-        // Arrange.
-        val processingSurface = createProcessingSurface(newImmediateSurfaceDeferrableSurface())
-
-        // Sets up the processor blocker to block the CaptureProcessor#process() function execution.
-        captureProcessor.setupProcessorBlocker()
-
-        // Monitors whether the ProcessingSurface termination future has been completed.
-        val terminationCountDownLatch = CountDownLatch(1)
-        processingSurface.terminationFuture.addListener({
-            terminationCountDownLatch.countDown()
-        }, Executors.newSingleThreadExecutor())
-
-        // Act: Sends one frame to processingSurface.
-        triggerImage(processingSurface, 1)
-
-        // Waits for that the CaptureProcessor#process() function is called. Otherwise, the
-        // following ProcessingSurface#close() function call may directly close the
-        // ProcessingSurface.
-        assertThat(captureProcessor.awaitProcessingState(1000, TimeUnit.MILLISECONDS)).isTrue()
-
-        // Act: triggers the ProcessingSurface close flow.
-        processingSurface.close()
-
-        // Assert: verify that the termination future won't be completed before
-        // CaptureProcessor#process() execution is finished.
-        assertThat(terminationCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isFalse()
-
-        // Act: releases the processor blocker to finish the CaptureProcessor#process() execution.
-        captureProcessor.releaseProcessorBlocker()
-
-        // Assert: verify that the termination future is completed after CaptureProcessor#process()
-        // execution is finished.
-        assertThat(terminationCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-    }
-
-    @RequiresApi(23)
-    @Throws(ExecutionException::class, InterruptedException::class)
-    private fun triggerImage(processingSurface: ProcessingSurface, timestamp: Long) {
-        val surface = processingSurface.surface.get()
-        val imageWriter = ImageWriter.newInstance(surface, 2)
-        val image = imageWriter.dequeueInputImage()
-        image.timestamp = timestamp
-        imageWriter.queueInputImage(image)
-        val callback = processingSurface.cameraCaptureCallback
-        val cameraCaptureResult = FakeCameraCaptureResult()
-        cameraCaptureResult.timestamp = timestamp
-        cameraCaptureResult.setTag(
-            TagBundle.create(
-                Pair(Integer.toString(captureStage.hashCode()), captureStage.id)
-            )
-        )
-        callback!!.onCaptureCompleted(cameraCaptureResult)
-    }
-
-    private fun createProcessingSurface(
-        deferrableSurface: DeferrableSurface
-    ): ProcessingSurface {
-        val processingSurface = ProcessingSurface(
-            RESOLUTION.width,
-            RESOLUTION.height,
-            FORMAT,
-            backgroundHandler,
-            captureStage,
-            captureProcessor,
-            deferrableSurface, Integer.toString(captureStage.hashCode())
-        )
-        processingSurfaces.add(processingSurface)
-        return processingSurface
-    }
-
-    private fun newImmediateSurfaceDeferrableSurface(): DeferrableSurface {
-        val imageReaderProxy = ImageReaderProxys.createIsolatedReader(
-            RESOLUTION.width, RESOLUTION.height,
-            ImageFormat.YUV_420_888, 2
-        )
-
-        val deferrableSurface = ImmediateSurface(imageReaderProxy.surface!!)
-
-        deferrableSurface.terminationFuture.addListener(
-            { imageReaderProxy.close() },
-            CameraXExecutors.directExecutor()
-        )
-
-        return deferrableSurface
-    }
-
-    companion object {
-        private val RESOLUTION: Size by lazy { Size(640, 480) }
-        private const val FORMAT = ImageFormat.YUV_420_888
-    }
-
-    /**
-     * Capture processor that can write out an empty image to exercise the pipeline.
-     *
-     * <p>The fake capture processor can be controlled to be blocked in the processing state and
-     * then release the blocker to complete it.
-     */
-    @RequiresApi(23)
-    private class FakeCaptureProcessor(private val captureStageId: Int) : CaptureProcessor {
-        private val processingCountDownLatch = CountDownLatch(1)
-        private var processorBlockerCountDownLatch: CountDownLatch? = null
-
-        var imageWriter: ImageWriter? = null
-        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
-            imageWriter = ImageWriter.newInstance(surface, 2)
-        }
-
-        override fun process(bundle: ImageProxyBundle) {
-            processingCountDownLatch.countDown()
-            try {
-                val imageProxyListenableFuture = bundle.getImageProxy(captureStageId)
-                val imageProxy = imageProxyListenableFuture[100, TimeUnit.MILLISECONDS]
-                val image = imageWriter!!.dequeueInputImage()
-                image.timestamp = imageProxy.imageInfo.timestamp
-                imageWriter!!.queueInputImage(image)
-
-                processorBlockerCountDownLatch?.await()
-            } catch (_: ExecutionException) {
-            } catch (_: TimeoutException) {
-            } catch (_: InterruptedException) {
-            }
-        }
-
-        override fun onResolutionUpdate(size: Size) {}
-
-        fun awaitProcessingState(timeout: Long, timeUnit: TimeUnit): Boolean {
-            return processingCountDownLatch.await(timeout, timeUnit)
-        }
-
-        fun setupProcessorBlocker() {
-            if (processorBlockerCountDownLatch == null) {
-                processorBlockerCountDownLatch = CountDownLatch(1)
-            }
-        }
-
-        fun releaseProcessorBlocker() {
-            processorBlockerCountDownLatch?.countDown()
-        }
-    }
-}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
index 9a165a8..38961da6 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
@@ -369,17 +369,11 @@
         return deferrableSurface;
     }
 
-    @SuppressWarnings("deprecation")
     @Test
     public void combineTwoSessionsSurfaces() {
         DeferrableSurface previewSurface = createSurface(Preview.class);
-        DeferrableSurface videoSurface = createSurface(androidx.camera.core.VideoCapture.class);
         DeferrableSurface imageCaptureSurface = createSurface(ImageCapture.class);
 
-        SessionConfig.Builder builder0 = new SessionConfig.Builder();
-        builder0.addSurface(videoSurface);
-        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
-
         SessionConfig.Builder builder1 = new SessionConfig.Builder();
         builder1.addSurface(previewSurface);
         builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
@@ -389,7 +383,6 @@
         builder2.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
 
         SessionConfig.ValidatingBuilder validatingBuilder = new SessionConfig.ValidatingBuilder();
-        validatingBuilder.add(builder0.build());
         validatingBuilder.add(builder1.build());
         validatingBuilder.add(builder2.build());
 
@@ -397,8 +390,7 @@
 
         List<DeferrableSurface> surfaces = sessionConfig.getSurfaces();
         // Ensures the surfaces are all added and sorted correctly.
-        assertThat(surfaces)
-                .containsExactly(previewSurface, imageCaptureSurface, videoSurface).inOrder();
+        assertThat(surfaces).containsExactly(previewSurface, imageCaptureSurface).inOrder();
     }
 
     @Test
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
index 8fad00e..bf3864f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
@@ -17,8 +17,6 @@
 
 import static androidx.core.util.Preconditions.checkArgument;
 
-import android.os.Build;
-
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -32,21 +30,49 @@
 /**
  * An effect for one or multiple camera outputs.
  *
- * <p>A {@link CameraEffect} class contains 2 types of information, the processor and the
- * configuration.
+ * <p>This API allows the implementer to inject code into CameraX pipeline and apply visual
+ * effects, such as a portrait effect. The {@link CameraEffect} class contains two types of
+ * information, the processor and the configuration.
  * <ul>
- * <li> The processor is an implementation of either {@link SurfaceProcessor} or
+ * <li>The processor is an implementation of either {@link SurfaceProcessor} or
  * {@link ImageProcessor}. It consumes original camera frames from CameraX, applies the effect,
  * and returns the processed frames back to CameraX.
- * <li> The configuration provides information on how the processor should be injected into the
- * pipeline. For example, the target {@link UseCase}s where the effect should be applied.
+ * <li>The configuration provides information on how the processor should be injected into the
+ * pipeline. For example, the target {@link UseCase}s where the effect should be applied. It may
+ * also contain information about camera configuration. For example, the exposure level.
  * </ul>
  *
- * @hide
+ * <p>If CameraX fails to send frames to the {@link CameraEffect}, the error will be
+ * delivered to the app via error callbacks such as
+ * {@link ImageCapture.OnImageCapturedCallback#onError}. If {@link CameraEffect} fails to
+ * process and return the frames, for example, unable to allocate the resources for image
+ * processing, it must throw {@link Exception} in the processor implementation. The
+ * {@link Exception} will be caught and forwarded to the app via error callbacks. Please see the
+ * Javadoc of the processor interfaces for details.
+ *
+ * <p>Extend this class to create specific effects. The {@link Executor} provided in the
+ * constructors will be used by CameraX to call the processors.
+ *
+ * <p>Code sample for a portrait effect that targets the {@link Preview} {@link UseCase}:
+ *
+ * <pre><code>
+ * class PortraitPreviewEffect extends CameraEffect {
+ *     PortraitPreviewEffect() {
+ *         super(PREVIEW, getExecutor(), getSurfaceProcessor());
+ *     }
+ *
+ *     private static Executor getExecutor() {
+ *         // Returns an executor for calling the SurfaceProcessor
+ *     }
+ *
+ *     private static SurfaceProcessor getSurfaceProcessor() {
+ *         // Return a SurfaceProcessor implementation that applies a portrait effect.
+ *     }
+ * }
+ * </code></pre>
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class CameraEffect {
+@RequiresApi(21)
+public abstract class CameraEffect {
 
     /**
      * Bitmask options for the effect targets.
@@ -54,8 +80,8 @@
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    @IntDef(flag = true, value = {PREVIEW, VIDEO_CAPTURE, IMAGE_CAPTURE})
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @IntDef(flag = true, value = {PREVIEW, IMAGE_CAPTURE})
     public @interface Targets {
     }
 
@@ -66,7 +92,10 @@
 
     /**
      * Bitmask option to indicate that CameraX should apply this effect to {@code VideoCapture}.
+     *
+     * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static final int VIDEO_CAPTURE = 1 << 1;
 
     /**
@@ -93,7 +122,9 @@
      * @param imageProcessor a {@link ImageProcessor} implementation. Once the effect is active,
      *                       CameraX will send frames to the {@link ImageProcessor} on the
      *                       {@param executor}, and deliver the processed frames to the app.
+     * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     protected CameraEffect(
             @Targets int targets,
             @NonNull Executor executor,
@@ -141,7 +172,7 @@
     /**
      * Gets the {@link Executor} associated with this effect.
      *
-     * <p>This method returns the value set in {@link CameraEffect}'s constructor.
+     * <p>This method returns the value set in the constructor.
      */
     @NonNull
     public Executor getExecutor() {
@@ -158,8 +189,11 @@
 
     /**
      * Gets the {@link ImageProcessor} associated with this effect.
+     *
+     * @hide
      */
     @Nullable
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public ImageProcessor getImageProcessor() {
         return mImageProcessor;
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CaptureProcessorPipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/CaptureProcessorPipeline.java
deleted file mode 100644
index 2a832e6..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/CaptureProcessorPipeline.java
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.graphics.ImageFormat;
-import android.media.ImageReader;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.camera.core.impl.ImageReaderProxy;
-import androidx.camera.core.impl.TagBundle;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-
-/**
- * A CaptureProcessor which can link two CaptureProcessors.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class CaptureProcessorPipeline implements CaptureProcessor {
-    private static final String TAG = "CaptureProcessorPipeline";
-    @NonNull
-    private final CaptureProcessor mPreCaptureProcessor;
-    @NonNull
-    private final CaptureProcessor mPostCaptureProcessor;
-    @NonNull
-    private final ListenableFuture<List<Void>> mUnderlyingCaptureProcessorsCloseFuture;
-    @NonNull
-    final Executor mExecutor;
-    private final int mMaxImages;
-    private ImageReaderProxy mIntermediateImageReader = null;
-    private ImageInfo mSourceImageInfo = null;
-
-    private final Object mLock = new Object();
-
-    @GuardedBy("mLock")
-    private boolean mClosed = false;
-
-    @GuardedBy("mLock")
-    private boolean mProcessing = false;
-
-    @GuardedBy("mLock")
-    CallbackToFutureAdapter.Completer<Void> mCloseCompleter;
-    @GuardedBy("mLock")
-    private ListenableFuture<Void> mCloseFuture;
-
-    /**
-     * Creates a {@link CaptureProcessorPipeline} to link two CaptureProcessors to process the
-     * captured images.
-     *
-     * @param preCaptureProcessor  The pre-processing {@link CaptureProcessor} which must output
-     *                             YUV_420_888 {@link ImageProxy} for the post-processing
-     *                             {@link CaptureProcessor} to process.
-     * @param maxImages            the maximum image buffer count used to create the intermediate
-     *                             {@link ImageReaderProxy} to receive the processed
-     *                             {@link ImageProxy} from the
-     *                             pre-processing {@link CaptureProcessor}.
-     * @param postCaptureProcessor The post-processing {@link CaptureProcessor} which can process
-     *                             an {@link ImageProxy} of YUV_420_888 format . It must be able
-     *                             to process the image without referencing to the
-     *                             {@link TagBundle} and capture id.
-     * @param executor             the {@link Executor} used by the post-processing
-     * {@link CaptureProcessor}
-     *                             to process the {@link ImageProxy}.
-     */
-    CaptureProcessorPipeline(@NonNull CaptureProcessor preCaptureProcessor, int maxImages,
-            @NonNull CaptureProcessor postCaptureProcessor, @NonNull Executor executor) {
-        mPreCaptureProcessor = preCaptureProcessor;
-        mPostCaptureProcessor = postCaptureProcessor;
-
-        List<ListenableFuture<Void>> closeFutureList = new ArrayList<>();
-        closeFutureList.add(mPreCaptureProcessor.getCloseFuture());
-        closeFutureList.add(mPostCaptureProcessor.getCloseFuture());
-        mUnderlyingCaptureProcessorsCloseFuture = Futures.allAsList(closeFutureList);
-
-        mExecutor = executor;
-        mMaxImages = maxImages;
-    }
-
-    @Override
-    public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
-        // Updates the output surface to the post-processing CaptureProcessor
-        mPostCaptureProcessor.onOutputSurface(surface, imageFormat);
-    }
-
-    @Override
-    public void process(@NonNull ImageProxyBundle bundle) {
-        synchronized (mLock) {
-            if (mClosed) {
-                return;
-            }
-
-            mProcessing = true;
-        }
-
-        List<Integer> ids = bundle.getCaptureIds();
-        ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(ids.get(0));
-        Preconditions.checkArgument(imageProxyListenableFuture.isDone());
-
-        ImageProxy imageProxy;
-        try {
-            imageProxy = imageProxyListenableFuture.get();
-            ImageInfo imageInfo = imageProxy.getImageInfo();
-            // Stores the imageInfo of source image that will be used when when the processed
-            // ImageProxy is received from the pre-processing CaptureProcessor.
-            mSourceImageInfo = imageInfo;
-        } catch (ExecutionException | InterruptedException e) {
-            throw new IllegalArgumentException("Can not successfully extract ImageProxy from the "
-                    + "ImageProxyBundle.");
-        }
-
-        // Calls the pre-processing CaptureProcessor to process the ImageProxyBundle
-        mPreCaptureProcessor.process(bundle);
-    }
-
-    @Override
-    public void onResolutionUpdate(@NonNull Size size) {
-        // Creates an intermediate ImageReader to receive the processed image from the
-        // pre-processing CaptureProcessor.
-        mIntermediateImageReader = new AndroidImageReaderProxy(
-                ImageReader.newInstance(size.getWidth(), size.getHeight(),
-                        ImageFormat.YUV_420_888, mMaxImages));
-        mPreCaptureProcessor.onOutputSurface(mIntermediateImageReader.getSurface(),
-                ImageFormat.YUV_420_888);
-        mPreCaptureProcessor.onResolutionUpdate(size);
-
-        // Updates the resolution information to the post-processing CaptureProcessor.
-        mPostCaptureProcessor.onResolutionUpdate(size);
-
-        // Register the ImageAvailableListener to receive the processed image from the
-        // pre-processing CaptureProcessor.
-        mIntermediateImageReader.setOnImageAvailableListener(
-                imageReader -> {
-                    ImageProxy image = imageReader.acquireNextImage();
-                    try {
-                        mExecutor.execute(() -> postProcess(image));
-                    } catch (RejectedExecutionException e) {
-                        Logger.e(TAG, "The executor for post-processing might have been "
-                                + "shutting down or terminated!");
-                        image.close();
-                    }
-                },
-                CameraXExecutors.directExecutor());
-    }
-
-    void postProcess(ImageProxy imageProxy) {
-        boolean closed;
-
-        synchronized (mLock) {
-            closed = mClosed;
-        }
-
-        if (!closed) {
-            Size resolution = new Size(imageProxy.getWidth(), imageProxy.getHeight());
-
-            // Retrieves information from ImageInfo of source image to create a
-            // SettableImageProxyBundle and calls the post-processing CaptureProcessor to process
-            // it.
-            Preconditions.checkNotNull(mSourceImageInfo);
-            String tagBundleKey = mSourceImageInfo.getTagBundle().listKeys().iterator().next();
-            int captureId = (Integer) mSourceImageInfo.getTagBundle().getTag(tagBundleKey);
-            SettableImageProxy settableImageProxy =
-                    new SettableImageProxy(imageProxy, resolution, mSourceImageInfo);
-            mSourceImageInfo = null;
-
-            SettableImageProxyBundle settableImageProxyBundle = new SettableImageProxyBundle(
-                    Collections.singletonList(captureId), tagBundleKey);
-            settableImageProxyBundle.addImageProxy(settableImageProxy);
-
-            try {
-                mPostCaptureProcessor.process(settableImageProxyBundle);
-            } catch (Exception e) {
-                Logger.e(TAG, "Post processing image failed! " + e.getMessage());
-            }
-        }
-
-        synchronized (mLock) {
-            mProcessing = false;
-        }
-
-        closeAndCompleteFutureIfNecessary();
-    }
-
-    /**
-     * Closes the objects generated when creating the {@link CaptureProcessorPipeline}.
-     */
-    @Override
-    public void close() {
-        synchronized (mLock) {
-            if (mClosed) {
-                return;
-            }
-
-            mClosed = true;
-        }
-
-        mPreCaptureProcessor.close();
-        mPostCaptureProcessor.close();
-        closeAndCompleteFutureIfNecessary();
-    }
-
-    private void closeAndCompleteFutureIfNecessary() {
-        boolean closed;
-        boolean processing;
-        CallbackToFutureAdapter.Completer<Void> closeCompleter;
-
-        synchronized (mLock) {
-            closed = mClosed;
-            processing = mProcessing;
-            closeCompleter = mCloseCompleter;
-
-            if (closed && !processing) {
-                mIntermediateImageReader.close();
-            }
-        }
-
-        if (closed && !processing && closeCompleter != null) {
-            // Complete the capture process pipeline's close future after the underlying pre and
-            // post capture processors are closed.
-            mUnderlyingCaptureProcessorsCloseFuture.addListener(() -> closeCompleter.set(null),
-                    CameraXExecutors.directExecutor());
-        }
-    }
-
-    /**
-     * Returns a future that will complete when the CaptureProcessorPipeline is actually closed.
-     *
-     * @return A future that signals when the CaptureProcessorPipeline is actually closed
-     * (after all processing). Cancelling this future has no effect.
-     */
-    @NonNull
-    @Override
-    public ListenableFuture<Void> getCloseFuture() {
-        ListenableFuture<Void> closeFuture;
-        synchronized (mLock) {
-            if (mClosed && !mProcessing) {
-                // Everything should be closed but still need to wait for underlying capture
-                // processors being closed.
-                closeFuture = Futures.transform(mUnderlyingCaptureProcessorsCloseFuture,
-                        nullVoid -> null, CameraXExecutors.directExecutor());
-            } else {
-                if (mCloseFuture == null) {
-                    mCloseFuture = CallbackToFutureAdapter.getFuture(completer -> {
-                        // Should already be locked, but lock again to satisfy linter.
-                        synchronized (mLock) {
-                            mCloseCompleter = completer;
-                        }
-                        return "CaptureProcessorPipeline-close";
-                    });
-                }
-                closeFuture = Futures.nonCancellationPropagating(mCloseFuture);
-            }
-        }
-        return closeFuture;
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index df23742..17254cc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -19,7 +19,6 @@
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_BUFFER_FORMAT;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_BUNDLE;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_PROCESSOR;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_SESSION_CONFIG;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_FLASH_MODE;
@@ -28,7 +27,6 @@
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IMAGE_READER_PROXY_PROVIDER;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IO_EXECUTOR;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_JPEG_COMPRESSION_QUALITY;
-import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_CAPTURE_STAGES;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_RESOLUTION;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SUPPORTED_RESOLUTIONS;
@@ -108,7 +106,6 @@
 import androidx.camera.core.impl.ImmediateSurface;
 import androidx.camera.core.impl.MutableConfig;
 import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.MutableTagBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
@@ -326,16 +323,6 @@
 
     private CaptureConfig mCaptureConfig;
 
-    /** The set of requests that will be sent to the camera for the final captured image. */
-    private CaptureBundle mCaptureBundle;
-    private int mMaxCaptureStages;
-
-    /**
-     * Processing that gets done to the mCaptureBundle to produce the final image that is produced
-     * by {@link #takePicture(Executor, OnImageCapturedCallback)}
-     */
-    private CaptureProcessor mCaptureProcessor;
-
     /**
      * Whether the software JPEG pipeline will be used.
      */
@@ -420,77 +407,30 @@
             };
         } else if (isSessionProcessorEnabledInCurrentCamera()) {
             ImageReaderProxy imageReader;
+            // SessionProcessor only outputs JPEG format.
             if (getImageFormat() == ImageFormat.JPEG) {
-                imageReader =
-                        new AndroidImageReaderProxy(ImageReader.newInstance(resolution.getWidth(),
-                                resolution.getHeight(), getImageFormat(), MAX_IMAGES));
-            } else if (getImageFormat() == ImageFormat.YUV_420_888) { // convert it into Jpeg
-                if (Build.VERSION.SDK_INT >= 26) {
-                    // Jpeg rotation / quality will be set to softwareJpegProcessor later in
-                    // ImageCaptureRequestProcessor.
-                    softwareJpegProcessor =
-                            new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
-
-                    ModifiableImageReaderProxy inputReader =
-                            new ModifiableImageReaderProxy(
-                                    ImageReader.newInstance(resolution.getWidth(),
-                                            resolution.getHeight(),
-                                            ImageFormat.YUV_420_888,
-                                            MAX_IMAGES));
-
-                    CaptureBundle captureBundle = CaptureBundles.singleDefaultCaptureBundle();
-                    ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
-                            inputReader,
-                            captureBundle,
-                            softwareJpegProcessor
-                    ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
-
-                    // Ensure the ImageProxy contains the same capture stage id expected from the
-                    // ProcessingImageReader.
-                    MutableTagBundle tagBundle = MutableTagBundle.create();
-                    // Implicit non-null type use for getCaptureStages().
-                    //noinspection ConstantConditions
-                    tagBundle.putTag(processingImageReader.getTagBundleKey(),
-                            captureBundle.getCaptureStages().get(0).getId());
-                    inputReader.setImageTagBundle(tagBundle);
-
-                    imageReader = processingImageReader;
-                } else {
-                    throw new UnsupportedOperationException("Does not support API level < 26");
-                }
+                // SessionProcessor can't guarantee that image and capture result have the same
+                // time stamp. Thus we can't use MetadataImageReader
+                imageReader = ImageReaderProxys.createIsolatedReader(resolution.getWidth(),
+                        resolution.getHeight(), ImageFormat.JPEG, MAX_IMAGES);
+                mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
+                };
             } else {
                 throw new IllegalArgumentException("Unsupported image format:" + getImageFormat());
             }
-            mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
-            };
             mImageReader = new SafeCloseImageReaderProxy(imageReader);
-        } else if (mCaptureProcessor != null || mUseSoftwareJpeg) {
-            // Capture processor set from configuration takes precedence over software JPEG.
-            CaptureProcessor captureProcessor = mCaptureProcessor;
+        } else if (mUseSoftwareJpeg) {
+            CaptureProcessor captureProcessor;
             int inputFormat = getImageFormat();
-            int outputFormat = getImageFormat();
-            if (mUseSoftwareJpeg) {
-                // API check to satisfy linter
-                if (Build.VERSION.SDK_INT >= 26) {
-                    Logger.i(TAG, "Using software JPEG encoder.");
-
-                    if (mCaptureProcessor != null) {
-                        softwareJpegProcessor = new YuvToJpegProcessor(getJpegQualityInternal(),
-                                mMaxCaptureStages);
-                        captureProcessor = new CaptureProcessorPipeline(
-                                mCaptureProcessor, mMaxCaptureStages, softwareJpegProcessor,
-                                mExecutor);
-                    } else {
-                        captureProcessor = softwareJpegProcessor =
-                                new YuvToJpegProcessor(getJpegQualityInternal(), mMaxCaptureStages);
-                    }
-
-                    outputFormat = ImageFormat.JPEG;
-                } else {
-                    // Note: This should never be hit due to SDK_INT check before setting
-                    // useSoftwareJpeg.
-                    throw new IllegalStateException("Software JPEG only supported on API 26+");
-                }
+            // API check to satisfy linter
+            if (Build.VERSION.SDK_INT >= 26) {
+                Logger.i(TAG, "Using software JPEG encoder.");
+                captureProcessor = softwareJpegProcessor =
+                        new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
+            } else {
+                // Note: This should never be hit due to SDK_INT check before setting
+                // useSoftwareJpeg.
+                throw new IllegalStateException("Software JPEG only supported on API 26+");
             }
 
             // TODO: To allow user to use an Executor for the image processing.
@@ -498,10 +438,10 @@
                     resolution.getWidth(),
                     resolution.getHeight(),
                     inputFormat,
-                    mMaxCaptureStages,
-                    getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
+                    MAX_IMAGES,
+                    CaptureBundles.singleDefaultCaptureBundle(),
                     captureProcessor
-            ).setPostProcessExecutor(mExecutor).setOutputFormat(outputFormat).build();
+            ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
 
             mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
             mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
@@ -671,13 +611,7 @@
     @Override
     protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
             @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
-        if (builder.getUseCaseConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
-                != null && Build.VERSION.SDK_INT >= 29) {
-            // TODO: The API level check can be removed if the ImageWriterCompat issue on API
-            //  level 28 devices (b182363220/) can be resolved.
-            Logger.i(TAG, "Requesting software JPEG due to a CaptureProcessor is set.");
-            builder.getMutableConfig().insertOption(OPTION_USE_SOFTWARE_JPEG_ENCODER, true);
-        } else if (cameraInfo.getCameraQuirks().contains(
+        if (cameraInfo.getCameraQuirks().contains(
                 SoftwareJpegEncodingPreferredQuirk.class)) {
             // Request software JPEG encoder if quirk exists on this device and the software JPEG
             // option has not already been explicitly set.
@@ -699,15 +633,13 @@
         Integer bufferFormat = builder.getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT,
                 null);
         if (bufferFormat != null) {
-            Preconditions.checkArgument(
-                    builder.getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
-                            == null,
-                    "Cannot set buffer format with CaptureProcessor defined.");
+            Preconditions.checkArgument(!(isSessionProcessorEnabledInCurrentCamera()
+                            &&  bufferFormat != ImageFormat.JPEG),
+                    "Cannot set non-JPEG buffer format with Extensions enabled.");
             builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
                     useSoftwareJpeg ? ImageFormat.YUV_420_888 : bufferFormat);
         } else {
-            if (builder.getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) != null
-                    || useSoftwareJpeg) {
+            if (useSoftwareJpeg) {
                 builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
                         ImageFormat.YUV_420_888);
             } else {
@@ -728,13 +660,6 @@
                 }
             }
         }
-
-        Integer maxCaptureStages =
-                builder.getMutableConfig().retrieveOption(OPTION_MAX_CAPTURE_STAGES, MAX_IMAGES);
-        checkNotNull(maxCaptureStages,
-                "Maximum outstanding image count must be at least 1");
-        Preconditions.checkArgument(maxCaptureStages >= 1,
-                "Maximum outstanding image count must be at least 1");
         return builder.getUseCaseConfig();
     }
 
@@ -1678,12 +1603,6 @@
         CaptureConfig.Builder captureBuilder = CaptureConfig.Builder.createFrom(useCaseConfig);
         mCaptureConfig = captureBuilder.build();
 
-        // Retrieve camera specific settings.
-        mCaptureProcessor = useCaseConfig.getCaptureProcessor(null);
-        mMaxCaptureStages = useCaseConfig.getMaxCaptureStages(MAX_IMAGES);
-        mCaptureBundle = useCaseConfig.getCaptureBundle(
-                CaptureBundles.singleDefaultCaptureBundle());
-
         // This will only be set to true if software JPEG was requested and
         // enforceSoftwareJpegConstraints() hasn't removed the request.
         mUseSoftwareJpeg = useCaseConfig.isSoftwareJpegEncoderRequested();
@@ -1746,29 +1665,7 @@
         if (mProcessingImageReader != null) {
             // If the Processor is provided, check if we have valid CaptureBundle and update
             // ProcessingImageReader before actually issuing a take picture request.
-            captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
-            if (captureBundle == null) {
-                return Futures.immediateFailedFuture(new IllegalArgumentException(
-                        "ImageCapture cannot set empty CaptureBundle."));
-            }
-
-            List<CaptureStage> captureStages = captureBundle.getCaptureStages();
-            if (captureStages == null) {
-                return Futures.immediateFailedFuture(new IllegalArgumentException(
-                        "ImageCapture has CaptureBundle with null capture stages"));
-            }
-
-            if (mCaptureProcessor == null && captureStages.size() > 1) {
-                return Futures.immediateFailedFuture(new IllegalArgumentException(
-                        "No CaptureProcessor can be found to process the images captured for "
-                                + "multiple CaptureStages."));
-            }
-
-            if (captureStages.size() > mMaxCaptureStages) {
-                return Futures.immediateFailedFuture(new IllegalArgumentException(
-                        "ImageCapture has CaptureStages > Max CaptureStage size"));
-            }
-
+            captureBundle = CaptureBundles.singleDefaultCaptureBundle();
             mProcessingImageReader.setCaptureBundle(captureBundle);
             mProcessingImageReader.setOnProcessingErrorCallback(
                     CameraXExecutors.directExecutor(),
@@ -1779,7 +1676,7 @@
                     });
             tagBundleKey = mProcessingImageReader.getTagBundleKey();
         } else {
-            captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
+            captureBundle = CaptureBundles.singleDefaultCaptureBundle();
             if (captureBundle == null) {
                 return Futures.immediateFailedFuture(new IllegalArgumentException(
                         "ImageCapture cannot set empty CaptureBundle."));
@@ -1837,15 +1734,6 @@
         return submitStillCaptureRequest(captureConfigs);
     }
 
-    private CaptureBundle getCaptureBundle(CaptureBundle defaultCaptureBundle) {
-        List<CaptureStage> captureStages = mCaptureBundle.getCaptureStages();
-        if (captureStages == null || captureStages.isEmpty()) {
-            return defaultCaptureBundle;
-        }
-
-        return CaptureBundles.createCaptureBundle(captureStages);
-    }
-
     /**
      * ===== New architecture start =====
      *
@@ -1905,10 +1793,6 @@
             // Use old pipeline for advanced Extensions.
             return false;
         }
-        if (mCaptureProcessor != null) {
-            // Use old pipeline for basic Extensions.
-            return false;
-        }
         if (getCaptureStageSize(config) > 1) {
             // Use old pipeline for multiple stages capture.
             return false;
@@ -2811,16 +2695,9 @@
             // is done)
             Integer bufferFormat = getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT, null);
             if (bufferFormat != null) {
-                Preconditions.checkArgument(
-                        getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) == null,
-                        "Cannot set buffer format with CaptureProcessor defined.");
                 getMutableConfig().insertOption(OPTION_INPUT_FORMAT, bufferFormat);
             } else {
-                if (getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) != null) {
-                    getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
-                } else {
-                    getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
-                }
+                getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
             }
 
             ImageCaptureConfig imageCaptureConfig = getUseCaseConfig();
@@ -2838,13 +2715,6 @@
                         targetResolution.getHeight()));
             }
 
-            Integer maxCaptureStages =
-                    getMutableConfig().retrieveOption(OPTION_MAX_CAPTURE_STAGES, MAX_IMAGES);
-            checkNotNull(maxCaptureStages,
-                    "Maximum outstanding image count must be at least 1");
-            Preconditions.checkArgument(maxCaptureStages >= 1,
-                    "Maximum outstanding image count must be at least 1");
-
             checkNotNull(getMutableConfig().retrieveOption(OPTION_IO_EXECUTOR,
                     CameraXExecutors.ioExecutor()), "The IO executor can't be null");
 
@@ -2914,20 +2784,6 @@
         }
 
         /**
-         * Sets the {@link CaptureProcessor}.
-         *
-         * @param captureProcessor The requested capture processor for extension.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setCaptureProcessor(@NonNull CaptureProcessor captureProcessor) {
-            getMutableConfig().insertOption(OPTION_CAPTURE_PROCESSOR, captureProcessor);
-            return this;
-        }
-
-        /**
          * Sets the {@link ImageFormat} of the {@link ImageProxy} returned by the
          * {@link ImageCapture.OnImageCapturedCallback}.
          *
@@ -2948,20 +2804,6 @@
             return this;
         }
 
-        /**
-         * Sets the max number of {@link CaptureStage}.
-         *
-         * @param maxCaptureStages The max CaptureStage number.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setMaxCaptureStages(int maxCaptureStages) {
-            getMutableConfig().insertOption(OPTION_MAX_CAPTURE_STAGES, maxCaptureStages);
-            return this;
-        }
-
         /** @hide */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index e7d869d..c69d3ab 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -34,6 +34,7 @@
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.ImageReaderProxy;
 import androidx.camera.core.internal.compat.ImageWriterCompat;
+import androidx.camera.core.internal.utils.ImageUtil;
 import androidx.core.util.Preconditions;
 
 import java.nio.ByteBuffer;
@@ -94,6 +95,45 @@
     }
 
     /**
+     * Writes a JPEG bytes data as an Image into the Surface. Returns true if it succeeds and false
+     * otherwise.
+     */
+    public static boolean writeJpegBytesToSurface(
+            @NonNull Surface surface,
+            @NonNull byte[] jpegBytes) {
+        Preconditions.checkNotNull(jpegBytes);
+        Preconditions.checkNotNull(surface);
+
+        if (nativeWriteJpegToSurface(jpegBytes, surface) != 0) {
+            Logger.e(TAG, "Failed to enqueue JPEG image.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Convert a YUV_420_888 ImageProxy to a JPEG bytes data as an Image into the Surface.
+     *
+     * <p>Returns true if it succeeds and false otherwise.
+     */
+    public static boolean convertYuvToJpegBytesIntoSurface(
+            @NonNull ImageProxy imageProxy,
+            @IntRange(from = 1, to = 100) int jpegQuality,
+            @ImageOutputConfig.RotationDegreesValue int rotationDegrees,
+            @NonNull Surface outputSurface) {
+        try {
+            byte[] jpegBytes =
+                    ImageUtil.yuvImageToJpegByteArray(
+                            imageProxy, null, jpegQuality, rotationDegrees);
+            return writeJpegBytesToSurface(outputSurface,
+                    jpegBytes);
+        } catch (ImageUtil.CodecFailedException e) {
+            Logger.e(TAG, "Failed to encode YUV to JPEG", e);
+            return false;
+        }
+    }
+
+    /**
      * Converts image proxy in YUV to RGB.
      *
      * Currently this config supports the devices which generated NV21, NV12, I420 YUV layout,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
index 947ac42f..a43a5e2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -201,7 +201,7 @@
             }
         } else if (imageFormat == ImageFormat.YUV_420_888) {
             return ImageUtil.yuvImageToJpegByteArray(image, shouldCropImage ? image.getCropRect() :
-                    null, jpegQuality);
+                    null, jpegQuality, 0 /* rotationDegrees */);
         } else {
             Logger.w(TAG, "Unrecognized image format: " + imageFormat);
         }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
deleted file mode 100644
index 4d9250b..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.graphics.Matrix;
-import android.media.ImageReader;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.TagBundle;
-
-/**
- * An ImageReaderProxy implementation that allows to modify the ImageInfo data of the images
- * retrieved.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class ModifiableImageReaderProxy extends AndroidImageReaderProxy {
-    private volatile TagBundle mTagBundle = null;
-    private volatile Long mTimestamp = null;
-    private volatile Integer mRotationDegrees = null;
-    private volatile Matrix mSensorToBufferTransformMatrix = null;
-
-    ModifiableImageReaderProxy(@NonNull ImageReader imageReader) {
-        super(imageReader);
-    }
-
-    void setImageTagBundle(@NonNull TagBundle tagBundle) {
-        mTagBundle = tagBundle;
-    }
-
-    void setImageTimeStamp(long timestamp) {
-        mTimestamp = timestamp;
-    }
-
-    void setImageRotationDegrees(int rotationDegrees) {
-        mRotationDegrees = rotationDegrees;
-    }
-
-    void setImageSensorToBufferTransformaMatrix(@NonNull Matrix matrix) {
-        mSensorToBufferTransformMatrix = matrix;
-    }
-
-    @Nullable
-    @Override
-    public ImageProxy acquireLatestImage() {
-        return modifyImage(super.acquireNextImage());
-    }
-
-    @Nullable
-    @Override
-    public ImageProxy acquireNextImage() {
-        return modifyImage(super.acquireNextImage());
-    }
-
-    private ImageProxy modifyImage(ImageProxy imageProxy) {
-        ImageInfo origin = imageProxy.getImageInfo();
-        ImageInfo  imageInfo = ImmutableImageInfo.create(
-                mTagBundle != null ? mTagBundle : origin.getTagBundle(),
-                mTimestamp != null ? mTimestamp.longValue() : origin.getTimestamp(),
-                mRotationDegrees != null ? mRotationDegrees.intValue() :
-                        origin.getRotationDegrees(),
-                mSensorToBufferTransformMatrix != null ? mSensorToBufferTransformMatrix :
-                        origin.getSensorToBufferTransformMatrix());
-        return new SettableImageProxy(imageProxy, imageInfo);
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 585948a..36096c8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -19,15 +19,12 @@
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
-import static androidx.camera.core.impl.PreviewConfig.IMAGE_INFO_PROCESSOR;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_BACKGROUND_EXECUTOR;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_RESOLUTION;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_SESSION_CONFIG;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_MAX_RESOLUTION;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_RGBA8888_SURFACE_REQUIRED;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
@@ -49,8 +46,6 @@
 import android.graphics.SurfaceTexture;
 import android.media.ImageReader;
 import android.media.MediaCodec;
-import android.os.Handler;
-import android.os.HandlerThread;
 import android.util.Pair;
 import android.util.Size;
 import android.view.Display;
@@ -66,18 +61,13 @@
 import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CameraCaptureResult;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.CaptureStage;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.ConfigProvider;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.ImageFormatConstants;
-import androidx.camera.core.impl.ImageInfoProcessor;
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.MutableConfig;
 import androidx.camera.core.impl.MutableOptionsBundle;
@@ -88,7 +78,6 @@
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.internal.CameraCaptureResultImageInfo;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.ThreadConfig;
@@ -222,18 +211,15 @@
 
         Threads.checkMainThread();
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
-        final CaptureProcessor captureProcessor = config.getCaptureProcessor(null);
 
         // Close previous session's deferrable surface before creating new one
         clearPipeline();
 
-        boolean isRGBA8888SurfaceRequired = config.isRgba8888SurfaceRequired(false);
-
         // TODO: Can be improved by only restarting part of the pipeline when using the
         //  CaptureProcessor. e.g. only update the output Surface (between Processor/App), and
         //  still use the same input Surface (between Camera/Processor). It's just simpler for now.
         final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
-                isRGBA8888SurfaceRequired, this::notifyReset);
+                /* isRGBA8888Required */ false, this::notifyReset);
         mCurrentSurfaceRequest = surfaceRequest;
 
         if (mSurfaceProvider != null) {
@@ -241,54 +227,7 @@
             sendSurfaceRequest();
         }
 
-        if (captureProcessor != null) {
-            CaptureStage captureStage = new CaptureStage.DefaultCaptureStage();
-            // TODO: To allow user to use an Executor for the processing.
-            HandlerThread handlerThread = new HandlerThread(
-                    CameraXThreads.TAG + "preview_processing");
-            handlerThread.start();
-
-            String tagBundleKey = Integer.toString(captureStage.hashCode());
-
-            ProcessingSurface processingSurface = new ProcessingSurface(
-                    resolution.getWidth(),
-                    resolution.getHeight(),
-                    config.getInputFormat(),
-                    new Handler(handlerThread.getLooper()),
-                    captureStage,
-                    captureProcessor,
-                    surfaceRequest.getDeferrableSurface(),
-                    tagBundleKey);
-
-            sessionConfigBuilder.addCameraCaptureCallback(
-                    processingSurface.getCameraCaptureCallback());
-
-            processingSurface.getTerminationFuture().addListener(handlerThread::quitSafely,
-                    CameraXExecutors.directExecutor());
-
-            mSessionDeferrableSurface = processingSurface;
-
-            // Use CaptureStage object as the key for TagBundle
-            sessionConfigBuilder.addTag(tagBundleKey, captureStage.getId());
-        } else {
-            final ImageInfoProcessor processor = config.getImageInfoProcessor(null);
-
-            if (processor != null) {
-                sessionConfigBuilder.addCameraCaptureCallback(new CameraCaptureCallback() {
-                    @Override
-                    public void onCaptureCompleted(
-                            @NonNull CameraCaptureResult cameraCaptureResult) {
-                        super.onCaptureCompleted(cameraCaptureResult);
-                        if (processor.process(
-                                new CameraCaptureResultImageInfo(cameraCaptureResult))) {
-                            notifyUpdated();
-                        }
-                    }
-                });
-            }
-            mSessionDeferrableSurface = surfaceRequest.getDeferrableSurface();
-        }
-
+        mSessionDeferrableSurface = surfaceRequest.getDeferrableSurface();
         addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, resolution);
         return sessionConfigBuilder;
     }
@@ -631,13 +570,8 @@
     @Override
     protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
             @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
-        if (builder.getMutableConfig().retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, null)
-                != null) {
-            builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
-        } else {
-            builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
-                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
-        }
+        builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
+                ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
 
         // Merges Preview's default max resolution setting when resolution selector is used
         ResolutionSelector resolutionSelector =
@@ -1207,41 +1141,6 @@
             return this;
         }
 
-        /**
-         * Sets if the surface requires RGBA8888 format.
-         *
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setIsRgba8888SurfaceRequired(boolean isRgba8888SurfaceRequired) {
-            getMutableConfig().insertOption(
-                    OPTION_RGBA8888_SURFACE_REQUIRED, isRgba8888SurfaceRequired);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setImageInfoProcessor(@NonNull ImageInfoProcessor processor) {
-            getMutableConfig().insertOption(IMAGE_INFO_PROCESSOR, processor);
-            return this;
-        }
-
-        /**
-         * Sets the {@link CaptureProcessor}.
-         *
-         * @param captureProcessor The requested capture processor for extension.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setCaptureProcessor(@NonNull CaptureProcessor captureProcessor) {
-            getMutableConfig().insertOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, captureProcessor);
-            return this;
-        }
-
         /** @hide */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java
new file mode 100644
index 0000000..139162e
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/**
+ * Exception throw when effects post-processing fails.
+ *
+ * <p>Implementation of {@link SurfaceProcessor} throws this exception from
+ * {@link SurfaceProcessor#onInputSurface} or {@link SurfaceProcessor#onOutputSurface} when an
+ * error occurs during effect processing.
+ *
+ * @see SurfaceProcessor
+ */
+public class ProcessingException extends Exception {
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java
deleted file mode 100644
index 23148dc..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-
-import android.graphics.PixelFormat;
-import android.graphics.SurfaceTexture;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.CaptureStage;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.ImageReaderProxy;
-import androidx.camera.core.impl.SingleImageProxyBundle;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.FutureChain;
-import androidx.camera.core.impl.utils.futures.Futures;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.Executor;
-
-/**
- * A {@link DeferrableSurface} that does processing and outputs a {@link SurfaceTexture}.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-final class ProcessingSurface extends DeferrableSurface {
-    private static final String TAG = "ProcessingSurfaceTextur";
-
-    // Synthetic Accessor
-    @SuppressWarnings("WeakerAccess")
-    final Object mLock = new Object();
-
-    // Callback when Image is ready from InputImageReader.
-    private final ImageReaderProxy.OnImageAvailableListener mTransformedListener =
-            reader -> {
-                synchronized (mLock) {
-                    imageIncoming(reader);
-                }
-            };
-
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @GuardedBy("mLock")
-    boolean mReleased = false;
-
-    @NonNull
-    private final Size mResolution;
-
-    private final MetadataImageReader mInputImageReader;
-
-    // The Surface that is backed by mInputImageReader
-    private final Surface mInputSurface;
-
-    private final Handler mImageReaderHandler;
-
-    // Maximum number of images in the input ImageReader
-    private static final int MAX_IMAGES = 2;
-
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    final CaptureStage mCaptureStage;
-
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @NonNull
-    @GuardedBy("mLock")
-    final CaptureProcessor mCaptureProcessor;
-
-    private final CameraCaptureCallback mCameraCaptureCallback;
-    private final DeferrableSurface mOutputDeferrableSurface;
-
-    private String mTagBundleKey;
-    /**
-     * Create a {@link ProcessingSurface} with specific configurations.
-     * @param width            Width of the ImageReader
-     * @param height           Height of the ImageReader
-     * @param format           Image format
-     * @param handler          Handler for executing
-     *                         {@link ImageReaderProxy.OnImageAvailableListener}. If
-     *                         this is
-     *                         {@code null} then execution will be done on the calling
-     *                         thread's
-     *                         {@link Looper}.
-     * @param captureStage     The {@link CaptureStage} includes the processing information
-     * @param captureProcessor The {@link CaptureProcessor} to be invoked when the
-     *                         Images are ready
-     * @param outputSurface    The {@link DeferrableSurface} used as the output of
-     *                         processing.
-     * @param tagBundleKey     The key for tagBundle to get correct image. Usually the key comes
-     *                         from the CaptureStage's hash code.
-     */
-    ProcessingSurface(int width, int height, int format, @Nullable Handler handler,
-            @NonNull CaptureStage captureStage, @NonNull CaptureProcessor captureProcessor,
-            @NonNull DeferrableSurface outputSurface, @NonNull String tagBundleKey) {
-        super(new Size(width, height), format);
-        mResolution = new Size(width, height);
-
-        if (handler != null) {
-            mImageReaderHandler = handler;
-        } else {
-            Looper looper = Looper.myLooper();
-
-            if (looper == null) {
-                throw new IllegalStateException(
-                        "Creating a ProcessingSurface requires a non-null Handler, or be created "
-                                + " on a thread with a Looper.");
-            }
-
-            mImageReaderHandler = new Handler(looper);
-        }
-
-        Executor executor = CameraXExecutors.newHandlerExecutor(mImageReaderHandler);
-
-        // input
-        mInputImageReader = new MetadataImageReader(
-                width,
-                height,
-                format,
-                MAX_IMAGES);
-        mInputImageReader.setOnImageAvailableListener(mTransformedListener, executor);
-        mInputSurface = mInputImageReader.getSurface();
-        mCameraCaptureCallback = mInputImageReader.getCameraCaptureCallback();
-
-        // processing
-        mCaptureProcessor = captureProcessor;
-        mCaptureProcessor.onResolutionUpdate(mResolution);
-        mCaptureStage = captureStage;
-
-        // output
-        mOutputDeferrableSurface = outputSurface;
-
-        mTagBundleKey = tagBundleKey;
-
-        Futures.addCallback(outputSurface.getSurface(),
-                new FutureCallback<Surface>() {
-                    @Override
-                    public void onSuccess(@Nullable Surface surface) {
-                        synchronized (mLock) {
-                            mCaptureProcessor.onOutputSurface(surface, PixelFormat.RGBA_8888);
-                        }
-                    }
-
-                    @Override
-                    public void onFailure(@NonNull Throwable t) {
-                        Logger.e(TAG, "Failed to extract Listenable<Surface>.", t);
-                    }
-                }, directExecutor());
-
-        getTerminationFuture().addListener(this::release, directExecutor());
-    }
-
-    @Override
-    @NonNull
-    public ListenableFuture<Surface> provideSurface() {
-        // Before returning input surface to configure the capture session, ensures
-        // output surface is ready because output surface will be needed just before
-        // configuring capture session.
-        return  FutureChain.from(mOutputDeferrableSurface.getSurface())
-                .transform(outputSurface -> mInputSurface, CameraXExecutors.directExecutor());
-    }
-
-    /**
-     * Returns necessary camera callbacks to retrieve metadata from camera result.
-     *
-     * @throws IllegalStateException if {@link #release()} has already been called
-     */
-    @Nullable
-    CameraCaptureCallback getCameraCaptureCallback() {
-        synchronized (mLock) {
-            if (mReleased) {
-                throw new IllegalStateException("ProcessingSurface already released!");
-            }
-
-            return mCameraCaptureCallback;
-        }
-    }
-
-    /**
-     * Close the {@link ProcessingSurface}.
-     *
-     * <p> After closing the ProcessingSurface it should not be used again. A new instance
-     * should be created.
-     *
-     * <p>This should only be called once the ProcessingSurface has been terminated, i.e., it's
-     * termination future retrieved via {@link #getTerminationFuture()}} has completed.
-     */
-    private void release() {
-        synchronized (mLock) {
-            if (mReleased) {
-                return;
-            }
-
-            mInputImageReader.clearOnImageAvailableListener();
-
-            // Since the ProcessingSurface DeferrableSurface has been terminated, it is safe to
-            // close the inputs.
-            mInputImageReader.close();
-            mInputSurface.release();
-
-            // Now that the inputs are closed, we can close the output surface.
-            mOutputDeferrableSurface.close();
-
-            mReleased = true;
-        }
-    }
-
-    // Incoming Image from InputImageReader. Acquires it and add to SettableImageProxyBundle.
-    @SuppressWarnings("WeakerAccess")
-    @GuardedBy("mLock")
-    void imageIncoming(ImageReaderProxy imageReader) {
-        if (mReleased) {
-            return;
-        }
-
-        ImageProxy image = null;
-        try {
-            image = imageReader.acquireNextImage();
-        } catch (IllegalStateException e) {
-            Logger.e(TAG, "Failed to acquire next image.", e);
-        }
-
-        if (image == null) {
-            return;
-        }
-
-        ImageInfo imageInfo = image.getImageInfo();
-        if (imageInfo == null) {
-            image.close();
-            return;
-        }
-
-        Integer tagValue = (Integer) imageInfo.getTagBundle().getTag(mTagBundleKey);
-        if (tagValue == null) {
-            image.close();
-            return;
-        }
-
-        if (mCaptureStage.getId() != tagValue) {
-            Logger.w(TAG, "ImageProxyBundle does not contain this id: " + tagValue);
-            image.close();
-        } else {
-            SingleImageProxyBundle imageProxyBundle = new SingleImageProxyBundle(image,
-                    mTagBundleKey);
-            try {
-                // Increments the use count to prevent the surface from being closed during the
-                // processing duration.
-                incrementUseCount();
-            } catch (SurfaceClosedException e) {
-                Logger.d(TAG, "The ProcessingSurface has been closed. Don't process the incoming "
-                        + "image.");
-                imageProxyBundle.close();
-                return;
-            }
-            mCaptureProcessor.process(imageProxyBundle);
-            imageProxyBundle.close();
-            // Decrements the use count to allow the surface to be closed.
-            decrementUseCount();
-        }
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
index 1ee0da0..3bde336 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
@@ -37,10 +37,8 @@
  * <p>Contains a {@link Surface} and its characteristics along with methods to manage the
  * lifecycle of the {@link Surface}.
  *
- * @hide
  * @see SurfaceProcessor#onOutputSurface(SurfaceOutput)
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public interface SurfaceOutput {
 
     /**
@@ -62,13 +60,7 @@
     /**
      * This field indicates that what purpose the {@link Surface} will be used for.
      *
-     * <ul>
-     * <li>{@link SurfaceProcessor#PREVIEW} if the {@link Surface} will be used for {@link Preview}.
-     * <li>{@link SurfaceProcessor#VIDEO_CAPTURE} if the {@link Surface} will be used for video
-     * capture.
-     * <li>{@link SurfaceProcessor#PREVIEW} | {@link SurfaceProcessor#VIDEO_CAPTURE} if the output
-     * {@link Surface} will be used for sharing a single stream for both preview and video capture.
-     * </ul>
+     * <p>{@link CameraEffect#PREVIEW} if the {@link Surface} will be used for {@link Preview}.
      */
     @CameraEffect.Targets
     int getTargets();
@@ -81,37 +73,60 @@
 
     /**
      * Gets the format of the {@link Surface}.
+     *
+     * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     int getFormat();
 
     /**
      * Get the rotation degrees.
+     *
+     * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     int getRotationDegrees();
 
     /**
      * Call this method to mark the {@link Surface} as no longer in use.
      *
-     * <p>After this is called, the implementation should stop writing to the {@link Surface}
-     * provided via {@link #getSurface}
+     * <p>Once the {@link SurfaceProcessor} implementation receives a request to close the
+     * {@link Surface}, it should call this method to acknowledge after stop writing to the
+     * {@link Surface}. Writing to the {@link Surface} after calling this method might cause
+     * errors.
      */
     void close();
 
     /**
-     * Updates the 4 x 4 transformation matrix retrieved from {@link SurfaceTexture
-     * #getTransformMatrix}.
+     * Updates the 4 x 4 transformation matrix retrieved from the input {@link Surface}.
      *
-     * <p>This method applies an additional transformation on top of the value of
-     * {@link SurfaceTexture#getTransformMatrix}. The result is matrix of the same format, which
-     * is a transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1)
-     * with s and t in the inclusive range [0, 1] to the texture coordinate that should be used
-     * to sample that location from the texture. The matrix is stored in column-major order so that
-     * it may be passed directly to OpenGL ES via the {@code glLoadMatrixf} or {@code
-     * glUniformMatrix4fv} functions.
+     * <p>When the input {@link Surface} of {@link SurfaceProcessor} is backed by a
+     * {@link SurfaceTexture}, use this method to update the texture transform matrix.
+     *
+     * <p>After retrieving the transform matrix from {@link SurfaceTexture#getTransformMatrix},
+     * the {@link SurfaceProcessor} implementation should always call this method to update the
+     * value. The result is a matrix of the same format, which is a transform matrix maps 2D
+     * homogeneous texture coordinates of the form (s, t, 0, 1) with s and t in the inclusive
+     * range [0, 1] to the texture coordinate that should be used to sample that location from
+     * the texture. The matrix is stored in column-major order so that it may be passed directly
+     * to OpenGL ES via the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions.
      *
      * <p>The additional transformation is calculated based on the target rotation, target
      * resolution and the {@link ViewPort} associated with the target {@link UseCase}. The value
-     * could also include workarounds for device specific quirks.
+     * could also include workarounds for device specific bugs. For example, correcting a
+     * stretched camera output stream.
+     *
+     * <p>Code sample:
+     * <pre><code>
+     * float[] transform = new float[16];
+     * float[] updatedTransform = new float[16];
+     *
+     * surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
+     *     surfaceTexture.getTransformMatrix(transform);
+     *     outputSurface.updateTransformMatrix(updatedTransform, transform);
+     *     // Use the value of updatedTransform for OpenGL rendering.
+     * });
+     * </code></pre>
      *
      * @param updated  the array into which the 4x4 matrix will be stored. The array must
      *                 have exactly 16 elements.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
index 56e6aec..a622897 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
@@ -20,20 +20,58 @@
 import android.view.Surface;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 import androidx.core.util.Consumer;
 
 /**
  * Interface to implement a GPU-based post-processing effect.
  *
- * <p>This interface is for implementing a GPU effect for the {@link Preview} and/or
- * {@code VideoCapture} {@link UseCase}. Both the input and the output of the implementation
- * are {@link Surface}s. It's recommended to use graphics API such as OpenGL or Vulkan to access
- * the {@link Surface}.
+ * <p>This interface is for implementing a GPU effect for the {@link Preview} {@link UseCase}.
+ * Both the input and the output of the implementation are {@link Surface}s. It's recommended to
+ * use graphics API such as OpenGL or Vulkan to access the {@link Surface}.
  *
- * @hide
+ * <p>If the implementation fails to process frames, for example, fails to allocate
+ * the resources, it should throw a {@link ProcessingException} in either {@link #onInputSurface} or
+ * {@link #onOutputSurface} to notify CameraX. If the implementation encounters an error after the
+ * pipeline is running, it should invalidate the input {@link Surface} by calling
+ * {@link SurfaceRequest#invalidate()}, then throwing a {@link ProcessingException} when
+ * {@link SurfaceProcessor#onInputSurface} is invoked again.
+ *
+ * <p>Once the implementation throws an exception, CameraX will treat it as an unrecoverable error
+ * and abort the pipeline. If the {@link SurfaceOutput#getTargets()} is
+ * {@link CameraEffect#PREVIEW}, CameraX will not propagate the error to the app. It's the
+ * implementation's responsibility to notify the app. For example:
+ *
+ * <pre><code>
+ * class SurfaceProcessorImpl implements SurfaceProcessor {
+ *
+ *     Consumer<Exception> mErrorListener;
+ *
+ *     SurfaceProcessorImpl(@NonNull Consumer<Exception> errorListener) {
+ *         mErrorListener = errorListener;
+ *     }
+ *
+ *     void onInputSurface(@NonNull SurfaceRequest request) throws ProcessingException {
+ *         try {
+ *             // Setup the input stream.
+ *         } catch (Exception e) {
+ *             // Notify the app before throwing a ProcessingException.
+ *             mErrorListener.accept(e)
+ *             throw new ProcessingException(e);
+ *         }
+ *     }
+ *
+ *     void onOutputSurface(@NonNull SurfaceRequest request) throws ProcessingException {
+ *         try {
+ *             // Setup the output streams.
+ *         } catch (Exception e) {
+ *             // Notify the app before throwing a ProcessingException.
+ *             mErrorListener.accept(e)
+ *             throw new ProcessingException(e);
+ *         }
+ *     }
+ * }
+ * </code></pre>
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public interface SurfaceProcessor {
 
     /**
@@ -44,15 +82,44 @@
      * listen for the {@link SurfaceTexture#setOnFrameAvailableListener} to get the incoming
      * frames.
      *
+     * <p>If the implementation encounters errors in creating the input {@link Surface}, it
+     * should throw an {@link ProcessingException} to notify CameraX.
+     *
+     * <p>The implementation can replace a previously provided {@link Surface} by invoking
+     * {@link SurfaceRequest#invalidate()}. Once invoked, CameraX will restart the camera
+     * pipeline and call {@link #onInputSurface} again with another {@link SurfaceRequest}.
+     *
      * <p>The value of the {@link SurfaceTexture#getTransformMatrix} will need an additional
      * transformation. CameraX calculates the additional transformation based on {@link UseCase}
      * configurations such as {@link ViewPort} and target rotation, and provide the value via
      * {@link SurfaceOutput#updateTransformMatrix(float[], float[])}.
      *
+     * Code sample:
+     * <pre><code>
+     * // Create Surface based on the request.
+     * SurfaceTexture surfaceTexture = SurfaceTexture(textureName);
+     * surfaceTexture.setDefaultBufferSize(request.resolution.width, request.resolution.height);
+     * Surface surface = Surface(surfaceTexture);
+     *
+     * // Provide the Surface to CameraX, and cleanup when it's no longer used.
+     * surfaceRequest.provideSurface(surface, executor, result -> {
+     *     surfaceTexture.setOnFrameAvailableListener(null)
+     *     surfaceTexture.release()
+     *     surface.release()
+     * });
+     *
+     * // Listen to the incoming frames.
+     * surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
+     *     // Process the incoming frames and draw to the output Surface from #onOutputSurface
+     * }, handler);
+     * </code></pre>
+     *
      * @param request a request to provide {@link Surface} for input.
+     * @throws ProcessingException if the implementation fails to fulfill the
+     *                             {@link SurfaceRequest}.
      * @see SurfaceRequest
      */
-    void onInputSurface(@NonNull SurfaceRequest request);
+    void onInputSurface(@NonNull SurfaceRequest request) throws ProcessingException;
 
     /**
      * Invoked when CameraX provides output Surface(s) for drawing processed frames.
@@ -63,12 +130,17 @@
      * {@link Surface}. Then, the implementation should call {@link SurfaceOutput#close()} after it
      * stops drawing to the {@link Surface}.
      *
-     * <p> When drawing to the {@link Surface}, the implementation should apply an additional
+     * <p>If the implementation encounters an error and cannot consume the {@link Surface},
+     * it should throw an {@link ProcessingException} to notify CameraX.
+     *
+     * <p>When drawing to the {@link Surface}, the implementation should apply an additional
      * transformation to the input {@link Surface} by calling
      * {@link SurfaceOutput#updateTransformMatrix(float[], float[])} with the value of
      * {@link SurfaceTexture#getTransformMatrix(float[])}} from the input {@link Surface}.
      *
      * @param surfaceOutput contains a {@link Surface} for drawing processed frames.
+     * @throws ProcessingException if the implementation fails to consume the {@link SurfaceOutput}.
+     * @see SurfaceOutput
      */
-    void onOutputSurface(@NonNull SurfaceOutput surfaceOutput);
+    void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) throws ProcessingException;
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
index 5008b5c..e621821 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -26,7 +26,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
 import androidx.lifecycle.Lifecycle;
 
 import java.util.ArrayList;
@@ -78,10 +77,7 @@
 
     /**
      * Gets the {@link CameraEffect}s.
-     *
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
     public List<CameraEffect> getEffects() {
         return mEffects;
@@ -127,10 +123,7 @@
          *
          * <p>Once added, CameraX will use the {@link CameraEffect}s to process the outputs of
          * the {@link UseCase}s.
-         *
-         * @hide
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @NonNull
         public Builder addEffect(@NonNull CameraEffect cameraEffect) {
             mEffects.add(cameraEffect);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
deleted file mode 100644
index bd6734b..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ /dev/null
@@ -1,2112 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_BIT_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_CHANNEL_COUNT;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_MIN_BUFFER_SIZE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_SAMPLE_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_BIT_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_INTRA_FRAME_INTERVAL;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_VIDEO_FRAME_RATE;
-import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
-import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
-import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
-import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
-
-import android.Manifest;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.location.Location;
-import android.media.AudioFormat;
-import android.media.AudioRecord;
-import android.media.CamcorderProfile;
-import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecInfo.CodecCapabilities;
-import android.media.MediaFormat;
-import android.media.MediaMuxer;
-import android.media.MediaRecorder.AudioSource;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-import android.util.Pair;
-import android.util.Size;
-import android.view.Display;
-import android.view.Surface;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.GuardedBy;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RequiresPermission;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.UiThread;
-import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.Config;
-import androidx.camera.core.impl.ConfigProvider;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.ImageOutputConfig;
-import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
-import androidx.camera.core.impl.ImmediateSurface;
-import androidx.camera.core.impl.MutableConfig;
-import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.OptionsBundle;
-import androidx.camera.core.impl.SessionConfig;
-import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.UseCaseConfigFactory;
-import androidx.camera.core.impl.VideoCaptureConfig;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.internal.ThreadConfig;
-import androidx.camera.core.internal.utils.VideoUtil;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A use case for taking a video.
- *
- * <p>This class is designed for simple video capturing. It gives basic configuration of the
- * recorded video such as resolution and file format.
- *
- * @deprecated Use {@link androidx.camera.video.VideoCapture} instead.
- *
- * @hide
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@Deprecated
-@RestrictTo(Scope.LIBRARY_GROUP)
-public final class VideoCapture extends UseCase {
-
-    ////////////////////////////////////////////////////////////////////////////////////////////
-    // [UseCase lifetime constant] - Stays constant for the lifetime of the UseCase. Which means
-    // they could be created in the constructor.
-    ////////////////////////////////////////////////////////////////////////////////////////////
-
-    /**
-     * An unknown error occurred.
-     *
-     * <p>See message parameter in onError callback or log for more details.
-     */
-    public static final int ERROR_UNKNOWN = 0;
-    /**
-     * An error occurred with encoder state, either when trying to change state or when an
-     * unexpected state change occurred.
-     */
-    public static final int ERROR_ENCODER = 1;
-    /** An error with muxer state such as during creation or when stopping. */
-    public static final int ERROR_MUXER = 2;
-    /**
-     * An error indicating start recording was called when video recording is still in progress.
-     */
-    public static final int ERROR_RECORDING_IN_PROGRESS = 3;
-    /**
-     * An error indicating the file saving operations.
-     */
-    public static final int ERROR_FILE_IO = 4;
-    /**
-     * An error indicating this VideoCapture is not bound to a camera.
-     */
-    public static final int ERROR_INVALID_CAMERA = 5;
-    /**
-     * An error indicating the video file is too short.
-     * <p> The output file will be deleted if the OutputFileOptions is backed by File or uri.
-     */
-    public static final int ERROR_RECORDING_TOO_SHORT = 6;
-
-    /**
-     * Provides a static configuration with implementation-agnostic options.
-     *
-     * @hide
-     */
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    public static final Defaults DEFAULT_CONFIG = new Defaults();
-    private static final String TAG = "VideoCapture";
-    /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
-    private static final int DEQUE_TIMEOUT_USEC = 10000;
-    /** Android preferred mime type for AVC video. */
-    private static final String VIDEO_MIME_TYPE = "video/avc";
-    private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
-    /** Camcorder profiles quality list */
-    private static final int[] CamcorderQuality = {
-            CamcorderProfile.QUALITY_2160P,
-            CamcorderProfile.QUALITY_1080P,
-            CamcorderProfile.QUALITY_720P,
-            CamcorderProfile.QUALITY_480P
-    };
-
-    private final BufferInfo mVideoBufferInfo = new BufferInfo();
-    private final Object mMuxerLock = new Object();
-    private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
-    private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
-    private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
-    private final BufferInfo mAudioBufferInfo = new BufferInfo();
-    /** For record the first sample written time. */
-    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    public final AtomicBoolean mIsFirstVideoKeyFrameWrite = new AtomicBoolean(false);
-    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    public final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
-
-    ////////////////////////////////////////////////////////////////////////////////////////////
-    // [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
-    ////////////////////////////////////////////////////////////////////////////////////////////
-
-    /** Thread on which all encoding occurs. */
-    private HandlerThread mVideoHandlerThread;
-    private Handler mVideoHandler;
-    /** Thread on which audio encoding occurs. */
-    private HandlerThread mAudioHandlerThread;
-    private Handler mAudioHandler;
-
-    @NonNull
-    MediaCodec mVideoEncoder;
-    @NonNull
-    private MediaCodec mAudioEncoder;
-    @Nullable
-    private ListenableFuture<Void> mRecordingFuture = null;
-    @NonNull
-    private SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
-
-    ////////////////////////////////////////////////////////////////////////////////////////////
-    // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
-    ////////////////////////////////////////////////////////////////////////////////////////////
-
-    /** The muxer that writes the encoding data to file. */
-    @GuardedBy("mMuxerLock")
-    private MediaMuxer mMuxer;
-    private final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
-    /** The index of the video track used by the muxer. */
-    @GuardedBy("mMuxerLock")
-    private int mVideoTrackIndex;
-    /** The index of the audio track used by the muxer. */
-    @GuardedBy("mMuxerLock")
-    private int mAudioTrackIndex;
-    /** Surface the camera writes to, which the videoEncoder uses as input. */
-    Surface mCameraSurface;
-
-    /** audio raw data */
-    @Nullable
-    private volatile AudioRecord mAudioRecorder;
-    private volatile int mAudioBufferSize;
-    private volatile boolean mIsRecording = false;
-    private int mAudioChannelCount;
-    private int mAudioSampleRate;
-    private int mAudioBitRate;
-    private DeferrableSurface mDeferrableSurface;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    volatile Uri mSavedVideoUri;
-    private volatile ParcelFileDescriptor mParcelFileDescriptor;
-    private final AtomicBoolean mIsAudioEnabled = new AtomicBoolean(true);
-
-    private VideoEncoderInitStatus mVideoEncoderInitStatus =
-            VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED;
-    @Nullable
-    private Throwable mVideoEncoderErrorMessage;
-
-    /**
-     * Creates a new video capture use case from the given configuration.
-     *
-     * @param config for this use case instance
-     */
-    VideoCapture(@NonNull VideoCaptureConfig config) {
-        super(config);
-    }
-
-    /** Creates a {@link MediaFormat} using parameters from the configuration */
-    private static MediaFormat createVideoMediaFormat(VideoCaptureConfig config, Size resolution) {
-        MediaFormat format =
-                MediaFormat.createVideoFormat(
-                        VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
-        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
-        format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
-        format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
-        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
-
-        return format;
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @hide
-     */
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    @Override
-    @Nullable
-    public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
-            @NonNull UseCaseConfigFactory factory) {
-        Config captureConfig = factory.getConfig(
-                UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
-                ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY);
-
-        if (applyDefaultConfig) {
-            captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
-        }
-
-        return captureConfig == null ? null :
-                getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @hide
-     */
-    @SuppressWarnings("WrongConstant")
-    @Override
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    public void onAttached() {
-        mVideoHandlerThread = new HandlerThread(CameraXThreads.TAG + "video encoding thread");
-        mAudioHandlerThread = new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
-
-        // video thread start
-        mVideoHandlerThread.start();
-        mVideoHandler = new Handler(mVideoHandlerThread.getLooper());
-
-        // audio thread start
-        mAudioHandlerThread.start();
-        mAudioHandler = new Handler(mAudioHandlerThread.getLooper());
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @hide
-     */
-    @Override
-    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    @NonNull
-    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
-        if (mCameraSurface != null) {
-            mVideoEncoder.stop();
-            mVideoEncoder.release();
-            mAudioEncoder.stop();
-            mAudioEncoder.release();
-            releaseCameraSurface(false);
-        }
-
-        try {
-            mVideoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
-            mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
-        } catch (IOException e) {
-            throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
-        }
-
-        setupEncoder(getCameraId(), suggestedResolution);
-        // VideoCapture has to be active to apply SessionConfig's template type.
-        notifyActive();
-        return suggestedResolution;
-    }
-
-    /**
-     * Starts recording video, which continues until {@link VideoCapture#stopRecording()} is
-     * called.
-     *
-     * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
-     * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
-     *
-     * @param outputFileOptions Location to save the video capture
-     * @param executor          The executor in which the callback methods will be run.
-     * @param callback          Callback for when the recorded video saving completion or failure.
-     */
-    @SuppressWarnings("ObjectToString")
-    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
-    public void startRecording(
-            @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
-            @NonNull OnVideoSavedCallback callback) {
-        if (Looper.getMainLooper() != Looper.myLooper()) {
-            CameraXExecutors.mainThreadExecutor().execute(() -> startRecording(outputFileOptions,
-                    executor, callback));
-            return;
-        }
-        Logger.i(TAG, "startRecording");
-        mIsFirstVideoKeyFrameWrite.set(false);
-        mIsFirstAudioSampleWrite.set(false);
-
-        OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
-
-        CameraInternal attachedCamera = getCamera();
-        if (attachedCamera == null) {
-            // Not bound. Notify callback.
-            postListener.onError(ERROR_INVALID_CAMERA,
-                    "Not bound to a Camera [" + VideoCapture.this + "]", null);
-            return;
-        }
-
-        // Check video encoder initialization status, if there is any error happened
-        // return error callback directly.
-        if (mVideoEncoderInitStatus
-                == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE
-                || mVideoEncoderInitStatus
-                == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED
-                || mVideoEncoderInitStatus
-                == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED) {
-            postListener.onError(ERROR_ENCODER, "Video encoder initialization failed before start"
-                    + " recording ", mVideoEncoderErrorMessage);
-            return;
-        }
-
-        if (!mEndOfAudioVideoSignal.get()) {
-            postListener.onError(
-                    ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
-                    null);
-            return;
-        }
-
-        if (mIsAudioEnabled.get()) {
-            try {
-                // Audio input start
-                if (mAudioRecorder.getState() == AudioRecord.STATE_INITIALIZED) {
-                    mAudioRecorder.startRecording();
-                }
-            } catch (IllegalStateException e) {
-                // Disable the audio if the audio input cannot start. And Continue the recording
-                // without audio.
-                Logger.i(TAG,
-                        "AudioRecorder cannot start recording, disable audio." + e.getMessage());
-                mIsAudioEnabled.set(false);
-                releaseAudioInputResource();
-            }
-
-            // Gets the AudioRecorder's state
-            if (mAudioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
-                Logger.i(TAG,
-                        "AudioRecorder startRecording failed - incorrect state: "
-                                + mAudioRecorder.getRecordingState());
-                mIsAudioEnabled.set(false);
-                releaseAudioInputResource();
-            }
-        }
-
-        AtomicReference<Completer<Void>> recordingCompleterRef = new AtomicReference<>();
-        mRecordingFuture = CallbackToFutureAdapter.getFuture(
-                completer -> {
-                    recordingCompleterRef.set(completer);
-                    return "startRecording";
-                });
-        Completer<Void> recordingCompleter =
-                Preconditions.checkNotNull(recordingCompleterRef.get());
-
-        mRecordingFuture.addListener(() -> {
-            mRecordingFuture = null;
-            // Do the setup of the videoEncoder at the end of video recording instead of at the
-            // start of recording because it requires attaching a new Surface. This causes a
-            // glitch so we don't want that to incur latency at the start of capture.
-            if (getCamera() != null) {
-                // Ensure the use case is bound. Asynchronous stopping procedure may occur after
-                // the use case is unbound, i.e. after onDetached().
-                setupEncoder(getCameraId(), getAttachedSurfaceResolution());
-                notifyReset();
-            }
-        }, CameraXExecutors.mainThreadExecutor());
-
-        try {
-            // video encoder start
-            Logger.i(TAG, "videoEncoder start");
-            mVideoEncoder.start();
-
-            // audio encoder start
-            if (mIsAudioEnabled.get()) {
-                Logger.i(TAG, "audioEncoder start");
-                mAudioEncoder.start();
-            }
-        } catch (IllegalStateException e) {
-            recordingCompleter.set(null);
-            postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail", e);
-            return;
-        }
-
-        try {
-            synchronized (mMuxerLock) {
-                mMuxer = initMediaMuxer(outputFileOptions);
-                Preconditions.checkNotNull(mMuxer);
-                mMuxer.setOrientationHint(getRelativeRotation(attachedCamera));
-
-                Metadata metadata = outputFileOptions.getMetadata();
-                if (metadata != null && metadata.location != null) {
-                    mMuxer.setLocation(
-                            (float) metadata.location.getLatitude(),
-                            (float) metadata.location.getLongitude());
-                }
-            }
-        } catch (IOException e) {
-            recordingCompleter.set(null);
-            postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
-            return;
-        }
-
-        mEndOfVideoStreamSignal.set(false);
-        mEndOfAudioStreamSignal.set(false);
-        mEndOfAudioVideoSignal.set(false);
-        mIsRecording = true;
-
-        // Attach Surface to repeating request.
-        mSessionConfigBuilder.clearSurfaces();
-        mSessionConfigBuilder.addSurface(mDeferrableSurface);
-        updateSessionConfig(mSessionConfigBuilder.build());
-        notifyUpdated();
-
-        if (mIsAudioEnabled.get()) {
-            mAudioHandler.post(() -> audioEncode(postListener));
-        }
-
-        String cameraId = getCameraId();
-        Size resolution = getAttachedSurfaceResolution();
-        mVideoHandler.post(
-                () -> {
-                    boolean errorOccurred = videoEncode(postListener, cameraId, resolution,
-                            outputFileOptions);
-                    if (!errorOccurred) {
-                        postListener.onVideoSaved(new OutputFileResults(mSavedVideoUri));
-                        mSavedVideoUri = null;
-                    }
-                    recordingCompleter.set(null);
-                });
-    }
-
-    /**
-     * Stops recording video, this must be called after {@link
-     * VideoCapture#startRecording(OutputFileOptions, Executor, OnVideoSavedCallback)} is
-     * called.
-     *
-     * <p>stopRecording() is asynchronous API. User need to check if {@link
-     * OnVideoSavedCallback#onVideoSaved(OutputFileResults)} or
-     * {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
-     * before startRecording.
-     */
-    public void stopRecording() {
-        if (Looper.getMainLooper() != Looper.myLooper()) {
-            CameraXExecutors.mainThreadExecutor().execute(() -> stopRecording());
-            return;
-        }
-        Logger.i(TAG, "stopRecording");
-
-        mSessionConfigBuilder.clearSurfaces();
-        mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
-        updateSessionConfig(mSessionConfigBuilder.build());
-        notifyUpdated();
-
-        if (mIsRecording) {
-            if (mIsAudioEnabled.get()) {
-                // Stop audio encoder thread, and wait video encoder and muxer stop.
-                mEndOfAudioStreamSignal.set(true);
-            } else {
-                // Audio is disabled, stop video encoder thread directly.
-                mEndOfVideoStreamSignal.set(true);
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @hide
-     */
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    @Override
-    public void onDetached() {
-        stopRecording();
-
-        if (mRecordingFuture != null) {
-            mRecordingFuture.addListener(() -> releaseResources(),
-                    CameraXExecutors.mainThreadExecutor());
-        } else {
-            releaseResources();
-        }
-    }
-
-    private void releaseResources() {
-        mVideoHandlerThread.quitSafely();
-
-        // audio encoder release
-        releaseAudioInputResource();
-
-        if (mCameraSurface != null) {
-            releaseCameraSurface(true);
-        }
-    }
-
-    private void releaseAudioInputResource() {
-        mAudioHandlerThread.quitSafely();
-        if (mAudioEncoder != null) {
-            mAudioEncoder.release();
-            mAudioEncoder = null;
-        }
-
-        if (mAudioRecorder != null) {
-            mAudioRecorder.release();
-            mAudioRecorder = null;
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @hide
-     */
-    @NonNull
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    @Override
-    public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
-        return Builder.fromConfig(config);
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @hide
-     */
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    @UiThread
-    @Override
-    public void onStateDetached() {
-        stopRecording();
-    }
-
-    @UiThread
-    private void releaseCameraSurface(final boolean releaseVideoEncoder) {
-        if (mDeferrableSurface == null) {
-            return;
-        }
-
-        final MediaCodec videoEncoder = mVideoEncoder;
-
-        // Calling close should allow termination future to complete and close the surface with
-        // the listener that was added after constructing the DeferrableSurface.
-        mDeferrableSurface.close();
-        mDeferrableSurface.getTerminationFuture().addListener(
-                () -> {
-                    if (releaseVideoEncoder && videoEncoder != null) {
-                        videoEncoder.release();
-                    }
-                }, CameraXExecutors.mainThreadExecutor());
-
-        if (releaseVideoEncoder) {
-            mVideoEncoder = null;
-        }
-        mCameraSurface = null;
-        mDeferrableSurface = null;
-    }
-
-    /**
-     * Sets the desired rotation of the output video.
-     *
-     * <p>In most cases this should be set to the current rotation returned by {@link
-     * Display#getRotation()}.
-     *
-     * @param rotation Desired rotation of the output video.
-     */
-    public void setTargetRotation(@RotationValue int rotation) {
-        setTargetRotationInternal(rotation);
-    }
-
-    /**
-     * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
-     * audio from selected audio source.
-     */
-    @UiThread
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
-    void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
-        VideoCaptureConfig config = (VideoCaptureConfig) getCurrentConfig();
-
-        // video encoder setup
-        mVideoEncoder.reset();
-        mVideoEncoderInitStatus = VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED;
-
-        // Configures a Video encoder, if there is any exception, will abort follow up actions
-        try {
-            mVideoEncoder.configure(
-                    createVideoMediaFormat(config, resolution), /*surface*/
-                    null, /*crypto*/
-                    null,
-                    MediaCodec.CONFIGURE_FLAG_ENCODE);
-        } catch (MediaCodec.CodecException e) {
-            int errorCode = 0;
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
-                errorCode = Api23Impl.getCodecExceptionErrorCode(e);
-                String diagnosticInfo = e.getDiagnosticInfo();
-                if (errorCode == MediaCodec.CodecException.ERROR_INSUFFICIENT_RESOURCE) {
-                    Logger.i(TAG,
-                            "CodecException: code: " + errorCode + " diagnostic: "
-                                    + diagnosticInfo);
-                    mVideoEncoderInitStatus =
-                            VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE;
-                } else if (errorCode == MediaCodec.CodecException.ERROR_RECLAIMED) {
-                    Logger.i(TAG,
-                            "CodecException: code: " + errorCode + " diagnostic: "
-                                    + diagnosticInfo);
-                    mVideoEncoderInitStatus =
-                            VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED;
-                }
-            } else {
-                mVideoEncoderInitStatus =
-                        VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED;
-            }
-            mVideoEncoderErrorMessage = e;
-            return;
-        } catch (IllegalArgumentException | IllegalStateException e) {
-            mVideoEncoderInitStatus =
-                    VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED;
-            mVideoEncoderErrorMessage = e;
-            return;
-        }
-
-        if (mCameraSurface != null) {
-            releaseCameraSurface(false);
-        }
-        Surface cameraSurface = mVideoEncoder.createInputSurface();
-        mCameraSurface = cameraSurface;
-
-        mSessionConfigBuilder = SessionConfig.Builder.createFrom(config);
-
-        if (mDeferrableSurface != null) {
-            mDeferrableSurface.close();
-        }
-        mDeferrableSurface = new ImmediateSurface(mCameraSurface, resolution, getImageFormat());
-        mDeferrableSurface.getTerminationFuture().addListener(
-                cameraSurface::release, CameraXExecutors.mainThreadExecutor()
-        );
-
-        mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
-
-        mSessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
-            @Override
-            @RequiresPermission(Manifest.permission.RECORD_AUDIO)
-            public void onError(@NonNull SessionConfig sessionConfig,
-                    @NonNull SessionConfig.SessionError error) {
-                // Ensure the attached camera has not changed before calling setupEncoder.
-                // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
-                //  to this use case so we don't need to do this check.
-                if (isCurrentCamera(cameraId)) {
-                    // Only reset the pipeline when the bound camera is the same.
-                    setupEncoder(cameraId, resolution);
-                    notifyReset();
-                }
-            }
-        });
-
-        updateSessionConfig(mSessionConfigBuilder.build());
-
-        // audio encoder setup
-        // reset audio inout flag
-        mIsAudioEnabled.set(true);
-
-        setAudioParametersByCamcorderProfile(resolution, cameraId);
-        mAudioEncoder.reset();
-        mAudioEncoder.configure(
-                createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-
-        if (mAudioRecorder != null) {
-            mAudioRecorder.release();
-        }
-        mAudioRecorder = autoConfigAudioRecordSource(config);
-        // check mAudioRecorder
-        if (mAudioRecorder == null) {
-            Logger.e(TAG, "AudioRecord object cannot initialized correctly!");
-            mIsAudioEnabled.set(false);
-        }
-
-        synchronized (mMuxerLock) {
-            mVideoTrackIndex = -1;
-            mAudioTrackIndex = -1;
-        }
-        mIsRecording = false;
-    }
-
-    /**
-     * Write a buffer that has been encoded to file.
-     *
-     * @param bufferIndex the index of the buffer in the videoEncoder that has available data
-     * @return returns true if this buffer is the end of the stream
-     */
-    private boolean writeVideoEncodedBuffer(int bufferIndex) {
-        if (bufferIndex < 0) {
-            Logger.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
-            return false;
-        }
-        // Get data from buffer
-        ByteBuffer outputBuffer = mVideoEncoder.getOutputBuffer(bufferIndex);
-
-        // Check if buffer is valid, if not then return
-        if (outputBuffer == null) {
-            Logger.d(TAG, "OutputBuffer was null.");
-            return false;
-        }
-
-        // Write data to mMuxer if available
-        if (mMuxerStarted.get()) {
-            if (mVideoBufferInfo.size > 0) {
-                outputBuffer.position(mVideoBufferInfo.offset);
-                outputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size);
-                mVideoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
-
-                synchronized (mMuxerLock) {
-                    if (!mIsFirstVideoKeyFrameWrite.get()) {
-                        boolean isKeyFrame =
-                                (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
-                        if (isKeyFrame) {
-                            Logger.i(TAG,
-                                    "First video key frame written.");
-                            mIsFirstVideoKeyFrameWrite.set(true);
-                        } else {
-                            // Request a sync frame immediately
-                            final Bundle syncFrame = new Bundle();
-                            syncFrame.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
-                            mVideoEncoder.setParameters(syncFrame);
-                        }
-                    }
-                    mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, mVideoBufferInfo);
-                }
-            } else {
-                Logger.i(TAG, "mVideoBufferInfo.size <= 0, index " + bufferIndex);
-            }
-        }
-
-        // Release data
-        mVideoEncoder.releaseOutputBuffer(bufferIndex, false);
-
-        // Return true if EOS is set
-        return (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
-    }
-
-    private boolean writeAudioEncodedBuffer(int bufferIndex) {
-        ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
-        buffer.position(mAudioBufferInfo.offset);
-        if (mMuxerStarted.get()) {
-            try {
-                if (mAudioBufferInfo.size > 0 && mAudioBufferInfo.presentationTimeUs > 0) {
-                    synchronized (mMuxerLock) {
-                        if (!mIsFirstAudioSampleWrite.get()) {
-                            Logger.i(TAG, "First audio sample written.");
-                            mIsFirstAudioSampleWrite.set(true);
-                        }
-                        mMuxer.writeSampleData(mAudioTrackIndex, buffer, mAudioBufferInfo);
-                    }
-                } else {
-                    Logger.i(TAG, "mAudioBufferInfo size: " + mAudioBufferInfo.size + " "
-                            + "presentationTimeUs: " + mAudioBufferInfo.presentationTimeUs);
-                }
-            } catch (Exception e) {
-                Logger.e(
-                        TAG,
-                        "audio error:size="
-                                + mAudioBufferInfo.size
-                                + "/offset="
-                                + mAudioBufferInfo.offset
-                                + "/timeUs="
-                                + mAudioBufferInfo.presentationTimeUs);
-                e.printStackTrace();
-            }
-        }
-        mAudioEncoder.releaseOutputBuffer(bufferIndex, false);
-        return (mAudioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
-    }
-
-    /**
-     * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
-     * main thread otherwise it will cause the application to block.
-     *
-     * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
-     */
-    boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
-            @NonNull Size resolution,
-            @NonNull OutputFileOptions outputFileOptions) {
-        // Main encoding loop. Exits on end of stream.
-        boolean errorOccurred = false;
-        boolean videoEos = false;
-        while (!videoEos && !errorOccurred) {
-            // Check for end of stream from main thread
-            if (mEndOfVideoStreamSignal.get()) {
-                mVideoEncoder.signalEndOfInputStream();
-                mEndOfVideoStreamSignal.set(false);
-            }
-
-            // Deque buffer to check for processing step
-            int outputBufferId =
-                    mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
-            switch (outputBufferId) {
-                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
-                    if (mMuxerStarted.get()) {
-                        videoSavedCallback.onError(
-                                ERROR_ENCODER,
-                                "Unexpected change in video encoding format.",
-                                null);
-                        errorOccurred = true;
-                    }
-
-                    synchronized (mMuxerLock) {
-                        mVideoTrackIndex = mMuxer.addTrack(mVideoEncoder.getOutputFormat());
-
-                        if ((mIsAudioEnabled.get() && mAudioTrackIndex >= 0
-                                && mVideoTrackIndex >= 0)
-                                || (!mIsAudioEnabled.get() && mVideoTrackIndex >= 0)) {
-                            Logger.i(TAG, "MediaMuxer started on video encode thread and audio "
-                                    + "enabled: " + mIsAudioEnabled);
-                            mMuxer.start();
-                            mMuxerStarted.set(true);
-                        }
-                    }
-                    break;
-                case MediaCodec.INFO_TRY_AGAIN_LATER:
-                    // Timed out. Just wait until next attempt to deque.
-                    break;
-                default:
-                    videoEos = writeVideoEncodedBuffer(outputBufferId);
-            }
-        }
-
-        try {
-            Logger.i(TAG, "videoEncoder stop");
-            mVideoEncoder.stop();
-        } catch (IllegalStateException e) {
-            videoSavedCallback.onError(ERROR_ENCODER,
-                    "Video encoder stop failed!", e);
-            errorOccurred = true;
-        }
-
-        try {
-            // new MediaMuxer instance required for each new file written, and release current one.
-            synchronized (mMuxerLock) {
-                if (mMuxer != null) {
-                    if (mMuxerStarted.get()) {
-                        Logger.i(TAG, "Muxer already started");
-                        mMuxer.stop();
-                    }
-                    mMuxer.release();
-                    mMuxer = null;
-                }
-            }
-
-            // A final checking for recording result, if the recorded file has no key
-            // frame, then the video file is not playable, needs to call
-            // onError() and will be removed.
-
-            boolean checkResult = removeRecordingResultIfNoVideoKeyFrameArrived(outputFileOptions);
-
-            if (!checkResult) {
-                videoSavedCallback.onError(ERROR_RECORDING_TOO_SHORT,
-                        "The file has no video key frame.", null);
-                errorOccurred = true;
-            }
-        } catch (IllegalStateException e) {
-            // The video encoder has not got the key frame yet.
-            Logger.i(TAG, "muxer stop IllegalStateException: " + System.currentTimeMillis());
-            Logger.i(TAG,
-                    "muxer stop exception, mIsFirstVideoKeyFrameWrite: "
-                            + mIsFirstVideoKeyFrameWrite.get());
-            if (mIsFirstVideoKeyFrameWrite.get()) {
-                // If muxer throws IllegalStateException at this moment and also the key frame
-                // has received, this will reported as a Muxer stop failed.
-                // Otherwise, this error will be ERROR_RECORDING_TOO_SHORT.
-                videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
-            } else {
-                videoSavedCallback.onError(ERROR_RECORDING_TOO_SHORT,
-                        "The file has no video key frame.", null);
-            }
-            errorOccurred = true;
-        }
-
-        if (mParcelFileDescriptor != null) {
-            try {
-                mParcelFileDescriptor.close();
-                mParcelFileDescriptor = null;
-            } catch (IOException e) {
-                videoSavedCallback.onError(ERROR_MUXER, "File descriptor close failed!", e);
-                errorOccurred = true;
-            }
-        }
-
-        mMuxerStarted.set(false);
-
-        // notify the UI thread that the video recording has finished
-        mEndOfAudioVideoSignal.set(true);
-        mIsFirstVideoKeyFrameWrite.set(false);
-
-        Logger.i(TAG, "Video encode thread end.");
-        return errorOccurred;
-    }
-
-    boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
-        // Audio encoding loop. Exits on end of stream.
-        boolean audioEos = false;
-        int outIndex;
-        long lastAudioTimestamp = 0;
-        while (!audioEos && mIsRecording) {
-            // Check for end of stream from main thread
-            if (mEndOfAudioStreamSignal.get()) {
-                mEndOfAudioStreamSignal.set(false);
-                mIsRecording = false;
-            }
-
-            // get audio deque input buffer
-            if (mAudioEncoder != null && mAudioRecorder != null) {
-                try {
-                    int index = mAudioEncoder.dequeueInputBuffer(-1);
-                    if (index >= 0) {
-                        final ByteBuffer buffer = getInputBuffer(mAudioEncoder, index);
-                        buffer.clear();
-                        int length = mAudioRecorder.read(buffer, mAudioBufferSize);
-                        if (length > 0) {
-                            mAudioEncoder.queueInputBuffer(
-                                    index,
-                                    0,
-                                    length,
-                                    (System.nanoTime() / 1000),
-                                    mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
-                        }
-                    }
-                } catch (MediaCodec.CodecException e) {
-                    Logger.i(TAG, "audio dequeueInputBuffer CodecException " + e.getMessage());
-                } catch (IllegalStateException e) {
-                    Logger.i(TAG,
-                            "audio dequeueInputBuffer IllegalStateException " + e.getMessage());
-                }
-
-                // start to dequeue audio output buffer
-                do {
-                    outIndex = mAudioEncoder.dequeueOutputBuffer(mAudioBufferInfo, 0);
-                    switch (outIndex) {
-                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
-                            synchronized (mMuxerLock) {
-                                mAudioTrackIndex = mMuxer.addTrack(mAudioEncoder.getOutputFormat());
-                                if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
-                                    Logger.i(TAG, "MediaMuxer start on audio encoder thread.");
-                                    mMuxer.start();
-                                    mMuxerStarted.set(true);
-                                }
-                            }
-                            break;
-                        case MediaCodec.INFO_TRY_AGAIN_LATER:
-                            break;
-                        default:
-                            // Drops out of order audio frame if the frame's earlier than last
-                            // frame.
-                            if (mAudioBufferInfo.presentationTimeUs > lastAudioTimestamp) {
-                                audioEos = writeAudioEncodedBuffer(outIndex);
-                                lastAudioTimestamp = mAudioBufferInfo.presentationTimeUs;
-                            } else {
-                                Logger.w(TAG,
-                                        "Drops frame, current frame's timestamp "
-                                                + mAudioBufferInfo.presentationTimeUs
-                                                + " is earlier that last frame "
-                                                + lastAudioTimestamp);
-                                // Releases this frame from output buffer
-                                mAudioEncoder.releaseOutputBuffer(outIndex, false);
-                            }
-                    }
-                } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
-            }
-        } // end of while loop
-
-        // Audio Stop
-        try {
-            Logger.i(TAG, "audioRecorder stop");
-            mAudioRecorder.stop();
-        } catch (IllegalStateException e) {
-            videoSavedCallback.onError(
-                    ERROR_ENCODER, "Audio recorder stop failed!", e);
-        }
-
-        try {
-            mAudioEncoder.stop();
-        } catch (IllegalStateException e) {
-            videoSavedCallback.onError(ERROR_ENCODER,
-                    "Audio encoder stop failed!", e);
-        }
-
-        Logger.i(TAG, "Audio encode thread end");
-        // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
-        // safe
-        mEndOfVideoStreamSignal.set(true);
-
-        return false;
-    }
-
-    private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
-        return codec.getInputBuffer(index);
-    }
-
-    private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
-        return codec.getOutputBuffer(index);
-    }
-
-    /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
-    private MediaFormat createAudioMediaFormat() {
-        MediaFormat format =
-                MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
-                        mAudioChannelCount);
-        format.setInteger(
-                MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
-        format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
-
-        return format;
-    }
-
-    /** Create a AudioRecord object to get raw data */
-    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
-    private AudioRecord autoConfigAudioRecordSource(VideoCaptureConfig config) {
-        // Use channel count to determine stereo vs mono
-        int channelConfig =
-                mAudioChannelCount == 1
-                        ? AudioFormat.CHANNEL_IN_MONO
-                        : AudioFormat.CHANNEL_IN_STEREO;
-
-        try {
-            // Use only ENCODING_PCM_16BIT because it mandatory supported.
-            int bufferSize =
-                    AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig,
-                            AudioFormat.ENCODING_PCM_16BIT);
-
-            if (bufferSize <= 0) {
-                bufferSize = config.getAudioMinBufferSize();
-            }
-
-            AudioRecord recorder =
-                    new AudioRecord(
-                            AudioSource.CAMCORDER,
-                            mAudioSampleRate,
-                            channelConfig,
-                            AudioFormat.ENCODING_PCM_16BIT,
-                            bufferSize * 2);
-
-            if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
-                mAudioBufferSize = bufferSize;
-                Logger.i(
-                        TAG,
-                        "source: "
-                                + AudioSource.CAMCORDER
-                                + " audioSampleRate: "
-                                + mAudioSampleRate
-                                + " channelConfig: "
-                                + channelConfig
-                                + " audioFormat: "
-                                + AudioFormat.ENCODING_PCM_16BIT
-                                + " bufferSize: "
-                                + bufferSize);
-                return recorder;
-            }
-        } catch (Exception e) {
-            Logger.e(TAG, "Exception, keep trying.", e);
-        }
-        return null;
-    }
-
-    /** Set audio record parameters by CamcorderProfile */
-    @SuppressWarnings("deprecation")
-    private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
-        CamcorderProfile profile;
-        boolean isCamcorderProfileFound = false;
-
-        try {
-            for (int quality : CamcorderQuality) {
-                if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
-                    profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
-                    if (currentResolution.getWidth() == profile.videoFrameWidth
-                            && currentResolution.getHeight() == profile.videoFrameHeight) {
-                        mAudioChannelCount = profile.audioChannels;
-                        mAudioSampleRate = profile.audioSampleRate;
-                        mAudioBitRate = profile.audioBitRate;
-                        isCamcorderProfileFound = true;
-                        break;
-                    }
-                }
-            }
-        } catch (NumberFormatException e) {
-            Logger.i(TAG, "The camera Id is not an integer because the camera may be a removable "
-                    + "device. Use the default values for the audio related settings.");
-        }
-
-        // In case no corresponding camcorder profile can be founded, * get default value from
-        // VideoCaptureConfig.
-        if (!isCamcorderProfileFound) {
-            VideoCaptureConfig config = (VideoCaptureConfig) getCurrentConfig();
-            mAudioChannelCount = config.getAudioChannelCount();
-            mAudioSampleRate = config.getAudioSampleRate();
-            mAudioBitRate = config.getAudioBitRate();
-        }
-    }
-
-    private boolean removeRecordingResultIfNoVideoKeyFrameArrived(
-            @NonNull OutputFileOptions outputFileOptions) {
-        boolean checkKeyFrame;
-
-        // 1. There should be one video key frame at least.
-        Logger.i(TAG,
-                "check Recording Result First Video Key Frame Write: "
-                        + mIsFirstVideoKeyFrameWrite.get());
-        if (!mIsFirstVideoKeyFrameWrite.get()) {
-            Logger.i(TAG, "The recording result has no key frame.");
-            checkKeyFrame = false;
-        } else {
-            checkKeyFrame = true;
-        }
-
-        // 2. If no key frame, remove file except the target is a file descriptor case.
-        if (outputFileOptions.isSavingToFile()) {
-            File outputFile = outputFileOptions.getFile();
-            if (!checkKeyFrame) {
-                Logger.i(TAG, "Delete file.");
-                outputFile.delete();
-            }
-        } else if (outputFileOptions.isSavingToMediaStore()) {
-            if (!checkKeyFrame) {
-                Logger.i(TAG, "Delete file.");
-                if (mSavedVideoUri != null) {
-                    ContentResolver contentResolver = outputFileOptions.getContentResolver();
-                    contentResolver.delete(mSavedVideoUri, null, null);
-                }
-            }
-        }
-
-        return checkKeyFrame;
-    }
-
-    @NonNull
-    private MediaMuxer initMediaMuxer(@NonNull OutputFileOptions outputFileOptions)
-            throws IOException {
-        MediaMuxer mediaMuxer;
-
-        if (outputFileOptions.isSavingToFile()) {
-            File savedVideoFile = outputFileOptions.getFile();
-            mSavedVideoUri = Uri.fromFile(outputFileOptions.getFile());
-
-            mediaMuxer = new MediaMuxer(savedVideoFile.getAbsolutePath(),
-                    MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
-        } else if (outputFileOptions.isSavingToFileDescriptor()) {
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
-                throw new IllegalArgumentException("Using a FileDescriptor to record a video is "
-                        + "only supported for Android 8.0 or above.");
-            }
-
-            mediaMuxer = Api26Impl.createMediaMuxer(outputFileOptions.getFileDescriptor(),
-                    MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
-        } else if (outputFileOptions.isSavingToMediaStore()) {
-            ContentValues values = outputFileOptions.getContentValues() != null
-                    ? new ContentValues(outputFileOptions.getContentValues())
-                    : new ContentValues();
-
-            mSavedVideoUri = outputFileOptions.getContentResolver().insert(
-                    outputFileOptions.getSaveCollection(), values);
-
-            if (mSavedVideoUri == null) {
-                throw new IOException("Invalid Uri!");
-            }
-
-            // Sine API 26, media muxer could be initiated by a FileDescriptor.
-            try {
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
-                    String savedLocationPath = VideoUtil.getAbsolutePathFromUri(
-                            outputFileOptions.getContentResolver(), mSavedVideoUri);
-
-                    Logger.i(TAG, "Saved Location Path: " + savedLocationPath);
-                    mediaMuxer = new MediaMuxer(savedLocationPath,
-                            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
-                } else {
-                    mParcelFileDescriptor =
-                            outputFileOptions.getContentResolver().openFileDescriptor(
-                                    mSavedVideoUri, "rw");
-                    mediaMuxer = Api26Impl.createMediaMuxer(
-                            mParcelFileDescriptor.getFileDescriptor(),
-                            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
-                }
-            } catch (IOException e) {
-                mSavedVideoUri = null;
-                throw e;
-            }
-        } else {
-            throw new IllegalArgumentException(
-                    "The OutputFileOptions should assign before recording");
-        }
-
-        return mediaMuxer;
-    }
-
-    /**
-     * Describes the error that occurred during video capture operations.
-     *
-     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
-     * VideoCapture.OnVideoSavedCallback#onError(int, String, Throwable)}.
-     *
-     * <p>See message parameter in onError callback or log for more details.
-     *
-     * @hide
-     */
-    @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS,
-            ERROR_FILE_IO, ERROR_INVALID_CAMERA, ERROR_RECORDING_TOO_SHORT})
-    @Retention(RetentionPolicy.SOURCE)
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    public @interface VideoCaptureError {
-    }
-
-    enum VideoEncoderInitStatus {
-        VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED,
-        VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED,
-        VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE,
-        VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED,
-    }
-
-    /** Listener containing callbacks for video file I/O events. */
-    public interface OnVideoSavedCallback {
-        /** Called when the video has been successfully saved. */
-        void onVideoSaved(@NonNull OutputFileResults outputFileResults);
-
-        /** Called when an error occurs while attempting to save the video. */
-        void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
-                @Nullable Throwable cause);
-    }
-
-    /**
-     * Provides a base static default configuration for the VideoCapture
-     *
-     * <p>These values may be overridden by the implementation. They only provide a minimum set of
-     * defaults that are implementation independent.
-     *
-     * @hide
-     */
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    public static final class Defaults
-            implements ConfigProvider<VideoCaptureConfig> {
-        private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
-        /** 8Mb/s the recommend rate for 30fps 1080p */
-        private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
-        /** Seconds between each key frame */
-        private static final int DEFAULT_INTRA_FRAME_INTERVAL = 1;
-        /** audio bit rate */
-        private static final int DEFAULT_AUDIO_BIT_RATE = 64000;
-        /** audio sample rate */
-        private static final int DEFAULT_AUDIO_SAMPLE_RATE = 8000;
-        /** audio channel count */
-        private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
-        /** audio default minimum buffer size */
-        private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
-        /** Current max resolution of VideoCapture is set as FHD */
-        private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
-        /** Surface occupancy priority to this use case */
-        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
-        private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_16_9;
-
-        private static final VideoCaptureConfig DEFAULT_CONFIG;
-
-        static {
-            Builder builder = new Builder()
-                    .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
-                    .setBitRate(DEFAULT_BIT_RATE)
-                    .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
-                    .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
-                    .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
-                    .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
-                    .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
-                    .setMaxResolution(DEFAULT_MAX_RESOLUTION)
-                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
-                    .setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
-
-            DEFAULT_CONFIG = builder.getUseCaseConfig();
-        }
-
-        @NonNull
-        @Override
-        public VideoCaptureConfig getConfig() {
-            return DEFAULT_CONFIG;
-        }
-    }
-
-    /** Holder class for metadata that should be saved alongside captured video. */
-    public static final class Metadata {
-        /** Data representing a geographic location. */
-        @Nullable
-        public Location location;
-    }
-
-    private static final class VideoSavedListenerWrapper implements OnVideoSavedCallback {
-
-        @NonNull
-        Executor mExecutor;
-        @NonNull
-        OnVideoSavedCallback mOnVideoSavedCallback;
-
-        VideoSavedListenerWrapper(@NonNull Executor executor,
-                @NonNull OnVideoSavedCallback onVideoSavedCallback) {
-            mExecutor = executor;
-            mOnVideoSavedCallback = onVideoSavedCallback;
-        }
-
-        @Override
-        public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
-            try {
-                mExecutor.execute(() -> mOnVideoSavedCallback.onVideoSaved(outputFileResults));
-            } catch (RejectedExecutionException e) {
-                Logger.e(TAG, "Unable to post to the supplied executor.");
-            }
-        }
-
-        @Override
-        public void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
-                @Nullable Throwable cause) {
-            try {
-                mExecutor.execute(
-                        () -> mOnVideoSavedCallback.onError(videoCaptureError, message, cause));
-            } catch (RejectedExecutionException e) {
-                Logger.e(TAG, "Unable to post to the supplied executor.");
-            }
-        }
-
-    }
-
-    /** Builder for a {@link VideoCapture}. */
-    @SuppressWarnings("ObjectToString")
-    public static final class Builder
-            implements
-            UseCaseConfig.Builder<VideoCapture, VideoCaptureConfig, Builder>,
-            ImageOutputConfig.Builder<Builder>,
-            ThreadConfig.Builder<Builder> {
-
-        private final MutableOptionsBundle mMutableConfig;
-
-        /** Creates a new Builder object. */
-        public Builder() {
-            this(MutableOptionsBundle.create());
-        }
-
-        private Builder(@NonNull MutableOptionsBundle mutableConfig) {
-            mMutableConfig = mutableConfig;
-
-            Class<?> oldConfigClass =
-                    mutableConfig.retrieveOption(OPTION_TARGET_CLASS, null);
-            if (oldConfigClass != null && !oldConfigClass.equals(VideoCapture.class)) {
-                throw new IllegalArgumentException(
-                        "Invalid target class configuration for "
-                                + Builder.this
-                                + ": "
-                                + oldConfigClass);
-            }
-
-            setTargetClass(VideoCapture.class);
-        }
-
-        /**
-         * Generates a Builder from another Config object.
-         *
-         * @param configuration An immutable configuration to pre-populate this builder.
-         * @return The new Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        static Builder fromConfig(@NonNull Config configuration) {
-            return new Builder(MutableOptionsBundle.from(configuration));
-        }
-
-
-        /**
-         * Generates a Builder from another Config object
-         *
-         * @param configuration An immutable configuration to pre-populate this builder.
-         * @return The new Builder.
-         */
-        @NonNull
-        public static Builder fromConfig(@NonNull VideoCaptureConfig configuration) {
-            return new Builder(MutableOptionsBundle.from(configuration));
-        }
-
-        /**
-         * {@inheritDoc}
-         *
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public MutableConfig getMutableConfig() {
-            return mMutableConfig;
-        }
-
-        /**
-         * {@inheritDoc}
-         *
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public VideoCaptureConfig getUseCaseConfig() {
-            return new VideoCaptureConfig(OptionsBundle.from(mMutableConfig));
-        }
-
-        /**
-         * Builds an immutable {@link VideoCaptureConfig} from the current state.
-         *
-         * @return A {@link VideoCaptureConfig} populated with the current state.
-         */
-        @Override
-        @NonNull
-        public VideoCapture build() {
-            VideoCaptureConfig videoCaptureConfig = getUseCaseConfig();
-            ImageOutputConfig.validateConfig(videoCaptureConfig);
-            return new VideoCapture(videoCaptureConfig);
-        }
-
-        /**
-         * Sets the recording frames per second.
-         *
-         * @param videoFrameRate The requested interval in seconds.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setVideoFrameRate(int videoFrameRate) {
-            getMutableConfig().insertOption(OPTION_VIDEO_FRAME_RATE, videoFrameRate);
-            return this;
-        }
-
-        /**
-         * Sets the encoding bit rate.
-         *
-         * @param bitRate The requested bit rate in bits per second.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setBitRate(int bitRate) {
-            getMutableConfig().insertOption(OPTION_BIT_RATE, bitRate);
-            return this;
-        }
-
-        /**
-         * Sets number of seconds between each key frame in seconds.
-         *
-         * @param interval The requested interval in seconds.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setIFrameInterval(int interval) {
-            getMutableConfig().insertOption(OPTION_INTRA_FRAME_INTERVAL, interval);
-            return this;
-        }
-
-        /**
-         * Sets the bit rate of the audio stream.
-         *
-         * @param bitRate The requested bit rate in bits/s.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setAudioBitRate(int bitRate) {
-            getMutableConfig().insertOption(OPTION_AUDIO_BIT_RATE, bitRate);
-            return this;
-        }
-
-        /**
-         * Sets the sample rate of the audio stream.
-         *
-         * @param sampleRate The requested sample rate in bits/s.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setAudioSampleRate(int sampleRate) {
-            getMutableConfig().insertOption(OPTION_AUDIO_SAMPLE_RATE, sampleRate);
-            return this;
-        }
-
-        /**
-         * Sets the number of audio channels.
-         *
-         * @param channelCount The requested number of audio channels.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setAudioChannelCount(int channelCount) {
-            getMutableConfig().insertOption(OPTION_AUDIO_CHANNEL_COUNT, channelCount);
-            return this;
-        }
-
-        /**
-         * Sets the audio min buffer size.
-         *
-         * @param minBufferSize The requested audio minimum buffer size, in bytes.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setAudioMinBufferSize(int minBufferSize) {
-            getMutableConfig().insertOption(OPTION_AUDIO_MIN_BUFFER_SIZE, minBufferSize);
-            return this;
-        }
-
-        // Implementations of TargetConfig.Builder default methods
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setTargetClass(@NonNull Class<VideoCapture> targetClass) {
-            getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
-
-            // If no name is set yet, then generate a unique name
-            if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
-                String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
-                setTargetName(targetName);
-            }
-
-            return this;
-        }
-
-        /**
-         * Sets the name of the target object being configured, used only for debug logging.
-         *
-         * <p>The name should be a value that can uniquely identify an instance of the object being
-         * configured.
-         *
-         * <p>If not set, the target name will default to an unique name automatically generated
-         * with the class canonical name and random UUID.
-         *
-         * @param targetName A unique string identifier for the instance of the class being
-         *                   configured.
-         * @return the current Builder.
-         */
-        @Override
-        @NonNull
-        public Builder setTargetName(@NonNull String targetName) {
-            getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
-            return this;
-        }
-
-        // Implementations of ImageOutputConfig.Builder default methods
-
-        /**
-         * Sets the aspect ratio of the intended target for images from this configuration.
-         *
-         * <p>It is not allowed to set both target aspect ratio and target resolution on the same
-         * use case.
-         *
-         * <p>The target aspect ratio is used as a hint when determining the resulting output aspect
-         * ratio which may differ from the request, possibly due to device constraints.
-         * Application code should check the resulting output's resolution.
-         *
-         * <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
-         * priority.
-         *
-         * @param aspectRatio A {@link AspectRatio} representing the ratio of the
-         *                    target's width and height.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public Builder setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
-            getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
-            return this;
-        }
-
-        /**
-         * Sets the rotation of the intended target for images from this configuration.
-         *
-         * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
-         * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
-         * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
-         *
-         * <p>If not set, the target rotation will default to the value of
-         * {@link Display#getRotation()} of the default display at the time the use case is
-         * created. The use case is fully created once it has been attached to a camera.
-         *
-         * @param rotation The rotation of the intended target.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public Builder setTargetRotation(@RotationValue int rotation) {
-            getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
-            return this;
-        }
-
-        /**
-         * Sets the resolution of the intended target from this configuration.
-         *
-         * <p>The target resolution attempts to establish a minimum bound for the image resolution.
-         * The actual image resolution will be the closest available resolution in size that is not
-         * smaller than the target resolution, as determined by the Camera implementation. However,
-         * if no resolution exists that is equal to or larger than the target resolution, the
-         * nearest available resolution smaller than the target resolution will be chosen.
-         *
-         * <p>It is not allowed to set both target aspect ratio and target resolution on the same
-         * use case.
-         *
-         * <p>The target aspect ratio will also be set the same as the aspect ratio of the provided
-         * {@link Size}. Make sure to set the target resolution with the correct orientation.
-         *
-         * @param resolution The target resolution to choose from supported output sizes list.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public Builder setTargetResolution(@NonNull Size resolution) {
-            getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
-            return this;
-        }
-
-        /**
-         * Sets the default resolution of the intended target from this configuration.
-         *
-         * @param resolution The default resolution to choose from supported output sizes list.
-         * @return The current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public Builder setDefaultResolution(@NonNull Size resolution) {
-            getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION, resolution);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public Builder setMaxResolution(@NonNull Size resolution) {
-            getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutions) {
-            getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
-            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
-            return this;
-        }
-
-        // Implementations of ThreadConfig.Builder default methods
-
-        /**
-         * Sets the default executor that will be used for background tasks.
-         *
-         * <p>If not set, the background executor will default to an automatically generated
-         * {@link Executor}.
-         *
-         * @param executor The executor which will be used for background tasks.
-         * @return the current Builder.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setBackgroundExecutor(@NonNull Executor executor) {
-            getMutableConfig().insertOption(OPTION_BACKGROUND_EXECUTOR, executor);
-            return this;
-        }
-
-        // Implementations of UseCaseConfig.Builder default methods
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
-            getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
-            getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setSessionOptionUnpacker(
-                @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
-            getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setCaptureOptionUnpacker(
-                @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
-            getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setSurfaceOccupancyPriority(int priority) {
-            getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY)
-        @Override
-        @NonNull
-        public Builder setCameraSelector(@NonNull CameraSelector cameraSelector) {
-            getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @Override
-        @NonNull
-        public Builder setUseCaseEventCallback(
-                @NonNull UseCase.EventCallback useCaseEventCallback) {
-            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public Builder setZslDisabled(boolean disabled) {
-            getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
-            return this;
-        }
-
-        /** @hide */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        @Override
-        public Builder setHighResolutionDisabled(boolean disabled) {
-            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
-            return this;
-        }
-    }
-
-    /**
-     * Info about the saved video file.
-     */
-    public static class OutputFileResults {
-        @Nullable
-        private Uri mSavedUri;
-
-        OutputFileResults(@Nullable Uri savedUri) {
-            mSavedUri = savedUri;
-        }
-
-        /**
-         * Returns the {@link Uri} of the saved video file.
-         *
-         * <p> This field is only returned if the {@link VideoCapture.OutputFileOptions} is
-         * backed by {@link MediaStore} constructed with
-         * {@link androidx.camera.core.VideoCapture.OutputFileOptions}.
-         */
-        @Nullable
-        public Uri getSavedUri() {
-            return mSavedUri;
-        }
-    }
-
-    /**
-     * Options for saving newly captured video.
-     *
-     * <p> this class is used to configure save location and metadata. Save location can be
-     * either a {@link File}, {@link MediaStore}. The metadata will be
-     * stored with the saved video.
-     */
-    public static final class OutputFileOptions {
-
-        // Empty metadata object used as a placeholder for no user-supplied metadata.
-        // Should be initialized to all default values.
-        private static final Metadata EMPTY_METADATA = new Metadata();
-
-        @Nullable
-        private final File mFile;
-        @Nullable
-        private final FileDescriptor mFileDescriptor;
-        @Nullable
-        private final ContentResolver mContentResolver;
-        @Nullable
-        private final Uri mSaveCollection;
-        @Nullable
-        private final ContentValues mContentValues;
-        @Nullable
-        private final Metadata mMetadata;
-
-        OutputFileOptions(@Nullable File file,
-                @Nullable FileDescriptor fileDescriptor,
-                @Nullable ContentResolver contentResolver,
-                @Nullable Uri saveCollection,
-                @Nullable ContentValues contentValues,
-                @Nullable Metadata metadata) {
-            mFile = file;
-            mFileDescriptor = fileDescriptor;
-            mContentResolver = contentResolver;
-            mSaveCollection = saveCollection;
-            mContentValues = contentValues;
-            mMetadata = metadata == null ? EMPTY_METADATA : metadata;
-        }
-
-        /** Returns the File object which is set by the {@link OutputFileOptions.Builder}. */
-        @Nullable
-        File getFile() {
-            return mFile;
-        }
-
-        /**
-         * Returns the FileDescriptor object which is set by the {@link OutputFileOptions.Builder}.
-         */
-        @Nullable
-        FileDescriptor getFileDescriptor() {
-            return mFileDescriptor;
-        }
-
-        /** Returns the content resolver which is set by the {@link OutputFileOptions.Builder}. */
-        @Nullable
-        ContentResolver getContentResolver() {
-            return mContentResolver;
-        }
-
-        /** Returns the URI which is set by the {@link OutputFileOptions.Builder}. */
-        @Nullable
-        Uri getSaveCollection() {
-            return mSaveCollection;
-        }
-
-        /** Returns the content values which is set by the {@link OutputFileOptions.Builder}. */
-        @Nullable
-        ContentValues getContentValues() {
-            return mContentValues;
-        }
-
-        /** Return the metadata which is set by the {@link OutputFileOptions.Builder}.. */
-        @Nullable
-        Metadata getMetadata() {
-            return mMetadata;
-        }
-
-        /** Checking the caller wants to save video to MediaStore. */
-        boolean isSavingToMediaStore() {
-            return getSaveCollection() != null && getContentResolver() != null
-                    && getContentValues() != null;
-        }
-
-        /** Checking the caller wants to save video to a File. */
-        boolean isSavingToFile() {
-            return getFile() != null;
-        }
-
-        /** Checking the caller wants to save video to a FileDescriptor. */
-        boolean isSavingToFileDescriptor() {
-            return getFileDescriptor() != null;
-        }
-
-        /**
-         * Builder class for {@link OutputFileOptions}.
-         */
-        public static final class Builder {
-            @Nullable
-            private File mFile;
-            @Nullable
-            private FileDescriptor mFileDescriptor;
-            @Nullable
-            private ContentResolver mContentResolver;
-            @Nullable
-            private Uri mSaveCollection;
-            @Nullable
-            private ContentValues mContentValues;
-            @Nullable
-            private Metadata mMetadata;
-
-            /**
-             * Creates options to write captured video to a {@link File}.
-             *
-             * @param file save location of the video.
-             */
-            public Builder(@NonNull File file) {
-                mFile = file;
-            }
-
-            /**
-             * Creates options to write captured video to a {@link FileDescriptor}.
-             *
-             * <p>Using a FileDescriptor to record a video is only supported for Android 8.0 or
-             * above.
-             *
-             * @param fileDescriptor to save the video.
-             * @throws IllegalArgumentException when the device is not running Android 8.0 or above.
-             */
-            public Builder(@NonNull FileDescriptor fileDescriptor) {
-                Preconditions.checkArgument(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
-                        "Using a FileDescriptor to record a video is only supported for Android 8"
-                                + ".0 or above.");
-
-                mFileDescriptor = fileDescriptor;
-            }
-
-            /**
-             * Creates options to write captured video to {@link MediaStore}.
-             *
-             * Example:
-             *
-             * <pre>{@code
-             *
-             * ContentValues contentValues = new ContentValues();
-             * contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
-             * contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
-             *
-             * OutputFileOptions options = new OutputFileOptions.Builder(
-             *         getContentResolver(),
-             *         MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-             *         contentValues).build();
-             *
-             * }</pre>
-             *
-             * @param contentResolver to access {@link MediaStore}
-             * @param saveCollection  The URL of the table to insert into.
-             * @param contentValues   to be included in the created video file.
-             */
-            public Builder(@NonNull ContentResolver contentResolver,
-                    @NonNull Uri saveCollection,
-                    @NonNull ContentValues contentValues) {
-                mContentResolver = contentResolver;
-                mSaveCollection = saveCollection;
-                mContentValues = contentValues;
-            }
-
-            /**
-             * Sets the metadata to be stored with the saved video.
-             *
-             * @param metadata Metadata to be stored with the saved video.
-             */
-            @NonNull
-            public Builder setMetadata(@NonNull Metadata metadata) {
-                mMetadata = metadata;
-                return this;
-            }
-
-            /**
-             * Builds {@link OutputFileOptions}.
-             */
-            @NonNull
-            public OutputFileOptions build() {
-                return new OutputFileOptions(mFile, mFileDescriptor, mContentResolver,
-                        mSaveCollection, mContentValues, mMetadata);
-            }
-        }
-    }
-
-    /**
-     * Nested class to avoid verification errors for methods introduced in Android 8.0 (API 26).
-     */
-    @RequiresApi(26)
-    private static class Api26Impl {
-
-        private Api26Impl() {
-        }
-
-        @DoNotInline
-        @NonNull
-        static MediaMuxer createMediaMuxer(@NonNull FileDescriptor fileDescriptor, int format)
-                throws IOException {
-            return new MediaMuxer(fileDescriptor, format);
-        }
-    }
-
-    /**
-     * Nested class to avoid verification errors for methods introduced in Android 6.0 (API 23).
-     */
-    @RequiresApi(23)
-    private static class Api23Impl {
-
-        private Api23Impl() {
-        }
-
-        @DoNotInline
-        static int getCodecExceptionErrorCode(MediaCodec.CodecException e) {
-            return e.getErrorCode();
-        }
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
index c7daab2..3113c4d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
@@ -17,31 +17,26 @@
 package androidx.camera.core.imagecapture;
 
 import static android.graphics.ImageFormat.JPEG;
-import static android.graphics.ImageFormat.NV21;
 import static android.graphics.ImageFormat.YUV_420_888;
 
 import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN;
 import static androidx.camera.core.impl.utils.Exif.createFromInputStream;
 import static androidx.camera.core.impl.utils.TransformUtils.updateSensorToBufferTransform;
 import static androidx.camera.core.internal.utils.ImageUtil.jpegImageToJpegByteArray;
-import static androidx.camera.core.internal.utils.ImageUtil.yuv_420_888toNv21;
 
-import static java.nio.ByteBuffer.allocateDirect;
 import static java.util.Objects.requireNonNull;
 
 import android.graphics.Rect;
-import android.graphics.YuvImage;
 import android.os.Build;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.impl.utils.Exif;
-import androidx.camera.core.impl.utils.ExifData;
-import androidx.camera.core.impl.utils.ExifOutputStream;
-import androidx.camera.core.internal.ByteBufferOutputStream;
+import androidx.camera.core.internal.utils.ImageUtil;
 import androidx.camera.core.processing.Operation;
 import androidx.camera.core.processing.Packet;
 
@@ -49,8 +44,6 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
 
 /**
  * Converts a {@link ImageProxy} to JPEG bytes.
@@ -95,16 +88,17 @@
         ImageProxy image = packet.getData();
         Rect cropRect = packet.getCropRect();
 
-        // Converts YUV_420_888 to NV21.
-        byte[] yuvBytes = yuv_420_888toNv21(image);
-        YuvImage yuvImage = new YuvImage(yuvBytes, NV21, image.getWidth(), image.getHeight(), null);
-
-        // Compress NV21 to JPEG and crop.
-        ByteBuffer buffer = allocateDirect(cropRect.width() * cropRect.height() * 2);
-        OutputStream outputStream = new ExifOutputStream(new ByteBufferOutputStream(buffer),
-                ExifData.create(image, packet.getRotationDegrees()));
-        yuvImage.compressToJpeg(cropRect, input.getJpegQuality(), outputStream);
-        byte[] jpegBytes = byteBufferToByteArray(buffer);
+        byte[] jpegBytes;
+        try {
+            jpegBytes = ImageUtil.yuvImageToJpegByteArray(
+                    image,
+                    cropRect,
+                    input.getJpegQuality(),
+                    packet.getRotationDegrees());
+        } catch (ImageUtil.CodecFailedException e) {
+            throw new ImageCaptureException(ImageCapture.ERROR_FILE_IO,
+                    "Failed to encode the image to JPEG.", e);
+        }
 
         // Return bytes with a new format, size, and crop rect.
         return Packet.of(
@@ -118,14 +112,6 @@
                 packet.getCameraCaptureResult());
     }
 
-    private static byte[] byteBufferToByteArray(@NonNull ByteBuffer buffer) {
-        int jpegSize = buffer.position();
-        byte[] bytes = new byte[jpegSize];
-        buffer.rewind();
-        buffer.get(bytes, 0, jpegSize);
-        return bytes;
-    }
-
     private static Exif extractExif(@NonNull byte[] jpegBytes) throws ImageCaptureException {
         try {
             return createFromInputStream(new ByteArrayInputStream(jpegBytes));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
index a9a7602..eb45dd8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
@@ -47,8 +47,6 @@
             Option.create("camerax.core.imageCapture.flashMode", int.class);
     public static final Option<CaptureBundle> OPTION_CAPTURE_BUNDLE =
             Option.create("camerax.core.imageCapture.captureBundle", CaptureBundle.class);
-    public static final Option<CaptureProcessor> OPTION_CAPTURE_PROCESSOR =
-            Option.create("camerax.core.imageCapture.captureProcessor", CaptureProcessor.class);
     public static final Option<Integer> OPTION_BUFFER_FORMAT =
             Option.create("camerax.core.imageCapture.bufferFormat", Integer.class);
     public static final Option<Integer> OPTION_MAX_CAPTURE_STAGES =
@@ -144,29 +142,6 @@
     }
 
     /**
-     * Returns the {@link CaptureProcessor}.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    @Nullable
-    public CaptureProcessor getCaptureProcessor(@Nullable CaptureProcessor valueIfMissing) {
-        return retrieveOption(OPTION_CAPTURE_PROCESSOR, valueIfMissing);
-    }
-
-    /**
-     * Returns the {@link CaptureProcessor}.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    @NonNull
-    public CaptureProcessor getCaptureProcessor() {
-        return retrieveOption(OPTION_CAPTURE_PROCESSOR);
-    }
-
-    /**
      * Returns the {@link ImageFormat} of the capture in memory.
      *
      * @param valueIfMissing The value to return if this configuration option has not been set.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
index 6d235ab..6a3ee7a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
@@ -17,7 +17,6 @@
 package androidx.camera.core.impl;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Preview;
 import androidx.camera.core.internal.ThreadConfig;
@@ -32,13 +31,6 @@
         ThreadConfig {
 
     // Options declarations
-
-    public static final Option<ImageInfoProcessor> IMAGE_INFO_PROCESSOR = Option.create(
-            "camerax.core.preview.imageInfoProcessor", ImageInfoProcessor.class);
-    public static final Option<CaptureProcessor> OPTION_PREVIEW_CAPTURE_PROCESSOR =
-            Option.create("camerax.core.preview.captureProcessor", CaptureProcessor.class);
-    public static final Option<Boolean> OPTION_RGBA8888_SURFACE_REQUIRED =
-            Option.create("camerax.core.preview.isRgba8888SurfaceRequired", Boolean.class);
     private final OptionsBundle mConfig;
 
     /** Creates a new configuration instance. */
@@ -53,50 +45,6 @@
     }
 
     /**
-     * Returns the {@link ImageInfoProcessor}.
-     *
-     * @return The stored value, if it exists in this configuration.
-     */
-    @Nullable
-    public ImageInfoProcessor getImageInfoProcessor(@Nullable ImageInfoProcessor valueIfMissing) {
-        return retrieveOption(IMAGE_INFO_PROCESSOR, valueIfMissing);
-    }
-
-    @NonNull
-    ImageInfoProcessor getImageInfoProcessor() {
-        return retrieveOption(IMAGE_INFO_PROCESSOR);
-    }
-
-    /**
-     * Returns the {@link CaptureProcessor}.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    @Nullable
-    public CaptureProcessor getCaptureProcessor(@Nullable CaptureProcessor valueIfMissing) {
-        return retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, valueIfMissing);
-    }
-
-    /**
-     * Returns the {@link CaptureProcessor}.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    @NonNull
-    public CaptureProcessor getCaptureProcessor() {
-        return retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR);
-    }
-
-    /**
-     * Returns if the preview surface requires RGBA8888 format.
-     */
-    public boolean isRgba8888SurfaceRequired(boolean valueIfMissing) {
-        return retrieveOption(OPTION_RGBA8888_SURFACE_REQUIRED, valueIfMissing);
-    }
-    /**
      * Retrieves the format of the image that is fed as input.
      *
      * <p>This should be YUV_420_888, when processing is run on the image. Otherwise it is PRIVATE.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
index ef6c237..e47c72a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
@@ -97,30 +97,30 @@
      * Callback to be invoked during the capture.
      */
     interface Callback {
-        void onCaptureStarted(
+        default void onCaptureStarted(
                 @NonNull Request request,
                 long frameNumber,
-                long timestamp);
+                long timestamp) {}
 
-        void onCaptureProgressed(
+        default void onCaptureProgressed(
                 @NonNull Request request,
-                @NonNull CameraCaptureResult captureResult);
+                @NonNull CameraCaptureResult captureResult) {}
 
-        void onCaptureCompleted(
+        default void onCaptureCompleted(
                 @NonNull Request request,
-                @NonNull CameraCaptureResult captureResult);
+                @NonNull CameraCaptureResult captureResult) {}
 
-        void onCaptureFailed(
+        default void onCaptureFailed(
                 @NonNull Request request,
-                @NonNull CameraCaptureFailure captureFailure);
+                @NonNull CameraCaptureFailure captureFailure) {}
 
-        void onCaptureBufferLost(
+        default void onCaptureBufferLost(
                 @NonNull Request request,
                 long frameNumber,
-                int outputConfigId);
+                int outputConfigId) {}
 
-        void onCaptureSequenceCompleted(int sequenceId, long frameNumber);
+        default void onCaptureSequenceCompleted(int sequenceId, long frameNumber) {}
 
-        void onCaptureSequenceAborted(int sequenceId);
+        default void onCaptureSequenceAborted(int sequenceId) {}
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
index bb86a51..f9858f5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
@@ -120,6 +120,13 @@
     void abortCapture(int captureSequenceId);
 
     /**
+     * Sends trigger-type single request such as AF/AE triggers.
+     */
+    default int startTrigger(@NonNull Config config, @NonNull CaptureCallback callback) {
+        return -1;
+    }
+
+    /**
      * Callback for {@link #startRepeating} and {@link #startCapture}.
      */
     interface CaptureCallback {
@@ -136,7 +143,7 @@
          *                          request or the timestamp at start of capture of the
          *                          first frame in a multi-frame capture, in nanoseconds.
          */
-        void onCaptureStarted(int captureSequenceId, long timestamp);
+        default void onCaptureStarted(int captureSequenceId, long timestamp) {}
 
         /**
          * This method is called when an image (or images in case of multi-frame
@@ -144,7 +151,7 @@
          *
          * @param captureSequenceId id of the current capture sequence
          */
-        void onCaptureProcessStarted(int captureSequenceId);
+        default void onCaptureProcessStarted(int captureSequenceId) {}
 
         /**
          * This method is called instead of {@link #onCaptureProcessStarted} when the camera
@@ -153,7 +160,7 @@
          *
          * @param captureSequenceId id of the current capture sequence
          */
-        void onCaptureFailed(int captureSequenceId);
+        default void onCaptureFailed(int captureSequenceId) {}
 
         /**
          * This method is called independently of the others in the CaptureCallback, when a capture
@@ -166,14 +173,14 @@
          *
          * @param captureSequenceId id of the current capture sequence
          */
-        void onCaptureSequenceCompleted(int captureSequenceId);
+        default void onCaptureSequenceCompleted(int captureSequenceId) {}
 
         /**
          * This method is called when a capture sequence aborts.
          *
          * @param captureSequenceId id of the current capture sequence
          */
-        void onCaptureSequenceAborted(int captureSequenceId);
+        default void onCaptureSequenceAborted(int captureSequenceId) {}
 
         /**
          * Capture result callback that needs to be called when the process capture results are
@@ -197,7 +204,7 @@
          *                             that those two settings and results are always supported and
          *                             applied by the corresponding framework.
          */
-        void onCaptureCompleted(long timestamp, int captureSequenceId,
-                @NonNull Map<CaptureResult.Key, Object> result);
+        default void onCaptureCompleted(long timestamp, int captureSequenceId,
+                @NonNull Map<CaptureResult.Key, Object> result) {}
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java
deleted file mode 100644
index db1c480..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core.impl;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.internal.ThreadConfig;
-
-/**
- * Config for a video capture use case.
- *
- * <p>In the earlier stage, the VideoCapture is deprioritized.
- */
-@SuppressWarnings("deprecation")
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class VideoCaptureConfig
-        implements UseCaseConfig<androidx.camera.core.VideoCapture>,
-        ImageOutputConfig,
-        ThreadConfig {
-
-    // Option Declarations:
-    // *********************************************************************************************
-
-    public static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
-            Option.create("camerax.core.videoCapture.recordingFrameRate", int.class);
-    public static final Option<Integer> OPTION_BIT_RATE =
-            Option.create("camerax.core.videoCapture.bitRate", int.class);
-    public static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
-            Option.create("camerax.core.videoCapture.intraFrameInterval", int.class);
-    public static final Option<Integer> OPTION_AUDIO_BIT_RATE =
-            Option.create("camerax.core.videoCapture.audioBitRate", int.class);
-    public static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
-            Option.create("camerax.core.videoCapture.audioSampleRate", int.class);
-    public static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
-            Option.create("camerax.core.videoCapture.audioChannelCount", int.class);
-    public static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
-            Option.create("camerax.core.videoCapture.audioMinBufferSize", int.class);
-
-    // *********************************************************************************************
-
-    private final OptionsBundle mConfig;
-
-    public VideoCaptureConfig(@NonNull OptionsBundle config) {
-        mConfig = config;
-    }
-
-    /**
-     * Returns the recording frames per second.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    public int getVideoFrameRate(int valueIfMissing) {
-        return retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
-    }
-
-    /**
-     * Returns the recording frames per second.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    public int getVideoFrameRate() {
-        return retrieveOption(OPTION_VIDEO_FRAME_RATE);
-    }
-
-    /**
-     * Returns the encoding bit rate.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    public int getBitRate(int valueIfMissing) {
-        return retrieveOption(OPTION_BIT_RATE, valueIfMissing);
-    }
-
-    /**
-     * Returns the encoding bit rate.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    public int getBitRate() {
-        return retrieveOption(OPTION_BIT_RATE);
-    }
-
-    /**
-     * Returns the number of seconds between each key frame.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    public int getIFrameInterval(int valueIfMissing) {
-        return retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
-    }
-
-    /**
-     * Returns the number of seconds between each key frame.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    public int getIFrameInterval() {
-        return retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
-    }
-
-    /**
-     * Returns the audio encoding bit rate.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    public int getAudioBitRate(int valueIfMissing) {
-        return retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
-    }
-
-    /**
-     * Returns the audio encoding bit rate.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    public int getAudioBitRate() {
-        return retrieveOption(OPTION_AUDIO_BIT_RATE);
-    }
-
-    /**
-     * Returns the audio sample rate.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    public int getAudioSampleRate(int valueIfMissing) {
-        return retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
-    }
-
-    /**
-     * Returns the audio sample rate.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    public int getAudioSampleRate() {
-        return retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
-    }
-
-    /**
-     * Returns the audio channel count.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    public int getAudioChannelCount(int valueIfMissing) {
-        return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
-    }
-
-    /**
-     * Returns the audio channel count.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    public int getAudioChannelCount() {
-        return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
-    }
-
-    /**
-     * Returns the audio minimum buffer size, in bytes.
-     *
-     * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
-     * configuration.
-     */
-    public int getAudioMinBufferSize(int valueIfMissing) {
-        return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
-    }
-
-    /**
-     * Returns the audio minimum buffer size, in bytes.
-     *
-     * @return The stored value, if it exists in this configuration.
-     * @throws IllegalArgumentException if the option does not exist in this configuration.
-     */
-    public int getAudioMinBufferSize() {
-        return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
-    }
-
-    /**
-     * Retrieves the format of the image that is fed as input.
-     *
-     * <p>This should always be PRIVATE for VideoCapture.
-     */
-    @Override
-    public int getInputFormat() {
-        return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
-    }
-
-    @NonNull
-    @Override
-    public Config getConfig() {
-        return mConfig;
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
index f8ebadf..36330a5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
@@ -308,7 +308,9 @@
     public static ExifData create(@NonNull ImageProxy imageProxy,
             @ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
         ExifData.Builder builder = ExifData.builderForDevice();
-        imageProxy.getImageInfo().populateExifData(builder);
+        if (imageProxy.getImageInfo() != null) {
+            imageProxy.getImageInfo().populateExifData(builder);
+        }
 
         // Overwrites the orientation degrees value of the output image because the capture
         // results might not have correct value when capturing image in YUV_420_888 format. See
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 75679d4..081ccc1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.internal.utils.ImageUtil;
 import androidx.core.util.Preconditions;
 
 import java.util.Locale;
@@ -36,7 +37,7 @@
  * {@link RectF}, a rotation degrees integer and a boolean flag for the rotation-direction
  * (clockwise v.s. counter-clockwise).
  *
- * TODO(b/179827713): merge this with {@link androidx.camera.core.internal.utils.ImageUtil}.
+ * TODO(b/179827713): merge this with {@link ImageUtil}.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class TransformUtils {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
index 9cce3f6..5d79905 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
@@ -60,10 +60,8 @@
         });
     }
 
-    @SuppressWarnings("deprecation")
     private int getSurfacePriority(@NonNull DeferrableSurface surface) {
-        if (surface.getContainerClass() == MediaCodec.class
-                || surface.getContainerClass() == androidx.camera.core.VideoCapture.class) {
+        if (surface.getContainerClass() == MediaCodec.class) {
             return PRIORITY_MEDIA_CODEC_SURFACE;
         } else if (surface.getContainerClass() == Preview.class) {
             return PRIORITY_PREVIEW_SURFACE;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
index 9c8101f..925e953 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -37,9 +37,12 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.ExifData;
+import androidx.camera.core.impl.utils.ExifOutputStream;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.ByteBuffer;
 
 /**
@@ -163,23 +166,37 @@
     /**
      * Converts YUV_420_888 {@link ImageProxy} to JPEG byte array. The input YUV_420_888 image
      * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
-     * be compressed by the specified quality value.
+     * be compressed by the specified quality value. The rotationDegrees is set to the EXIF of
+     * the JPEG if it is not 0.
      */
     @NonNull
     public static byte[] yuvImageToJpegByteArray(@NonNull ImageProxy image,
-            @Nullable Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)
-            throws CodecFailedException {
+            @Nullable Rect cropRect,
+            @IntRange(from = 1, to = 100)
+            int jpegQuality,
+            int rotationDegrees) throws CodecFailedException {
         if (image.getFormat() != ImageFormat.YUV_420_888) {
             throw new IllegalArgumentException(
                     "Incorrect image format of the input image proxy: " + image.getFormat());
         }
 
-        return ImageUtil.nv21ToJpeg(
-                ImageUtil.yuv_420_888toNv21(image),
-                image.getWidth(),
-                image.getHeight(),
-                cropRect,
-                jpegQuality);
+        byte[] yuvBytes = yuv_420_888toNv21(image);
+        YuvImage yuv = new YuvImage(yuvBytes, ImageFormat.NV21, image.getWidth(), image.getHeight(),
+                null);
+
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        OutputStream out = new ExifOutputStream(
+                byteArrayOutputStream, ExifData.create(image, rotationDegrees));
+        if (cropRect == null) {
+            cropRect = new Rect(0, 0, image.getWidth(), image.getHeight());
+        }
+        boolean success =
+                yuv.compressToJpeg(cropRect, jpegQuality, out);
+        if (!success) {
+            throw new CodecFailedException("YuvImage failed to encode jpeg.",
+                    CodecFailedException.FailureType.ENCODE_FAILED);
+        }
+        return byteArrayOutputStream.toByteArray();
     }
 
     /** {@link android.media.Image} to NV21 byte array. */
@@ -364,21 +381,6 @@
         return dispatchCropRect;
     }
 
-    private static byte[] nv21ToJpeg(@NonNull byte[] nv21, int width, int height,
-            @Nullable Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)
-            throws CodecFailedException {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
-        boolean success =
-                yuv.compressToJpeg(cropRect == null ? new Rect(0, 0, width, height) : cropRect,
-                        jpegQuality, out);
-        if (!success) {
-            throw new CodecFailedException("YuvImage failed to encode jpeg.",
-                    CodecFailedException.FailureType.ENCODE_FAILED);
-        }
-        return out.toByteArray();
-    }
-
     private static boolean isCropAspectRatioHasEffect(@NonNull Size sourceSize,
             @NonNull Rational aspectRatio) {
         int sourceWidth = sourceSize.getWidth();
@@ -423,7 +425,7 @@
             UNKNOWN
         }
 
-        private FailureType mFailureType;
+        private final FailureType mFailureType;
 
         CodecFailedException(@NonNull String message) {
             super(message);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index efaa57f..de60c70 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -35,6 +35,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.core.ProcessingException;
 import androidx.camera.core.SurfaceOutput;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
@@ -59,6 +61,8 @@
 @SuppressWarnings("UnusedVariable")
 public class SurfaceProcessorNode implements Node<SurfaceEdge, SurfaceEdge> {
 
+    private static final String TAG = "SurfaceProcessorNode";
+
     @NonNull
     final SurfaceProcessorInternal mSurfaceProcessor;
     @NonNull
@@ -153,8 +157,12 @@
                     @Override
                     public void onSuccess(@Nullable SurfaceOutput surfaceOutput) {
                         Preconditions.checkNotNull(surfaceOutput);
-                        mSurfaceProcessor.onOutputSurface(surfaceOutput);
-                        mSurfaceProcessor.onInputSurface(surfaceRequest);
+                        try {
+                            mSurfaceProcessor.onOutputSurface(surfaceOutput);
+                            mSurfaceProcessor.onInputSurface(surfaceRequest);
+                        } catch (ProcessingException e) {
+                            Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
+                        }
                     }
 
                     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
index 87a8644..3873031 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
@@ -18,8 +18,13 @@
 
 import static androidx.core.util.Preconditions.checkState;
 
+import android.os.Build;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.Logger;
+import androidx.camera.core.ProcessingException;
 import androidx.camera.core.SurfaceOutput;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
@@ -33,8 +38,11 @@
  * makes sure that CameraX always invoke the {@link SurfaceProcessor} on the correct
  * {@link Executor}.
  */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 public class SurfaceProcessorWithExecutor implements SurfaceProcessorInternal {
 
+    private static final String TAG = "SurfaceProcessor";
+
     @NonNull
     private final SurfaceProcessor mSurfaceProcessor;
     @NonNull
@@ -63,12 +71,24 @@
 
     @Override
     public void onInputSurface(@NonNull SurfaceRequest request) {
-        mExecutor.execute(() -> mSurfaceProcessor.onInputSurface(request));
+        mExecutor.execute(() -> {
+            try {
+                mSurfaceProcessor.onInputSurface(request);
+            } catch (ProcessingException e) {
+                Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
+            }
+        });
     }
 
     @Override
     public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
-        mExecutor.execute(() -> mSurfaceProcessor.onOutputSurface(surfaceOutput));
+        mExecutor.execute(() -> {
+            try {
+                mSurfaceProcessor.onOutputSurface(surfaceOutput);
+            } catch (ProcessingException e) {
+                Logger.e(TAG, "Failed to setup SurfaceProcessor output.", e);
+            }
+        });
     }
 
     @Override
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/CaptureProcessorPipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/CaptureProcessorPipelineTest.kt
deleted file mode 100644
index c13e598..0000000
--- a/camera/camera-core/src/test/java/androidx/camera/core/CaptureProcessorPipelineTest.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.os.Build
-import android.util.Size
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.concurrent.futures.CallbackToFutureAdapter
-import com.google.common.truth.Truth
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-
-@RunWith(RobolectricTestRunner::class)
-@DoNotInstrument
-@Config(
-    minSdk = Build.VERSION_CODES.LOLLIPOP
-)
-class CaptureProcessorPipelineTest {
-    companion object {
-        private val DEFAULT_SIZE = Size(640, 480)
-    }
-
-    @Test
-    fun canCloseUnderlyingCaptureProcessors() {
-        // Sets up pre-processor
-        val preProcessor = Mockito.mock(
-            CaptureProcessor::class.java
-        )
-        var preProcessorCompleter: CallbackToFutureAdapter.Completer<Void>? = null
-        val preProcessorCloseFuture =
-            CallbackToFutureAdapter.getFuture<Void> { completer ->
-                preProcessorCompleter = completer
-                "preProcessorCloseFuture"
-            }
-        Mockito.`when`(preProcessor.closeFuture).thenReturn(preProcessorCloseFuture)
-
-        // Sets up post-processor
-        val postProcessor = Mockito.mock(
-            CaptureProcessor::class.java
-        )
-        var postProcessorCompleter: CallbackToFutureAdapter.Completer<Void>? = null
-        val postProcessorCloseFuture =
-            CallbackToFutureAdapter.getFuture<Void> { completer ->
-                postProcessorCompleter = completer
-                "postProcessorCloseFuture"
-            }
-        Mockito.`when`(postProcessor.closeFuture).thenReturn(postProcessorCloseFuture)
-
-        val captureProcessorPipeline = CaptureProcessorPipeline(
-            preProcessor,
-            2,
-            postProcessor,
-            Executors.newSingleThreadExecutor()
-        )
-
-        // Sets up the resolution to create the intermediate image reader
-        captureProcessorPipeline.onResolutionUpdate(DEFAULT_SIZE)
-
-        // Calls the close() function of the CaptureProcessorPipeline
-        captureProcessorPipeline.close()
-
-        // Verifies whether close() function of the underlying capture processors are called
-        Mockito.verify(preProcessor, Mockito.times(1)).close()
-        Mockito.verify(postProcessor, Mockito.times(1)).close()
-
-        // Sets up the listener to monitor whether the close future is closed or not.
-        val closedLatch = CountDownLatch(1)
-        captureProcessorPipeline.closeFuture.addListener(
-            { closedLatch.countDown() },
-            CameraXExecutors.directExecutor()
-        )
-
-        // Checks that the close future is not completed before the underlying capture processor
-        // complete their close futures
-        Truth.assertThat(closedLatch.await(1000, TimeUnit.MILLISECONDS)).isFalse()
-
-        // Completes the completer of the underlying capture processors to complete their close
-        // futures
-        preProcessorCompleter!!.set(null)
-        postProcessorCompleter!!.set(null)
-
-        // Checks whether the close future of CaptureProcessorPipeline is completed after the
-        // underlying capture processors complete their close futures
-        Truth.assertThat(closedLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index 8bfe101..2eae4e7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -25,16 +25,17 @@
 import android.os.Looper.getMainLooper
 import android.util.Pair
 import android.util.Rational
-import android.util.Size
 import android.view.Surface
 import androidx.camera.core.ImageCapture.ImageCaptureRequest
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
+import androidx.camera.core.impl.CameraConfig
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.ImageProxyBundle
+import androidx.camera.core.impl.Identifier
+import androidx.camera.core.impl.OptionsBundle
 import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionProcessor
 import androidx.camera.core.impl.TagBundle
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -51,6 +52,7 @@
 import androidx.camera.testing.fakes.FakeImageInfo
 import androidx.camera.testing.fakes.FakeImageProxy
 import androidx.camera.testing.fakes.FakeImageReaderProxy
+import androidx.camera.testing.fakes.FakeSessionProcessor
 import androidx.concurrent.futures.ResolvableFuture
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
@@ -258,21 +260,14 @@
         ).isFalse()
     }
 
+    @Config(minSdk = 28)
     @Test
     fun extensionIsOn_pipelineDisabled() {
         assertThat(
             bindImageCapture(
                 useProcessingPipeline = true,
-                captureProcessor = object : CaptureProcessor {
-                    override fun onOutputSurface(surface: Surface, imageFormat: Int) {
-                    }
-
-                    override fun process(bundle: ImageProxyBundle) {
-                    }
-
-                    override fun onResolutionUpdate(size: Size) {
-                    }
-                }
+                bufferFormat = ImageFormat.JPEG,
+                sessionProcessor = FakeSessionProcessor(null, null)
             ).isProcessingPipelineEnabled
         ).isFalse()
     }
@@ -616,14 +611,13 @@
         bufferFormat: Int = ImageFormat.YUV_420_888,
         imageReaderProxyProvider: ImageReaderProxyProvider? = null,
         useProcessingPipeline: Boolean? = null,
-        captureProcessor: CaptureProcessor? = null
+        sessionProcessor: SessionProcessor? = null
     ): ImageCapture {
         // Arrange.
         val imageCapture = createImageCapture(
             captureMode,
             bufferFormat,
             imageReaderProxyProvider,
-            captureProcessor
         )
         if (useProcessingPipeline != null) {
             imageCapture.mUseProcessingPipeline = useProcessingPipeline
@@ -636,6 +630,28 @@
         )
 
         cameraUseCaseAdapter.setViewPort(viewPort)
+        if (sessionProcessor != null) {
+            cameraUseCaseAdapter.setExtendedConfig(object : CameraConfig {
+                override fun getConfig(): androidx.camera.core.impl.Config {
+                    return OptionsBundle.emptyBundle()
+                }
+
+                override fun getSessionProcessor(
+                    valueIfMissing: SessionProcessor?
+                ): SessionProcessor? {
+                    return sessionProcessor
+                }
+
+                override fun getSessionProcessor(): SessionProcessor {
+                    return sessionProcessor
+                }
+
+                override fun getCompatibilityId(): Identifier {
+                    return Identifier.create(Any())
+                }
+            })
+        }
+
         cameraUseCaseAdapter.addUseCases(Collections.singleton<UseCase>(imageCapture))
         return imageCapture
     }
@@ -644,8 +660,7 @@
         captureMode: Int = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
         // Set non jpg format by default so it doesn't trigger the exif code path.
         bufferFormat: Int = ImageFormat.YUV_420_888,
-        imageReaderProxyProvider: ImageReaderProxyProvider? = null,
-        captureProcessor: CaptureProcessor? = null
+        imageReaderProxyProvider: ImageReaderProxyProvider? = null
     ): ImageCapture {
         val builder = ImageCapture.Builder()
             .setTargetRotation(Surface.ROTATION_0)
@@ -654,11 +669,7 @@
             .setCaptureOptionUnpacker { _: UseCaseConfig<*>?, _: CaptureConfig.Builder? -> }
             .setSessionOptionUnpacker { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
 
-        if (captureProcessor != null) {
-            builder.setCaptureProcessor(captureProcessor)
-        } else {
-            builder.setBufferFormat(bufferFormat)
-        }
+        builder.setBufferFormat(bufferFormat)
         if (imageReaderProxyProvider != null) {
             builder.setImageReaderProxyProvider(imageReaderProxyProvider)
         }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
deleted file mode 100644
index 184b2b8..0000000
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.content.Context
-import android.os.Build
-import android.os.Looper
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.utils.SizeUtil
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.fakes.FakeAppConfig
-import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeCameraFactory
-import androidx.test.core.app.ApplicationProvider
-import java.io.File
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-import org.junit.Assert
-
-@RunWith(RobolectricTestRunner::class)
-@Suppress("DEPRECATION")
-@DoNotInstrument
-@Config(
-    minSdk = Build.VERSION_CODES.LOLLIPOP
-)
-class VideoCaptureTest {
-    @Before
-    fun setUp() {
-        val camera = FakeCamera()
-
-        val cameraFactoryProvider =
-            CameraFactory.Provider { _, _, _ ->
-                val cameraFactory = FakeCameraFactory()
-                cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
-                    camera
-                }
-                cameraFactory
-            }
-        val cameraXConfig = CameraXConfig.Builder.fromConfig(FakeAppConfig.create())
-            .setCameraFactoryProvider(cameraFactoryProvider)
-            .build()
-        val context = ApplicationProvider.getApplicationContext<Context>()
-        CameraXUtil.initialize(context, cameraXConfig).get()
-    }
-
-    @After
-    fun tearDown() {
-        CameraXUtil.shutdown().get()
-    }
-
-    @Test
-    fun startRecording_beforeUseCaseIsBound() {
-        val videoCapture = VideoCapture.Builder().build()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputFileOptions = VideoCapture.OutputFileOptions.Builder(file).build()
-        val callback = mock(VideoCapture.OnVideoSavedCallback::class.java)
-        videoCapture.startRecording(
-            outputFileOptions,
-            CameraXExecutors.mainThreadExecutor(),
-            callback
-        )
-        shadowOf(Looper.getMainLooper()).idle()
-
-        verify(callback).onError(eq(VideoCapture.ERROR_INVALID_CAMERA), anyString(), any())
-    }
-
-    @Test
-    fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
-        Assert.assertThrows(IllegalArgumentException::class.java) {
-            VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
-                .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
-        }
-    }
-
-    @Test
-    fun throwException_whenSetTargetResolutionWithResolutionSelector() {
-        Assert.assertThrows(IllegalArgumentException::class.java) {
-            VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
-                .setResolutionSelector(ResolutionSelector.Builder().build())
-                .build()
-        }
-    }
-
-    @Test
-    fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
-        Assert.assertThrows(IllegalArgumentException::class.java) {
-            VideoCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
-                .setResolutionSelector(ResolutionSelector.Builder().build())
-                .build()
-        }
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
index 2f2c339..92da45c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
@@ -88,50 +88,6 @@
         assertThat(outputConfigs6.last()).isEqualTo(videoOutput)
     }
 
-    @Suppress("DEPRECATION")
-    @Test
-    fun sort_previewSurfaceIsInTheFirstAndVideoCaptureSurfaceIsInTheLast() {
-        // Arrange.
-        val videoOutput = SessionConfig.OutputConfig.builder(
-            createSurface(containerClass = androidx.camera.core.VideoCapture::class.java)).build()
-        val previewOutput = SessionConfig.OutputConfig.builder(
-            createSurface(containerClass = Preview::class.java)).build()
-        val imageOutput = SessionConfig.OutputConfig.builder(
-            createSurface(containerClass = ImageCapture::class.java)).build()
-        val surfaceSorter = SurfaceSorter()
-
-        // All combinations
-        val outputConfigs1 = mutableListOf(previewOutput, videoOutput, imageOutput)
-        val outputConfigs2 = mutableListOf(previewOutput, imageOutput, videoOutput)
-        val outputConfigs3 = mutableListOf(videoOutput, previewOutput, imageOutput)
-        val outputConfigs4 = mutableListOf(videoOutput, imageOutput, previewOutput)
-        val outputConfigs5 = mutableListOf(imageOutput, videoOutput, previewOutput)
-        val outputConfigs6 = mutableListOf(imageOutput, previewOutput, videoOutput)
-
-        // Act.
-        surfaceSorter.sort(outputConfigs1)
-        surfaceSorter.sort(outputConfigs2)
-        surfaceSorter.sort(outputConfigs3)
-        surfaceSorter.sort(outputConfigs4)
-        surfaceSorter.sort(outputConfigs5)
-        surfaceSorter.sort(outputConfigs6)
-
-        // Assert.
-        assertThat(outputConfigs1.first()).isEqualTo(previewOutput)
-        assertThat(outputConfigs2.first()).isEqualTo(previewOutput)
-        assertThat(outputConfigs3.first()).isEqualTo(previewOutput)
-        assertThat(outputConfigs4.first()).isEqualTo(previewOutput)
-        assertThat(outputConfigs5.first()).isEqualTo(previewOutput)
-        assertThat(outputConfigs6.first()).isEqualTo(previewOutput)
-
-        assertThat(outputConfigs1.last()).isEqualTo(videoOutput)
-        assertThat(outputConfigs2.last()).isEqualTo(videoOutput)
-        assertThat(outputConfigs3.last()).isEqualTo(videoOutput)
-        assertThat(outputConfigs4.last()).isEqualTo(videoOutput)
-        assertThat(outputConfigs5.last()).isEqualTo(videoOutput)
-        assertThat(outputConfigs6.last()).isEqualTo(videoOutput)
-    }
-
     private fun createSurface(
         containerClass: Class<*>
     ): DeferrableSurface {
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index f78b5e8..3c1cbc4 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -40,6 +40,7 @@
     testImplementation(libs.truth)
     testImplementation(project(":camera:camera-testing"))
     testImplementation(project(":camera:camera-extensions-stub"))
+    testImplementation(libs.testCore)
     // To use the extensions-stub for testing directly.
 
     androidTestImplementation(libs.testExtJunit)
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionTest.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionTest.java
deleted file mode 100644
index ba39474..0000000
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.extensions;
-
-import static androidx.camera.extensions.util.ExtensionsTestUtil.isTargetDeviceAvailableForExtensions;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNotNull;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Build;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallback;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Preview;
-import androidx.camera.core.impl.ImageCaptureConfig;
-import androidx.camera.core.impl.PreviewConfig;
-import androidx.camera.extensions.util.ExtensionsTestUtil;
-import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.fakes.FakeLifecycleOwner;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.LargeTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@LargeTest
-@RunWith(Parameterized.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
-public class ExtensionTest {
-
-    @Rule
-    public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
-            new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
-    );
-
-    @Parameterized.Parameters(name = "effect = {0}, facing = {1}")
-    public static Collection<Object[]> getParameters() {
-        return ExtensionsTestUtil.getAllExtensionsLensFacingCombinations();
-    }
-
-    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
-    private final Context mContext = ApplicationProvider.getApplicationContext();
-
-    @ExtensionMode.Mode
-    private final int mExtensionMode;
-    @CameraSelector.LensFacing
-    private final int mLensFacing;
-
-    private ProcessCameraProvider mProcessCameraProvider = null;
-    private FakeLifecycleOwner mFakeLifecycleOwner;
-    private CameraSelector mBaseCameraSelector;
-    private CameraSelector mExtensionsCameraSelector;
-    private ExtensionsManager mExtensionsManager;
-
-    public ExtensionTest(@ExtensionMode.Mode int extensionMode,
-            @CameraSelector.LensFacing int lensFacing) {
-        mExtensionMode = extensionMode;
-        mLensFacing = lensFacing;
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions(mLensFacing,
-                mExtensionMode));
-
-        mProcessCameraProvider = ProcessCameraProvider.getInstance(mContext).get(10000,
-                TimeUnit.MILLISECONDS);
-        mExtensionsManager = ExtensionsManager.getInstanceAsync(mContext,
-                mProcessCameraProvider).get(10000, TimeUnit.MILLISECONDS);
-        assumeTrue(isTargetDeviceAvailableForExtensions(mLensFacing, mExtensionMode));
-        mBaseCameraSelector = new CameraSelector.Builder().requireLensFacing(mLensFacing).build();
-        assumeTrue(mExtensionsManager.isExtensionAvailable(mBaseCameraSelector, mExtensionMode));
-
-        mExtensionsCameraSelector = mExtensionsManager.getExtensionEnabledCameraSelector(
-                mBaseCameraSelector, mExtensionMode);
-
-        mFakeLifecycleOwner = new FakeLifecycleOwner();
-        mFakeLifecycleOwner.startAndResume();
-    }
-
-    @After
-    public void cleanUp() throws InterruptedException, ExecutionException, TimeoutException {
-        if (mProcessCameraProvider != null) {
-            mInstrumentation.runOnMainSync(() -> mProcessCameraProvider.unbindAll());
-            mProcessCameraProvider.shutdown().get(10000, TimeUnit.MILLISECONDS);
-            mExtensionsManager.shutdown().get(10000, TimeUnit.MILLISECONDS);
-        }
-    }
-
-    @Test
-    public void testEventCallbackInConfig() {
-        Preview preview = new Preview.Builder().build();
-        ImageCapture imageCapture = new ImageCapture.Builder().build();
-
-        mInstrumentation.runOnMainSync(
-                () -> mProcessCameraProvider.bindToLifecycle(mFakeLifecycleOwner,
-                        mExtensionsCameraSelector, preview, imageCapture));
-
-        // Verify Preview config should have related callback.
-        PreviewConfig previewConfig = (PreviewConfig) preview.getCurrentConfig();
-        assertNotNull(previewConfig.getUseCaseEventCallback());
-        CameraEventCallbacks callback1 = new Camera2ImplConfig(
-                previewConfig).getCameraEventCallback(
-                null);
-        assertNotNull(callback1);
-        assertEquals(callback1.getAllItems().size(), 1);
-        assertThat(callback1.getAllItems().get(0)).isInstanceOf(CameraEventCallback.class);
-
-        // Verify ImageCapture config should have related callback.
-        ImageCaptureConfig imageCaptureConfig =
-                (ImageCaptureConfig) imageCapture.getCurrentConfig();
-        assertNotNull(imageCaptureConfig.getUseCaseEventCallback());
-        assertNotNull(imageCaptureConfig.getCaptureBundle());
-        CameraEventCallbacks callback2 = new Camera2ImplConfig(
-                imageCaptureConfig).getCameraEventCallback(null);
-        assertNotNull(callback2);
-        assertEquals(callback2.getAllItems().size(), 1);
-        assertThat(callback2.getAllItems().get(0)).isInstanceOf(CameraEventCallback.class);
-    }
-}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
index 88efac3..90e2733 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
@@ -17,11 +17,14 @@
 package androidx.camera.extensions
 
 import android.hardware.camera2.CameraCharacteristics
+import androidx.annotation.NonNull
 import androidx.camera.camera2.interop.Camera2CameraInfo
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraInfo
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.MutableStateObservable
 import androidx.camera.extensions.internal.ExtensionVersion
 import androidx.camera.extensions.internal.Version
 import androidx.camera.extensions.internal.VersionName
@@ -30,6 +33,9 @@
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.video.MediaSpec
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoOutput
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -404,7 +410,6 @@
         }
     }
 
-    @Suppress("DEPRECATION")
     @Test
     fun throwIllegalArgumentException_whenBindingVideoCapture(): Unit = runBlocking {
         val extensionCameraSelector = checkExtensionAvailabilityAndInit()
@@ -416,7 +421,7 @@
                 cameraProvider.bindToLifecycle(
                     fakeLifecycleOwner,
                     extensionCameraSelector,
-                    androidx.camera.core.VideoCapture.Builder().build()
+                    createVideoCapture()
                 )
             }
         }
@@ -458,4 +463,27 @@
             characteristics
         ) && previewExtenderImpl.isExtensionAvailable(cameraId, characteristics)
     }
+
+    private fun createVideoCapture(): VideoCapture<TestVideoOutput> {
+        val mediaSpec = MediaSpec.builder().build()
+        val videoOutput = TestVideoOutput()
+        videoOutput.mediaSpecObservable.setState(mediaSpec)
+        return VideoCapture.withOutput(videoOutput)
+    }
+
+    /** A fake implementation of VideoOutput  */
+    private class TestVideoOutput : VideoOutput {
+        val mediaSpecObservable: MutableStateObservable<MediaSpec> =
+            MutableStateObservable.withInitialState(MediaSpec.builder().build())
+        var surfaceRequest: SurfaceRequest? = null
+        var sourceState: VideoOutput.SourceState? = null
+
+        override fun onSurfaceRequested(@NonNull request: SurfaceRequest) {
+            surfaceRequest = request
+        }
+        override fun getMediaSpec() = mediaSpecObservable
+        override fun onSourceStateChanged(@NonNull sourceState: VideoOutput.SourceState) {
+            this.sourceState = sourceState
+        }
+    }
 }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index acd5027..e640f36 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -201,53 +201,6 @@
         verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture, imageAnalysis)
     }
 
-    @SdkSuppress(minSdkVersion = 29) // YUV to JPEG requires api level >= 29
-    @Test
-    fun useCasesCanWork_captureOutputFormatIsYUV() = runBlocking {
-        var captureOutputSurface: Surface? = null
-        var intermediaConfigId = -1
-        var captureOutputSurfaceFormat = 0
-        val fakeSessionProcessImpl = FakeSessionProcessImpl(
-            // Directly use output surface
-            previewConfigBlock = { outputSurfaceImpl ->
-                Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
-                    .build()
-            },
-            // Has intermediate image reader to process YUV.
-            captureConfigBlock = { outputSurfaceImpl ->
-                captureOutputSurfaceFormat = outputSurfaceImpl.imageFormat
-                captureOutputSurface = outputSurfaceImpl.surface
-                Camera2OutputConfigImplBuilder
-                    .newImageReaderConfig(outputSurfaceImpl.size, ImageFormat.YUV_420_888, 2)
-                    .build()
-                    .also {
-                        intermediaConfigId = it.id
-                    }
-            },
-            onCaptureSessionStarted = {
-                // Emulates the processing, write an image to the output surface.
-                val imageWriter = ImageWriter.newInstance(captureOutputSurface!!, 2)
-                it.setImageProcessor(intermediaConfigId) { _, _, image, _ ->
-                    val inputImage = imageWriter.dequeueInputImage()
-                    imageWriter.queueInputImage(inputImage)
-                    image.decrement()
-                }
-            }
-        )
-
-        val preview = Preview.Builder().build()
-        val imageCapture = ImageCapture.Builder()
-            .setSupportedResolutions(
-                listOf(
-                    android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480)))
-                )
-            )
-            .build()
-        verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture)
-        assertThat(captureOutputSurfaceFormat).isEqualTo(ImageFormat.YUV_420_888)
-    }
-
     private suspend fun assumeAllowsSharedSurface() = withContext(Dispatchers.Main) {
         val imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2)
         val maxSharedSurfaceCount =
@@ -318,7 +271,7 @@
     }
 
     // Test if physicalCameraId is set and returned in the image received in the image processor.
-    @SdkSuppress(minSdkVersion = 29) // YUV to JPEG requires api level >= 29
+    @SdkSuppress(minSdkVersion = 28) // physical camera id is supported in API28+
     @Test
     fun useCasesCanWork_setPhysicalCameraId() = runBlocking {
         assumeAllowsSharedSurface()
@@ -326,9 +279,9 @@
         assumeTrue(physicalCameraIdList.isNotEmpty())
 
         val physicalCameraId = physicalCameraIdList[0]
-        var captureOutputSurface: Surface? = null
-        var intermediaConfigId = -1
+        var analysisOutputSurface: Surface? = null
         var sharedConfigId = -1
+        var intermediaConfigId = -1
         val deferredImagePhysicalCameraId = CompletableDeferred<String?>()
         val deferredSharedImagePhysicalCameraId = CompletableDeferred<String?>()
 
@@ -340,25 +293,29 @@
                     .setPhysicalCameraId(physicalCameraId)
                     .build()
             },
+            // Directly use output surface
+            captureConfigBlock = {
+                Camera2OutputConfigImplBuilder
+                    .newSurfaceConfig(it.surface)
+                    .build()
+            },
             // Has intermediate image reader to process YUV
-            captureConfigBlock = { outputSurfaceImpl ->
-                captureOutputSurface = outputSurfaceImpl.surface
+            analysisConfigBlock = { outputSurfaceImpl ->
+                analysisOutputSurface = outputSurfaceImpl.surface
                 val sharedConfig = Camera2OutputConfigImplBuilder.newImageReaderConfig(
                     outputSurfaceImpl.size, outputSurfaceImpl.imageFormat, 2
-                ).setPhysicalCameraId(physicalCameraId).build()
-                sharedConfigId = sharedConfig.id
+                ).setPhysicalCameraId(physicalCameraId)
+                    .build().also { sharedConfigId = it.id }
 
                 Camera2OutputConfigImplBuilder
                     .newImageReaderConfig(outputSurfaceImpl.size, ImageFormat.YUV_420_888, 2)
                     .setPhysicalCameraId(physicalCameraId)
                     .addSurfaceSharingOutputConfig(sharedConfig)
                     .build()
-                    .also {
-                        intermediaConfigId = it.id
-                    }
+                    .also { intermediaConfigId = it.id }
             },
             onCaptureSessionStarted = { requestProcessor ->
-                val imageWriter = ImageWriter.newInstance(captureOutputSurface!!, 2)
+                val imageWriter = ImageWriter.newInstance(analysisOutputSurface!!, 2)
                 requestProcessor.setImageProcessor(intermediaConfigId) {
                         _, _, image, physicalCameraIdOfImage ->
                     deferredImagePhysicalCameraId.complete(physicalCameraIdOfImage)
@@ -375,12 +332,9 @@
         )
 
         val preview = Preview.Builder().build()
-        val imageCapture = ImageCapture.Builder()
-            .setSupportedResolutions(
-                listOf(android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480))))
-            )
-            .build()
-        verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture)
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture, imageAnalysis)
         assertThat(deferredImagePhysicalCameraId.awaitWithTimeout(2000))
             .isEqualTo(physicalCameraId)
         assertThat(deferredSharedImagePhysicalCameraId.awaitWithTimeout(2000))
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
index 7085869..fabe251 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import android.graphics.ImageFormat
 import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CaptureRequest
 import android.util.Pair
 import android.util.Size
 import androidx.camera.camera2.Camera2Config
@@ -28,8 +27,6 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.extensions.ExtensionMode
-import androidx.camera.extensions.impl.CaptureProcessorImpl
-import androidx.camera.extensions.impl.CaptureStageImpl
 import androidx.camera.extensions.impl.ImageCaptureExtenderImpl
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -53,9 +50,6 @@
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -97,64 +91,6 @@
 
     @Test
     @MediumTest
-    fun extenderLifeCycleTest_noMoreInteractionsBeforeAndAfterInitDeInit(): Unit =
-        runBlocking {
-            val mockImageCaptureExtenderImpl = mock(ImageCaptureExtenderImpl::class.java)
-            val captureStages = mutableListOf<CaptureStageImpl>()
-
-            captureStages.add(FakeCaptureStage())
-
-            Mockito.`when`(mockImageCaptureExtenderImpl.captureStages).thenReturn(captureStages)
-            Mockito.`when`(mockImageCaptureExtenderImpl.captureProcessor).thenReturn(
-                mock(CaptureProcessorImpl::class.java)
-            )
-            val mockVendorExtender = mock(BasicVendorExtender::class.java)
-            Mockito.`when`(mockVendorExtender.imageCaptureExtenderImpl)
-                .thenReturn(mockImageCaptureExtenderImpl)
-
-            val imageCapture = createImageCaptureWithExtenderImpl(mockVendorExtender)
-
-            // Binds the use case to trigger the camera pipeline operations
-            withContext(Dispatchers.Main) {
-                cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, imageCapture)
-            }
-
-            // To verify the event callbacks in order, and to verification of the onEnableSession()
-            // is also used to wait for the capture session created. The test for the unbind
-            // would come after the capture session was created.
-            verify(mockImageCaptureExtenderImpl, timeout(3000)).captureProcessor
-            verify(mockImageCaptureExtenderImpl, timeout(3000)).maxCaptureStage
-
-            verify(mockVendorExtender, timeout(3000)).supportedCaptureOutputResolutions
-
-            val inOrder = Mockito.inOrder(mockImageCaptureExtenderImpl)
-            inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce()).captureStages
-            inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000)).onInit(
-                any(String::class.java),
-                any(CameraCharacteristics::class.java),
-                any(Context::class.java)
-            )
-            inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce())
-                .onPresetSession()
-            inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce())
-                .onEnableSession()
-
-            withContext(Dispatchers.Main) {
-                // Unbind the use case to test the onDisableSession and onDeInit.
-                cameraProvider.unbind(imageCapture)
-            }
-
-            // To verify the onDisableSession and onDeInit.
-            inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce())
-                .onDisableSession()
-            inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000)).onDeInit()
-
-            // To verify there is no any other calls on the mock.
-            verifyNoMoreInteractions(mockImageCaptureExtenderImpl)
-        }
-
-    @Test
-    @MediumTest
     fun canSetSupportedResolutionsToConfigTest(): Unit = runBlocking {
         assumeTrue(CameraUtil.deviceHasCamera())
 
@@ -172,11 +108,8 @@
             targetFormatResolutionsPairList
         )
 
-        val mockVendorExtender = mock(BasicVendorExtender::class.java)
-        Mockito.`when`(mockVendorExtender.imageCaptureExtenderImpl)
-            .thenReturn(mockImageCaptureExtenderImpl)
-
-        val preview = createImageCaptureWithExtenderImpl(mockVendorExtender)
+        val vendorExtender = BasicVendorExtender(mockImageCaptureExtenderImpl, null)
+        val preview = createImageCaptureWithExtenderImpl(vendorExtender)
 
         withContext(Dispatchers.Main) {
             cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
@@ -197,12 +130,19 @@
         }
     }
 
-    private fun createImageCaptureWithExtenderImpl(basicVendorExtender: BasicVendorExtender) =
-        ImageCapture.Builder().also {
-            ImageCaptureConfigProvider(extensionMode, basicVendorExtender, context).apply {
-                updateBuilderConfig(it, extensionMode, basicVendorExtender, context)
+    private suspend fun createImageCaptureWithExtenderImpl(
+        basicVendorExtender: BasicVendorExtender
+    ): ImageCapture {
+        withContext(Dispatchers.Main) {
+            val camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+            basicVendorExtender.init(camera.cameraInfo)
+        }
+        return ImageCapture.Builder().also {
+            ImageCaptureConfigProvider(extensionMode, basicVendorExtender).apply {
+                updateBuilderConfig(it, extensionMode, basicVendorExtender)
             }
         }.build()
+    }
 
     private fun generateImageCaptureSupportedResolutions(): List<Pair<Int, Array<Size>>> {
         val formatResolutionsPairList = mutableListOf<Pair<Int, Array<Size>>>()
@@ -218,9 +158,4 @@
 
         return formatResolutionsPairList
     }
-
-    private class FakeCaptureStage : CaptureStageImpl {
-        override fun getId() = 0
-        override fun getParameters(): List<Pair<CaptureRequest.Key<*>, Any>> = emptyList()
-    }
 }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
index fc1d2f0..6734ece 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
@@ -18,11 +18,7 @@
 
 import android.content.Context
 import android.graphics.ImageFormat
-import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CaptureRequest
-import android.hardware.camera2.TotalCaptureResult
-import android.media.Image
 import android.util.Pair
 import android.util.Size
 import androidx.camera.camera2.Camera2Config
@@ -31,15 +27,10 @@
 import androidx.camera.core.Preview
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.extensions.ExtensionMode
-import androidx.camera.extensions.impl.CaptureStageImpl
 import androidx.camera.extensions.impl.PreviewExtenderImpl
-import androidx.camera.extensions.impl.PreviewImageProcessorImpl
-import androidx.camera.extensions.impl.RequestUpdateProcessorImpl
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -56,14 +47,9 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -76,20 +62,6 @@
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
 
-    private val surfaceTextureCallback: SurfaceTextureCallback =
-        object : SurfaceTextureCallback {
-            override fun onSurfaceTextureReady(
-                surfaceTexture: SurfaceTexture,
-                resolution: Size
-            ) {
-                // No-op.
-            }
-
-            override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
-                // No-op.
-            }
-        }
-
     // Tests in this class majorly use mock objects to run the test. No matter which extension
     // mode is use, it should not affect the test results.
     @ExtensionMode.Mode
@@ -118,158 +90,6 @@
     }
 
     @Test
-    fun extenderLifeCycleTest_noMoreInvokeBeforeAndAfterInitDeInit(): Unit = runBlocking {
-        val mockPreviewExtenderImpl = mock(PreviewExtenderImpl::class.java)
-
-        Mockito.`when`(mockPreviewExtenderImpl.processorType).thenReturn(
-            PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR
-        )
-        Mockito.`when`(mockPreviewExtenderImpl.processor)
-            .thenReturn(mock(PreviewImageProcessorImpl::class.java))
-        Mockito.`when`(
-            mockPreviewExtenderImpl.isExtensionAvailable(
-                any(String::class.java),
-                any(CameraCharacteristics::class.java)
-            )
-        ).thenReturn(true)
-
-        val mockVendorExtender = mock(BasicVendorExtender::class.java)
-        Mockito.`when`(mockVendorExtender.previewExtenderImpl)
-            .thenReturn(mockPreviewExtenderImpl)
-
-        val preview = createPreviewWithExtenderImpl(mockVendorExtender)
-
-        withContext(Dispatchers.Main) {
-            // To set the update listener and Preview will change to active state.
-            preview.setSurfaceProvider(
-                SurfaceTextureProvider.createSurfaceTextureProvider(surfaceTextureCallback)
-            )
-
-            cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
-        }
-
-        // To verify the call in order after bind to life cycle, and to verification of the
-        // getCaptureStages() is also used to wait for the capture session created. The test for
-        // the unbind would come after the capture session was created. Ignore any of the calls
-        // unrelated to the ExtenderStateListener.
-        verify(mockPreviewExtenderImpl, timeout(3000)).processorType
-        verify(mockPreviewExtenderImpl, timeout(3000)).processor
-
-        verify(mockVendorExtender, timeout(3000)).supportedPreviewOutputResolutions
-
-        val inOrder = Mockito.inOrder(*Mockito.ignoreStubs(mockPreviewExtenderImpl))
-        inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onInit(
-            any(String::class.java),
-            any(CameraCharacteristics::class.java),
-            any(Context::class.java)
-        )
-
-        inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onPresetSession()
-        inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onEnableSession()
-        inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).captureStage
-
-        withContext(Dispatchers.Main) {
-            // Unbind the use case to test the onDisableSession and onDeInit.
-            cameraProvider.unbind(preview)
-        }
-
-        // To verify the onDisableSession and onDeInit.
-        inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onDisableSession()
-        inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onDeInit()
-
-        // To verify there is no any other calls on the mock.
-        verifyNoMoreInteractions(mockPreviewExtenderImpl)
-    }
-
-    @Test
-    fun getCaptureStagesTest_shouldSetToRepeatingRequest(): Unit = runBlocking {
-        // Set up a result for getCaptureStages() testing.
-        val fakeCaptureStageImpl: CaptureStageImpl = FakeCaptureStageImpl()
-        val mockPreviewExtenderImpl = mock(PreviewExtenderImpl::class.java)
-        val mockRequestUpdateProcessorImpl = mock(RequestUpdateProcessorImpl::class.java)
-
-        // The mock an RequestUpdateProcessorImpl to capture the returned TotalCaptureResult
-        Mockito.`when`(mockPreviewExtenderImpl.processorType).thenReturn(
-            PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY
-        )
-        Mockito.`when`(mockPreviewExtenderImpl.processor).thenReturn(mockRequestUpdateProcessorImpl)
-        Mockito.`when`(
-            mockPreviewExtenderImpl.isExtensionAvailable(
-                any(String::class.java),
-                any(CameraCharacteristics::class.java)
-            )
-        ).thenReturn(true)
-        Mockito.`when`(mockPreviewExtenderImpl.captureStage).thenReturn(fakeCaptureStageImpl)
-
-        val mockVendorExtender = mock(BasicVendorExtender::class.java)
-        Mockito.`when`(mockVendorExtender.previewExtenderImpl)
-            .thenReturn(mockPreviewExtenderImpl)
-        val preview = createPreviewWithExtenderImpl(mockVendorExtender)
-
-        withContext(Dispatchers.Main) {
-            // To set the update listener and Preview will change to active state.
-            preview.setSurfaceProvider(
-                SurfaceTextureProvider.createSurfaceTextureProvider(surfaceTextureCallback)
-            )
-
-            cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
-        }
-
-        val captureResultArgumentCaptor = ArgumentCaptor.forClass(
-            TotalCaptureResult::class.java
-        )
-        verify(mockRequestUpdateProcessorImpl, timeout(3000).atLeastOnce()).process(
-            captureResultArgumentCaptor.capture()
-        )
-
-        // TotalCaptureResult might be captured multiple times. Only care to get one instance of
-        // it, since they should all have the same value for the tested key
-        val totalCaptureResult = captureResultArgumentCaptor.value
-
-        // To verify the capture result should include the parameter of the getCaptureStages().
-        val parameters = fakeCaptureStageImpl.parameters
-        for (parameter: Pair<CaptureRequest.Key<*>?, Any> in parameters) {
-            assertThat(totalCaptureResult.request[parameter.first] == parameter.second)
-        }
-    }
-
-    @Test
-    fun processShouldBeInvoked_typeImageProcessor(): Unit = runBlocking {
-        // The type image processor will invoke PreviewImageProcessor.process()
-        val mockPreviewImageProcessorImpl = mock(PreviewImageProcessorImpl::class.java)
-        val mockPreviewExtenderImpl = mock(PreviewExtenderImpl::class.java)
-
-        Mockito.`when`(mockPreviewExtenderImpl.processor).thenReturn(mockPreviewImageProcessorImpl)
-        Mockito.`when`(mockPreviewExtenderImpl.processorType)
-            .thenReturn(PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR)
-        Mockito.`when`(
-            mockPreviewExtenderImpl.isExtensionAvailable(
-                any(String::class.java),
-                any(CameraCharacteristics::class.java)
-            )
-        ).thenReturn(true)
-
-        val mockVendorExtender = mock(BasicVendorExtender::class.java)
-        Mockito.`when`(mockVendorExtender.previewExtenderImpl)
-            .thenReturn(mockPreviewExtenderImpl)
-
-        val preview = createPreviewWithExtenderImpl(mockVendorExtender)
-
-        withContext(Dispatchers.Main) {
-            // To set the update listener and Preview will change to active state.
-            preview.setSurfaceProvider(
-                SurfaceTextureProvider.createSurfaceTextureProvider(surfaceTextureCallback)
-            )
-
-            cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
-        }
-
-        // To verify the process() method was invoked with non-null TotalCaptureResult input.
-        verify(mockPreviewImageProcessorImpl, Mockito.timeout(3000).atLeastOnce())
-            .process(any(Image::class.java), ArgumentMatchers.any(TotalCaptureResult::class.java))
-    }
-
-    @Test
     @MediumTest
     fun canSetSupportedResolutionsToConfigTest(): Unit = runBlocking {
         assumeTrue(CameraUtil.deviceHasCamera())
@@ -290,9 +110,7 @@
             targetFormatResolutionsPairList
         )
 
-        val mockVendorExtender = mock(BasicVendorExtender::class.java)
-        Mockito.`when`(mockVendorExtender.previewExtenderImpl)
-            .thenReturn(mockPreviewExtenderImpl)
+        val mockVendorExtender = BasicVendorExtender(null, mockPreviewExtenderImpl)
 
         val preview = createPreviewWithExtenderImpl(mockVendorExtender)
 
@@ -315,12 +133,19 @@
         }
     }
 
-    private fun createPreviewWithExtenderImpl(basicVendorExtender: BasicVendorExtender) =
-        Preview.Builder().also {
-            PreviewConfigProvider(extensionMode, basicVendorExtender, context).apply {
-                updateBuilderConfig(it, extensionMode, basicVendorExtender, context)
+    private suspend fun createPreviewWithExtenderImpl(
+        basicVendorExtender: BasicVendorExtender
+    ): Preview {
+        withContext(Dispatchers.Main) {
+            val camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+            basicVendorExtender.init(camera.cameraInfo)
+        }
+        return Preview.Builder().also {
+            PreviewConfigProvider(extensionMode, basicVendorExtender).apply {
+                updateBuilderConfig(it, extensionMode, basicVendorExtender)
             }
         }.build()
+    }
 
     private fun generatePreviewSupportedResolutions(): List<Pair<Int, Array<Size>>> {
         val formatResolutionsPairList = mutableListOf<Pair<Int, Array<Size>>>()
@@ -336,14 +161,4 @@
 
         return formatResolutionsPairList
     }
-
-    private class FakeCaptureStageImpl : CaptureStageImpl {
-        override fun getId() = 0
-        override fun getParameters(): List<Pair<CaptureRequest.Key<*>, Any>> = mutableListOf(
-            Pair.create(
-                CaptureRequest.CONTROL_EFFECT_MODE,
-                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA
-            )
-        )
-    }
 }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
new file mode 100644
index 0000000..3c3aa9d
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -0,0 +1,991 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.media.ImageWriter
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Pair
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.interop.Camera2CameraInfo
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraFilter
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraState
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.ImageReaderProxys
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCaseGroup
+import androidx.camera.core.impl.CameraConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
+import androidx.camera.core.impl.Identifier
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.OutputSurface
+import androidx.camera.core.impl.RequestProcessor
+import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.extensions.impl.CaptureProcessorImpl
+import androidx.camera.extensions.impl.CaptureStageImpl
+import androidx.camera.extensions.impl.ExtenderStateListener
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl
+import androidx.camera.extensions.impl.PreviewExtenderImpl
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_NONE
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl
+import androidx.camera.extensions.impl.ProcessResultImpl
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.lifecycle.Observer
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29) // Extensions supported on API 29+
+@RunWith(Parameterized::class)
+class BasicExtenderSessionProcessorTest(
+    private val hasCaptureProcessor: Boolean,
+    private val previewProcessorType: ProcessorType
+) {
+    companion object {
+        @Parameterized.Parameters(name = "hasCaptureProcessor = {0}, previewProcessorType = {1}")
+        @JvmStatic
+        fun parameters() = listOf(
+            arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_NONE),
+            arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_NONE),
+            arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_REQUEST_UPDATE_ONLY),
+            arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_REQUEST_UPDATE_ONLY),
+            arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_IMAGE_PROCESSOR),
+            arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_IMAGE_PROCESSOR)
+        )
+
+        private fun createCaptureStage(
+            id: Int = 0,
+            parameters: List<Pair<CaptureRequest.Key<*>, Any>> = mutableListOf()
+        ): CaptureStageImpl {
+            return object : CaptureStageImpl {
+                override fun getId() = id
+                override fun getParameters() = parameters
+            }
+        }
+    }
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var cameraProvider: ProcessCameraProvider
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+    private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+    private lateinit var fakePreviewExtenderImpl: FakePreviewExtenderImpl
+    private lateinit var fakeCaptureExtenderImpl: FakeImageCaptureExtenderImpl
+    private lateinit var basicExtenderSessionProcessor: BasicExtenderSessionProcessor
+
+    @Before
+    fun setUp() = runBlocking {
+        cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner()
+            fakeLifecycleOwner.startAndResume()
+        }
+
+        if (hasCaptureProcessor || previewProcessorType == PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+            assumeTrue(areYuvYuvYuvAndYuvYuvPrivateSupported())
+        }
+
+        fakePreviewExtenderImpl = FakePreviewExtenderImpl(previewProcessorType)
+        fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(hasCaptureProcessor)
+        basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+        )
+    }
+
+    private suspend fun areYuvYuvYuvAndYuvYuvPrivateSupported(): Boolean {
+        if (Build.BRAND.equals("SAMSUNG", ignoreCase = true)) {
+            return true
+        }
+        val camera = withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+        }
+        val hardwareLevel = Camera2CameraInfo.from(camera.cameraInfo).getCameraCharacteristic(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+        )
+
+        return hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3 ||
+            hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+    }
+
+    @After
+    fun tearDown() = runBlocking {
+        if (::cameraProvider.isInitialized) {
+            withContext(Dispatchers.Main) {
+                cameraProvider.unbindAll()
+                cameraProvider.shutdown()[10, TimeUnit.SECONDS]
+            }
+        }
+    }
+
+    @Test
+    fun canOutputCorrectly(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        val analysisSemaphore = Semaphore(0)
+        verifyUseCasesOutput(
+            preview,
+            imageCapture,
+            imageAnalysis,
+            previewSemaphore,
+            analysisSemaphore
+        )
+    }
+
+    @Test
+    fun imageCaptureError(): Unit = runBlocking {
+        assumeTrue(hasCaptureProcessor)
+        fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(
+            hasCaptureProcessor, throwErrorOnProcess = true
+        )
+        basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+        )
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        assertThrows<ImageCaptureException> {
+            verifyUseCasesOutput(preview, imageCapture, imageAnalysis)
+        }
+    }
+
+    @Test
+    fun canOutputCorrectly_withoutAnalysis(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        verifyUseCasesOutput(
+            preview = preview,
+            imageCapture = imageCapture,
+            previewFrameSemaphore = previewSemaphore
+        )
+    }
+
+    suspend fun getSensorRotationDegrees(rotation: Int): Int {
+        return withContext(Dispatchers.Main) {
+            val camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+            camera.cameraInfo.getSensorRotationDegrees(rotation)
+        }
+    }
+
+    @Test
+    fun canOutputCorrectly_setTargetRotation(): Unit = runBlocking {
+        assumeTrue(hasCaptureProcessor)
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder()
+            .setTargetRotation(Surface.ROTATION_0)
+            .build()
+        val previewSemaphore = Semaphore(0)
+        verifyUseCasesOutput(
+            preview = preview,
+            imageCapture = imageCapture,
+            previewFrameSemaphore = previewSemaphore,
+            expectedExifRotation = getSensorRotationDegrees(Surface.ROTATION_0)
+        )
+    }
+
+    @Test
+    fun canOutputCorrectlyAfterStopStart(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        val analysisSemaphore = Semaphore(0)
+
+        verifyUseCasesOutput(
+            preview,
+            imageCapture,
+            imageAnalysis,
+            previewSemaphore,
+            analysisSemaphore
+        )
+
+        fakeLifecycleOwner.pauseAndStop()
+
+        delay(1000)
+        previewSemaphore.drainPermits()
+        analysisSemaphore.drainPermits()
+        fakeLifecycleOwner.startAndResume()
+
+        assertThat(previewSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+
+        imageAnalysis.let {
+            assertThat(analysisSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        }
+
+        verifyStillCapture(imageCapture)
+    }
+
+    @Test
+    fun canInvokeEventsInOrder(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        val analysisSemaphore = Semaphore(0)
+        val camera = verifyUseCasesOutput(
+            preview,
+            imageCapture,
+            imageAnalysis,
+            previewSemaphore,
+            analysisSemaphore
+        )
+
+        val cameraClosedLatch = CountDownLatch(1)
+        withContext(Dispatchers.Main) {
+            camera.cameraInfo.cameraState.observeForever(object : Observer<CameraState?> {
+                override fun onChanged(cameraState: CameraState?) {
+                    if (cameraState?.type == CameraState.Type.CLOSED) {
+                        cameraClosedLatch.countDown()
+                        camera.cameraInfo.cameraState.removeObserver(this)
+                    }
+                }
+            })
+        }
+
+        fakeLifecycleOwner.pauseAndStop()
+        assertThat(cameraClosedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        fakeCaptureExtenderImpl.assertInvokeOrder(listOf(
+            "onInit",
+            "onPresetSession",
+            "onEnableSession",
+            "onDisableSession",
+            "onDeInit",
+        ))
+
+        fakePreviewExtenderImpl.assertInvokeOrder(listOf(
+            "onInit",
+            "onPresetSession",
+            "onEnableSession",
+            "onDisableSession",
+            "onDeInit",
+        ))
+    }
+
+    class ResultMonitor {
+        private var latch: CountDownLatch? = null
+        private var keyToCheck: CaptureRequest.Key<*>? = null
+        private var valueToCheck: Any? = null
+
+        fun onCaptureRequestReceived(captureRequest: CaptureRequest) {
+            if (latch != null) {
+                keyToCheck?.let {
+                    if (captureRequest.get(keyToCheck) == valueToCheck) {
+                        latch!!.countDown()
+                    }
+                }
+            }
+        }
+        fun assertCaptureKey(key: CaptureRequest.Key<*>, value: Any) {
+            keyToCheck = key
+            valueToCheck = value
+            latch = CountDownLatch(1)
+            assertThat(latch!!.await(3, TimeUnit.SECONDS)).isTrue()
+        }
+    }
+
+    @Test
+    fun repeatingRequest_containsPreviewCaptureStagesParameters(): Unit = runBlocking {
+        val previewBuilder = Preview.Builder()
+        val resultMonitor = ResultMonitor()
+        Camera2Interop.Extender(previewBuilder)
+            .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
+                override fun onCaptureCompleted(
+                    session: CameraCaptureSession,
+                    request: CaptureRequest,
+                    result: TotalCaptureResult
+                ) {
+                    resultMonitor.onCaptureRequestReceived(request)
+                }
+            })
+        val preview = previewBuilder.build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        val analysisSemaphore = Semaphore(0)
+        fakePreviewExtenderImpl.captureStage = createCaptureStage(
+            parameters = listOf(Pair(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF
+            ))
+        )
+
+        verifyUseCasesOutput(
+            preview,
+            imageCapture,
+            imageAnalysis,
+            previewSemaphore,
+            analysisSemaphore
+        )
+
+        resultMonitor.assertCaptureKey(
+            CaptureRequest.CONTROL_AF_MODE,
+            CaptureRequest.CONTROL_AF_MODE_OFF
+        )
+    }
+
+    @Test
+    fun processorRequestUpdateOnly_canUpdateRepeating(): Unit = runBlocking {
+        assumeTrue(previewProcessorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)
+        val previewBuilder = Preview.Builder()
+        val resultMonitor = ResultMonitor()
+        Camera2Interop.Extender(previewBuilder)
+            .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
+                override fun onCaptureCompleted(
+                    session: CameraCaptureSession,
+                    request: CaptureRequest,
+                    result: TotalCaptureResult
+                ) {
+                    resultMonitor.onCaptureRequestReceived(request)
+                }
+            })
+        val preview = previewBuilder.build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        val analysisSemaphore = Semaphore(0)
+        verifyUseCasesOutput(
+            preview,
+            imageCapture,
+            imageAnalysis,
+            previewSemaphore,
+            analysisSemaphore
+        )
+
+        // Trigger RequestUpdateProcessor to update repeating request to have new parameters.
+        fakePreviewExtenderImpl.fakeRequestUpdateProcessor?.captureStage =
+            createCaptureStage(
+                parameters = listOf(Pair(CaptureRequest.CONTROL_AE_MODE,
+                    CaptureRequest.CONTROL_AE_MODE_OFF)
+                )
+            )
+
+        resultMonitor.assertCaptureKey(
+            CaptureRequest.CONTROL_AE_MODE,
+            CaptureRequest.CONTROL_AE_MODE_OFF
+        )
+    }
+
+    @Test
+    fun imageCapture_captureRequestParametersAreCorrect(): Unit = runBlocking {
+        initBasicExtenderSessionProcessor().use {
+            fakeCaptureExtenderImpl.captureStages = listOf(
+                createCaptureStage(
+                    0, listOf(
+                        Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF)
+                    )
+                ),
+                createCaptureStage(
+                    1, listOf(
+                        Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
+                    )
+                ),
+            )
+            fakePreviewExtenderImpl.captureStage = createCaptureStage(
+                0, listOf(
+                    Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+                )
+            )
+
+            val fakeRequestProcessor = FakeRequestProcessor()
+            basicExtenderSessionProcessor.onCaptureSessionStart(fakeRequestProcessor)
+
+            basicExtenderSessionProcessor.startRepeating(object :
+                SessionProcessor.CaptureCallback {})
+            basicExtenderSessionProcessor.startCapture(object : SessionProcessor.CaptureCallback {})
+
+            val submittedRequests = withTimeout(2000) {
+                fakeRequestProcessor.awaitRequestSubmitted()
+            }
+            assertThat(submittedRequests.size).isEqualTo(2)
+            val submittedRequestParameter0 = submittedRequests[0].toParametersList()
+            val submittedRequestParameter1 = submittedRequests[1].toParametersList()
+
+            // Capture request parameters should include both Image capture capture stage and
+            // preview capture stage.
+            assertThat(submittedRequestParameter0).containsExactly(
+                // Set by image capture CaptureStageImpl "0"
+                Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF),
+                // Set by preview getCaptureStage()
+                Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+            )
+
+            assertThat(submittedRequestParameter1).containsExactly(
+                // Set by image capture CaptureStageImpl "1"
+                Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF),
+                // Set by preview getCaptureStage()
+                Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+            )
+        }
+    }
+
+    @Test
+    fun onEnableDisableRequestsAreSent(): Unit = runBlocking {
+        initBasicExtenderSessionProcessor().use {
+            // Verify onEnableSession
+            fakePreviewExtenderImpl.onEnableSessionCaptureStage = createCaptureStage(
+                0, listOf(
+                    Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+                )
+            )
+
+            fakeCaptureExtenderImpl.onEnableSessionCaptureStage = createCaptureStage(
+                0, listOf(
+                    Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
+                )
+            )
+
+            val fakeRequestProcessor = FakeRequestProcessor()
+            basicExtenderSessionProcessor.onCaptureSessionStart(fakeRequestProcessor)
+
+            val onEnableSessionRequest = withTimeout(2000) {
+                fakeRequestProcessor.awaitRequestSubmitted()
+            }
+            assertThat(onEnableSessionRequest.size).isEqualTo(2)
+            assertThat(onEnableSessionRequest[0].toParametersList()).containsExactly(
+                Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF),
+            )
+            assertThat(onEnableSessionRequest[1].toParametersList()).containsExactly(
+                Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF),
+            )
+
+            // Verify onDisableSession
+            fakePreviewExtenderImpl.onDisableSessionCaptureStage = createCaptureStage(
+                0, listOf(
+                    Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF)
+                )
+            )
+            fakeCaptureExtenderImpl.onDisableSessionCaptureStage = createCaptureStage(
+                0, listOf(
+                    Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+                )
+            )
+            basicExtenderSessionProcessor.onCaptureSessionEnd()
+            val onDisableSessionRequest = withTimeout(2000) {
+                fakeRequestProcessor.awaitRequestSubmitted()
+            }
+            assertThat(onDisableSessionRequest.size).isEqualTo(2)
+            assertThat(onDisableSessionRequest[0].toParametersList()).containsExactly(
+                Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF),
+            )
+            assertThat(onDisableSessionRequest[1].toParametersList()).containsExactly(
+                Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF),
+            )
+        }
+    }
+
+    private fun initBasicExtenderSessionProcessor(): AutoCloseable {
+        val width = 640
+        val height = 480
+        val maxImages = 2
+        val cameraInfo = cameraProvider.availableCameraInfos[0]
+        val handlerThread = HandlerThread("CameraX-AutoDrainThread")
+        handlerThread.start()
+        val handler = Handler(handlerThread.looper)
+        val surfaceTextureHolder = SurfaceTextureProvider.createAutoDrainingSurfaceTexture(
+            CameraXExecutors.newHandlerExecutor(handler), width, height
+        ) {}
+        val previewOutputSurface = OutputSurface.create(
+            Surface(surfaceTextureHolder.surfaceTexture),
+            Size(width, height),
+            ImageFormat.PRIVATE
+        )
+        val jpegImageReader =
+            ImageReaderProxys.createIsolatedReader(width, height, ImageFormat.JPEG, maxImages)
+        val captureOutputSurface = OutputSurface.create(
+            jpegImageReader.surface!!,
+            Size(width, height),
+            ImageFormat.JPEG
+        )
+
+        basicExtenderSessionProcessor.initSession(
+            cameraInfo,
+            previewOutputSurface,
+            captureOutputSurface,
+            null
+        )
+
+        return AutoCloseable {
+            jpegImageReader.close()
+            surfaceTextureHolder.close()
+            handlerThread.quitSafely()
+        }
+    }
+
+    private fun RequestProcessor.Request.toParametersList():
+        List<Pair<CaptureRequest.Key<Any>?, Any?>> {
+        val submittedRequestParameter1 = parameters.listOptions().map {
+            @Suppress("UNCHECKED_CAST")
+            val key = it.token as CaptureRequest.Key<Any>?
+            val value = parameters.retrieveOption(it)
+            Pair(key, value)
+        }
+        return submittedRequestParameter1
+    }
+
+    /**
+     * Verify if the given use cases have expected output.
+     * 1) Preview frame is received
+     * 2) imageCapture gets a captured JPEG image
+     * 3) imageAnalysis gets a Image in Analyzer.
+     */
+    private suspend fun verifyUseCasesOutput(
+        preview: Preview,
+        imageCapture: ImageCapture,
+        imageAnalysis: ImageAnalysis? = null,
+        previewFrameSemaphore: Semaphore? = null,
+        analysisSemaphore: Semaphore? = null,
+        expectedExifRotation: Int = 0,
+    ): Camera {
+        val camera =
+            withContext(Dispatchers.Main) {
+                preview.setSurfaceProvider(
+                    SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider {
+                        if (previewFrameSemaphore?.availablePermits() == 0) {
+                            previewFrameSemaphore.release()
+                        }
+                    }
+                )
+                imageAnalysis?.setAnalyzer(CameraXExecutors.mainThreadExecutor()) {
+                    it.close()
+                    if (analysisSemaphore?.availablePermits() == 0) {
+                        analysisSemaphore.release()
+                    }
+                }
+                val cameraSelector =
+                    getCameraSelectorWithSessionProcessor(
+                        cameraSelector,
+                        basicExtenderSessionProcessor
+                    )
+
+                val useCaseGroupBuilder = UseCaseGroup.Builder()
+                useCaseGroupBuilder.addUseCase(preview)
+                useCaseGroupBuilder.addUseCase(imageCapture)
+                imageAnalysis?.let { useCaseGroupBuilder.addUseCase(it) }
+
+                cameraProvider.bindToLifecycle(
+                    fakeLifecycleOwner,
+                    cameraSelector,
+                    useCaseGroupBuilder.build()
+                )
+            }
+
+        previewFrameSemaphore?.let {
+            assertThat(it.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        }
+
+        analysisSemaphore?.let {
+            assertThat(analysisSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        }
+
+        verifyStillCapture(imageCapture, expectedExifRotation)
+        return camera
+    }
+
+    private suspend fun verifyStillCapture(
+        imageCapture: ImageCapture,
+        expectExifRotation: Int = 0
+    ) {
+        val deferCapturedImage = CompletableDeferred<ImageProxy>()
+        imageCapture.takePicture(
+            CameraXExecutors.mainThreadExecutor(),
+            object : ImageCapture.OnImageCapturedCallback() {
+                override fun onCaptureSuccess(image: ImageProxy) {
+                    deferCapturedImage.complete(image)
+                }
+
+                override fun onError(exception: ImageCaptureException) {
+                    deferCapturedImage.completeExceptionally(exception)
+                }
+            })
+        withTimeout(6000) {
+            deferCapturedImage.await().use {
+                assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+                if (expectExifRotation != 0) {
+                    val exif = Exif.createFromImageProxy(it)
+                    assertThat(exif.rotation).isEqualTo(expectExifRotation)
+                }
+            }
+        }
+    }
+
+    private fun getCameraSelectorWithSessionProcessor(
+        cameraSelector: CameraSelector,
+        sessionProcessor: SessionProcessor
+    ): CameraSelector {
+        val identifier = Identifier.create("idStr")
+        ExtendedCameraConfigProviderStore.addConfig(identifier) { _, _ ->
+            object : CameraConfig {
+                override fun getConfig(): Config {
+                    return MutableOptionsBundle.create()
+                }
+
+                override fun getCompatibilityId(): Identifier {
+                    return Identifier.create(0)
+                }
+
+                override fun getSessionProcessor(
+                    valueIfMissing: SessionProcessor?
+                ): SessionProcessor {
+                    return sessionProcessor
+                }
+
+                override fun getSessionProcessor(): SessionProcessor {
+                    return sessionProcessor
+                }
+            }
+        }
+        val builder = CameraSelector.Builder.fromSelector(cameraSelector)
+        builder.addCameraFilter(object : CameraFilter {
+            override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
+                val newCameraInfos = mutableListOf<CameraInfo>()
+                newCameraInfos.addAll(cameraInfos)
+                return newCameraInfos
+            }
+
+            override fun getIdentifier(): Identifier {
+                return identifier
+            }
+        })
+        return builder.build()
+    }
+
+    open class FakeExtenderStateListener : ExtenderStateListener {
+        private val invokeList = mutableListOf<String>()
+        fun recordInvoking(action: String) {
+            invokeList.add(action)
+        }
+
+        fun assertInvokeOrder(expectList: List<String>) {
+            assertThat(expectList).containsExactlyElementsIn(invokeList).inOrder()
+        }
+        override fun onInit(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics,
+            context: Context
+        ) {
+            recordInvoking("onInit")
+        }
+
+        override fun onDeInit() {
+            recordInvoking("onDeInit")
+        }
+        override fun onPresetSession(): CaptureStageImpl? {
+            recordInvoking("onPresetSession")
+            return null
+        }
+        override fun onEnableSession(): CaptureStageImpl? {
+            recordInvoking("onEnableSession")
+            return onEnableSessionCaptureStage
+        }
+        override fun onDisableSession(): CaptureStageImpl? {
+            recordInvoking("onDisableSession")
+            return onDisableSessionCaptureStage
+        }
+
+        var onEnableSessionCaptureStage: CaptureStageImpl? = null
+        var onDisableSessionCaptureStage: CaptureStageImpl? = null
+    }
+
+    private class FakePreviewExtenderImpl(
+        private var processorType: ProcessorType = PROCESSOR_TYPE_NONE
+    ) : PreviewExtenderImpl, FakeExtenderStateListener() {
+        var fakePreviewImageProcessorImpl: FakePreviewImageProcessorImpl? = null
+        var fakeRequestUpdateProcessor: FakeRequestUpdateProcessor? = null
+
+        init {
+            when (processorType) {
+                PROCESSOR_TYPE_REQUEST_UPDATE_ONLY ->
+                    fakeRequestUpdateProcessor = FakeRequestUpdateProcessor()
+                PROCESSOR_TYPE_IMAGE_PROCESSOR ->
+                    fakePreviewImageProcessorImpl = FakePreviewImageProcessorImpl()
+                PROCESSOR_TYPE_NONE -> {}
+            }
+        }
+        override fun isExtensionAvailable(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics
+        ): Boolean {
+            return true
+        }
+        override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {
+            recordInvoking("init")
+        }
+
+        private var _captureStage: CaptureStageImpl? = null
+        override fun getCaptureStage(): CaptureStageImpl {
+            // Return CaptureStage if it is set already.
+            if (_captureStage != null) {
+                return _captureStage!!
+            }
+
+            // For PROCESSOR_TYPE_REQUEST_UPDATE_ONLY, getCaptureStage() should be in sync with
+            // RequestUpdateProcessor.
+            if (processorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) {
+                return fakeRequestUpdateProcessor!!.captureStage
+            }
+            return createCaptureStage()
+        }
+
+        fun setCaptureStage(captureStage: CaptureStageImpl) {
+            _captureStage = captureStage
+        }
+        override fun getProcessorType() = processorType
+        override fun getProcessor() =
+            when (processorType) {
+                PROCESSOR_TYPE_NONE -> null
+                PROCESSOR_TYPE_REQUEST_UPDATE_ONLY -> fakeRequestUpdateProcessor
+                PROCESSOR_TYPE_IMAGE_PROCESSOR -> fakePreviewImageProcessorImpl
+            }
+
+        override fun getSupportedResolutions() = null
+        override fun onDeInit() {
+            recordInvoking("onDeInit")
+            fakePreviewImageProcessorImpl?.close()
+        }
+    }
+
+    private class FakeImageCaptureExtenderImpl(
+        private val hasCaptureProcessor: Boolean = false,
+        private val throwErrorOnProcess: Boolean = false
+    ) : ImageCaptureExtenderImpl, FakeExtenderStateListener() {
+        val fakeCaptureProcessorImpl: FakeCaptureProcessorImpl? by lazy {
+            if (hasCaptureProcessor) {
+                FakeCaptureProcessorImpl(throwErrorOnProcess)
+            } else {
+                null
+            }
+        }
+
+        override fun isExtensionAvailable(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics
+        ): Boolean {
+            return true
+        }
+
+        override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {
+            recordInvoking("init")
+        }
+        override fun getCaptureProcessor() = fakeCaptureProcessorImpl
+
+        override fun getCaptureStages() = _captureStages
+        private var _captureStages: List<CaptureStageImpl> = listOf(createCaptureStage())
+        fun setCaptureStages(captureStages: List<CaptureStageImpl>) {
+            _captureStages = captureStages
+        }
+
+        override fun getMaxCaptureStage(): Int {
+            return 2
+        }
+
+        override fun getSupportedResolutions() = null
+        override fun getEstimatedCaptureLatencyRange(size: Size?) = null
+        override fun getAvailableCaptureRequestKeys(): MutableList<CaptureRequest.Key<Any>> {
+            return mutableListOf()
+        }
+
+        override fun getAvailableCaptureResultKeys(): MutableList<CaptureResult.Key<Any>> {
+            return mutableListOf()
+        }
+
+        override fun onDeInit() {
+            fakeCaptureProcessorImpl?.close()
+            recordInvoking("onDeInit")
+        }
+    }
+
+    private class FakeCaptureProcessorImpl(
+        val throwErrorOnProcess: Boolean = false
+    ) : CaptureProcessorImpl {
+        private var imageWriter: ImageWriter? = null
+        override fun process(results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?) {
+            if (throwErrorOnProcess) {
+                throw RuntimeException("Process failed")
+            }
+            val image = imageWriter!!.dequeueInputImage()
+            imageWriter!!.queueInputImage(image)
+        }
+
+        override fun process(
+            results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?,
+            resultCallback: ProcessResultImpl?,
+            executor: Executor?
+        ) {
+            process(results)
+        }
+
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            imageWriter = ImageWriter.newInstance(surface, 2)
+        }
+
+        override fun onResolutionUpdate(size: Size) {}
+        override fun onImageFormatUpdate(imageFormat: Int) {}
+        fun close() {
+            imageWriter?.close()
+            imageWriter = null
+        }
+    }
+
+    private class FakePreviewImageProcessorImpl : PreviewImageProcessorImpl {
+        private var imageWriter: ImageWriter? = null
+        override fun process(image: Image?, result: TotalCaptureResult?) {
+            val emptyImage = imageWriter!!.dequeueInputImage()
+            imageWriter!!.queueInputImage(emptyImage)
+        }
+
+        override fun process(
+            image: Image?,
+            result: TotalCaptureResult?,
+            resultCallback: ProcessResultImpl?,
+            executor: Executor?
+        ) {
+            process(image, result)
+        }
+
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            imageWriter = ImageWriter.newInstance(surface, 2)
+        }
+
+        override fun onResolutionUpdate(size: Size) {}
+        override fun onImageFormatUpdate(imageFormat: Int) {}
+        fun close() {
+            imageWriter?.close()
+            imageWriter = null
+        }
+    }
+
+    private class FakeRequestUpdateProcessor : RequestUpdateProcessorImpl {
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            throw RuntimeException("Should not invoke this")
+        }
+
+        override fun onResolutionUpdate(size: Size) {
+            throw RuntimeException("Should not invoke this")
+        }
+
+        override fun onImageFormatUpdate(imageFormat: Int) {
+            throw RuntimeException("Should not invoke this")
+        }
+
+        override fun process(result: TotalCaptureResult?): CaptureStageImpl? {
+            return if (hasCaptureStageChange) {
+                hasCaptureStageChange = false
+                captureStage // return non-null result to trigger a repeating request update.
+            } else {
+                null
+            }
+        }
+
+        var hasCaptureStageChange = false
+        var captureStage: CaptureStageImpl = createCaptureStage()
+            set(value) {
+                hasCaptureStageChange = true
+                field = value
+            }
+    }
+
+    class FakeRequestProcessor : RequestProcessor {
+        private var deferredSubmit = CompletableDeferred<List<RequestProcessor.Request>>()
+
+        suspend fun awaitRequestSubmitted(): List<RequestProcessor.Request> {
+            return deferredSubmit.await().also {
+                // renew another deferred
+                deferredSubmit = CompletableDeferred<List<RequestProcessor.Request>>()
+            }
+        }
+        override fun submit(
+            request: RequestProcessor.Request,
+            callback: RequestProcessor.Callback
+        ): Int {
+            return submit(mutableListOf(request), callback)
+        }
+
+        override fun submit(
+            requests: MutableList<RequestProcessor.Request>,
+            callback: RequestProcessor.Callback
+        ): Int {
+            deferredSubmit.complete(requests)
+            return 0
+        }
+
+        override fun setRepeating(
+            request: RequestProcessor.Request,
+            callback: RequestProcessor.Callback
+        ): Int {
+            return 0
+        }
+
+        override fun abortCaptures() {
+        }
+
+        override fun stopRepeating() {
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt
new file mode 100644
index 0000000..2ae872f
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.media.ImageReader
+import android.media.ImageWriter
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraXThreads
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl
+import androidx.camera.extensions.impl.ProcessResultImpl
+import androidx.camera.testing.Camera2Util
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import java.util.concurrent.Executor
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29) // Extensions supported on API 29+
+@RunWith(AndroidJUnit4::class)
+class PreviewProcessorTest {
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+
+    private lateinit var surfaceTexture: SurfaceTexture
+    private lateinit var previewSurface: Surface
+    private lateinit var previewProcessor: PreviewProcessor
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+    private lateinit var backgroundThread: HandlerThread
+    private lateinit var backgroundHandler: Handler
+    private lateinit var fakePreviewImageProcessorImpl: FakePreviewImageProcessorImpl
+
+    companion object {
+        const val CAMERA_ID = "0"
+        const val WIDTH = 640
+        const val HEIGHT = 480
+        const val MAX_IMAGES = 6
+    }
+
+    @Before
+    fun setUp() {
+        Assume.assumeTrue(CameraUtil.deviceHasCamera())
+
+        backgroundThread = HandlerThread(
+            CameraXThreads.TAG + "preview_processor_test"
+        )
+        backgroundThread.start()
+        backgroundHandler = Handler(backgroundThread.looper)
+        surfaceTexture = SurfaceTexture(0)
+        surfaceTexture.setDefaultBufferSize(WIDTH, HEIGHT)
+        surfaceTexture.detachFromGLContext()
+
+        previewSurface = Surface(surfaceTexture)
+        fakePreviewImageProcessorImpl = FakePreviewImageProcessorImpl()
+        previewProcessor = PreviewProcessor(
+            fakePreviewImageProcessorImpl, previewSurface, Size(WIDTH, HEIGHT)
+        )
+    }
+
+    @After
+    fun tearDown() {
+        if (::backgroundThread.isInitialized) {
+            backgroundThread.quitSafely()
+        }
+
+        if (::surfaceTexture.isInitialized) {
+            surfaceTexture.release()
+        }
+        if (::previewSurface.isInitialized) {
+            previewSurface.release()
+        }
+
+        if (::previewProcessor.isInitialized) {
+            previewProcessor.close()
+        }
+
+        if (::fakePreviewImageProcessorImpl.isInitialized) {
+            fakePreviewImageProcessorImpl.close()
+        }
+    }
+
+    @Test
+    fun canOutputToPreview(): Unit = runBlocking {
+        val cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+        val imageReaderYuv = ImageReader.newInstance(
+            WIDTH, HEIGHT, ImageFormat.YUV_420_888, MAX_IMAGES
+        )
+        val session = Camera2Util.openCaptureSession(
+            cameraDevice,
+            arrayListOf(imageReaderYuv.surface),
+            backgroundHandler
+        )
+
+        previewProcessor.start()
+
+        imageReaderYuv.setOnImageAvailableListener({
+            val image = it.acquireNextImage()
+            previewProcessor.notifyImage(createImageReference(image))
+        }, backgroundHandler)
+        Camera2Util.startRepeating(cameraDevice, session, arrayListOf(imageReaderYuv.surface)) {
+            previewProcessor.notifyCaptureResult(it)
+        }
+        val previewUpdateDeferred = CompletableDeferred<Boolean>()
+        surfaceTexture.setOnFrameAvailableListener {
+            previewUpdateDeferred.complete(true)
+        }
+
+        withTimeout(3000) {
+            assertTrue(previewUpdateDeferred.await())
+        }
+    }
+
+    @Test
+    fun canCloseProcessor(): Unit = runBlocking {
+        val cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+        val imageReaderYuv = ImageReader.newInstance(
+            WIDTH, HEIGHT, ImageFormat.YUV_420_888, MAX_IMAGES
+        )
+        val session = Camera2Util.openCaptureSession(
+            cameraDevice,
+            arrayListOf(imageReaderYuv.surface),
+            backgroundHandler
+        )
+
+        previewProcessor.start()
+
+        imageReaderYuv.setOnImageAvailableListener({
+            val image = it.acquireNextImage()
+            previewProcessor.notifyImage(createImageReference(image))
+        }, backgroundHandler)
+
+        Camera2Util.startRepeating(cameraDevice, session, arrayListOf(imageReaderYuv.surface)) {
+            previewProcessor.notifyCaptureResult(it)
+        }
+        val previewUpdateDeferred = CompletableDeferred<Boolean>()
+        surfaceTexture.setOnFrameAvailableListener {
+            previewUpdateDeferred.complete(true)
+        }
+
+        withTimeout(3000) {
+            previewUpdateDeferred.await()
+        }
+        previewProcessor.close()
+        // close the preview surface to see if closing causes any issues.
+        surfaceTexture.release()
+        previewSurface.release()
+
+        // Delay a little while to see if the close() causes any issue.
+        delay(1000)
+    }
+
+    private fun createImageReference(image: Image): ImageReference {
+        return object : ImageReference {
+            private var refCount = 1
+            override fun increment(): Boolean {
+                if (refCount <= 0) return false
+                refCount++
+                return true
+            }
+
+            override fun decrement(): Boolean {
+                if (refCount <= 0) return false
+                refCount--
+                if (refCount <= 0) {
+                    image.close()
+                }
+                return true
+            }
+
+            override fun get(): Image? {
+                return image
+            }
+        }
+    }
+
+    private class FakePreviewImageProcessorImpl : PreviewImageProcessorImpl {
+        private var imageWriter: ImageWriter? = null
+        override fun process(image: Image?, result: TotalCaptureResult?) {
+            val emptyImage = imageWriter!!.dequeueInputImage()
+            imageWriter!!.queueInputImage(emptyImage)
+        }
+
+        override fun process(
+            image: Image?,
+            result: TotalCaptureResult?,
+            resultCallback: ProcessResultImpl?,
+            executor: Executor?
+        ) {
+            val blankImage = imageWriter!!.dequeueInputImage()
+            imageWriter!!.queueInputImage(blankImage)
+        }
+
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            imageWriter = ImageWriter.newInstance(surface, 2)
+        }
+
+        override fun onResolutionUpdate(size: Size) {
+        }
+
+        override fun onImageFormatUpdate(imageFormat: Int) {
+        }
+
+        fun close() {
+            imageWriter?.close()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
new file mode 100644
index 0000000..401ad8d
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.media.ImageReader
+import android.media.ImageWriter
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraXThreads
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.ImageReaderProxys
+import androidx.camera.core.impl.ImageReaderProxy
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.extensions.impl.CaptureProcessorImpl
+import androidx.camera.extensions.impl.ProcessResultImpl
+import androidx.camera.extensions.internal.sessionprocessor.StillCaptureProcessor.OnCaptureResultCallback
+import androidx.camera.testing.Camera2Util
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29) // Extensions supported on API 29+
+@RunWith(AndroidJUnit4::class)
+class StillCaptureProcessorTest {
+    private lateinit var fakeCaptureProcessorImpl: FakeCaptureProcessorImpl
+    private lateinit var stillCaptureProcessor: StillCaptureProcessor
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+    private lateinit var backgroundThread: HandlerThread
+    private lateinit var backgroundHandler: Handler
+    private lateinit var imageReaderJpeg: ImageReaderProxy
+    private var cameraDevice: CameraDevice? = null
+    private var cameraYuvImageReader: ImageReader? = null
+    companion object {
+        const val CAMERA_ID = "0"
+        const val WIDTH = 640
+        const val HEIGHT = 480
+    }
+
+    @Before
+    fun setUp() {
+        Assume.assumeTrue(CameraUtil.deviceHasCamera())
+
+        cameraDevice?.close()
+        cameraYuvImageReader?.close()
+        backgroundThread = HandlerThread(
+            CameraXThreads.TAG + "still_capture_processor_test"
+        )
+        backgroundThread.start()
+        backgroundHandler = Handler(backgroundThread.looper)
+        fakeCaptureProcessorImpl = FakeCaptureProcessorImpl()
+        imageReaderJpeg = ImageReaderProxys.createIsolatedReader(WIDTH, HEIGHT, ImageFormat.JPEG, 2)
+        stillCaptureProcessor = StillCaptureProcessor(
+            fakeCaptureProcessorImpl, imageReaderJpeg.surface!!, Size(WIDTH, HEIGHT)
+        )
+    }
+
+    @After
+    fun tearDown() {
+        if (::stillCaptureProcessor.isInitialized) {
+            stillCaptureProcessor.close()
+        }
+        if (::backgroundThread.isInitialized) {
+            backgroundThread.quitSafely()
+        }
+        if (::imageReaderJpeg.isInitialized) {
+            imageReaderJpeg.close()
+        }
+
+        if (::fakeCaptureProcessorImpl.isInitialized) {
+            fakeCaptureProcessorImpl.close()
+        }
+    }
+
+    @Test
+    fun canOutputJpeg_3CaptureStages(): Unit = runBlocking {
+        withTimeout(10000) {
+            openCameraAndCaptureImageAwait(listOf(1, 2, 3))
+        }.use {
+            assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+        }
+    }
+
+    @Test
+    fun canOutputJpeg_1CaptureStage(): Unit = runBlocking {
+        withTimeout(10000) {
+            openCameraAndCaptureImageAwait(listOf(1))
+        }.use {
+            assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+        }
+    }
+
+    @Test
+    fun onErrorInvoked_oemProcessingFailed(): Unit = runBlocking {
+        fakeCaptureProcessorImpl.enableThrowExceptionDuringProcess()
+        assertThrows<Exception> {
+            withTimeout(3000) {
+                openCameraAndCaptureImageAwait(listOf(1)).close()
+            }
+        }
+    }
+
+    @Test
+    fun onErrorInvoked_jpegConversionFailed(): Unit = runBlocking {
+        val fakeYuvToJpegConverter = object : YuvToJpegConverter(100, imageReaderJpeg.surface!!) {
+            override fun writeYuvImage(imageProxy: ImageProxy) {
+                throw ConversionFailedException(
+                    "Failed to convert JPEG to YUV", null)
+            }
+        }
+        stillCaptureProcessor = StillCaptureProcessor(
+            fakeCaptureProcessorImpl,
+            imageReaderJpeg.surface!!,
+            Size(WIDTH, HEIGHT),
+            fakeYuvToJpegConverter
+        )
+        assertThrows<Exception> {
+            withTimeout(3000) {
+                openCameraAndCaptureImageAwait(listOf(1)).close()
+            }
+        }
+    }
+
+    private suspend fun captureImage(
+        cameraDevice: CameraDevice,
+        cameraCaptureSession: CameraCaptureSession,
+        cameraYuvImageReader: ImageReader,
+        captureStageIdList: List<Int>
+    ): ImageProxy {
+
+        cameraYuvImageReader.setOnImageAvailableListener(
+            {
+                val image = it.acquireNextImage()
+                stillCaptureProcessor.notifyImage(createImageReference(image))
+            }, backgroundHandler
+        )
+        val deferredCaptureCompleted = CompletableDeferred<Unit>()
+        stillCaptureProcessor.startCapture(captureStageIdList, object : OnCaptureResultCallback {
+            override fun onCompleted() {
+                deferredCaptureCompleted.complete(Unit)
+            }
+
+            override fun onError(e: Exception) {
+                deferredCaptureCompleted.completeExceptionally(e)
+            }
+        })
+
+        val outputJpegDeferred = CompletableDeferred<ImageProxy>()
+        imageReaderJpeg.setOnImageAvailableListener({
+            val image = it.acquireNextImage()
+            outputJpegDeferred.complete(image!!)
+        }, CameraXExecutors.newHandlerExecutor(backgroundHandler))
+
+        captureStageIdList.forEach { captureStageId ->
+            val captureResult = Camera2Util.submitSingleRequest(
+                cameraDevice,
+                cameraCaptureSession,
+                listOf(cameraYuvImageReader.surface),
+                backgroundHandler
+            )
+            stillCaptureProcessor.notifyCaptureResult(captureResult, captureStageId)
+        }
+        deferredCaptureCompleted.await()
+        return outputJpegDeferred.await()
+    }
+
+    @Test
+    fun canStartCaptureMultipleTimes(): Unit = runBlocking {
+        val captureStageIdList = listOf(0, 1, 2)
+        cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+        cameraYuvImageReader = ImageReader.newInstance(
+            WIDTH, HEIGHT, ImageFormat.YUV_420_888,
+            captureStageIdList.size /* maxImages */
+        )
+        val captureSession = Camera2Util.openCaptureSession(
+            cameraDevice!!, listOf(cameraYuvImageReader!!.surface), backgroundHandler
+        )
+
+        withTimeout(30000) {
+            repeat(3) {
+                captureImage(
+                    cameraDevice!!, captureSession, cameraYuvImageReader!!, listOf(0, 1, 2)
+                ).use {
+                    assertThat(it).isNotNull()
+                }
+            }
+        }
+    }
+
+    @Test
+    fun canSetRotation(): Unit = runBlocking {
+        val rotationDegrees = 270
+        withTimeout(10000) {
+            openCameraAndCaptureImageAwait(listOf(1), rotationDegrees = rotationDegrees)
+        }.use {
+            val exif = Exif.createFromImageProxy(it)
+            assertThat(exif.rotation).isEqualTo(rotationDegrees)
+        }
+    }
+
+    private suspend fun openCameraAndCaptureImageAwait(
+        captureStageIdList: List<Int>,
+        rotationDegrees: Int = 0,
+        onBeforeInputYuvReady: suspend () -> Unit = {},
+        onJpegProcessDone: suspend () -> Unit = {},
+    ): ImageProxy {
+        val (deferredCapture, deferredJpeg) = openCameraAndCaptureImage(
+            captureStageIdList,
+            rotationDegrees,
+            onBeforeInputYuvReady,
+            onJpegProcessDone
+        )
+        deferredCapture.await()
+        return deferredJpeg.await()
+    }
+
+    private suspend fun openCameraAndCaptureImage(
+        captureStageIdList: List<Int>,
+        rotationDegrees: Int = 0,
+        onBeforeInputYuvReady: suspend () -> Unit = {},
+        onJpegProcessDone: suspend () -> Unit = {},
+    ): Pair<Deferred<Unit>, Deferred<ImageProxy>> {
+        stillCaptureProcessor.setRotationDegrees(rotationDegrees)
+        cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+        cameraYuvImageReader = ImageReader.newInstance(
+            WIDTH, HEIGHT, ImageFormat.YUV_420_888,
+            captureStageIdList.size /* maxImages */
+        )
+        val captureSession = Camera2Util.openCaptureSession(
+            cameraDevice!!, listOf(cameraYuvImageReader!!.surface), backgroundHandler
+        )
+
+        val deferredCapture = CompletableDeferred<Unit>()
+        stillCaptureProcessor.startCapture(captureStageIdList, object : OnCaptureResultCallback {
+            override fun onCompleted() {
+                deferredCapture.complete(Unit)
+            }
+
+            override fun onError(e: java.lang.Exception) {
+                deferredCapture.completeExceptionally(e)
+            }
+        })
+
+        val deferredOutputJpeg = CompletableDeferred<ImageProxy>()
+        imageReaderJpeg.setOnImageAvailableListener({
+            val image = it.acquireNextImage()
+            deferredOutputJpeg.complete(image!!)
+        }, CameraXExecutors.newHandlerExecutor(backgroundHandler))
+
+        cameraYuvImageReader!!.setOnImageAvailableListener(
+            {
+                val image = it.acquireNextImage()
+                stillCaptureProcessor.notifyImage(createImageReference(image))
+            }, backgroundHandler
+        )
+
+        onBeforeInputYuvReady.invoke()
+
+        for (id in captureStageIdList) {
+            val captureResult = Camera2Util.submitSingleRequest(
+                cameraDevice!!,
+                captureSession,
+                listOf(cameraYuvImageReader!!.surface),
+                backgroundHandler
+            )
+            stillCaptureProcessor.notifyCaptureResult(captureResult, id)
+        }
+
+        onJpegProcessDone.invoke()
+
+        return Pair(deferredCapture, deferredOutputJpeg)
+    }
+
+    @Test
+    fun canCloseBeforeProcessing(): Unit = runBlocking {
+        withTimeout(3000) {
+            openCameraAndCaptureImage(
+                listOf(0, 1),
+                onBeforeInputYuvReady = {
+                    // Close the StillCaptureProcessor before it starts the processing.
+                    stillCaptureProcessor.close()
+                    // Close output jpeg image reader to see if processing failed.
+                    imageReaderJpeg.close()
+                },
+                onJpegProcessDone = {
+                    // Delay a little while to see if close causes any issue
+                    delay(1000)
+                }
+            )
+        }
+    }
+
+    @Test
+    fun canCloseBeforeJpegConversion(): Unit = runBlocking {
+        withTimeout(3000) {
+            openCameraAndCaptureImage(
+                listOf(0, 1),
+                onJpegProcessDone = {
+                    // Close the StillCaptureProcessor before it starts the JPEG encoding.
+                    stillCaptureProcessor.close()
+                    // Close output jpeg image reader to see if processing failed.
+                    imageReaderJpeg.close()
+                    // Delay a little while to see if close causes any issue
+                    delay(1000)
+                }
+            )
+        }
+    }
+
+    private fun createImageReference(image: Image): ImageReference {
+        return object : ImageReference {
+            private var refCount = 1
+            override fun increment(): Boolean {
+                if (refCount <= 0) return false
+                refCount++
+                return true
+            }
+
+            override fun decrement(): Boolean {
+                if (refCount <= 0) return false
+                refCount--
+                if (refCount <= 0) {
+                    image.close()
+                }
+                return true
+            }
+
+            override fun get(): Image? {
+                return image
+            }
+        }
+    }
+
+    // A fake CaptureProcessorImpl that simply output a blank Image.
+    class FakeCaptureProcessorImpl : CaptureProcessorImpl {
+        private var imageWriter: ImageWriter? = null
+
+        private var throwExceptionDuringProcess = false
+
+        fun enableThrowExceptionDuringProcess() {
+            throwExceptionDuringProcess = true
+        }
+        override fun process(
+            results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>?
+        ) {
+            if (throwExceptionDuringProcess) {
+                throw RuntimeException("Process failed")
+            }
+            val image = imageWriter!!.dequeueInputImage()
+            imageWriter!!.queueInputImage(image)
+        }
+
+        override fun process(
+            results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>?,
+            resultCallback: ProcessResultImpl?,
+            executor: Executor?
+        ) {
+            process(results)
+        }
+
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            imageWriter = ImageWriter.newInstance(surface, 2)
+        }
+
+        override fun onResolutionUpdate(size: Size) {
+        }
+
+        override fun onImageFormatUpdate(imageFormat: Int) {
+        }
+
+        fun close() {
+            imageWriter?.close()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt
new file mode 100644
index 0000000..b459806
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.ImageReaderProxys
+import androidx.camera.core.ImmutableImageInfo
+import androidx.camera.core.impl.ImageReaderProxy
+import androidx.camera.core.impl.TagBundle
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.TestImageUtil
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class YuvToJpegConverterTest {
+    companion object {
+        const val WIDTH = 640
+        const val HEIGHT = 480
+        const val MAX_IMAGES = 2
+    }
+
+    private lateinit var jpegImageReaderProxy: ImageReaderProxy
+    private lateinit var yuvToJpegConverter: YuvToJpegConverter
+
+    @Before
+    fun setUp() {
+        jpegImageReaderProxy = ImageReaderProxys.createIsolatedReader(
+            WIDTH, HEIGHT, ImageFormat.JPEG, MAX_IMAGES
+        )
+        yuvToJpegConverter = YuvToJpegConverter(
+            100, jpegImageReaderProxy.surface!!
+        )
+    }
+
+    @After
+    fun tearDown() {
+        jpegImageReaderProxy.close()
+    }
+
+    private fun generateYuvImage(): ImageProxy {
+        return TestImageUtil.createYuvFakeImageProxy(
+            ImmutableImageInfo.create(
+                TagBundle.emptyBundle(), 0, 0, Matrix()
+            ), WIDTH, HEIGHT
+        )
+    }
+
+    @Test
+    fun canOutputJpeg() = runBlocking {
+        val deferredImage = CompletableDeferred<ImageProxy>()
+        jpegImageReaderProxy.setOnImageAvailableListener({ imageReader ->
+            imageReader.acquireNextImage()?.let { deferredImage.complete(it) }
+        }, CameraXExecutors.ioExecutor())
+
+        val imageYuv = generateYuvImage()
+        yuvToJpegConverter.writeYuvImage(imageYuv)
+
+        withTimeout(1000) {
+            deferredImage.await().use {
+                assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+                assertExifWidthAndHeight(it, WIDTH, HEIGHT)
+            }
+        }
+    }
+
+    private fun assertExifWidthAndHeight(imageProxy: ImageProxy, width: Int, height: Int) {
+        val exif = Exif.createFromImageProxy(imageProxy)
+        assertThat(exif.width).isEqualTo(width)
+        assertThat(exif.height).isEqualTo(height)
+    }
+
+    @Test
+    fun canSetRotation() = runBlocking {
+        val rotationDegrees = 270
+        val deferredImage = CompletableDeferred<ImageProxy>()
+        jpegImageReaderProxy.setOnImageAvailableListener({ imageReader ->
+            imageReader.acquireNextImage()?.let { deferredImage.complete(it) }
+        }, CameraXExecutors.ioExecutor())
+
+        val imageYuv = generateYuvImage()
+        yuvToJpegConverter.setRotationDegrees(rotationDegrees)
+        yuvToJpegConverter.writeYuvImage(imageYuv)
+
+        withTimeout(1000) {
+            deferredImage.await().use {
+                assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+                val exif = Exif.createFromImageProxy(it)
+                assertThat(exif.rotation).isEqualTo(rotationDegrees)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
index f620986..512ebce 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
@@ -204,7 +204,7 @@
                 vendorExtender.init(cameraInfo);
 
                 ExtensionsUseCaseConfigFactory factory = new
-                        ExtensionsUseCaseConfigFactory(mode, vendorExtender, context);
+                        ExtensionsUseCaseConfigFactory(mode, vendorExtender);
 
                 ExtensionsConfig.Builder builder = new ExtensionsConfig.Builder()
                         .setExtensionMode(mode)
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index 37b5610..f419244 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -20,6 +20,7 @@
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Range;
 import android.util.Size;
@@ -47,8 +48,12 @@
 import androidx.camera.extensions.impl.NightPreviewExtenderImpl;
 import androidx.camera.extensions.impl.PreviewExtenderImpl;
 import androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator;
+import androidx.camera.extensions.internal.sessionprocessor.BasicExtenderSessionProcessor;
 import androidx.core.util.Preconditions;
 
+import org.jetbrains.annotations.TestOnly;
+
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -61,13 +66,11 @@
     private static final String TAG = "BasicVendorExtender";
     private final ExtensionDisabledValidator mExtensionDisabledValidator =
             new ExtensionDisabledValidator();
-    private final @ExtensionMode.Mode int mMode;
     private PreviewExtenderImpl mPreviewExtenderImpl = null;
     private ImageCaptureExtenderImpl mImageCaptureExtenderImpl = null;
     private CameraInfo mCameraInfo;
 
     public BasicVendorExtender(@ExtensionMode.Mode int mode) {
-        mMode = mode;
         try {
             switch (mode) {
                 case ExtensionMode.BOKEH:
@@ -99,24 +102,11 @@
         }
     }
 
-    /**
-     * Return the {@link PreviewExtenderImpl} instance which could be null if the implementation
-     * doesn't exist. This method will be removed once the existing basic extender implementation
-     * is migrated to the unified vendor extender.
-     */
-    @Nullable
-    public PreviewExtenderImpl getPreviewExtenderImpl() {
-        return mPreviewExtenderImpl;
-    }
-
-    /**
-     * Return the {@link ImageCaptureExtenderImpl} instance which could be null if the
-     * implementation doesn't exist.. This method will be removed once the existing basic
-     * extender implementation is migrated to the unified vendor extender.
-     */
-    @Nullable
-    public ImageCaptureExtenderImpl getImageCaptureExtenderImpl() {
-        return mImageCaptureExtenderImpl;
+    @TestOnly
+    BasicVendorExtender(ImageCaptureExtenderImpl imageCaptureExtenderImpl,
+            PreviewExtenderImpl previewExtenderImpl) {
+        mPreviewExtenderImpl = previewExtenderImpl;
+        mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
     }
 
     @Override
@@ -152,7 +142,6 @@
         mPreviewExtenderImpl.init(cameraId, cameraCharacteristics);
         mImageCaptureExtenderImpl.init(cameraId, cameraCharacteristics);
 
-        Logger.d(TAG, "Extension init Mode = " + mMode);
         Logger.d(TAG, "PreviewExtender processorType= " + mPreviewExtenderImpl.getProcessorType());
         Logger.d(TAG, "ImageCaptureExtender processor= "
                 + mImageCaptureExtenderImpl.getCaptureProcessor());
@@ -180,11 +169,7 @@
         return map.getOutputSizes(imageFormat);
     }
 
-    private int getPreviewOutputImageFormat() {
-        return ImageFormat.PRIVATE;
-    }
-
-    private int getCaptureOutputImageFormat() {
+    private int getCaptureInputImageFormat() {
         if (mImageCaptureExtenderImpl != null
                 && mImageCaptureExtenderImpl.getCaptureProcessor() != null) {
             return ImageFormat.YUV_420_888;
@@ -193,6 +178,16 @@
         }
     }
 
+    private int getPreviewInputImageFormat() {
+        if (mPreviewExtenderImpl != null
+                && mPreviewExtenderImpl.getProcessorType()
+                == PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+            return ImageFormat.YUV_420_888;
+        } else {
+            return ImageFormat.PRIVATE;
+        }
+    }
+
     @NonNull
     @Override
     public List<Pair<Integer, Size[]>> getSupportedPreviewOutputResolutions() {
@@ -204,17 +199,25 @@
                 List<Pair<Integer, Size[]>> result =
                         mPreviewExtenderImpl.getSupportedResolutions();
                 if (result != null) {
-                    return result;
+                    // Ensure the PRIVATE format is in the list.
+                    // PreviewExtenderImpl.getSupportedResolutions() returns the supported size
+                    // for input surface. We need to ensure output surface format is supported.
+                    return replaceImageFormatIfMissing(result,
+                            ImageFormat.YUV_420_888 /* formatToBeReplaced */,
+                            ImageFormat.PRIVATE /* newFormat */);
                 }
             } catch (NoSuchMethodError e) {
             }
         }
 
         // Returns output sizes from stream configuration map if OEM returns null or OEM does not
-        // implement the function. It is required to return all supported sizes so it must fetch
-        // all sizes from the stream configuration map here.
-        int imageformat = getPreviewOutputImageFormat();
-        return Arrays.asList(new Pair<>(imageformat, getOutputSizes(imageformat)));
+        // implement the function. BasicVendorExtender's SessionProcessor will always output
+        // to PRIVATE surface, but the input image which connect to the camera could be
+        // either YUV or PRIVATE. Since the input image from input surface is guaranteed to be
+        // able to output to the output surface, therefore we fetch the sizes from the
+        // input image format for the output format.
+        int inputImageFormat = getPreviewInputImageFormat();
+        return Arrays.asList(new Pair<>(ImageFormat.PRIVATE, getOutputSizes(inputImageFormat)));
     }
 
 
@@ -228,17 +231,51 @@
                 List<Pair<Integer, Size[]>> result =
                         mImageCaptureExtenderImpl.getSupportedResolutions();
                 if (result != null) {
-                    return result;
+                    // Ensure the JPEG format is in the list.
+                    // ImageCaptureExtenderImpl.getSupportedResolutions() returns the supported
+                    // size for input surface. We need to ensure output surface format is supported.
+                    return replaceImageFormatIfMissing(result,
+                            ImageFormat.YUV_420_888 /* formatToBeReplaced */,
+                            ImageFormat.JPEG /* newFormat */);
                 }
             } catch (NoSuchMethodError e) {
             }
         }
 
         // Returns output sizes from stream configuration map if OEM returns null or OEM does not
-        // implement the function. It is required to return all supported sizes so it must fetch
-        // all sizes from the stream configuration map here.
-        int imageFormat = getCaptureOutputImageFormat();
-        return Arrays.asList(new Pair<>(imageFormat, getOutputSizes(imageFormat)));
+        // implement the function. BasicVendorExtender's SessionProcessor will always output
+        // JPEG Images, but the input image which connect to the camera could be either YUV or
+        // JPEG. Since the input image from input surface is guaranteed to be able to output to
+        // the output surface, therefore we fetch the sizes from the input image format for the
+        // output format.
+        int inputImageFormat = getCaptureInputImageFormat();
+        return Arrays.asList(new Pair<>(ImageFormat.JPEG, getOutputSizes(inputImageFormat)));
+    }
+
+    private List<Pair<Integer, Size[]>> replaceImageFormatIfMissing(
+            List<Pair<Integer, Size[]>> input, int formatToBeReplaced, int newFormat) {
+        for (Pair<Integer, Size[]> pair : input) {
+            if (pair.first == newFormat) {
+                return input;
+            }
+        }
+
+        List<Pair<Integer, Size[]>> output = new ArrayList<>();
+        boolean formatFound = false;
+        for (Pair<Integer, Size[]> pair : input) {
+            if (pair.first == formatToBeReplaced) {
+                formatFound = true;
+                output.add(new Pair<>(newFormat, pair.second));
+            } else {
+                output.add(pair);
+            }
+        }
+
+        if (!formatFound) {
+            throw new IllegalArgumentException(
+                    "Supported resolution should contain " + newFormat + " format.");
+        }
+        return output;
     }
 
     @NonNull
@@ -252,9 +289,11 @@
     @Override
     public SessionProcessor createSessionProcessor(@NonNull Context context) {
         Preconditions.checkNotNull(mCameraInfo, "VendorExtender#init() must be called first");
-        /* Return null to keep using existing flow for basic extender to ensure compatibility for
-         * now. We will switch to SessionProcessor implementation once compatibility is ensured.
-         */
-        return null;
+        if (Build.VERSION.SDK_INT >= 26) {
+            return new BasicExtenderSessionProcessor(
+                    mPreviewExtenderImpl, mImageCaptureExtenderImpl, context);
+        } else {
+            throw new IllegalArgumentException("SessionProcessor is not supported");
+        }
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
index 76bff3f..35f2a92 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
@@ -18,8 +18,6 @@
 
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 
-import android.content.Context;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -41,10 +39,9 @@
 
     public ExtensionsUseCaseConfigFactory(
             @ExtensionMode.Mode int mode,
-            @NonNull VendorExtender vendorExtender,
-            @NonNull Context context) {
-        mImageCaptureConfigProvider = new ImageCaptureConfigProvider(mode, vendorExtender, context);
-        mPreviewConfigProvider = new PreviewConfigProvider(mode, vendorExtender, context);
+            @NonNull VendorExtender vendorExtender) {
+        mImageCaptureConfigProvider = new ImageCaptureConfigProvider(mode, vendorExtender);
+        mPreviewConfigProvider = new PreviewConfigProvider(mode, vendorExtender);
     }
 
     /**
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index fa38f9c..f3c9824 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -16,38 +16,17 @@
 
 package androidx.camera.extensions.internal;
 
-import android.content.Context;
-import android.hardware.camera2.CameraCharacteristics;
-import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallback;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
-import androidx.camera.camera2.interop.Camera2CameraInfo;
-import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
-import androidx.camera.core.CameraInfo;
 import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Logger;
-import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.CaptureBundle;
-import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.CaptureStage;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.ConfigProvider;
 import androidx.camera.core.impl.ImageCaptureConfig;
 import androidx.camera.extensions.ExtensionMode;
-import androidx.camera.extensions.impl.CaptureProcessorImpl;
-import androidx.camera.extensions.impl.CaptureStageImpl;
-import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
-import androidx.core.util.Preconditions;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -55,31 +34,26 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ImageCaptureConfigProvider implements ConfigProvider<ImageCaptureConfig> {
-    private static final String TAG = "ImageCaptureConfigProvider";
     static final Config.Option<Integer> OPTION_IMAGE_CAPTURE_CONFIG_PROVIDER_MODE =
             Config.Option.create("camerax.extensions.imageCaptureConfigProvider.mode",
                     Integer.class);
 
     private final VendorExtender mVendorExtender;
-    private final Context mContext;
     @ExtensionMode.Mode
     private final int mEffectMode;
 
-    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     public ImageCaptureConfigProvider(
             @ExtensionMode.Mode int mode,
-            @NonNull VendorExtender vendorExtender,
-            @NonNull Context context) {
+            @NonNull VendorExtender vendorExtender) {
         mEffectMode = mode;
         mVendorExtender = vendorExtender;
-        mContext = context;
     }
 
     @NonNull
     @Override
     public ImageCaptureConfig getConfig() {
         ImageCapture.Builder builder = new ImageCapture.Builder();
-        updateBuilderConfig(builder, mEffectMode, mVendorExtender, mContext);
+        updateBuilderConfig(builder, mEffectMode, mVendorExtender);
 
         return builder.getUseCaseConfig();
     }
@@ -87,41 +61,9 @@
     /**
      * Update extension related configs to the builder.
      */
-    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     void updateBuilderConfig(@NonNull ImageCapture.Builder builder,
-            @ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender,
-            @NonNull Context context) {
-        if (vendorExtender instanceof BasicVendorExtender) {
-            ImageCaptureExtenderImpl imageCaptureExtenderImpl =
-                    ((BasicVendorExtender) vendorExtender).getImageCaptureExtenderImpl();
-
-            if (imageCaptureExtenderImpl != null) {
-                CaptureProcessorImpl captureProcessor =
-                        imageCaptureExtenderImpl.getCaptureProcessor();
-                AdaptingCaptureProcessor adaptingCaptureProcessor = null;
-                if (captureProcessor != null) {
-                    adaptingCaptureProcessor = new AdaptingCaptureProcessor(captureProcessor);
-                    builder.setCaptureProcessor(adaptingCaptureProcessor);
-                }
-
-                if (imageCaptureExtenderImpl.getMaxCaptureStage() > 0) {
-                    builder.setMaxCaptureStages(
-                            imageCaptureExtenderImpl.getMaxCaptureStage());
-                }
-
-                ImageCaptureEventAdapter imageCaptureEventAdapter =
-                        new ImageCaptureEventAdapter(imageCaptureExtenderImpl,
-                                context, adaptingCaptureProcessor);
-                new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback(
-                        new CameraEventCallbacks(imageCaptureEventAdapter));
-                builder.setUseCaseEventCallback(imageCaptureEventAdapter);
-
-                builder.setCaptureBundle(imageCaptureEventAdapter);
-            } else {
-                Logger.e(TAG, "ImageCaptureExtenderImpl is null!");
-            }
-        }
-
+            @ExtensionMode.Mode int effectMode,
+            @NonNull VendorExtender vendorExtender) {
         builder.getMutableConfig().insertOption(OPTION_IMAGE_CAPTURE_CONFIG_PROVIDER_MODE,
                 effectMode);
         List<Pair<Integer, Size[]>> supportedResolutions =
@@ -129,122 +71,4 @@
         builder.setSupportedResolutions(supportedResolutions);
         builder.setHighResolutionDisabled(true);
     }
-
-
-    /**
-     * An implementation to adapt the OEM provided implementation to core.
-     */
-    private static class ImageCaptureEventAdapter extends CameraEventCallback implements
-            UseCase.EventCallback,
-            CaptureBundle {
-        @NonNull
-        private final ImageCaptureExtenderImpl mImpl;
-        @NonNull
-        private final Context mContext;
-        @Nullable
-        private VendorProcessor mVendorCaptureProcessor;
-        @Nullable
-        private volatile CameraInfo mCameraInfo;
-        ImageCaptureEventAdapter(@NonNull ImageCaptureExtenderImpl impl,
-                @NonNull Context context,
-                @Nullable VendorProcessor vendorCaptureProcessor) {
-            mImpl = impl;
-            mContext = context;
-            mVendorCaptureProcessor = vendorCaptureProcessor;
-        }
-
-        // Invoked from main thread
-        @Override
-        public void onAttach(@NonNull CameraInfo cameraInfo) {
-            mCameraInfo = cameraInfo;
-        }
-
-        // Invoked from main thread
-        @Override
-        public void onDetach() {
-        }
-
-        // Invoked from camera thread
-        @Override
-        @Nullable
-        @OptIn(markerClass = ExperimentalCamera2Interop.class)
-        public CaptureConfig onInitSession() {
-            Preconditions.checkNotNull(mCameraInfo,
-                    "ImageCaptureConfigProvider was not attached.");
-            String cameraId = Camera2CameraInfo.from(mCameraInfo).getCameraId();
-            CameraCharacteristics cameraCharacteristics =
-                    Camera2CameraInfo.extractCameraCharacteristics(mCameraInfo);
-            Logger.d(TAG, "ImageCapture onInit");
-            mImpl.onInit(cameraId, cameraCharacteristics, mContext);
-
-            if (mVendorCaptureProcessor != null) {
-                mVendorCaptureProcessor.onInit();
-            }
-
-            CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
-            if (captureStageImpl != null) {
-                if (Build.VERSION.SDK_INT >= 28) {
-                    return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
-                } else {
-                    Logger.w(TAG, "The CaptureRequest parameters returned from "
-                            + "onPresetSession() will be passed to the camera device as part "
-                            + "of the capture session via "
-                            + "SessionConfiguration#setSessionParameters(CaptureRequest) "
-                            + "which only supported from API level 28!");
-                }
-            }
-            return null;
-        }
-
-        // Invoked from camera thread
-        @Override
-        @Nullable
-        public CaptureConfig onEnableSession() {
-            Logger.d(TAG, "ImageCapture onEnableSession");
-            CaptureStageImpl captureStageImpl = mImpl.onEnableSession();
-            if (captureStageImpl != null) {
-                return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
-            }
-
-            return null;
-        }
-
-        // Invoked from camera thread
-        @Override
-        @Nullable
-        public CaptureConfig onDisableSession() {
-            Logger.d(TAG, "ImageCapture onDisableSession");
-            CaptureStageImpl captureStageImpl = mImpl.onDisableSession();
-            if (captureStageImpl != null) {
-                return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
-            }
-
-            return null;
-        }
-
-        // Invoked from main thread
-        @Override
-        @Nullable
-        public List<CaptureStage> getCaptureStages() {
-            List<CaptureStageImpl> captureStages = mImpl.getCaptureStages();
-            if (captureStages != null && !captureStages.isEmpty()) {
-                ArrayList<CaptureStage> ret = new ArrayList<>();
-                for (CaptureStageImpl s : captureStages) {
-                    ret.add(new AdaptingCaptureStage(s));
-                }
-                return ret;
-            }
-
-            return null;
-        }
-
-        // Invoked from camera thread
-        @Override
-        public void onDeInitSession() {
-            if (mVendorCaptureProcessor != null) {
-                mVendorCaptureProcessor.onDeInit();
-            }
-            mImpl.onDeInit();
-        }
-    }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index 2019e0b..f714479 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -16,35 +16,16 @@
 
 package androidx.camera.extensions.internal;
 
-import android.content.Context;
-import android.hardware.camera2.CameraCharacteristics;
-import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 
-import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallback;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
-import androidx.camera.camera2.interop.Camera2CameraInfo;
-import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
-import androidx.camera.core.CameraInfo;
-import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
-import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.ConfigProvider;
 import androidx.camera.core.impl.PreviewConfig;
 import androidx.camera.extensions.ExtensionMode;
-import androidx.camera.extensions.impl.CaptureStageImpl;
-import androidx.camera.extensions.impl.PreviewExtenderImpl;
-import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
-import androidx.core.util.Preconditions;
 
 import java.util.List;
 
@@ -57,211 +38,33 @@
     static final Config.Option<Integer> OPTION_PREVIEW_CONFIG_PROVIDER_MODE = Config.Option.create(
             "camerax.extensions.previewConfigProvider.mode", Integer.class);
     private final VendorExtender mVendorExtender;
-    private final Context mContext;
     @ExtensionMode.Mode
     private final int mEffectMode;
 
-    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     public PreviewConfigProvider(
             @ExtensionMode.Mode int mode,
-            @NonNull VendorExtender vendorExtender,
-            @NonNull Context context) {
+            @NonNull VendorExtender vendorExtender) {
         mEffectMode = mode;
         mVendorExtender = vendorExtender;
-        mContext = context;
     }
 
     @NonNull
     @Override
     public PreviewConfig getConfig() {
         Preview.Builder builder = new Preview.Builder();
-        updateBuilderConfig(builder, mEffectMode, mVendorExtender, mContext);
-
+        updateBuilderConfig(builder, mEffectMode, mVendorExtender);
         return builder.getUseCaseConfig();
     }
 
     /**
      * Update extension related configs to the builder.
      */
-    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     void updateBuilderConfig(@NonNull Preview.Builder builder,
-            @ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender,
-            @NonNull Context context) {
-        if (vendorExtender instanceof BasicVendorExtender) {
-            PreviewExtenderImpl previewExtenderImpl =
-                    ((BasicVendorExtender) vendorExtender).getPreviewExtenderImpl();
-
-            if (previewExtenderImpl != null) {
-                PreviewEventAdapter previewEventAdapter;
-
-                switch (previewExtenderImpl.getProcessorType()) {
-                    case PROCESSOR_TYPE_REQUEST_UPDATE_ONLY:
-                        AdaptingRequestUpdateProcessor adaptingRequestUpdateProcessor =
-                                new AdaptingRequestUpdateProcessor(previewExtenderImpl);
-                        builder.setImageInfoProcessor(adaptingRequestUpdateProcessor);
-                        previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
-                                adaptingRequestUpdateProcessor);
-                        break;
-                    case PROCESSOR_TYPE_IMAGE_PROCESSOR:
-                        AdaptingPreviewProcessor adaptingPreviewProcessor = new
-                                AdaptingPreviewProcessor(
-                                (PreviewImageProcessorImpl) previewExtenderImpl.getProcessor());
-                        builder.setCaptureProcessor(adaptingPreviewProcessor);
-                        builder.setIsRgba8888SurfaceRequired(true);
-                        previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
-                                adaptingPreviewProcessor);
-                        break;
-                    default:
-                        previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
-                                null);
-                }
-                new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback(
-                        new CameraEventCallbacks(previewEventAdapter));
-                builder.setUseCaseEventCallback(previewEventAdapter);
-            } else {
-                Logger.e(TAG, "PreviewExtenderImpl is null!");
-            }
-        } else { // Advanced extensions interface.
-            // Set RGB8888 = true always since we have no way to tell if the OEM implementation does
-            // the processing or not.
-            builder.setIsRgba8888SurfaceRequired(true);
-        }
-
+            @ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender) {
         builder.getMutableConfig().insertOption(OPTION_PREVIEW_CONFIG_PROVIDER_MODE, effectMode);
         List<Pair<Integer, Size[]>> supportedResolutions =
                 vendorExtender.getSupportedPreviewOutputResolutions();
         builder.setSupportedResolutions(supportedResolutions);
         builder.setHighResolutionDisabled(true);
     }
-
-    /**
-     * An implementation to adapt the OEM provided implementation to core.
-     */
-    private static class PreviewEventAdapter extends CameraEventCallback implements
-            UseCase.EventCallback {
-        @NonNull
-        final PreviewExtenderImpl mImpl;
-        @NonNull
-        private final Context mContext;
-        @Nullable
-        private final VendorProcessor mPreviewProcessor;
-        @Nullable
-        CameraInfo mCameraInfo;
-        @GuardedBy("mLock")
-        final Object mLock = new Object();
-
-        PreviewEventAdapter(@NonNull PreviewExtenderImpl impl,
-                @NonNull Context context, @Nullable VendorProcessor previewProcessor) {
-            mImpl = impl;
-            mContext = context;
-            mPreviewProcessor = previewProcessor;
-        }
-
-        // Invoked from main thread
-        @Override
-        public void onAttach(@NonNull CameraInfo cameraInfo) {
-            synchronized (mLock) {
-                mCameraInfo = cameraInfo;
-            }
-        }
-
-        // Invoked from main thread
-        @Override
-        public void onDetach() {
-            synchronized (mLock) {
-                if (mPreviewProcessor != null) {
-                    mPreviewProcessor.close();
-                }
-            }
-        }
-
-        // Invoked from camera thread
-        @Override
-        @Nullable
-        @OptIn(markerClass = ExperimentalCamera2Interop.class)
-        public CaptureConfig onInitSession() {
-            synchronized (mLock) {
-                Preconditions.checkNotNull(mCameraInfo,
-                        "PreviewConfigProvider was not attached.");
-                String cameraId = Camera2CameraInfo.from(mCameraInfo).getCameraId();
-                CameraCharacteristics cameraCharacteristics =
-                        Camera2CameraInfo.extractCameraCharacteristics(mCameraInfo);
-                Logger.d(TAG, "Preview onInit");
-                mImpl.onInit(cameraId, cameraCharacteristics, mContext);
-                if (mPreviewProcessor != null) {
-                    mPreviewProcessor.onInit();
-                }
-                CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
-                if (captureStageImpl != null) {
-                    if (Build.VERSION.SDK_INT >= 28) {
-                        return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
-                    } else {
-                        Logger.w(TAG, "The CaptureRequest parameters returned from "
-                                + "onPresetSession() will be passed to the camera device as part "
-                                + "of the capture session via "
-                                + "SessionConfiguration#setSessionParameters(CaptureRequest) "
-                                + "which only supported from API level 28!");
-                    }
-                }
-            }
-
-            return null;
-        }
-
-        // Invoked from camera thread
-        @Override
-        @Nullable
-        public CaptureConfig onEnableSession() {
-            synchronized (mLock) {
-                Logger.d(TAG, "Preview onEnableSession");
-                CaptureStageImpl captureStageImpl = mImpl.onEnableSession();
-                if (captureStageImpl != null) {
-                    return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
-                }
-            }
-
-            return null;
-        }
-
-        // Invoked from camera thread
-        @Override
-        @Nullable
-        public CaptureConfig onDisableSession() {
-            synchronized (mLock) {
-                Logger.d(TAG, "Preview onDisableSession");
-                CaptureStageImpl captureStageImpl = mImpl.onDisableSession();
-                if (captureStageImpl != null) {
-                    return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
-                }
-            }
-
-            return null;
-        }
-
-        // Invoked from camera thread
-        @Override
-        @Nullable
-        public CaptureConfig onRepeating() {
-            synchronized (mLock) {
-                CaptureStageImpl captureStageImpl = mImpl.getCaptureStage();
-                if (captureStageImpl != null) {
-                    return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
-                }
-            }
-
-            return null;
-        }
-
-        // Invoked from camera thread
-        @Override
-        public void onDeInitSession() {
-            synchronized (mLock) {
-                Logger.d(TAG, "Preview onDeInit");
-                if (mPreviewProcessor != null) {
-                    mPreviewProcessor.onDeInit();
-                }
-                mImpl.onDeInit();
-            }
-        }
-    }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java
index e1f7380..8c882ac 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java
@@ -69,7 +69,7 @@
     Range<Long> getEstimatedCaptureLatencyRange(@Nullable Size size);
 
     /**
-     * Gets the supported output resolutions for preview.
+     * Gets the supported output resolutions for preview. PRIVATE format must be supported.
      *
      * <p>Pair list composed with {@link ImageFormat} and {@link Size} array will be returned.
      *
@@ -84,7 +84,7 @@
     List<Pair<Integer, Size[]>> getSupportedPreviewOutputResolutions();
 
     /**
-     * Gets the supported output resolutions for image capture.
+     * Gets the supported output resolutions for image capture. JPEG format must be supported.
      *
      * <p>Pair list composed with {@link ImageFormat} and {@link Size} array will be returned.
      *
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
new file mode 100644
index 0000000..74de26b
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import static androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR;
+import static androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.util.Pair;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
+import androidx.camera.camera2.interop.CaptureRequestOptions;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraCaptureFailure;
+import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.OutputSurface;
+import androidx.camera.core.impl.RequestProcessor;
+import androidx.camera.core.impl.SessionProcessor;
+import androidx.camera.extensions.impl.CaptureProcessorImpl;
+import androidx.camera.extensions.impl.CaptureStageImpl;
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
+import androidx.camera.extensions.impl.PreviewExtenderImpl;
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A {@link SessionProcessor} based on OEMs' basic extender implementation.
+ */
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
+@RequiresApi(26) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class BasicExtenderSessionProcessor extends SessionProcessorBase {
+    private static final String TAG = "BasicSessionProcessor";
+
+    private static final int PREVIEW_PROCESS_MAX_IMAGES = 2;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final PreviewExtenderImpl mPreviewExtenderImpl;
+    @NonNull
+    private final ImageCaptureExtenderImpl mImageCaptureExtenderImpl;
+
+    final Object mLock = new Object();
+    volatile StillCaptureProcessor mStillCaptureProcessor = null;
+    volatile PreviewProcessor mPreviewProcessor = null;
+    volatile RequestUpdateProcessorImpl mRequestUpdateProcessor = null;
+    private volatile Camera2OutputConfig mPreviewOutputConfig;
+    private volatile Camera2OutputConfig mCaptureOutputConfig;
+    @Nullable
+    private volatile Camera2OutputConfig mAnalysisOutputConfig = null;
+    private volatile OutputSurface mPreviewOutputSurfaceConfig;
+    private volatile OutputSurface mCaptureOutputSurfaceConfig;
+    private volatile RequestProcessor mRequestProcessor;
+    volatile boolean mIsCapturing = false;
+    private final AtomicInteger mNextCaptureSequenceId = new AtomicInteger(0);
+    static AtomicInteger sLastOutputConfigId = new AtomicInteger(0);
+    @GuardedBy("mLock")
+    private final Map<CaptureRequest.Key<?>, Object> mParameters = new LinkedHashMap<>();
+
+    public BasicExtenderSessionProcessor(@NonNull PreviewExtenderImpl previewExtenderImpl,
+            @NonNull ImageCaptureExtenderImpl imageCaptureExtenderImpl,
+            @NonNull Context context) {
+        mPreviewExtenderImpl = previewExtenderImpl;
+        mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
+        mContext = context;
+    }
+
+    @NonNull
+    @Override
+    protected Camera2SessionConfig initSessionInternal(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> cameraCharacteristicsMap,
+            @NonNull OutputSurface previewSurfaceConfig,
+            @NonNull OutputSurface imageCaptureSurfaceConfig,
+            @Nullable OutputSurface imageAnalysisSurfaceConfig) {
+        Logger.d(TAG, "PreviewExtenderImpl.onInit");
+        mPreviewExtenderImpl.onInit(cameraId, cameraCharacteristicsMap.get(cameraId),
+                mContext);
+        Logger.d(TAG, "ImageCaptureExtenderImpl.onInit");
+        mImageCaptureExtenderImpl.onInit(cameraId, cameraCharacteristicsMap.get(cameraId),
+                mContext);
+
+        mPreviewOutputSurfaceConfig = previewSurfaceConfig;
+        mCaptureOutputSurfaceConfig = imageCaptureSurfaceConfig;
+
+        // Preview
+        PreviewExtenderImpl.ProcessorType processorType =
+                mPreviewExtenderImpl.getProcessorType();
+        Logger.d(TAG, "preview processorType=" + processorType);
+        if (processorType == PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+            mPreviewOutputConfig = ImageReaderOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    previewSurfaceConfig.getSize(),
+                    ImageFormat.YUV_420_888,
+                    PREVIEW_PROCESS_MAX_IMAGES);
+            PreviewImageProcessorImpl previewImageProcessor =
+                    (PreviewImageProcessorImpl) mPreviewExtenderImpl.getProcessor();
+            mPreviewProcessor = new PreviewProcessor(
+                    previewImageProcessor, mPreviewOutputSurfaceConfig.getSurface(),
+                    mPreviewOutputSurfaceConfig.getSize());
+        } else if (processorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) {
+            mPreviewOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    previewSurfaceConfig.getSurface());
+            mRequestUpdateProcessor =
+                    (RequestUpdateProcessorImpl) mPreviewExtenderImpl.getProcessor();
+        } else {
+            mPreviewOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    previewSurfaceConfig.getSurface());
+        }
+
+        // Image Capture
+        CaptureProcessorImpl captureProcessor = mImageCaptureExtenderImpl.getCaptureProcessor();
+        Logger.d(TAG, "CaptureProcessor=" + captureProcessor);
+
+        if (captureProcessor != null) {
+            mCaptureOutputConfig = ImageReaderOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    imageCaptureSurfaceConfig.getSize(),
+                    ImageFormat.YUV_420_888,
+                    mImageCaptureExtenderImpl.getMaxCaptureStage());
+            mStillCaptureProcessor = new StillCaptureProcessor(
+                    captureProcessor, mCaptureOutputSurfaceConfig.getSurface(),
+                    mCaptureOutputSurfaceConfig.getSize());
+        } else {
+            mCaptureOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    imageCaptureSurfaceConfig.getSurface());
+        }
+
+        // Image Analysis
+        if (imageAnalysisSurfaceConfig != null) {
+            mAnalysisOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    imageAnalysisSurfaceConfig.getSurface());
+        }
+
+        Camera2SessionConfigBuilder builder =
+                new Camera2SessionConfigBuilder()
+                        .addOutputConfig(mPreviewOutputConfig)
+                        .addOutputConfig(mCaptureOutputConfig)
+                        .setSessionTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+
+        if (mAnalysisOutputConfig != null) {
+            builder.addOutputConfig(mAnalysisOutputConfig);
+        }
+
+        CaptureStageImpl captureStagePreview = mPreviewExtenderImpl.onPresetSession();
+        Logger.d(TAG, "preview onPresetSession:" + captureStagePreview);
+
+        CaptureStageImpl captureStageCapture = mImageCaptureExtenderImpl.onPresetSession();
+        Logger.d(TAG, "capture onPresetSession:" + captureStageCapture);
+
+        if (captureStagePreview != null && captureStagePreview.getParameters() != null) {
+            for (Pair<CaptureRequest.Key, Object> parameter :
+                    captureStagePreview.getParameters()) {
+                builder.addSessionParameter(parameter.first, parameter.second);
+            }
+        }
+
+        if (captureStageCapture != null && captureStageCapture.getParameters() != null) {
+            for (Pair<CaptureRequest.Key, Object> parameter :
+                    captureStageCapture.getParameters()) {
+                builder.addSessionParameter(parameter.first, parameter.second);
+            }
+        }
+        return builder.build();
+    }
+
+    @Override
+    protected void deInitSessionInternal() {
+        Logger.d(TAG, "preview onDeInit");
+        mPreviewExtenderImpl.onDeInit();
+        Logger.d(TAG, "capture onDeInit");
+        mImageCaptureExtenderImpl.onDeInit();
+
+        if (mPreviewProcessor != null) {
+            mPreviewProcessor.close();
+            mPreviewProcessor = null;
+        }
+        if (mStillCaptureProcessor != null) {
+            mStillCaptureProcessor.close();
+            mStillCaptureProcessor = null;
+        }
+    }
+
+    @Override
+    public void setParameters(@NonNull Config config) {
+        synchronized (mLock) {
+            HashMap<CaptureRequest.Key<?>, Object> map = new HashMap<>();
+
+            CaptureRequestOptions options =
+                    CaptureRequestOptions.Builder.from(config).build();
+
+            for (Config.Option<?> option : options.listOptions()) {
+                @SuppressWarnings("unchecked")
+                CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+                map.put(key, options.retrieveOption(option));
+            }
+            mParameters.clear();
+            mParameters.putAll(map);
+            applyRotationAndJpegQualityToProcessor();
+        }
+    }
+
+    @Override
+    public void onCaptureSessionStart(@NonNull RequestProcessor requestProcessor) {
+        mRequestProcessor = requestProcessor;
+
+        List<CaptureStageImpl> captureStages = new ArrayList<>();
+        CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onEnableSession();
+        Logger.d(TAG, "preview onEnableSession: " + captureStage1);
+        if (captureStage1 != null) {
+            captureStages.add(captureStage1);
+        }
+        CaptureStageImpl captureStage2 = mImageCaptureExtenderImpl.onEnableSession();
+        Logger.d(TAG, "capture onEnableSession:" + captureStage2);
+        if (captureStage2 != null) {
+            captureStages.add(captureStage2);
+        }
+
+        if (!captureStages.isEmpty()) {
+            submitRequestByCaptureStages(requestProcessor, captureStages);
+        }
+
+        if (mPreviewProcessor != null) {
+            setImageProcessor(mPreviewOutputConfig.getId(),
+                    new ImageProcessor() {
+                        @Override
+                        public void onNextImageAvailable(int outputStreamId, long timestampNs,
+                                @NonNull ImageReference imageReference,
+                                @Nullable String physicalCameraId) {
+                            if (mPreviewProcessor != null) {
+                                mPreviewProcessor.notifyImage(imageReference);
+                            }
+                        }
+                    });
+            mPreviewProcessor.start();
+        }
+    }
+
+    private void applyParameters(RequestBuilder builder) {
+        synchronized (mLock) {
+            for (CaptureRequest.Key<?> key : mParameters.keySet()) {
+                Object value = mParameters.get(key);
+                if (value != null) {
+                    builder.setParameters(key, value);
+                }
+            }
+        }
+    }
+
+    private void applyRotationAndJpegQualityToProcessor() {
+        synchronized (mLock) {
+            if (mStillCaptureProcessor == null) {
+                return;
+            }
+            Integer orientationObj = (Integer) mParameters.get(CaptureRequest.JPEG_ORIENTATION);
+            if (orientationObj != null) {
+                mStillCaptureProcessor.setRotationDegrees(orientationObj);
+            }
+
+            Byte qualityObj = (Byte) mParameters.get(CaptureRequest.JPEG_QUALITY);
+            if (qualityObj != null) {
+                mStillCaptureProcessor.setJpegQuality((int) qualityObj);
+            }
+        }
+    }
+
+
+    private void submitRequestByCaptureStages(RequestProcessor requestProcessor,
+            List<CaptureStageImpl> captureStageList) {
+        List<RequestProcessor.Request> requestList = new ArrayList<>();
+        for (CaptureStageImpl captureStage : captureStageList) {
+            RequestBuilder builder = new RequestBuilder();
+            builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+            if (mAnalysisOutputConfig != null) {
+                builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+            }
+            for (Pair<CaptureRequest.Key, Object> keyObjectPair : captureStage.getParameters()) {
+                builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+            }
+            builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+            requestList.add(builder.build());
+        }
+        requestProcessor.submit(requestList, new RequestProcessor.Callback() {
+        });
+    }
+
+    @Override
+    public void onCaptureSessionEnd() {
+        List<CaptureStageImpl> captureStages = new ArrayList<>();
+        CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onDisableSession();
+        Logger.d(TAG, "preview onDisableSession: " + captureStage1);
+        if (captureStage1 != null) {
+            captureStages.add(captureStage1);
+        }
+        CaptureStageImpl captureStage2 = mImageCaptureExtenderImpl.onDisableSession();
+        Logger.d(TAG, "capture onDisableSession:" + captureStage2);
+        if (captureStage2 != null) {
+            captureStages.add(captureStage2);
+        }
+
+        if (!captureStages.isEmpty()) {
+            submitRequestByCaptureStages(mRequestProcessor, captureStages);
+        }
+        mRequestProcessor = null;
+        mIsCapturing = false;
+    }
+
+    @Override
+    public int startRepeating(@NonNull CaptureCallback captureCallback) {
+        int repeatingCaptureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+        if (mRequestProcessor == null) {
+            captureCallback.onCaptureFailed(repeatingCaptureSequenceId);
+            captureCallback.onCaptureSequenceAborted(repeatingCaptureSequenceId);
+        } else {
+            updateRepeating(repeatingCaptureSequenceId, captureCallback);
+        }
+
+        return repeatingCaptureSequenceId;
+    }
+
+    void updateRepeating(int repeatingCaptureSequenceId, @NonNull CaptureCallback captureCallback) {
+        if (mRequestProcessor == null) {
+            Logger.d(TAG, "mRequestProcessor is null, ignore repeating request");
+            return;
+        }
+        RequestBuilder builder = new RequestBuilder();
+        builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+        if (mAnalysisOutputConfig != null) {
+            builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+        }
+        builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+        applyParameters(builder);
+        applyPreviewStagesParameters(builder);
+
+        RequestProcessor.Callback callback = new RequestProcessor.Callback() {
+            @Override
+            public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                CaptureResult captureResult =
+                        Camera2CameraCaptureResultConverter.getCaptureResult(
+                                cameraCaptureResult);
+                Preconditions.checkArgument(captureResult instanceof TotalCaptureResult,
+                        "Cannot get TotalCaptureResult from the cameraCaptureResult ");
+                TotalCaptureResult totalCaptureResult = (TotalCaptureResult) captureResult;
+
+                if (mPreviewProcessor != null) {
+                    mPreviewProcessor.notifyCaptureResult(totalCaptureResult);
+                }
+
+                if (mRequestUpdateProcessor != null) {
+                    CaptureStageImpl captureStage =
+                            mRequestUpdateProcessor.process(totalCaptureResult);
+
+                    if (captureStage != null) {
+                        updateRepeating(repeatingCaptureSequenceId, captureCallback);
+                    }
+                }
+
+                captureCallback.onCaptureSequenceCompleted(repeatingCaptureSequenceId);
+            }
+        };
+
+        Logger.d(TAG, "requestProcessor setRepeating");
+        mRequestProcessor.setRepeating(builder.build(), callback);
+    }
+
+    private void applyPreviewStagesParameters(RequestBuilder builder) {
+        CaptureStageImpl captureStage = mPreviewExtenderImpl.getCaptureStage();
+        if (captureStage != null) {
+            for (Pair<CaptureRequest.Key, Object> keyObjectPair :
+                    captureStage.getParameters()) {
+                builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+            }
+        }
+    }
+
+    @Override
+    public void stopRepeating() {
+        mRequestProcessor.stopRepeating();
+    }
+
+    @Override
+    public int startCapture(@NonNull CaptureCallback captureCallback) {
+        int captureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+
+        if (mRequestProcessor == null || mIsCapturing) {
+            Logger.d(TAG, "startCapture failed");
+            captureCallback.onCaptureFailed(captureSequenceId);
+            captureCallback.onCaptureSequenceAborted(captureSequenceId);
+            return captureSequenceId;
+        }
+        mIsCapturing = true;
+
+        List<RequestProcessor.Request> requestList = new ArrayList<>();
+        List<CaptureStageImpl> captureStages = mImageCaptureExtenderImpl.getCaptureStages();
+        List<Integer> captureIdList = new ArrayList<>();
+
+        for (CaptureStageImpl captureStage : captureStages) {
+            RequestBuilder builder = new RequestBuilder();
+            builder.addTargetOutputConfigIds(mCaptureOutputConfig.getId());
+            builder.setTemplateId(CameraDevice.TEMPLATE_STILL_CAPTURE);
+            builder.setCaptureStageId(captureStage.getId());
+
+            captureIdList.add(captureStage.getId());
+
+            applyParameters(builder);
+            applyPreviewStagesParameters(builder);
+
+            for (Pair<CaptureRequest.Key, Object> keyObjectPair :
+                    captureStage.getParameters()) {
+                builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+            }
+            requestList.add(builder.build());
+        }
+
+        Logger.d(TAG, "Wait for capture stage id: " + captureIdList);
+
+        RequestProcessor.Callback callback = new RequestProcessor.Callback() {
+            boolean mIsCaptureFailed = false;
+            boolean mIsCaptureStarted = false;
+
+            @Override
+            public void onCaptureStarted(@NonNull RequestProcessor.Request request,
+                    long frameNumber, long timestamp) {
+                if (!mIsCaptureStarted) {
+                    mIsCaptureStarted = true;
+                    captureCallback.onCaptureStarted(captureSequenceId, timestamp);
+                }
+            }
+
+            @Override
+            public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                CaptureResult captureResult =
+                        Camera2CameraCaptureResultConverter.getCaptureResult(
+                                cameraCaptureResult);
+                Preconditions.checkArgument(captureResult instanceof TotalCaptureResult,
+                        "Cannot get capture TotalCaptureResult from the cameraCaptureResult ");
+                TotalCaptureResult totalCaptureResult = (TotalCaptureResult) captureResult;
+
+                RequestBuilder.RequestProcessorRequest requestProcessorRequest =
+                        (RequestBuilder.RequestProcessorRequest) request;
+
+                if (mStillCaptureProcessor != null) {
+                    mStillCaptureProcessor.notifyCaptureResult(
+                            totalCaptureResult,
+                            requestProcessorRequest.getCaptureStageId());
+                } else {
+                    captureCallback.onCaptureProcessStarted(captureSequenceId);
+                    captureCallback.onCaptureSequenceCompleted(captureSequenceId);
+                    mIsCapturing = false;
+                }
+            }
+
+            @Override
+            public void onCaptureFailed(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureFailure captureFailure) {
+                if (!mIsCaptureFailed) {
+                    mIsCaptureFailed = true;
+                    captureCallback.onCaptureFailed(captureSequenceId);
+                    captureCallback.onCaptureSequenceAborted(captureSequenceId);
+                    mIsCapturing = false;
+                }
+            }
+
+            @Override
+            public void onCaptureSequenceAborted(int sequenceId) {
+                captureCallback.onCaptureSequenceAborted(captureSequenceId);
+                mIsCapturing = false;
+            }
+        };
+
+        Logger.d(TAG, "startCapture");
+        if (mStillCaptureProcessor != null) {
+            mStillCaptureProcessor.startCapture(captureIdList,
+                    new StillCaptureProcessor.OnCaptureResultCallback() {
+                        @Override
+                        public void onCompleted() {
+                            captureCallback.onCaptureSequenceCompleted(captureSequenceId);
+                            mIsCapturing = false;
+                        }
+
+                        @Override
+                        public void onError(@NonNull Exception e) {
+                            captureCallback.onCaptureFailed(captureSequenceId);
+                            mIsCapturing = false;
+                        }
+                    });
+        }
+        setImageProcessor(mCaptureOutputConfig.getId(),
+                new ImageProcessor() {
+                    boolean mIsFirstFrame = true;
+
+                    @Override
+                    public void onNextImageAvailable(int outputStreamId, long timestampNs,
+                            @NonNull ImageReference imageReference,
+                            @Nullable String physicalCameraId) {
+                        Logger.d(TAG,
+                                "onNextImageAvailable  outputStreamId=" + outputStreamId);
+                        if (mStillCaptureProcessor != null) {
+                            mStillCaptureProcessor.notifyImage(imageReference);
+                        }
+
+                        if (mIsFirstFrame) {
+                            captureCallback.onCaptureProcessStarted(captureSequenceId);
+                            mIsFirstFrame = false;
+                        }
+                    }
+                });
+        mRequestProcessor.submit(requestList, callback);
+        return captureSequenceId;
+    }
+
+    @Override
+    public void abortCapture(int captureSequenceId) {
+        mRequestProcessor.abortCaptures();
+    }
+
+    @Override
+    public int startTrigger(@NonNull Config config, @NonNull CaptureCallback callback) {
+        Logger.d(TAG, "startTrigger");
+        int captureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+        RequestBuilder builder = new RequestBuilder();
+        builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+        if (mAnalysisOutputConfig != null) {
+            builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+        }
+        builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+        applyParameters(builder);
+        applyPreviewStagesParameters(builder);
+
+        CaptureRequestOptions options =
+                CaptureRequestOptions.Builder.from(config).build();
+        for (Config.Option<?> option : options.listOptions()) {
+            @SuppressWarnings("unchecked")
+            CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+            builder.setParameters(key, options.retrieveOption(option));
+        }
+
+        mRequestProcessor.submit(builder.build(), new RequestProcessor.Callback() {
+            @Override
+            public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureResult captureResult) {
+                callback.onCaptureSequenceCompleted(captureSequenceId);
+            }
+
+            @Override
+            public void onCaptureFailed(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureFailure captureFailure) {
+                callback.onCaptureFailed(captureSequenceId);
+            }
+        });
+
+        return captureSequenceId;
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
index 60e13b7..57baff7 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
@@ -55,8 +55,8 @@
      * Sets session parameters.
      */
     @NonNull
-    <T> Camera2SessionConfigBuilder addSessionParameter(
-            @NonNull CaptureRequest.Key<T> key, @Nullable T value) {
+    Camera2SessionConfigBuilder addSessionParameter(
+            @NonNull CaptureRequest.Key key, @Nullable Object value) {
         mSessionParameters.put(key, value);
         return this;
     }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/CaptureResultImageMatcher.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/CaptureResultImageMatcher.java
new file mode 100644
index 0000000..96b2bd5
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/CaptureResultImageMatcher.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.media.Image;
+import android.util.LongSparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * To match {@link ImageReference} with {@link TotalCaptureResult} by timestamp.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class CaptureResultImageMatcher {
+    private final Object mLock = new Object();
+    private static final int INVALID_TIMESTAMP = -1;
+    /** TotalCaptureResults that haven't been matched with Image. */
+    @GuardedBy("mLock")
+    private final LongSparseArray<TotalCaptureResult> mPendingCaptureResults =
+            new LongSparseArray<>();
+
+    /** To store the capture stage ids for each TotalCaptureResult */
+    @GuardedBy("mLock")
+    Map<TotalCaptureResult, Integer> mCaptureStageIdMap = new HashMap<>();
+
+    /** Images that haven't been matched with timestamp. */
+    @GuardedBy("mLock")
+    private final LongSparseArray<ImageReference> mPendingImages = new LongSparseArray<>();
+
+    @GuardedBy("mLock")
+    ImageReferenceListener mImageReferenceListener;
+
+    CaptureResultImageMatcher() {
+    }
+
+    void clear() {
+        synchronized (mLock) {
+            mPendingCaptureResults.clear();
+            for (int i = 0; i < mPendingImages.size(); i++) {
+                long key = mPendingImages.keyAt(i);
+                mPendingImages.get(key).decrement();
+            }
+            mPendingImages.clear();
+            mCaptureStageIdMap.clear();
+        }
+    }
+
+    void setImageReferenceListener(
+            @NonNull ImageReferenceListener imageReferenceListener) {
+        synchronized (mLock) {
+            mImageReferenceListener = imageReferenceListener;
+        }
+    }
+
+    void clearImageReferenceListener() {
+        synchronized (mLock) {
+            mImageReferenceListener = null;
+        }
+    }
+
+    void imageIncoming(@NonNull ImageReference imageReference) {
+        synchronized (mLock) {
+            Image image = imageReference.get();
+            mPendingImages.put(image.getTimestamp(), imageReference);
+        }
+        matchImages();
+    }
+
+    void captureResultIncoming(@NonNull TotalCaptureResult captureResult) {
+        captureResultIncoming(captureResult, 0);
+    }
+
+    void captureResultIncoming(@NonNull TotalCaptureResult captureResult,
+            int captureStageId) {
+        synchronized (mLock) {
+            long timestamp = getTimeStampFromCaptureResult(captureResult);
+            if (timestamp == INVALID_TIMESTAMP) {
+                return;
+            }
+            // Add the incoming CameraCaptureResult to pending list and do the matching logic.
+            mPendingCaptureResults.put(timestamp, captureResult);
+            mCaptureStageIdMap.put(captureResult, captureStageId);
+        }
+        matchImages();
+    }
+
+    private long getTimeStampFromCaptureResult(TotalCaptureResult captureResult) {
+        Long timestamp = captureResult.get(CaptureResult.SENSOR_TIMESTAMP);
+        long timestampValue = INVALID_TIMESTAMP;
+        if (timestamp != null) {
+            timestampValue = timestamp;
+        }
+
+        return timestampValue;
+    }
+
+
+    private void notifyImage(ImageReference imageReference,
+            TotalCaptureResult totalCaptureResult) {
+        ImageReferenceListener listenerToInvoke = null;
+        Integer captureStageId = null;
+        synchronized (mLock) {
+            if (mImageReferenceListener != null) {
+                listenerToInvoke = mImageReferenceListener;
+                captureStageId = mCaptureStageIdMap.get(totalCaptureResult);
+            } else {
+                imageReference.decrement();
+            }
+        }
+
+        if (listenerToInvoke != null) {
+            listenerToInvoke.onImageReferenceIncoming(imageReference,
+                    totalCaptureResult, captureStageId);
+        }
+    }
+
+    // Remove the stale {@link ImageReference} from the pending queue if there
+    // are any missing which can happen if the camera is momentarily shut off.
+    // The ImageReference timestamps are assumed to be monotonically increasing. This
+    // means any ImageReference which has a timestamp older (smaller in value) than the
+    // oldest timestamp in the other queue will never get matched, so they should be removed.
+    //
+    // This should only be called at the end of matchImages(). The assumption is that there are no
+    // matching timestamps.
+    private void removeStaleData() {
+        synchronized (mLock) {
+            // No stale data to remove
+            if (mPendingImages.size() == 0 || mPendingCaptureResults.size() == 0) {
+                return;
+            }
+
+            Long minImageRefTimestamp = mPendingImages.keyAt(0);
+            Long minCaptureResultTimestamp = mPendingCaptureResults.keyAt(0);
+
+            // If timestamps are equal then matchImages did not correctly match up the capture
+            // result and Image
+            Preconditions.checkArgument(!minCaptureResultTimestamp.equals(minImageRefTimestamp));
+
+            if (minCaptureResultTimestamp > minImageRefTimestamp) {
+                for (int i = mPendingImages.size() - 1; i >= 0; i--) {
+                    if (mPendingImages.keyAt(i) < minCaptureResultTimestamp) {
+                        ImageReference imageReference = mPendingImages.valueAt(i);
+                        imageReference.decrement();
+                        mPendingImages.removeAt(i);
+                    }
+                }
+            } else {
+                for (int i = mPendingCaptureResults.size() - 1; i >= 0; i--) {
+                    if (mPendingCaptureResults.keyAt(i) < minImageRefTimestamp) {
+                        mPendingCaptureResults.removeAt(i);
+                    }
+                }
+            }
+        }
+    }
+
+    private void matchImages() {
+        ImageReference imageToNotify = null;
+        TotalCaptureResult resultToNotify = null;
+        synchronized (mLock) {
+            // Iterate in reverse order so that capture result can be removed in place
+            for (int i = mPendingCaptureResults.size() - 1; i >= 0; i--) {
+                TotalCaptureResult captureResult = mPendingCaptureResults.valueAt(i);
+                long timestamp = getTimeStampFromCaptureResult(captureResult);
+
+                ImageReference imageReference = mPendingImages.get(timestamp);
+
+                if (imageReference != null) {
+                    mPendingImages.remove(timestamp);
+                    mPendingCaptureResults.removeAt(i);
+                    imageToNotify = imageReference;
+                    resultToNotify = captureResult;
+                }
+            }
+            removeStaleData();
+        }
+
+        if (imageToNotify != null && resultToNotify != null) {
+            notifyImage(imageToNotify, resultToNotify);
+        }
+    }
+
+    interface ImageReferenceListener {
+        void onImageReferenceIncoming(@NonNull ImageReference imageReference,
+                @NonNull TotalCaptureResult totalCaptureResult, int captureStageId);
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
index 454e4a0..1ae12a1 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
@@ -24,6 +24,7 @@
 
 import com.google.auto.value.AutoValue;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -42,6 +43,12 @@
         return new AutoValue_ImageReaderOutputConfig(id, surfaceGroupId, physicalCameraId,
                 sharedOutputConfigs, size, imageFormat, maxImages);
     }
+
+    static ImageReaderOutputConfig create(
+            int id, @NonNull Size size, int imageFormat, int maxImages) {
+        return new AutoValue_ImageReaderOutputConfig(id, -1, null,
+                Collections.emptyList(), size, imageFormat, maxImages);
+    }
     /**
      * Returns the size of the surface.
      */
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
new file mode 100644
index 0000000..0f34acb
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.hardware.camera2.TotalCaptureResult;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
+
+/**
+ * A preview processor that is responsible for invoking OEM's PreviewImageProcessorImpl and
+ * output to the given PRIVATE surface.
+ *
+ * <p>To start the processing, invoke {@link #start()} and then feed {@link ImageReference} and
+ * {@link TotalCaptureResult} instances by {@link #notifyImage(ImageReference)} and
+ * {@link #notifyCaptureResult(TotalCaptureResult)} respectively. The output will be written to the
+ * given output surface. Invoke {@link #close()} to close the processor and reclaim the resources.
+ *
+ * <p>Please note that output preview surface must be closed AFTER this processor is closed.
+ */
+@RequiresApi(26)
+class PreviewProcessor {
+    private static final String TAG = "PreviewProcessor";
+    @NonNull
+    final PreviewImageProcessorImpl mPreviewImageProcessor;
+    @NonNull
+    final CaptureResultImageMatcher mCaptureResultImageMatcher = new CaptureResultImageMatcher();
+    final Object mLock = new Object();
+    @GuardedBy("mLock")
+    boolean mIsClosed = false;
+
+    PreviewProcessor(@NonNull PreviewImageProcessorImpl previewImageProcessor,
+            @NonNull Surface previewOutputSurface, @NonNull Size surfaceSize) {
+        mPreviewImageProcessor = previewImageProcessor;
+        mPreviewImageProcessor.onResolutionUpdate(surfaceSize);
+        mPreviewImageProcessor.onOutputSurface(previewOutputSurface, PixelFormat.RGBA_8888);
+        mPreviewImageProcessor.onImageFormatUpdate(ImageFormat.YUV_420_888);
+    }
+
+    void start() {
+        mCaptureResultImageMatcher.setImageReferenceListener(
+                (imageReference, totalCaptureResult, captureStageId) -> {
+                    synchronized (mLock) {
+                        if (mIsClosed) {
+                            imageReference.decrement();
+                            Logger.d(TAG, "Ignore image in closed state");
+                            return;
+                        }
+                        mPreviewImageProcessor.process(imageReference.get(),
+                                totalCaptureResult);
+                        imageReference.decrement();
+                    }
+                });
+    }
+
+    void notifyCaptureResult(@NonNull TotalCaptureResult captureResult) {
+        mCaptureResultImageMatcher.captureResultIncoming(captureResult);
+    }
+
+    void notifyImage(@NonNull ImageReference imageReference) {
+        mCaptureResultImageMatcher.imageIncoming(imageReference);
+    }
+
+    /**
+     * Close the processor. Please note that output preview surface must be closed AFTER this
+     * processor is closed.
+     */
+    void close() {
+        synchronized (mLock) {
+            mIsClosed = true;
+            mCaptureResultImageMatcher.clear();
+            mCaptureResultImageMatcher.clearImageReferenceListener();
+        }
+    }
+
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java
new file mode 100644
index 0000000..32d17b9
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.RequestProcessor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A builder for building {@link androidx.camera.core.impl.RequestProcessor.Request}.
+ */
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class RequestBuilder {
+    private List<Integer> mTargetOutputConfigIds = new ArrayList<>();
+    private Map<CaptureRequest.Key<?>, Object> mParameters = new HashMap<>();
+    private int mTemplateId = CameraDevice.TEMPLATE_PREVIEW;
+    int mCaptureStageId;
+
+    RequestBuilder() {
+    }
+
+    @NonNull
+    RequestBuilder addTargetOutputConfigIds(int targetOutputConfigId) {
+        mTargetOutputConfigIds.add(targetOutputConfigId);
+        return this;
+    }
+
+    @NonNull
+    RequestBuilder setParameters(@NonNull CaptureRequest.Key<?> key,
+            @NonNull Object value) {
+        mParameters.put(key, value);
+        return this;
+    }
+
+    @NonNull
+    RequestBuilder setTemplateId(int templateId) {
+        mTemplateId = templateId;
+        return this;
+    }
+
+    @NonNull
+    public RequestBuilder setCaptureStageId(int captureStageId) {
+        mCaptureStageId = captureStageId;
+        return this;
+    }
+
+    @NonNull
+    RequestProcessor.Request build() {
+        return new RequestProcessorRequest(
+                mTargetOutputConfigIds, mParameters, mTemplateId, mCaptureStageId);
+    }
+
+    static class RequestProcessorRequest implements RequestProcessor.Request {
+        final List<Integer> mTargetOutputConfigIds;
+        final Config mParameterConfig;
+        final int mTemplateId;
+        final int mCaptureStageId;
+
+        RequestProcessorRequest(List<Integer> targetOutputConfigIds,
+                Map<CaptureRequest.Key<?>, Object> parameters,
+                int templateId,
+                int captureStageId) {
+            mTargetOutputConfigIds = targetOutputConfigIds;
+            mTemplateId = templateId;
+            mCaptureStageId = captureStageId;
+
+            Camera2ImplConfig.Builder camera2ConfigBuilder = new Camera2ImplConfig.Builder();
+            for (CaptureRequest.Key<?> key : parameters.keySet()) {
+                @SuppressWarnings("unchecked")
+                CaptureRequest.Key<Object> objKey = (CaptureRequest.Key<Object>) key;
+                camera2ConfigBuilder.setCaptureRequestOption(objKey,
+                        parameters.get(objKey));
+            }
+            mParameterConfig = camera2ConfigBuilder.build();
+        }
+
+        @Override
+        @NonNull
+        public List<Integer> getTargetOutputConfigIds() {
+            return mTargetOutputConfigIds;
+        }
+
+        @Override
+        @NonNull
+        public Config getParameters() {
+            return mParameterConfig;
+        }
+
+        @Override
+        public int getTemplateId() {
+            return mTemplateId;
+        }
+
+        public int getCaptureStageId() {
+            return mCaptureStageId;
+        }
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
new file mode 100644
index 0000000..d3f47fa
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.TotalCaptureResult;
+import android.media.Image;
+import android.util.Pair;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.Camera2CameraCaptureResult;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ImageReaderProxys;
+import androidx.camera.core.Logger;
+import androidx.camera.core.SettableImageProxy;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.ImageReaderProxy;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.CameraCaptureResultImageInfo;
+import androidx.camera.extensions.impl.CaptureProcessorImpl;
+
+import org.jetbrains.annotations.TestOnly;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * A processor that is responsible for invoking OEM's CaptureProcessorImpl and converting the
+ * processed YUV images into JPEG images. The JPEG images are written to the given output surface.
+ *
+ * <p> It only outputs JPEG format images, meaning that the given output surface must be of JPEG
+ * format. To use this processor, follow the steps below:
+ * <pre>
+ * 1. Invoke start() with the required capture stage id list. A listener is given to
+ *     be invoked when the processing is done and the resulting image is ready to appear in the
+ *     output surface.
+ * 2. When input images from camera and TotalCaptureResult arrives, notify the processor by
+ *   {@link #notifyImage(ImageReference)} and {@link #notifyCaptureResult(TotalCaptureResult, int)}.
+ * 3. Invoke {@link #setJpegQuality(int)} to adjust the jpeg quality and invoke
+ *    {@link #setRotationDegrees(int)} if the device rotation changes.
+ * 4. When camera session is finishing, invoke {@link #close()} to reclaim the resources.
+ *    Please note that the output JPEG surface should be closed AFTER this processor is closed().
+ * </pre>
+ */
+@RequiresApi(26)
+class StillCaptureProcessor {
+    private static final String TAG = "StillCaptureProcessor";
+    private static final int MAX_IMAGES = 2;
+    @NonNull
+    final CaptureProcessorImpl mCaptureProcessorImpl;
+    @NonNull
+    final CaptureResultImageMatcher mCaptureResultImageMatcher = new CaptureResultImageMatcher();
+    @NonNull
+    final ImageReaderProxy mProcessedYuvImageReader;
+    @NonNull
+    YuvToJpegConverter mYuvToJpegConverter;
+
+    final Object mLock = new Object();
+    @GuardedBy("mLock")
+    @NonNull
+    HashMap<Integer, Pair<ImageReference, TotalCaptureResult>> mCaptureResults =
+            new HashMap<>();
+
+    @GuardedBy("mLock")
+    OnCaptureResultCallback mOnCaptureResultCallback = null;
+    // Stores the first capture result for injecting into the output JPEG ImageProxy.
+    @GuardedBy("mLock")
+    TotalCaptureResult mSourceCaptureResult = null;
+    @GuardedBy("mLock")
+    boolean mIsClosed = false;
+    StillCaptureProcessor(@NonNull CaptureProcessorImpl captureProcessorImpl,
+            @NonNull Surface captureOutputSurface,
+            @NonNull Size surfaceSize) {
+        mCaptureProcessorImpl = captureProcessorImpl;
+        /*
+           Processing flow:
+           --> Collecting YUV images (from camera) via notifyImage and TotalCaptureResults
+               via notifyCaptureResult
+           --> mCaptureProcessorImpl.process (OEM process)
+           --> mProcessedYuvImageReader
+           --> mYuvToJpegProcessor (written to captureOutputSurface)
+         */
+        mProcessedYuvImageReader = ImageReaderProxys.createIsolatedReader(
+                surfaceSize.getWidth(),
+                surfaceSize.getHeight(),
+                ImageFormat.YUV_420_888, MAX_IMAGES);
+
+        mYuvToJpegConverter = new YuvToJpegConverter(100, captureOutputSurface);
+        mProcessedYuvImageReader.setOnImageAvailableListener(
+                imageReader -> {
+                    OnCaptureResultCallback onCaptureResultCallback = null;
+                    Exception errorException = null;
+                    synchronized (mLock) {
+                        if (mIsClosed) {
+                            Logger.d(TAG, "Ignore JPEG processing in closed state");
+                            return;
+                        }
+                        ImageProxy imageProxy = imageReader.acquireNextImage();
+                        if (mSourceCaptureResult != null) {
+                            imageProxy = new SettableImageProxy(imageProxy, null,
+                                    new CameraCaptureResultImageInfo(
+                                            new Camera2CameraCaptureResult(mSourceCaptureResult)));
+                            mSourceCaptureResult = null;
+                        }
+                        if (imageProxy != null) {
+                            try {
+                                mYuvToJpegConverter.writeYuvImage(imageProxy);
+                            } catch (YuvToJpegConverter.ConversionFailedException e) {
+                                errorException = e;
+                            }
+
+                            if (mOnCaptureResultCallback != null) {
+                                onCaptureResultCallback = mOnCaptureResultCallback;
+                                mOnCaptureResultCallback = null;
+                            }
+                        }
+                    }
+
+                    if (onCaptureResultCallback != null) {
+                        if (errorException != null) {
+                            onCaptureResultCallback.onError(errorException);
+                        } else {
+                            onCaptureResultCallback.onCompleted();
+                        }
+                    }
+                }, CameraXExecutors.ioExecutor());
+
+        mCaptureProcessorImpl.onOutputSurface(mProcessedYuvImageReader.getSurface(),
+                ImageFormat.YUV_420_888);
+        mCaptureProcessorImpl.onImageFormatUpdate(ImageFormat.YUV_420_888);
+        mCaptureProcessorImpl.onResolutionUpdate(surfaceSize);
+    }
+
+    @TestOnly
+    StillCaptureProcessor(@NonNull CaptureProcessorImpl captureProcessorImpl,
+            @NonNull Surface captureOutputSurface,
+            @NonNull Size surfaceSize,
+            @NonNull YuvToJpegConverter yuvToJpegConverter) {
+        this(captureProcessorImpl, captureOutputSurface, surfaceSize);
+        mYuvToJpegConverter = yuvToJpegConverter;
+    }
+
+    interface OnCaptureResultCallback {
+        void onCompleted();
+        void onError(@NonNull Exception e);
+    }
+
+    void clearCaptureResults() {
+        synchronized (mLock) {
+            for (Pair<ImageReference, TotalCaptureResult> value :
+                    mCaptureResults.values()) {
+                value.first.decrement();
+            }
+            mCaptureResults.clear();
+        }
+    }
+    void startCapture(@NonNull List<Integer> captureIdList,
+            @NonNull OnCaptureResultCallback onCaptureResultCallback) {
+        Logger.d(TAG, "Start the processor");
+
+        synchronized (mLock) {
+            mOnCaptureResultCallback = onCaptureResultCallback;
+            clearCaptureResults();
+        }
+
+        mCaptureResultImageMatcher.clear();
+        mCaptureResultImageMatcher.setImageReferenceListener(
+                (imageReference, totalCaptureResult, captureStageId) -> {
+                    Exception errorException = null;
+                    synchronized (mLock) {
+                        if (mIsClosed) {
+                            imageReference.decrement();
+                            Logger.d(TAG, "Ignore image in closed state");
+                            return;
+                        }
+                        Logger.d(TAG, "onImageReferenceIncoming  captureStageId=" + captureStageId);
+
+                        mCaptureResults.put(captureStageId, new Pair<>(imageReference,
+                                totalCaptureResult));
+
+                        Logger.d(TAG, "mCaptureResult has capture stage Id: "
+                                + mCaptureResults.keySet());
+                        if (mCaptureResults.keySet().containsAll(captureIdList)) {
+                            HashMap<Integer, Pair<Image, TotalCaptureResult>> convertedResult =
+                                    new HashMap<>();
+                            for (Integer id : mCaptureResults.keySet()) {
+                                Pair<ImageReference, TotalCaptureResult> pair =
+                                        mCaptureResults.get(id);
+                                convertedResult.put(id,
+                                        new Pair<>(pair.first.get(), pair.second));
+                            }
+                            Logger.d(TAG, "CaptureProcessorImpl.process()");
+                            try {
+                                mCaptureProcessorImpl.process(convertedResult);
+                            } catch (Exception e) {
+                                mOnCaptureResultCallback = null;
+                                errorException = e;
+                            }
+                            clearCaptureResults();
+                        }
+                    }
+                    if (errorException != null) {
+                        if (onCaptureResultCallback != null) {
+                            onCaptureResultCallback.onError(errorException);
+                        }
+                    }
+                });
+    }
+
+    void notifyCaptureResult(@NonNull TotalCaptureResult captureResult,
+            int captureStageId) {
+        mCaptureResultImageMatcher.captureResultIncoming(captureResult,
+                captureStageId);
+
+        synchronized (mLock) {
+            if (mSourceCaptureResult == null) {
+                mSourceCaptureResult = captureResult;
+            }
+        }
+    }
+
+    void notifyImage(@NonNull ImageReference imageReference) {
+        mCaptureResultImageMatcher.imageIncoming(imageReference);
+    }
+
+    void setJpegQuality(@IntRange(from = 0, to = 100) int quality) {
+        mYuvToJpegConverter.setJpegQuality(quality);
+    }
+
+    void setRotationDegrees(
+            @ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
+        mYuvToJpegConverter.setRotationDegrees(rotationDegrees);
+    }
+
+    /**
+     * Close the processor. Please note that captureOutputSurface passed in must be closed AFTER
+     * invoking this function.
+     */
+    void close() {
+        Logger.d(TAG, "Close the processor");
+        synchronized (mLock) {
+            mIsClosed = true;
+            clearCaptureResults();
+            mProcessedYuvImageReader.clearOnImageAvailableListener();
+            mCaptureResultImageMatcher.clearImageReferenceListener();
+            mCaptureResultImageMatcher.clear();
+            mProcessedYuvImageReader.close();
+        }
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
index 31a7144..4c56295 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
@@ -24,6 +24,7 @@
 
 import com.google.auto.value.AutoValue;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -44,6 +45,10 @@
                 sharedOutputConfigs, surface);
     }
 
+    static SurfaceOutputConfig create(int id, @NonNull Surface surface) {
+        return create(id, -1, null, Collections.emptyList(), surface);
+    }
+
     /**
      * Get the {@link Surface}. It'll return a valid surface only when type is TYPE_SURFACE.
      */
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java
new file mode 100644
index 0000000..c8e1ef0
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.graphics.ImageFormat;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageProcessingUtil;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.core.util.Preconditions;
+
+/**
+ * A image converter for YUV_420_888 to JPEG. The converted JPEG images were written to the given
+ * output surface after {@link #writeYuvImage(ImageProxy)} is invoked.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class YuvToJpegConverter {
+    private static final String TAG = "YuvToJpegConverter";
+    private final Surface mOutputJpegSurface;
+    @IntRange(from = 1, to = 100)
+    private volatile int mJpegQuality;
+    @ImageOutputConfig.RotationDegreesValue
+    private volatile int mRotationDegrees = 0;
+
+    YuvToJpegConverter(int jpegQuality, @NonNull Surface outputJpegSurface) {
+        mJpegQuality = jpegQuality;
+        mOutputJpegSurface = outputJpegSurface;
+    }
+
+    public void setRotationDegrees(@ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
+        mRotationDegrees = rotationDegrees;
+    }
+
+    void setJpegQuality(int jpgQuality) {
+        mJpegQuality = jpgQuality;
+    }
+
+    static class ConversionFailedException extends Exception {
+        ConversionFailedException(String message) {
+            super(message);
+        }
+        ConversionFailedException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     * Writes an YUV_420_888 image and converts it into a JPEG image.
+     */
+    void writeYuvImage(@NonNull ImageProxy imageProxy) throws ConversionFailedException {
+        Preconditions.checkState(imageProxy.getFormat() == ImageFormat.YUV_420_888,
+                "Input image is not expected YUV_420_888 image format");
+        try {
+            // TODO(b/258667618): remove extra copy by writing the jpeg data directly into the
+            //  Surface's ByteBuffer.
+            boolean success = ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(
+                    imageProxy,
+                    mJpegQuality,
+                    mRotationDegrees,
+                    mOutputJpegSurface);
+            if (!success) {
+                throw new ConversionFailedException("Failed to process YUV -> JPEG");
+            }
+        } catch (Exception e) {
+            Logger.e(TAG, "Failed to process YUV -> JPEG", e);
+            throw new ConversionFailedException("Failed to process YUV -> JPEG", e);
+        } finally {
+            imageProxy.close();
+        }
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/Camera2Util.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/Camera2Util.kt
new file mode 100644
index 0000000..cbee160
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/Camera2Util.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.TotalCaptureResult
+import android.os.Handler
+import android.os.Looper
+import android.view.Surface
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import kotlinx.coroutines.CompletableDeferred
+
+/**
+ * Convenient suspend functions for invoking camera2 APIs.
+ */
+@RequiresApi(21)
+object Camera2Util {
+    /**
+     * Open the camera device and return the [CameraDevice] instance.
+     */
+    @DoNotInline
+    suspend fun openCameraDevice(
+        cameraManager: CameraManager,
+        cameraId: String,
+        handler: Handler
+    ): CameraDevice {
+        val deferred = CompletableDeferred<CameraDevice>()
+        cameraManager.openCamera(
+            cameraId,
+            object : CameraDevice.StateCallback() {
+                override fun onOpened(cameraDevice: CameraDevice) {
+                    deferred.complete(cameraDevice)
+                }
+
+                override fun onDisconnected(cameraDevice: CameraDevice) {
+                    deferred.completeExceptionally(RuntimeException("Camera Disconnected"))
+                }
+
+                override fun onError(cameraDevice: CameraDevice, error: Int) {
+                    deferred.completeExceptionally(
+                        RuntimeException("Camera onError(error=$cameraDevice)")
+                    )
+                }
+            }, handler
+        )
+        return deferred.await()
+    }
+
+    /**
+     * Creates and returns a configured [CameraCaptureSession].
+     */
+    @RequiresApi(21)
+    suspend fun openCaptureSession(
+        cameraDevice: CameraDevice,
+        surfaceList: List<Surface>,
+        handler: Handler
+    ): CameraCaptureSession {
+        val deferred = CompletableDeferred<CameraCaptureSession>()
+        @Suppress("deprecation")
+        cameraDevice.createCaptureSession(
+            surfaceList,
+            object : CameraCaptureSession.StateCallback() {
+
+                override fun onConfigured(session: CameraCaptureSession) {
+                    deferred.complete(session)
+                }
+
+                override fun onConfigureFailed(session: CameraCaptureSession) {
+                    deferred.completeExceptionally(RuntimeException("onConfigureFailed"))
+                }
+            },
+            handler
+        )
+        return deferred.await()
+    }
+
+    /**
+     * Submits a single capture request to the [CameraCaptureSession] and returns the
+     * [TotalCaptureResult].
+     */
+    suspend fun submitSingleRequest(
+        cameraDevice: CameraDevice,
+        session: CameraCaptureSession,
+        surfaces: List<Surface>,
+        handler: Handler
+    ): TotalCaptureResult {
+        val builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
+        for (surface in surfaces) {
+            builder.addTarget(surface)
+        }
+        val deferredCapture = CompletableDeferred<TotalCaptureResult>()
+        session.capture(builder.build(), object : CameraCaptureSession.CaptureCallback() {
+            override fun onCaptureCompleted(
+                session: CameraCaptureSession,
+                request: CaptureRequest,
+                result: TotalCaptureResult
+            ) {
+                deferredCapture.complete(result)
+            }
+
+            override fun onCaptureFailed(
+                session: CameraCaptureSession,
+                request: CaptureRequest,
+                failure: CaptureFailure
+            ) {
+                deferredCapture.completeExceptionally(RuntimeException("capture failed"))
+            }
+        }, handler)
+        return deferredCapture.await()
+    }
+
+    /**
+     * Starts the repeating request, and invokes the given block when [TotalCaptureResult] arrives.
+     */
+    fun startRepeating(
+        cameraDevice: CameraDevice,
+        session: CameraCaptureSession,
+        surfaces: List<Surface>,
+        blockForCaptureResult: (TotalCaptureResult) -> Unit
+    ) {
+        val builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+        for (surface in surfaces) {
+            builder.addTarget(surface)
+        }
+        val deferredCapture = CompletableDeferred<TotalCaptureResult>()
+        session.setRepeatingRequest(
+            builder.build(),
+            object : CameraCaptureSession.CaptureCallback() {
+                override fun onCaptureCompleted(
+                    session: CameraCaptureSession,
+                    request: CaptureRequest,
+                    result: TotalCaptureResult
+                ) {
+                    blockForCaptureResult.invoke(result)
+                }
+
+                override fun onCaptureFailed(
+                    session: CameraCaptureSession,
+                    request: CaptureRequest,
+                    failure: CaptureFailure
+                ) {
+                    deferredCapture.completeExceptionally(RuntimeException("capture failed"))
+                }
+            },
+            Handler(Looper.getMainLooper())
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
index 8aa0f5c..8387b97 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
@@ -30,6 +30,7 @@
 import android.view.TextureView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
@@ -132,6 +133,21 @@
      */
     @NonNull
     public static Preview.SurfaceProvider createAutoDrainingSurfaceTextureProvider() {
+        return createAutoDrainingSurfaceTextureProvider(null);
+    }
+
+    /**
+     * Creates a {@link Preview.SurfaceProvider} that is backed by a {@link SurfaceTexture}.
+     *
+     * <p>This method also creates a backing OpenGL thread that will automatically drain frames
+     * from the SurfaceTexture as they become available.
+     *
+     * @param frameAvailableListener listener to be invoked when frame is updated.
+     */
+    @NonNull
+    public static Preview.SurfaceProvider createAutoDrainingSurfaceTextureProvider(
+            @Nullable SurfaceTexture.OnFrameAvailableListener frameAvailableListener
+    ) {
         return (surfaceRequest) -> {
             HandlerThread handlerThread = new HandlerThread(String.format("CameraX"
                     + "-AutoDrainThread-%x", surfaceRequest.hashCode()));
@@ -147,8 +163,12 @@
                 SurfaceTexture surfaceTexture = new SurfaceTexture(textureIds[0]);
                 surfaceTexture.setDefaultBufferSize(surfaceRequest.getResolution().getWidth(),
                         surfaceRequest.getResolution().getHeight());
-                surfaceTexture.setOnFrameAvailableListener(
-                        SurfaceTexture::updateTexImage, handler);
+                surfaceTexture.setOnFrameAvailableListener((st) -> {
+                    st.updateTexImage();
+                    if (frameAvailableListener != null) {
+                        frameAvailableListener.onFrameAvailable(st);
+                    }
+                }, handler);
 
                 Surface surface = new Surface(surfaceTexture);
                 surfaceRequest.provideSurface(surface,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
index c2a9eef..8d803f1 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -25,6 +25,8 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
 import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraConfig;
+import androidx.camera.core.impl.CameraConfigs;
 import androidx.camera.core.impl.CameraControlInternal;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
@@ -69,6 +71,8 @@
 
     private List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList();
 
+    private CameraConfig mCameraConfig = CameraConfigs.emptyConfig();
+
     public FakeCamera() {
         this(DEFAULT_CAMERA_ID, /*cameraControl=*/null,
                 new FakeCameraInfoInternal(DEFAULT_CAMERA_ID));
@@ -375,4 +379,15 @@
         // notifySurfaceDetached calls.
         mConfiguredDeferrableSurfaces.clear();
     }
+
+    @NonNull
+    @Override
+    public CameraConfig getExtendedConfig() {
+        return mCameraConfig;
+    }
+
+    @Override
+    public void setExtendedConfig(@Nullable CameraConfig cameraConfig) {
+        mCameraConfig = cameraConfig;
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
index 2c5dec8..e08f64e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
@@ -17,18 +17,19 @@
 package androidx.camera.testing.fakes
 
 import android.hardware.camera2.CameraDevice
-import android.media.ImageReader
+import android.hardware.camera2.CaptureRequest
 import android.media.ImageWriter
-import android.os.Handler
-import android.os.Looper
 import android.os.SystemClock
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.core.CameraInfo
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageReaderProxys
 import androidx.camera.core.impl.CameraCaptureFailure
 import androidx.camera.core.impl.CameraCaptureResult
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.ImageReaderProxy
 import androidx.camera.core.impl.OptionsBundle
 import androidx.camera.core.impl.OutputSurface
 import androidx.camera.core.impl.RequestProcessor
@@ -43,20 +44,21 @@
 
 const val FAKE_CAPTURE_SEQUENCE_ID = 1
 
-@RequiresApi(23)
+@RequiresApi(28) // writing to PRIVATE surface requires API 28+
 class FakeSessionProcessor(
     val inputFormatPreview: Int?,
     val inputFormatCapture: Int?
 ) : SessionProcessor {
     private lateinit var previewProcessorSurface: DeferrableSurface
     private lateinit var captureProcessorSurface: DeferrableSurface
-    private var intermediaPreviewImageReader: ImageReader? = null
-    private var intermediaCaptureImageReader: ImageReader? = null
+    private var imageAnalysisProcessorSurface: DeferrableSurface? = null
+    private var intermediaPreviewImageReader: ImageReaderProxy? = null
+    private var intermediaCaptureImageReader: ImageReaderProxy? = null
     private var intermediaPreviewImageWriter: ImageWriter? = null
-    private var intermediaCaptureImageWriter: ImageWriter? = null
 
     private val previewOutputConfigId = 1
     private val captureOutputConfigId = 2
+    private val analysisOutputConfigId = 3
 
     private var requestProcessor: RequestProcessor? = null
 
@@ -68,9 +70,13 @@
     private val startRepeatingCalled = CompletableDeferred<Long>()
     private val startCaptureCalled = CompletableDeferred<Long>()
     private val setParametersCalled = CompletableDeferred<Config>()
+    private val startTriggerCalled = CompletableDeferred<Config>()
     private var latestParameters: Config = OptionsBundle.emptyBundle()
     private var blockRunAfterInitSession: () -> Unit = {}
 
+    private var rotationDegrees = 0
+    private var jpegQuality = 100
+
     fun releaseSurfaces() {
         intermediaPreviewImageReader?.close()
         intermediaCaptureImageReader?.close()
@@ -87,20 +93,18 @@
         imageAnalysisSurfaceConfig: OutputSurface?
     ): SessionConfig {
         initSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
-        val handler = Handler(Looper.getMainLooper())
-
-        var sessionBuilder = SessionConfig.Builder()
+        val sessionBuilder = SessionConfig.Builder()
 
         // Preview
         lateinit var previewTransformedSurface: Surface
         if (inputFormatPreview == null) { // no conversion, use origin surface.
             previewTransformedSurface = previewSurfaceConfig.surface
         } else {
-            intermediaPreviewImageReader = ImageReader.newInstance(
-                640, 480,
+            intermediaPreviewImageReader = ImageReaderProxys.createIsolatedReader(
+                previewSurfaceConfig.size.width, previewSurfaceConfig.size.height,
                 inputFormatPreview, 2
             )
-            previewTransformedSurface = intermediaPreviewImageReader!!.surface
+            previewTransformedSurface = intermediaPreviewImageReader!!.surface!!
 
             intermediaPreviewImageWriter = ImageWriter.newInstance(
                 previewSurfaceConfig.surface, 2
@@ -113,7 +117,7 @@
                         intermediaPreviewImageWriter!!.queueInputImage(imageDequeued)
                     }
                 },
-                handler
+                CameraXExecutors.ioExecutor()
             )
         }
         previewProcessorSurface =
@@ -132,24 +136,22 @@
         if (inputFormatCapture == null) { // no conversion, use origin surface.
             captureTransformedSurface = imageCaptureSurfaceConfig.surface
         } else {
-            intermediaCaptureImageReader = ImageReader.newInstance(
-                640, 480,
+            intermediaCaptureImageReader = ImageReaderProxys.createIsolatedReader(
+                imageCaptureSurfaceConfig.size.width, imageCaptureSurfaceConfig.size.height,
                 inputFormatCapture, 2
             )
-            captureTransformedSurface = intermediaCaptureImageReader!!.surface
-
-            intermediaCaptureImageWriter = ImageWriter.newInstance(
-                imageCaptureSurfaceConfig.surface, 2
-            )
+            captureTransformedSurface = intermediaCaptureImageReader!!.surface!!
 
             intermediaCaptureImageReader!!.setOnImageAvailableListener(
                 {
-                    it.acquireNextImage().use {
-                        val imageDequeued = intermediaCaptureImageWriter!!.dequeueInputImage()
-                        intermediaCaptureImageWriter!!.queueInputImage(imageDequeued)
+                    it.acquireNextImage().use { imageProxy ->
+                        ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(
+                            imageProxy!!, jpegQuality, rotationDegrees,
+                            imageCaptureSurfaceConfig.surface
+                        )
                     }
                 },
-                handler
+                CameraXExecutors.ioExecutor()
             )
         }
         captureProcessorSurface =
@@ -158,12 +160,17 @@
         captureProcessorSurface.terminationFuture.addListener(
             {
                 intermediaCaptureImageReader?.close()
-                intermediaCaptureImageWriter?.close()
             },
             CameraXExecutors.directExecutor()
         )
         sessionBuilder.addSurface(captureProcessorSurface)
 
+        imageAnalysisSurfaceConfig?.let {
+            imageAnalysisProcessorSurface = SessionProcessorSurface(
+                it.surface, analysisOutputConfigId
+            )
+            sessionBuilder.addSurface(imageAnalysisProcessorSurface!!)
+        }
         sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
         val sessionConfig = sessionBuilder.build()
         blockRunAfterInitSession()
@@ -174,11 +181,25 @@
         deInitSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
         previewProcessorSurface.close()
         captureProcessorSurface.close()
+        imageAnalysisProcessorSurface?.close()
     }
 
     override fun setParameters(config: Config) {
         setParametersCalled.complete(config)
         latestParameters = config
+        config.listOptions().filter {
+            it.token is CaptureRequest.Key<*>
+        }.forEach {
+            @Suppress("UNCHECKED_CAST")
+            val key = it.token as CaptureRequest.Key<Any>?
+            if (key == CaptureRequest.JPEG_ORIENTATION) {
+                rotationDegrees = config.retrieveOption(it) as Int
+            }
+
+            if (key == CaptureRequest.JPEG_QUALITY) {
+                jpegQuality = (config.retrieveOption(it) as Byte).toInt()
+            }
+        }
     }
 
     override fun onCaptureSessionStart(_requestProcessor: RequestProcessor) {
@@ -299,6 +320,12 @@
         return FAKE_CAPTURE_SEQUENCE_ID
     }
 
+    override fun startTrigger(config: Config, callback: SessionProcessor.CaptureCallback): Int {
+        startTriggerCalled.complete(config)
+        callback.onCaptureSequenceCompleted(FAKE_CAPTURE_SEQUENCE_ID)
+        return FAKE_CAPTURE_SEQUENCE_ID
+    }
+
     override fun abortCapture(captureSequenceId: Int) {
     }
 
@@ -340,6 +367,10 @@
         return setParametersCalled.awaitWithTimeout(3000)
     }
 
+    suspend fun assertStartTriggerInvoked(): Config {
+        return startTriggerCalled.awaitWithTimeout(3000)
+    }
+
     private suspend fun <T> Deferred<T>.awaitWithTimeout(timeMillis: Long): T {
         return withTimeout(timeMillis) {
             await()
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index 1661eea..729cb0a 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -27,7 +27,7 @@
   @RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
     ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
     method public androidx.camera.video.FileDescriptorOutputOptions build();
-    method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
   }
@@ -39,7 +39,7 @@
   @RequiresApi(21) public static final class FileOutputOptions.Builder {
     ctor public FileOutputOptions.Builder(java.io.File);
     method public androidx.camera.video.FileOutputOptions build();
-    method public androidx.camera.video.FileOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
   }
@@ -55,13 +55,13 @@
     ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
     method public androidx.camera.video.MediaStoreOutputOptions build();
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
-    method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
   }
 
   @RequiresApi(21) public abstract class OutputOptions {
-    method @IntRange(from=0) public long getDurationLimit();
+    method @IntRange(from=0) public long getDurationLimitMillis();
     method @IntRange(from=0) public long getFileSizeLimit();
     method public android.location.Location? getLocation();
     field public static final int DURATION_UNLIMITED = 0; // 0x0
diff --git a/camera/camera-video/api/public_plus_experimental_current.txt b/camera/camera-video/api/public_plus_experimental_current.txt
index 1661eea..729cb0a 100644
--- a/camera/camera-video/api/public_plus_experimental_current.txt
+++ b/camera/camera-video/api/public_plus_experimental_current.txt
@@ -27,7 +27,7 @@
   @RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
     ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
     method public androidx.camera.video.FileDescriptorOutputOptions build();
-    method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
   }
@@ -39,7 +39,7 @@
   @RequiresApi(21) public static final class FileOutputOptions.Builder {
     ctor public FileOutputOptions.Builder(java.io.File);
     method public androidx.camera.video.FileOutputOptions build();
-    method public androidx.camera.video.FileOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
   }
@@ -55,13 +55,13 @@
     ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
     method public androidx.camera.video.MediaStoreOutputOptions build();
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
-    method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
   }
 
   @RequiresApi(21) public abstract class OutputOptions {
-    method @IntRange(from=0) public long getDurationLimit();
+    method @IntRange(from=0) public long getDurationLimitMillis();
     method @IntRange(from=0) public long getFileSizeLimit();
     method public android.location.Location? getLocation();
     field public static final int DURATION_UNLIMITED = 0; // 0x0
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index 1661eea..729cb0a 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -27,7 +27,7 @@
   @RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
     ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
     method public androidx.camera.video.FileDescriptorOutputOptions build();
-    method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
   }
@@ -39,7 +39,7 @@
   @RequiresApi(21) public static final class FileOutputOptions.Builder {
     ctor public FileOutputOptions.Builder(java.io.File);
     method public androidx.camera.video.FileOutputOptions build();
-    method public androidx.camera.video.FileOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
   }
@@ -55,13 +55,13 @@
     ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
     method public androidx.camera.video.MediaStoreOutputOptions build();
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
-    method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+    method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
     method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
   }
 
   @RequiresApi(21) public abstract class OutputOptions {
-    method @IntRange(from=0) public long getDurationLimit();
+    method @IntRange(from=0) public long getDurationLimitMillis();
     method @IntRange(from=0) public long getFileSizeLimit();
     method public android.location.Location? getLocation();
     field public static final int DURATION_UNLIMITED = 0; // 0x0
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
index dce6a36..12ef8da 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
@@ -118,12 +118,12 @@
     fun canBuildOutputOptions() {
         val outputOptions = FakeOutputOptions.Builder()
             .setFileSizeLimit(FILE_SIZE_LIMIT)
-            .setDurationLimit(DURATION_LIMIT)
+            .setDurationLimitMillis(DURATION_LIMIT)
             .build()
 
         assertThat(outputOptions).isNotNull()
         assertThat(outputOptions.fileSizeLimit).isEqualTo(FILE_SIZE_LIMIT)
-        assertThat(outputOptions.durationLimit).isEqualTo(DURATION_LIMIT)
+        assertThat(outputOptions.durationLimitMillis).isEqualTo(DURATION_LIMIT)
     }
 
     @Test
@@ -132,7 +132,7 @@
 
         assertThat(outputOptions.location).isNull()
         assertThat(outputOptions.fileSizeLimit).isEqualTo(OutputOptions.FILE_SIZE_UNLIMITED)
-        assertThat(outputOptions.durationLimit).isEqualTo(OutputOptions.DURATION_UNLIMITED)
+        assertThat(outputOptions.durationLimitMillis).isEqualTo(OutputOptions.DURATION_UNLIMITED)
     }
 
     @Test
@@ -145,7 +145,7 @@
     @Test
     fun invalidDurationLimit_throwsException() {
         assertThrows(IllegalArgumentException::class.java) {
-            FakeOutputOptions.Builder().setDurationLimit(INVALID_DURATION_LIMIT)
+            FakeOutputOptions.Builder().setDurationLimitMillis(INVALID_DURATION_LIMIT)
         }
     }
 
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 2f648b8..b7568be 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
@@ -597,7 +597,7 @@
         val durationTolerance = 50L
         val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
         val outputOptions = FileOutputOptions.Builder(file)
-            .setDurationLimit(durationLimitMs)
+            .setDurationLimitMillis(durationLimitMs)
             .build()
 
         val recording = recorder
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
index 127b5a0..c7015d9 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
@@ -53,7 +53,7 @@
     /**
      * Gets the limit for the file size in bytes.
      *
-     * @return the file size limit in bytes or zero if it's unlimited.
+     * @return the file size limit in bytes or {@link #FILE_SIZE_UNLIMITED} if it's unlimited.
      */
     @IntRange(from = 0)
     public long getFileSizeLimit() {
@@ -64,7 +64,7 @@
      * Returns a {@link Location} object representing the geographic location where the video was
      * recorded.
      *
-     * @return The location object or {@code null} if no location was set.
+     * @return the location object or {@code null} if no location was set.
      */
     @Nullable
     public Location getLocation() {
@@ -74,11 +74,12 @@
     /**
      * Gets the limit for the video duration in milliseconds.
      *
-     * @return the video duration limit in milliseconds or zero if it's unlimited.
+     * @return the video duration limit in milliseconds or {@link #DURATION_UNLIMITED} if it's
+     * unlimited.
      */
     @IntRange(from = 0)
-    public long getDurationLimit() {
-        return mOutputOptionsInternal.getDurationLimit();
+    public long getDurationLimitMillis() {
+        return mOutputOptionsInternal.getDurationLimitMillis();
     }
 
     /**
@@ -93,7 +94,7 @@
             mRootInternalBuilder = builder;
             // Apply default value
             mRootInternalBuilder.setFileSizeLimit(FILE_SIZE_UNLIMITED);
-            mRootInternalBuilder.setDurationLimit(DURATION_UNLIMITED);
+            mRootInternalBuilder.setDurationLimitMillis(DURATION_UNLIMITED);
         }
 
         /**
@@ -109,12 +110,12 @@
          *
          * @param fileSizeLimitBytes the file size limit in bytes.
          * @return this Builder.
-         * @throws IllegalArgumentException if the specified file size limit is less than zero.
+         * @throws IllegalArgumentException if the specified file size limit is negative.
          */
         @NonNull
         public B setFileSizeLimit(@IntRange(from = 0) long fileSizeLimitBytes) {
             Preconditions.checkArgument(fileSizeLimitBytes >= 0, "The specified file size limit "
-                    + "should be greater than zero.");
+                    + "can't be negative.");
             mRootInternalBuilder.setFileSizeLimit(fileSizeLimitBytes);
             return (B) this;
         }
@@ -130,15 +131,15 @@
          * unlimited}. If set with a negative value, an {@link IllegalArgumentException} will be
          * thrown.
          *
-         * @param durationLimitMs the video duration limit in milliseconds.
+         * @param durationLimitMillis the video duration limit in milliseconds.
          * @return this Builder.
-         * @throws IllegalArgumentException if the specified duration limit is less than zero.
+         * @throws IllegalArgumentException if the specified duration limit is negative.
          */
         @NonNull
-        public B setDurationLimit(@IntRange(from = 0) long durationLimitMs) {
-            Preconditions.checkArgument(durationLimitMs >= 0, "The specified duration limit "
-                    + "should be greater than zero.");
-            mRootInternalBuilder.setDurationLimit(durationLimitMs);
+        public B setDurationLimitMillis(@IntRange(from = 0) long durationLimitMillis) {
+            Preconditions.checkArgument(durationLimitMillis >= 0, "The specified duration limit "
+                    + "can't be negative.");
+            mRootInternalBuilder.setDurationLimitMillis(durationLimitMillis);
             return (B) this;
         }
 
@@ -154,7 +155,8 @@
          * value is {@code null}.
          *
          * @throws IllegalArgumentException if the latitude of the location is not in the range
-         * [-90, 90] or the longitude of the location is not in the range [-180, 180].
+         * {@code [-90, 90]} or the longitude of the location is not in the range {@code [-180,
+         * 180]}.
          */
         @NonNull
         public B setLocation(@Nullable Location location) {
@@ -184,7 +186,7 @@
         abstract long getFileSizeLimit();
 
         @IntRange(from = 0)
-        abstract long getDurationLimit();
+        abstract long getDurationLimitMillis();
 
         @Nullable
         abstract Location getLocation();
@@ -197,7 +199,7 @@
             abstract B setFileSizeLimit(@IntRange(from = 0) long fileSizeLimitBytes);
 
             @NonNull
-            abstract B setDurationLimit(@IntRange(from = 0) long durationLimitMs);
+            abstract B setDurationLimitMillis(@IntRange(from = 0) long durationLimitMillis);
 
             @NonNull
             abstract B setLocation(@Nullable Location location);
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index ad44acd..d36a8bb 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -1472,9 +1472,9 @@
             mFileSizeLimitInBytes = OutputOptions.FILE_SIZE_UNLIMITED;
         }
 
-        if (recordingToStart.getOutputOptions().getDurationLimit() > 0) {
+        if (recordingToStart.getOutputOptions().getDurationLimitMillis() > 0) {
             mDurationLimitNs = TimeUnit.MILLISECONDS.toNanos(
-                    recordingToStart.getOutputOptions().getDurationLimit());
+                    recordingToStart.getOutputOptions().getDurationLimitMillis());
             Logger.d(TAG, "Duration limit in nanoseconds: " + mDurationLimitNs);
         } else {
             mDurationLimitNs = OutputOptions.DURATION_UNLIMITED;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 5f8641f..3b47dfb 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -45,6 +45,7 @@
 import static java.util.Collections.singletonList;
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.SuppressLint;
 import android.graphics.ImageFormat;
 import android.graphics.Rect;
 import android.hardware.camera2.CameraDevice;
@@ -491,6 +492,7 @@
         return null;
     }
 
+    @SuppressLint("WrongConstant")
     @MainThread
     @NonNull
     private SessionConfig.Builder createPipeline(@NonNull String cameraId,
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index b618ac7..ff30013 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -202,6 +202,7 @@
     Long mLastDataStopTimestamp = null;
     @SuppressWarnings("WeakerAccess") // synthetic accessor
     Future<?> mStopTimeoutFuture = null;
+    private MediaCodecCallback mMediaCodecCallback = null;
 
     private boolean mIsFlushedAfterEndOfStream = false;
     private boolean mSourceStoppedSignalled = false;
@@ -282,7 +283,12 @@
             mStopTimeoutFuture.cancel(true);
             mStopTimeoutFuture = null;
         }
-        mMediaCodec.setCallback(new MediaCodecCallback());
+        if (mMediaCodecCallback != null) {
+            mMediaCodecCallback.stop();
+        }
+        mMediaCodecCallback = new MediaCodecCallback();
+        mMediaCodec.setCallback(mMediaCodecCallback);
+
         mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 
         if (mEncoderInput instanceof SurfaceInput) {
@@ -1014,6 +1020,7 @@
         private long mLastSentAdjustedTimeUs = 0L;
         private boolean mIsOutputBufferInPauseState = false;
         private boolean mIsKeyFrameRequired = false;
+        private boolean mStopped = false;
 
         MediaCodecCallback() {
             if (mIsVideoEncoder) {
@@ -1032,6 +1039,10 @@
         @Override
         public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
             mEncoderExecutor.execute(() -> {
+                if (mStopped) {
+                    Logger.w(mTag, "Receives input frame after codec is reset.");
+                    return;
+                }
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -1057,6 +1068,10 @@
         public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
                 @NonNull BufferInfo bufferInfo) {
             mEncoderExecutor.execute(() -> {
+                if (mStopped) {
+                    Logger.w(mTag, "Receives frame after codec is reset.");
+                    return;
+                }
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -1374,6 +1389,10 @@
         public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
                 @NonNull MediaFormat mediaFormat) {
             mEncoderExecutor.execute(() -> {
+                if (mStopped) {
+                    Logger.w(mTag, "Receives onOutputFormatChanged after codec is reset.");
+                    return;
+                }
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -1404,6 +1423,12 @@
                 }
             });
         }
+
+        /** Stop process further frame output. */
+        @ExecutedBy("mEncoderExecutor")
+        void stop() {
+            mStopped = true;
+        }
     }
 
     @SuppressWarnings("WeakerAccess") // synthetic accessor
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index f92738c..ce1a726 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -1164,13 +1164,14 @@
      * permission; without it, starting a recording will fail with a {@link SecurityException}.
      *
      * @param outputOptions the options to store the newly captured video.
-     * @param audioConfig the configuration of audio.
-     * @param executor the executor that the event listener will be run on.
-     * @param listener the event listener to handle video record events.
+     * @param audioConfig   the configuration of audio.
+     * @param executor      the executor that the event listener will be run on.
+     * @param listener      the event listener to handle video record events.
      * @return a {@link Recording} that provides controls for new active recordings.
      * @throws IllegalStateException if there is an unfinished active recording.
-     * @throws SecurityException if the audio config specifies audio should be enabled but the
-     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+     * @throws SecurityException     if the audio config specifies audio should be enabled but the
+     *                               {@link android.Manifest.permission#RECORD_AUDIO} permission
+     *                               is denied.
      */
     @SuppressLint("MissingPermission")
     @ExperimentalVideo
@@ -1204,13 +1205,14 @@
      * permission; without it, starting a recording will fail with a {@link SecurityException}.
      *
      * @param outputOptions the options to store the newly captured video.
-     * @param audioConfig the configuration of audio.
-     * @param executor the executor that the event listener will be run on.
-     * @param listener the event listener to handle video record events.
+     * @param audioConfig   the configuration of audio.
+     * @param executor      the executor that the event listener will be run on.
+     * @param listener      the event listener to handle video record events.
      * @return a {@link Recording} that provides controls for new active recordings.
      * @throws IllegalStateException if there is an unfinished active recording.
-     * @throws SecurityException if the audio config specifies audio should be enabled but the
-     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+     * @throws SecurityException     if the audio config specifies audio should be enabled but the
+     *                               {@link android.Manifest.permission#RECORD_AUDIO} permission
+     *                               is denied.
      */
     @SuppressLint("MissingPermission")
     @ExperimentalVideo
@@ -1242,13 +1244,14 @@
      * permission; without it, starting a recording will fail with a {@link SecurityException}.
      *
      * @param outputOptions the options to store the newly captured video.
-     * @param audioConfig the configuration of audio.
-     * @param executor the executor that the event listener will be run on.
-     * @param listener the event listener to handle video record events.
+     * @param audioConfig   the configuration of audio.
+     * @param executor      the executor that the event listener will be run on.
+     * @param listener      the event listener to handle video record events.
      * @return a {@link Recording} that provides controls for new active recordings.
      * @throws IllegalStateException if there is an unfinished active recording.
-     * @throws SecurityException if the audio config specifies audio should be enabled but the
-     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+     * @throws SecurityException     if the audio config specifies audio should be enabled but the
+     *                               {@link android.Manifest.permission#RECORD_AUDIO} permission
+     *                               is denied.
      */
     @SuppressLint("MissingPermission")
     @ExperimentalVideo
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
index 5e26cc6..0f3784f 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
@@ -36,6 +36,7 @@
 import androidx.test.espresso.IdlingPolicies
 import androidx.test.espresso.IdlingRegistry
 import androidx.test.espresso.IdlingResource
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
@@ -102,6 +103,7 @@
         }
     }
 
+    @FlakyTest(bugId = 259726932)
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // Known issue, checkout b/147393563.
     fun testCameraDisconnect() {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 1781ca6..d13c4c4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -61,16 +61,29 @@
 
 /**
  * Waits until an image has been saved and its idling resource has become idle.
+ *
+ * @param captureRequestsCount the capture requests count to issue to continuously take pictures
+ * without waiting for the previous capture requests to be done.
  */
-internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
+internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle(
+    captureRequestsCount: Int = 1
+) {
     val idlingResource = withActivity {
         cleanTakePictureErrorMessage()
         imageSavedIdlingResource
     }
     try {
-        IdlingRegistry.getInstance().register(idlingResource)
         // Perform click to take a picture.
-        Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(click())
+        Espresso.onView(ViewMatchers.withId(R.id.Picture)).apply {
+            repeat(captureRequestsCount) {
+                perform(click())
+            }
+        }
+        // Registers the idling resource and wait for it being idle after performing the click
+        // operations. So that the click operations can be performed continuously without wait for
+        // previous capture results.
+        IdlingRegistry.getInstance().register(idlingResource)
+        Espresso.onIdle()
     } finally { // Always release the idling resource, in case of timeout exceptions.
         IdlingRegistry.getInstance().unregister(idlingResource)
         withActivity {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 7a17ca1..1d83c85 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -26,7 +26,6 @@
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.TotalCaptureResult
 import android.location.Location
-import android.media.ImageWriter
 import android.os.Build
 import android.os.Environment
 import android.provider.MediaStore
@@ -34,7 +33,6 @@
 import android.util.Size
 import android.view.Surface
 import androidx.annotation.OptIn
-import androidx.annotation.RequiresApi
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
@@ -51,16 +49,11 @@
 import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.ViewPort
 import androidx.camera.core.impl.CameraConfig
-import androidx.camera.core.impl.CaptureBundle
-import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.CaptureStage
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
 import androidx.camera.core.impl.Identifier
 import androidx.camera.core.impl.ImageCaptureConfig
 import androidx.camera.core.impl.ImageOutputConfig
-import androidx.camera.core.impl.ImageProxyBundle
 import androidx.camera.core.impl.MutableOptionsBundle
 import androidx.camera.core.impl.SessionProcessor
 import androidx.camera.core.impl.utils.CameraOrientationUtil
@@ -70,7 +63,6 @@
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.SurfaceTextureProvider
-import androidx.camera.testing.fakes.FakeCaptureStage
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.camera.testing.fakes.FakeSessionProcessor
 import androidx.core.content.ContextCompat
@@ -83,7 +75,6 @@
 import java.io.ByteArrayInputStream
 import java.io.File
 import java.io.FileOutputStream
-import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicInteger
 import kotlin.math.abs
@@ -632,91 +623,23 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test(expected = IllegalArgumentException::class)
-    fun constructor_withBufferFormatAndCaptureProcessor_throwsException() {
-        val captureProcessor = object : CaptureProcessor {
-            override fun onOutputSurface(surface: Surface, imageFormat: Int) {}
-            override fun process(bundle: ImageProxyBundle) {}
-            override fun onResolutionUpdate(size: Size) {}
-        }
-        ImageCapture.Builder()
+    fun constructor_withBufferFormatAndSessionProcessorIsSet_throwsException(): Unit = runBlocking {
+        val sessionProcessor = FakeSessionProcessor(
+            inputFormatPreview = null, // null means using the same output surface
+            inputFormatCapture = ImageFormat.YUV_420_888
+        )
+
+        val imageCapture = ImageCapture.Builder()
             .setBufferFormat(ImageFormat.RAW_SENSOR)
-            .setCaptureProcessor(captureProcessor)
             .build()
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun constructor_maxCaptureStageInvalid_throwsException() {
-        ImageCapture.Builder().setMaxCaptureStages(0).build()
-    }
-
-    @Test
-    fun captureStagesAbove1_withoutCaptureProcessor() = runBlocking {
-        val captureBundle = CaptureBundle {
-            listOf(
-                FakeCaptureStage(0, CaptureConfig.Builder().build()),
-                FakeCaptureStage(1, CaptureConfig.Builder().build())
-            )
-        }
-
-        val imageCapture = ImageCapture.Builder()
-            .setCaptureBundle(captureBundle)
-            .build()
-
+        val preview = Preview.Builder().build()
         withContext(Dispatchers.Main) {
-            cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
-        }
-
-        val callback = FakeImageCaptureCallback(capturesCount = 1)
-
-        imageCapture.takePicture(mainExecutor, callback)
-
-        // Wait for the signal that the image capture has failed.
-        callback.awaitCapturesAndAssert(errorsCount = 1)
-
-        val error = callback.errors.first()
-        assertThat(error).isInstanceOf(ImageCaptureException::class.java)
-        assertThat(error.cause).isInstanceOf(IllegalArgumentException::class.java)
-    }
-
-    @Test
-    fun captureStageExceedMaxCaptureStage_whenIssueTakePicture() = runBlocking {
-        // Initial the captureStages not greater than the maximum count to bypass the CaptureStage
-        // count checking during bindToLifeCycle.
-        val captureStages = mutableListOf<CaptureStage>()
-        captureStages.add(FakeCaptureStage(0, CaptureConfig.Builder().build()))
-
-        val captureBundle = CaptureBundle { captureStages.toList() }
-        val captureProcessor = object : CaptureProcessor {
-            override fun onOutputSurface(surface: Surface, imageFormat: Int) {}
-            override fun process(bundle: ImageProxyBundle) {}
-            override fun onResolutionUpdate(size: Size) {}
-        }
-        val imageCapture = ImageCapture.Builder()
-            .setMaxCaptureStages(1)
-            .setCaptureBundle(captureBundle)
-            .setCaptureProcessor(captureProcessor)
-            .build()
-
-        withContext(Dispatchers.Main) {
-            cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
-        }
-
-        // Add an additional capture stage to test the case
-        // captureStage.size() > mMaxCaptureStages during takePicture.
-        captureStages.add(FakeCaptureStage(1, CaptureConfig.Builder().build()))
-
-        val callback = FakeImageCaptureCallback(capturesCount = 2)
-
-        // Take 2 photos.
-        imageCapture.takePicture(mainExecutor, callback)
-        imageCapture.takePicture(mainExecutor, callback)
-
-        // It should get onError() callback twice.
-        callback.awaitCapturesAndAssert(errorsCount = 2)
-
-        callback.errors.forEach { error ->
-            assertThat(error.cause).isInstanceOf(IllegalArgumentException::class.java)
+            val cameraSelector =
+                getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
+            cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner, cameraSelector, imageCapture, preview)
         }
     }
 
@@ -1343,40 +1266,8 @@
         assertThat(imageProperties.format).isEqualTo(ImageFormat.YUV_420_888)
     }
 
-    // Output JPEG format image when setting a CaptureProcessor is only enabled for devices that
-    // API level is at least 29.
     @Test
-    @SdkSuppress(minSdkVersion = 29)
-    fun returnJpegImage_whenCaptureProcessorIsSet() = runBlocking {
-        val builder = ImageCapture.Builder()
-        val simpleCaptureProcessor = SimpleCaptureProcessor()
-
-        // Set a CaptureProcessor to directly pass the image to output surface.
-        val useCase = builder.setCaptureProcessor(simpleCaptureProcessor).build()
-        var camera: Camera
-        withContext(Dispatchers.Main) {
-            camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
-        }
-
-        val callback = FakeImageCaptureCallback(capturesCount = 1)
-        useCase.takePicture(mainExecutor, callback)
-
-        // Wait for the signal that the image has been captured.
-        callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
-        val imageProperties = callback.results.first()
-
-        // Check the output image rotation degrees value is correct.
-        assertThat(imageProperties.rotationDegrees).isEqualTo(
-            camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
-        )
-        // Check the output format is correct.
-        assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
-        simpleCaptureProcessor.close()
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 29)
+    @SdkSuppress(minSdkVersion = 28)
     fun returnJpegImage_whenSessionProcessorIsSet_outputFormantYuv() = runBlocking {
         val builder = ImageCapture.Builder()
         val sessionProcessor = FakeSessionProcessor(
@@ -1384,12 +1275,7 @@
             inputFormatCapture = ImageFormat.YUV_420_888
         )
 
-        val imageCapture = builder
-            .setSupportedResolutions(
-                listOf(android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480))))
-            )
-            .build()
-
+        val imageCapture = builder.build()
         val preview = Preview.Builder().build()
 
         var camera: Camera
@@ -1419,25 +1305,19 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 29)
+    @SdkSuppress(minSdkVersion = 28)
     fun returnJpegImage_whenSessionProcessorIsSet_outputFormantJpeg() = runBlocking {
         assumeFalse(
             "Cuttlefish does not correctly handle Jpeg exif. Unable to test.",
             Build.MODEL.contains("Cuttlefish")
         )
 
-        val builder = ImageCapture.Builder()
         val sessionProcessor = FakeSessionProcessor(
             inputFormatPreview = null, // null means using the same output surface
             inputFormatCapture = null
         )
 
-        val imageCapture = builder
-            .setSupportedResolutions(
-                listOf(android.util.Pair(ImageFormat.JPEG, arrayOf(Size(640, 480))))
-            )
-            .build()
-
+        val imageCapture = ImageCapture.Builder().build()
         val preview = Preview.Builder().build()
 
         withContext(Dispatchers.Main) {
@@ -1507,45 +1387,6 @@
         return builder.build()
     }
 
-    // Output JPEG format image when setting a CaptureProcessor is only enabled for devices that
-    // API level is at least 29.
-    @Test
-    @SdkSuppress(minSdkVersion = 29)
-    fun returnJpegImage_whenSoftwareJpegIsEnabledWithCaptureProcessor() = runBlocking {
-        val builder = ImageCapture.Builder()
-        val simpleCaptureProcessor = SimpleCaptureProcessor()
-
-        // Set a CaptureProcessor to directly pass the image to output surface.
-        val useCase = builder.setCaptureProcessor(simpleCaptureProcessor).build()
-
-        // Enables software Jpeg
-        builder.mutableConfig.insertOption(
-            ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER,
-            true
-        )
-
-        var camera: Camera
-        withContext(Dispatchers.Main) {
-            camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
-        }
-
-        val callback = FakeImageCaptureCallback(capturesCount = 1)
-        useCase.takePicture(mainExecutor, callback)
-
-        // Wait for the signal that the image has been captured.
-        callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
-        val imageProperties = callback.results.first()
-
-        // Check the output image rotation degrees value is correct.
-        assertThat(imageProperties.rotationDegrees).isEqualTo(
-            camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
-        )
-        // Check the output format is correct.
-        assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
-        simpleCaptureProcessor.close()
-    }
-
     @kotlin.OptIn(
         androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop::class
     )
@@ -1706,42 +1547,6 @@
         }
     }
 
-    private class SimpleCaptureProcessor : CaptureProcessor {
-
-        private var imageWriter: ImageWriter? = null
-
-        @RequiresApi(Build.VERSION_CODES.M)
-        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
-            imageWriter = ImageWriter.newInstance(surface, 2)
-        }
-
-        @RequiresApi(Build.VERSION_CODES.M)
-        override fun process(bundle: ImageProxyBundle) {
-            val imageProxyListenableFuture = bundle.getImageProxy(bundle.captureIds[0])
-            try {
-                val imageProxy = imageProxyListenableFuture.get()
-                // Directly passing the input YUV image to the output surface.
-                imageWriter!!.queueInputImage(imageProxy.image)
-            } catch (exception: ExecutionException) {
-                throw IllegalArgumentException(
-                    "Can't extract ImageProxy from the ImageProxyBundle.",
-                    exception
-                )
-            } catch (exception: InterruptedException) {
-                throw IllegalArgumentException(
-                    "Can't extract ImageProxy from the ImageProxyBundle.",
-                    exception
-                )
-            }
-        }
-
-        override fun onResolutionUpdate(size: Size) {}
-
-        override fun close() {
-            imageWriter?.close()
-        }
-    }
-
     private class CountdownDeferred(count: Int) {
 
         private val deferredItems = mutableListOf<CompletableDeferred<Unit>>().apply {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
index a447c7f..578ae88 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
@@ -273,4 +273,24 @@
         val randomDelayDuration = (Random.nextInt(maxDelaySeconds) + 1).toLong()
         delay(TimeUnit.SECONDS.toMillis(randomDelayDuration))
     }
+
+    @LabTestRule.LabTestOnly
+    @Test
+    @RepeatRule.Repeat(times = LARGE_STRESS_TEST_REPEAT_COUNT)
+    fun launchActivity_thenTakeMultiplePictures_withoutWaitingPreviousResults() {
+        val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+
+        // Launches CameraXActivity and wait for the preview ready.
+        val activityScenario =
+            launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+        with(activityScenario) {
+            use {
+                // Checks whether multiple images can be captured successfully
+                repeat(STRESS_TEST_OPERATION_REPEAT_COUNT) {
+                    takePictureAndWaitForImageSavedIdle(3)
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
index d294dbc..9d7efbd 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
@@ -22,29 +22,19 @@
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.interop.Camera2CameraInfo
 import androidx.camera.core.CameraSelector
-import androidx.camera.core.impl.CaptureBundle
-import androidx.camera.core.impl.ImageCaptureConfig
 import androidx.camera.extensions.ExtensionsManager
 import androidx.camera.extensions.internal.ExtensionVersion
 import androidx.camera.extensions.internal.Version
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
-import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.launchCameraExtensionsActivity
-import androidx.camera.integration.extensions.util.HOME_TIMEOUT_MS
-import androidx.camera.integration.extensions.util.waitForPreviewViewStreaming
 import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
-import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.Dispatchers
@@ -188,53 +178,4 @@
         // the same.
         assertThat(latencyInfo).isEqualTo(expectedLatencyInfo)
     }
-
-    @LargeTest
-    @Test
-    fun returnCaptureStages_whenCaptureProcessorIsNotNull(): Unit = runBlocking {
-        // Clear the device UI and check if there is no dialog or lock screen on the top of the
-        // window before starting the test.
-        CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
-
-        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
-            setOrientationNatural()
-        }
-
-        val activityScenario = launchCameraExtensionsActivity(
-            config.cameraId,
-            config.extensionMode
-        )
-
-        with(activityScenario) {
-            use {
-                var captureBundle: CaptureBundle? = null
-                withActivity {
-                    // Retrieves the CaptureProcessor from ImageCapture's config
-                    val captureProcessor = imageCapture!!.currentConfig.retrieveOption(
-                        ImageCaptureConfig.OPTION_CAPTURE_PROCESSOR, null
-                    )
-
-                    assumeTrue(captureProcessor != null)
-
-                    // Retrieves the CaptureBundle from ImageCapture's config
-                    captureBundle = imageCapture!!.currentConfig.retrieveOption(
-                        ImageCaptureConfig.OPTION_CAPTURE_BUNDLE
-                    )
-                }
-
-                waitForPreviewViewStreaming()
-
-                // Calls CaptureBundle#getCaptureStages() will call
-                // ImageCaptureExtenderImpl#getCaptureStages(). Checks the returned value is
-                // not empty.
-                assertThat(captureBundle!!.captureStages).isNotEmpty()
-            }
-        }
-
-        // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
-        // to other tests :)
-        device.unfreezeRotation()
-        device.pressHome()
-        device.waitForIdle(HOME_TIMEOUT_MS)
-    }
 }
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt
deleted file mode 100644
index 571c3bf..0000000
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt
+++ /dev/null
@@ -1,414 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.integration.extensions
-
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.graphics.SurfaceTexture.OnFrameAvailableListener
-import android.os.Build
-import android.os.Handler
-import android.os.HandlerThread
-import android.util.Size
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.interop.Camera2CameraInfo
-import androidx.camera.core.CameraFilter
-import androidx.camera.core.CameraInfo
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.CameraConfig
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
-import androidx.camera.core.impl.Identifier
-import androidx.camera.core.impl.MutableOptionsBundle
-import androidx.camera.core.impl.OptionsBundle
-import androidx.camera.core.impl.PreviewConfig
-import androidx.camera.core.impl.UseCaseConfigFactory
-import androidx.camera.extensions.ExtensionMode
-import androidx.camera.extensions.ExtensionsManager
-import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
-import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
-import androidx.camera.integration.extensions.utils.CameraSelectorUtil
-import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.GLUtil
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
-import androidx.camera.testing.TimestampCaptureProcessor
-import androidx.camera.testing.TimestampCaptureProcessor.TimestampListener
-import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import junit.framework.AssertionFailedError
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.After
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeNotNull
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = 21)
-class PreviewProcessorTimestampTest(private val config: CameraIdExtensionModePair) {
-    @get:Rule
-    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        PreTestCameraIdList(Camera2Config.defaultConfig())
-    )
-
-    private val context = ApplicationProvider.getApplicationContext<Context>()
-
-    private lateinit var cameraProvider: ProcessCameraProvider
-    private lateinit var extensionsManager: ExtensionsManager
-    private lateinit var baseCameraSelector: CameraSelector
-    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
-
-    private val inputTimestampsLatch = CountDownLatch(1)
-    private val outputTimestampsLatch = CountDownLatch(1)
-    private val surfaceTextureLatch = CountDownLatch(1)
-    private val inputTimestamps = hashSetOf<Long>()
-    private val outputTimestamps = hashSetOf<Long>()
-
-    private val timestampListener = object : TimestampListener {
-        private var complete = false
-
-        override fun onTimestampAvailable(timestamp: Long) {
-            if (complete) {
-                return
-            }
-
-            inputTimestamps.add(timestamp)
-            if (inputTimestamps.size >= 10) {
-                inputTimestampsLatch.countDown()
-                complete = true
-            }
-        }
-    }
-
-    private var isSurfaceTextureReleased = false
-    private val isSurfaceTextureReleasedLock = Any()
-
-    private val onFrameAvailableListener = object : OnFrameAvailableListener {
-        private var complete = false
-
-        override fun onFrameAvailable(surfaceTexture: SurfaceTexture): Unit = runBlocking {
-            if (complete) {
-                return@runBlocking
-            }
-
-            withContext(Dispatchers.Main) {
-                synchronized(isSurfaceTextureReleasedLock) {
-                    if (!isSurfaceTextureReleased) {
-                        surfaceTexture.updateTexImage()
-                    }
-                }
-            }
-
-            outputTimestamps.add(surfaceTexture.timestamp)
-
-            if (outputTimestamps.size >= 10) {
-                outputTimestampsLatch.countDown()
-                complete = true
-            }
-        }
-    }
-
-    private val processingHandler: Handler
-    private val processingHandlerThread = HandlerThread("Processing").also {
-        it.start()
-        processingHandler = Handler(it.looper)
-    }
-
-    @Before
-    fun setUp(): Unit = runBlocking {
-        assumeTrue(CameraXExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
-        assumeFalse(Build.BRAND.equals("Samsung", ignoreCase = true))
-        cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
-        extensionsManager = ExtensionsManager.getInstanceAsync(
-            context,
-            cameraProvider
-        )[10000, TimeUnit.MILLISECONDS]
-
-        val (cameraId, extensionMode) = config
-        baseCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
-        assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
-
-        withContext(Dispatchers.Main) {
-            fakeLifecycleOwner = FakeLifecycleOwner()
-            fakeLifecycleOwner.startAndResume()
-        }
-    }
-
-    @After
-    fun cleanUp(): Unit = runBlocking {
-        if (::cameraProvider.isInitialized) {
-            withContext(Dispatchers.Main) {
-                cameraProvider.unbindAll()
-                cameraProvider.shutdown()[10000, TimeUnit.MILLISECONDS]
-            }
-        }
-
-        if (::extensionsManager.isInitialized) {
-            extensionsManager.shutdown()[10000, TimeUnit.MILLISECONDS]
-        }
-    }
-
-    @Test
-    fun timestampIsCorrect(): Unit = runBlocking {
-        withContext(Dispatchers.Main) {
-            val preview = Preview.Builder().build()
-
-            preview.setSurfaceProvider(
-                SurfaceTextureProvider.createSurfaceTextureProvider(createSurfaceTextureCallback())
-            )
-
-            // Retrieves the camera selector which a timestamp capture processor is applied
-            val timestampExtensionEnabledCameraSelector =
-                getTimestampExtensionEnabledCameraSelector(
-                    extensionsManager,
-                    config.extensionMode,
-                    baseCameraSelector
-                )
-
-            cameraProvider.bindToLifecycle(
-                fakeLifecycleOwner,
-                timestampExtensionEnabledCameraSelector,
-                preview
-            )
-        }
-
-        // Waits for the surface texture being ready
-        assertThat(surfaceTextureLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-
-        // Waits for 10 input and output frame timestamps are collected
-        assertThat(inputTimestampsLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-        assertThat(outputTimestampsLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-
-        // Verifies that the input and output frame timestamps are the same
-        assertThat(outputTimestamps).containsExactlyElementsIn(inputTimestamps)
-    }
-
-    private fun createSurfaceTextureCallback(): SurfaceTextureCallback =
-        object : SurfaceTextureCallback {
-            override fun onSurfaceTextureReady(
-                surfaceTexture: SurfaceTexture,
-                resolution: Size
-            ) {
-                surfaceTexture.attachToGLContext(GLUtil.getTexIdFromGLContext())
-                surfaceTexture.setOnFrameAvailableListener(
-                    onFrameAvailableListener, processingHandler
-                )
-                surfaceTextureLatch.countDown()
-            }
-
-            override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
-                synchronized(isSurfaceTextureReleasedLock) {
-                    isSurfaceTextureReleased = true
-                    surfaceTexture.release()
-                }
-            }
-        }
-
-    companion object {
-        @JvmStatic
-        @get:Parameterized.Parameters(name = "config = {0}")
-        val parameters: Collection<CameraIdExtensionModePair>
-            get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
-
-        /**
-         * Retrieves the default extended camera config provider id string
-         */
-        private fun getExtendedCameraConfigProviderId(@ExtensionMode.Mode mode: Int): String =
-            when (mode) {
-                ExtensionMode.BOKEH -> "EXTENSION_MODE_BOKEH"
-                ExtensionMode.HDR -> "EXTENSION_MODE_HDR"
-                ExtensionMode.NIGHT -> "EXTENSION_MODE_NIGHT"
-                ExtensionMode.FACE_RETOUCH -> "EXTENSION_MODE_FACE_RETOUCH"
-                ExtensionMode.AUTO -> "EXTENSION_MODE_AUTO"
-                ExtensionMode.NONE -> "EXTENSION_MODE_NONE"
-                else -> throw IllegalArgumentException("Invalid extension mode!")
-            }.let {
-                return ":camera:camera-extensions-$it"
-            }
-
-        /**
-         * Retrieves the timestamp extended camera config provider id string
-         */
-        private fun getTimestampCameraConfigProviderId(@ExtensionMode.Mode mode: Int): String =
-            "${getExtendedCameraConfigProviderId(mode)}-timestamp"
-    }
-
-    /**
-     * Gets the camera selector which a timestamp capture processor is applied
-     */
-    private fun getTimestampExtensionEnabledCameraSelector(
-        extensionsManager: ExtensionsManager,
-        extensionMode: Int,
-        baseCameraSelector: CameraSelector
-    ): CameraSelector {
-        // Injects the TimestampExtensionsUseCaseConfigFactory which allows to monitor and verify
-        // the frames' timestamps
-        injectTimestampExtensionsUseCaseConfigFactory(
-            extensionsManager,
-            extensionMode,
-            baseCameraSelector
-        )
-
-        val builder = CameraSelector.Builder.fromSelector(baseCameraSelector)
-        // Add a TimestampExtensionCameraFilter which includes the CameraFilter to check whether
-        // the camera is supported for the extension mode or not and also includes the identifier
-        // to find the extended camera config provider from ExtendedCameraConfigProviderStore
-        builder.addCameraFilter(TimestampExtensionCameraFilter(extensionsManager, extensionMode))
-        return builder.build()
-    }
-
-    /**
-     * Injects the TimestampExtensionsUseCaseConfigFactory which allows to monitor and verify the
-     * frames' timestamps
-     */
-    private fun injectTimestampExtensionsUseCaseConfigFactory(
-        extensionsManager: ExtensionsManager,
-        extensionMode: Int,
-        baseCameraSelector: CameraSelector
-    ): Unit = runBlocking {
-        val timestampConfigProviderId =
-            Identifier.create(getTimestampCameraConfigProviderId(extensionMode))
-
-        // Calling the ExtensionsManager#getExtensionEnabledCameraSelector() function to add the
-        // default extended camera config provider to ExtendedCameraConfigProviderStore
-        extensionsManager.getExtensionEnabledCameraSelector(baseCameraSelector, extensionMode)
-
-        ExtendedCameraConfigProviderStore.addConfig(timestampConfigProviderId) {
-                cameraInfo: CameraInfo, context: Context ->
-
-            // Retrieves the default extended camera config provider and
-            // ExtensionsUseCaseConfigFactory
-            val defaultConfigProviderId =
-                Identifier.create(getExtendedCameraConfigProviderId(extensionMode))
-            val defaultCameraConfigProvider =
-                ExtendedCameraConfigProviderStore.getConfigProvider(defaultConfigProviderId)
-            val defaultCameraConfig = defaultCameraConfigProvider.getConfig(cameraInfo, context)!!
-            val defaultExtensionsUseCaseConfigFactory =
-                defaultCameraConfig.retrieveOption(CameraConfig.OPTION_USECASE_CONFIG_FACTORY)
-
-            // Creates a new TimestampExtensionsUseCaseConfigFactory on top of the default
-            // ExtensionsUseCaseConfigFactory to monitor the frames' timestamps
-            val timestampExtensionsUseCaseConfigFactory = TimestampExtensionsUseCaseConfigFactory(
-                defaultExtensionsUseCaseConfigFactory!!,
-                timestampListener
-            )
-
-            // Creates the config from the original config and replaces its use case config factory
-            // with the TimestampExtensionsUseCaseConfigFactory
-            val mutableOptionsBundle = MutableOptionsBundle.from(defaultCameraConfig)
-            mutableOptionsBundle.insertOption(
-                CameraConfig.OPTION_USECASE_CONFIG_FACTORY,
-                timestampExtensionsUseCaseConfigFactory
-            )
-
-            // Returns a CameraConfig implemented with the updated config
-            object : CameraConfig {
-                val config = OptionsBundle.from(mutableOptionsBundle)
-
-                override fun getConfig(): Config {
-                    return config
-                }
-
-                override fun getCompatibilityId(): Identifier {
-                    return config.retrieveOption(CameraConfig.OPTION_COMPATIBILITY_ID)!!
-                }
-            }
-        }
-    }
-
-    /**
-     * A TimestampExtensionCameraFilter which includes the CameraFilter to check whether the camera
-     * is supported for the extension mode or not and also includes the identifier to find the
-     * extended camera config provider from ExtendedCameraConfigProviderStore.
-     */
-    private class TimestampExtensionCameraFilter constructor(
-        private val extensionManager: ExtensionsManager,
-        @ExtensionMode.Mode private val mode: Int
-    ) : CameraFilter {
-        override fun getIdentifier(): Identifier {
-            return Identifier.create(getTimestampCameraConfigProviderId(mode))
-        }
-
-        override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
-            val resultInfos = mutableListOf<CameraInfo>()
-
-            cameraInfos.forEach {
-                val cameraId = Camera2CameraInfo.from(it).cameraId
-                val cameraIdCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
-                if (extensionManager.isExtensionAvailable(cameraIdCameraSelector, mode)) {
-                    resultInfos.add(it)
-                }
-            }
-
-            return resultInfos
-        }
-    }
-
-    /**
-     * A UseCaseConfigFactory implemented on top of the default ExtensionsUseCaseConfigFactory to
-     * monitor the frames' timestamps
-     */
-    private class TimestampExtensionsUseCaseConfigFactory constructor(
-        private val useCaseConfigFactory: UseCaseConfigFactory,
-        private val timestampListener: TimestampListener
-    ) :
-        UseCaseConfigFactory {
-        override fun getConfig(
-            captureType: UseCaseConfigFactory.CaptureType,
-            captureMode: Int
-        ): Config? {
-            // Retrieves the config from the default ExtensionsUseCaseConfigFactory
-            val mutableOptionsBundle = useCaseConfigFactory.getConfig(
-                captureType, captureMode)?.let {
-                MutableOptionsBundle.from(it)
-            } ?: throw AssertionFailedError("Can not retrieve config for capture type $captureType")
-
-            // Replaces the PreviewCaptureProcessor by the TimestampCaptureProcessor to monitor the
-            // frames' timestamps
-            if (captureType.equals(UseCaseConfigFactory.CaptureType.PREVIEW)) {
-                val previewCaptureProcessor = mutableOptionsBundle.retrieveOption(
-                    PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR,
-                    null
-                )
-
-                assumeNotNull(previewCaptureProcessor)
-
-                mutableOptionsBundle.insertOption(
-                    PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR,
-                    TimestampCaptureProcessor(previewCaptureProcessor!!, timestampListener)
-                )
-            }
-
-            return OptionsBundle.from(mutableOptionsBundle)
-        }
-    }
-}
\ No newline at end of file
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 306320a..0497df5 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -482,6 +482,15 @@
 
 }
 
+package androidx.car.app.mediaextensions {
+
+  public final class MetadataExtras {
+    field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+    field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+  }
+
+}
+
 package androidx.car.app.model {
 
   @androidx.car.app.annotations.CarProtocol public final class Action {
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 5bc3d2c..9fb0b45 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -844,6 +844,15 @@
 
 }
 
+package androidx.car.app.mediaextensions {
+
+  public final class MetadataExtras {
+    field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+    field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+  }
+
+}
+
 package androidx.car.app.messaging {
 
   @androidx.car.app.annotations.ExperimentalCarApi public class MessagingServiceConstants {
@@ -1405,7 +1414,7 @@
   }
 
   @androidx.car.app.annotations.CarProtocol public final class Row implements androidx.car.app.model.Item {
-    method @androidx.car.app.annotations.ExperimentalCarApi public java.util.List<androidx.car.app.model.Action!> getActions();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public java.util.List<androidx.car.app.model.Action!> getActions();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata? getMetadata();
     method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public int getNumericDecoration();
@@ -1426,7 +1435,7 @@
 
   public static final class Row.Builder {
     ctor public Row.Builder();
-    method @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.Row.Builder addAction(androidx.car.app.model.Action);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public androidx.car.app.model.Row.Builder addAction(androidx.car.app.model.Action);
     method public androidx.car.app.model.Row.Builder addText(CharSequence);
     method public androidx.car.app.model.Row.Builder addText(androidx.car.app.model.CarText);
     method public androidx.car.app.model.Row build();
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 306320a..0497df5 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -482,6 +482,15 @@
 
 }
 
+package androidx.car.app.mediaextensions {
+
+  public final class MetadataExtras {
+    field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+    field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+  }
+
+}
+
 package androidx.car.app.model {
 
   @androidx.car.app.annotations.CarProtocol public final class Action {
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java
new file mode 100644
index 0000000..1ad83f2
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java
@@ -0,0 +1,96 @@
+/*
+ * 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.car.app.mediaextensions;
+
+import android.os.Bundle;
+
+/**
+ * Defines constants for extra keys in {@link android.support.v4.media.MediaMetadataCompat} or
+ * {@link androidx.media3.common.MediaMetadata}.
+ */
+public final class MetadataExtras {
+
+    // Do not instantiate
+    private MetadataExtras() {
+    }
+
+    /**
+     * {@link Bundle} key used in the extras of a media item to indicate that the subtitle of the
+     * corresponding media item can be linked to another media item ID.
+     * <p>The value of the extra is set to the media ID of this other item.
+     *
+     * <p>NOTE: media1 and media3 apps setting this extra <b>must implement</b>
+     * {@link androidx.media.MediaBrowserServiceCompat#onLoadItem} or
+     * {@link  androidx.media3.session.MediaLibraryService.Callback#onGetItem} respectively.
+     * <p>NOTE: media apps setting this extra <b>must explicitly set the subtitle property</b>.
+     * <p>See {@link android.support.v4.media.MediaMetadataCompat#METADATA_KEY_DISPLAY_SUBTITLE}
+     * <p>See {@link androidx.media3.common.MediaMetadata#subtitle}
+     *
+     * <p>TYPE: String.
+     * <p>
+     * <p> Example:
+     * <pre>
+     *   "Source" MediaItem
+     *      + mediaId:                  “Beethoven-9th-symphony”    // ID
+     *      + title:                    “9th symphony”              // Track
+     *      + subtitle:                 “The best of Beethoven”     // Album
+     * ╔════+ subtitleLinkMediaId:      “Beethoven-best-of”         // Album ID
+     * ║    + description:              “Beethoven”                 // Artist
+     * ║    + descriptionLinkMediaId:   “artist:Beethoven”          // Artist ID
+     * ║
+     * ║ "Destination" MediaItem
+     * ╚════+ mediaId:                  “Beethoven-best-of”         // ID
+     *      + title:                    “The best of Beethoven”     // Album
+     *      + subtitle:                 “Beethoven”                 // Artist
+     *      + subtitleLinkMediaId:      “artist:Beethoven”          // Artist ID
+     * </pre>
+     **/
+    public static final String KEY_SUBTITLE_LINK_MEDIA_ID =
+            "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+
+    /**
+     * {@link Bundle} key used in the extras of a media item to indicate that the description of the
+     * corresponding media item can be linked to another media item ID.
+     * <p>The value of the extra is set to the media ID of this other item.
+     *
+     * <p>NOTE: media1 and media3 apps setting this extra <b>must implement</b>
+     * {@link androidx.media.MediaBrowserServiceCompat#onLoadItem} or
+     * {@link androidx.media3.session.MediaLibraryService.Callback#onGetItem} respectively.
+     * <p>NOTE: media apps setting this extra <b>must explicitly set the description property</b>.
+     * <p>See {@link android.support.v4.media.MediaMetadataCompat#METADATA_KEY_DISPLAY_DESCRIPTION}
+     * <p>See {@link androidx.media3.common.MediaMetadata#description}
+     *
+     * <p>TYPE: String.
+     * <p>
+     * <p> Example:
+     * <pre>
+     *   "Source" MediaItem
+     *      + mediaId:                  “Beethoven-9th-symphony”    // ID
+     *      + title:                    “9th symphony”              // Track
+     *      + subtitle:                 “The best of Beethoven”     // Album
+     *      + subtitleLinkMediaId:      “Beethoven-best-of”         // Album ID
+     *      + description:              “Beethoven”                 // Artist
+     * ╔════+ descriptionLinkMediaId:   “artist:Beethoven”          // Artist ID
+     * ║
+     * ║ "Destination" MediaItem
+     * ╚════+ mediaId:                  “artist:Beethoven”          // ID
+     *      + title:                    “Beethoven”                 // Artist
+     * </pre>
+     **/
+    public static final String KEY_DESCRIPTION_LINK_MEDIA_ID =
+            "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/package-info.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/package-info.java
new file mode 100644
index 0000000..c86a9d5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * The mediaextensions package defines car specific extensions to the
+ * {@link android.support.v4.media}, {@link androidx.media} and {@link androidx.media3} libraries.
+ */
+package androidx.car.app.mediaextensions;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index e278047..955c258b 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -165,6 +165,7 @@
      */
     @ExperimentalCarApi
     @NonNull
+    @RequiresCarApi(6)
     public List<Action> getActions() {
         return mActions;
     }
@@ -540,6 +541,7 @@
          */
         @ExperimentalCarApi
         @NonNull
+        @RequiresCarApi(6)
         public Builder addAction(@NonNull Action action) {
             List<Action> mActionsCopy = new ArrayList<>(mActions);
             mActionsCopy.add(requireNonNull(action));
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 696229d..4ff282e 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -144,11 +144,3 @@
 android {
     namespace "androidx.compose.animation.core"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all"
-        ]
-    }
-}
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index 5d96f14..1e1ab09 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -131,11 +131,3 @@
 android {
     namespace "androidx.compose.animation.graphics"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/animation/animation-tooling-internal/build.gradle b/compose/animation/animation-tooling-internal/build.gradle
index 084c4ff..87aeae3 100644
--- a/compose/animation/animation-tooling-internal/build.gradle
+++ b/compose/animation/animation-tooling-internal/build.gradle
@@ -27,14 +27,6 @@
     implementation(libs.kotlinStdlib)
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
-
 androidx {
     name = "Compose Animation Tooling"
     description = "Compose Animation APIs for tooling support. Internal use only."
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 229e0fc..cc0c1c7 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -130,11 +130,3 @@
 android {
     namespace "androidx.compose.animation"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
index f809ad9..3038c88 100644
--- a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
@@ -58,6 +58,7 @@
     }
 }
 
+@JvmDefaultWithCompatibility
 interface DaemonCompiler {
     fun compile(
         args: Array<String>,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 26f1440..c421acc 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -1626,4 +1626,69 @@
             }
         """
     )
+
+    @Test
+    fun testForEarlyExit() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Test(condition: Boolean) {
+                val value = remember { mutableStateOf(false) }
+                if (!value.value && !condition) return
+                val value2 = remember { mutableStateOf(false) }
+                Text("Text ${'$'}{value.value}, ${'$'}{value2.value}")
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<rememb...>,<Text("...>:Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                val value = %composer.cache(false) {
+                  mutableStateOf(
+                    value = false
+                  )
+                }
+                if (!value.value && !condition) {
+                  if (isTraceInProgress()) {
+                    traceEventEnd()
+                  }
+                  %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                    Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                  }
+                  return
+                }
+                val value2 = remember({
+                  mutableStateOf(
+                    value = false
+                  )
+                }, %composer, 0)
+                Text("Text %{value.value}, %{value2.value}", %composer, 0)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Text(value: String) { }
+        """
+    )
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt
index c11e7ee..0c868f1 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt
@@ -38,6 +38,7 @@
 import org.jetbrains.kotlin.name.FqName
 import org.jetbrains.kotlin.psi.KtParameter
 
+@JvmDefaultWithCompatibility
 interface FunctionMetrics {
     val isEmpty: Boolean get() = false
     val packageName: FqName
@@ -81,6 +82,7 @@
     fun print(out: Appendable, src: IrSourcePrinterVisitor)
 }
 
+@JvmDefaultWithCompatibility
 interface ModuleMetrics {
     val isEmpty get() = false
 
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 33fecf3f8..2c3f561 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -590,9 +590,6 @@
     protected fun IrType.binaryOperator(name: Name, paramType: IrType): IrFunctionSymbol =
         context.symbols.getBinaryOperator(name, this, paramType)
 
-    protected fun IrType.unaryOperator(name: Name): IrFunctionSymbol =
-        context.symbols.getUnaryOperator(name, this)
-
     protected fun irAnd(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
         return irCall(
             lhs.type.binaryOperator(OperatorNameConventions.AND, rhs.type),
@@ -603,15 +600,6 @@
         )
     }
 
-    protected fun irInv(lhs: IrExpression): IrCallImpl {
-        val int = context.irBuiltIns.intType
-        return irCall(
-            int.unaryOperator(OperatorNameConventions.INV),
-            null,
-            lhs
-        )
-    }
-
     protected fun irOr(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
         val int = context.irBuiltIns.intType
         return irCall(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 3541e35..a3f26b6 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -287,6 +287,7 @@
         changedParamCount(realValueParams, thisParams)
 }
 
+@JvmDefaultWithCompatibility
 interface IrChangedBitMaskValue {
     val used: Boolean
     val declarations: List<IrValueDeclaration>
@@ -313,6 +314,7 @@
     fun putAsValueArgumentIn(fn: IrFunctionAccessExpression, startIndex: Int)
 }
 
+@JvmDefaultWithCompatibility
 interface IrChangedBitMaskVariable : IrChangedBitMaskValue {
     fun asStatements(): List<IrStatement>
     fun irOrSetBitsAtSlot(slot: Int, value: IrExpression): IrExpression
@@ -2627,7 +2629,7 @@
                                 }
                             }
                         }
-
+                        scope.updateIntrinsiceRememberSafety(false)
                         break@loop
                     }
                     if (scope.isInlinedLambda && scope.inComposableCall)
@@ -4544,7 +4546,7 @@
                 temp,
                 irAnd(
                     irGet(temp),
-                    irInv(irConst(ParamState.Mask.bitsForSlot(slot)))
+                    irConst(ParamState.Mask.bitsForSlot(slot).inv())
                 )
             )
         }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
index b535bdc..5c22d57 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
@@ -17,6 +17,7 @@
 package androidx.compose.compiler.plugins.kotlin.lower
 
 import androidx.compose.compiler.plugins.kotlin.KtxNameConventions
+import java.util.Locale
 import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
 import org.jetbrains.kotlin.descriptors.Modality
 import org.jetbrains.kotlin.ir.IrBuiltIns
@@ -98,6 +99,7 @@
 import org.jetbrains.kotlin.ir.types.classOrNull
 import org.jetbrains.kotlin.ir.types.isAny
 import org.jetbrains.kotlin.ir.types.isInt
+import org.jetbrains.kotlin.ir.types.isMarkedNullable
 import org.jetbrains.kotlin.ir.types.isNullableAny
 import org.jetbrains.kotlin.ir.types.isUnit
 import org.jetbrains.kotlin.ir.util.isAnnotationClass
@@ -110,9 +112,6 @@
 import org.jetbrains.kotlin.resolve.descriptorUtil.isAnnotationConstructor
 import org.jetbrains.kotlin.types.Variance
 import org.jetbrains.kotlin.utils.Printer
-import java.util.Locale
-import kotlin.math.abs
-import org.jetbrains.kotlin.ir.types.isMarkedNullable
 
 fun IrElement.dumpSrc(): String {
     val sb = StringBuilder()
@@ -999,14 +998,14 @@
 
     private fun intAsBinaryString(value: Int): String {
         if (value == 0) return "0"
-        var current = abs(value)
+        var current = if (value >= 0) value else value.inv()
         var result = ""
         while (current != 0 || result.length % 4 != 0) {
             val nextBit = current and 1 != 0
-            current = current shr 1
+            current = current ushr 1
             result = "${if (nextBit) "1" else "0"}$result"
         }
-        return "${if (value < 0) "-" else ""}0b$result"
+        return "0b$result" + if (value < 0) ".inv()" else ""
     }
 
     override fun visitConst(expression: IrConst<*>) {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
index 915066b..9c6fce4 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
@@ -50,6 +50,7 @@
 import org.jetbrains.kotlin.ir.util.module
 import org.jetbrains.kotlin.ir.util.remapTypeParameters
 
+@JvmDefaultWithCompatibility
 internal interface DecoyTransformBase {
     val context: IrPluginContext
     val signatureBuilder: IdSignatureSerializer
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 4ce01dd..2385d19 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -139,11 +139,3 @@
 android {
     namespace "androidx.compose.foundation.layout"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all"
-        ]
-    }
-}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 05e0a32..a01aa33 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -157,11 +157,8 @@
     method public suspend Object? consumePreFling(long velocity, kotlin.coroutines.Continuation<? super androidx.compose.ui.unit.Velocity>);
     method public long consumePreScroll(long scrollDelta, int source);
     method public androidx.compose.ui.Modifier getEffectModifier();
-    method public boolean isEnabled();
     method public boolean isInProgress();
-    method public void setEnabled(boolean);
     property public abstract androidx.compose.ui.Modifier effectModifier;
-    property public abstract boolean isEnabled;
     property public abstract boolean isInProgress;
   }
 
@@ -912,6 +909,7 @@
   @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface LazyStaggeredGridLayoutInfo {
     method public int getAfterContentPadding();
     method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
     method public int getTotalItemsCount();
     method public int getViewportEndOffset();
     method public long getViewportSize();
@@ -919,6 +917,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo> getVisibleItemsInfo();
     property public abstract int afterContentPadding;
     property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
     property public abstract int totalItemsCount;
     property public abstract int viewportEndOffset;
     property public abstract long viewportSize;
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
index 82f9b23..e8c54a3 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.benchmark.lazy.MotionEventHelper
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -32,6 +33,7 @@
 import androidx.compose.foundation.overscroll
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.testutils.LayeredComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
@@ -123,11 +125,19 @@
         view = LocalView.current
         if (!::motionEventHelper.isInitialized) motionEventHelper = MotionEventHelper(view)
         val scrollState = rememberScrollState()
-        val overscrollEffect = ScrollableDefaults.overscrollEffect().apply { isEnabled = true }
+        val wrappedScrollState = remember(scrollState) {
+            object : ScrollableState by scrollState {
+                override val canScrollForward: Boolean
+                    get() = true
+                override val canScrollBackward: Boolean
+                    get() = true
+            }
+        }
+        val overscrollEffect = ScrollableDefaults.overscrollEffect()
         Box(
             Modifier
                 .scrollable(
-                    scrollState,
+                    wrappedScrollState,
                     orientation = Orientation.Vertical,
                     reverseDirection = true,
                     overscrollEffect = overscrollEffect
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index dd28bf30..1988c5c 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -177,11 +177,3 @@
     description = "Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers"
     legacyDisableKotlinStrictApiMode = true
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EmojiCompatDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EmojiCompatDemo.kt
new file mode 100644
index 0000000..b6995d6
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EmojiCompatDemo.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.foundation.demos.text
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun EmojiCompatDemo() {
+    val emoji14MeltingFace = "\uD83E\uDEE0"
+    val emoji13WomanFeedingBaby = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF7C"
+    val emoji13DisguisedFace = "\uD83E\uDD78"
+    val emoji12HoldingHands =
+        "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D\uD83D\uDC68\uD83C\uDFFF"
+    val emoji12Flamingo = "\uD83E\uDDA9"
+    val emoji11PartyingFace = "\uD83E\uDD73"
+
+    val text = "11: $emoji11PartyingFace 12: $emoji12Flamingo $emoji12HoldingHands " +
+        "13: $emoji13DisguisedFace $emoji13WomanFeedingBaby " +
+        "14: $emoji14MeltingFace"
+
+    Column(modifier = Modifier
+        .fillMaxSize()
+        .padding(16.dp)) {
+        Text(text = text, modifier = Modifier.padding(16.dp))
+
+        val textFieldValue = remember { mutableStateOf(TextFieldValue(text)) }
+
+        TextField(
+            value = textFieldValue.value,
+            modifier = Modifier.padding(16.dp),
+            onValueChange = { textFieldValue.value = it }
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index eb423ca..6562a8e 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -73,6 +73,7 @@
                 ComposableDemo("Variable Fonts") { VariableFontsDemo() },
                 ComposableDemo("FontFamily fallback") { FontFamilyDemo() },
                 ComposableDemo("All system font families") { SystemFontFamilyDemo() },
+                ComposableDemo("Emoji Compat") { EmojiCompatDemo() },
             )
         ),
         DemoCategory(
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
index 02c04af..072596d 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
@@ -69,7 +69,7 @@
             // relaxation: when we are in progress of the overscroll and user scrolls in the
             // different direction = substract the overscroll first
             val sameDirection = sign(scrollDelta.y) == sign(overscrollOffset.value)
-            return if (abs(overscrollOffset.value) > 0.5 && !sameDirection && isEnabled) {
+            return if (abs(overscrollOffset.value) > 0.5 && !sameDirection) {
                 val prevOverscrollValue = overscrollOffset.value
                 val newOverscrollValue = overscrollOffset.value + scrollDelta.y
                 if (sign(prevOverscrollValue) != sign(newOverscrollValue)) {
@@ -93,7 +93,7 @@
             source: NestedScrollSource
         ) {
             // if it is a drag, not a fling, add the delta left to our over scroll value
-            if (abs(overscrollDelta.y) > 0.5 && isEnabled && source == NestedScrollSource.Drag) {
+            if (abs(overscrollDelta.y) > 0.5 && source == NestedScrollSource.Drag) {
                 scope.launch {
                     // multiply by 0.1 for the sake of parallax effect
                     overscrollOffset.snapTo(overscrollOffset.value + overscrollDelta.y * 0.1f)
@@ -105,17 +105,13 @@
 
         override suspend fun consumePostFling(velocity: Velocity) {
             // when the fling happens - we just gradually animate our overscroll to 0
-            if (isEnabled) {
-                overscrollOffset.animateTo(
-                    targetValue = 0f,
-                    initialVelocity = velocity.y,
-                    animationSpec = spring()
-                )
-            }
+            overscrollOffset.animateTo(
+                targetValue = 0f,
+                initialVelocity = velocity.y,
+                animationSpec = spring()
+            )
         }
 
-        override var isEnabled: Boolean by mutableStateOf(true)
-
         override val isInProgress: Boolean
             get() = overscrollOffset.isRunning
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
index b994fe6f..0db23d9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
@@ -110,7 +110,6 @@
         )
 
         rule.runOnIdle {
-            // we passed isContentScrolls = 1, so initial draw must occur
             assertThat(controller.drawCallsCount).isEqualTo(1)
         }
 
@@ -144,7 +143,6 @@
         )
 
         rule.runOnIdle {
-            // we passed isContentScrolls = 1, so initial draw must occur
             assertThat(controller.drawCallsCount).isEqualTo(1)
         }
 
@@ -158,7 +156,7 @@
             // since we consume 1/10 of the delta in the pre scroll during overscroll, expect 9/10
             assertThat(abs(acummulatedScroll - 1000f * 9 / 10)).isWithin(0.1f)
 
-            assertThat(controller.preScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
+            assertThat(controller.lastPreScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
             assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Drag)
         }
 
@@ -168,15 +166,24 @@
     }
 
     @Test
-    fun overscrollEffect_scrollable_skipsDeltasIfDisabled() {
+    fun overscrollEffect_scrollable_skipsDeltasIfCannotScroll() {
         var acummulatedScroll = 0f
         val controller = TestOverscrollEffect(consumePreCycles = true)
+
+        var canScroll = true
+
         val scrollableState = ScrollableState { delta ->
             acummulatedScroll += delta
             delta
         }
+
         val viewConfig = rule.setOverscrollContentAndReturnViewConfig(
-            scrollableState = scrollableState,
+            scrollableState = object : ScrollableState by scrollableState {
+                override val canScrollForward: Boolean
+                    get() = canScroll
+                override val canScrollBackward: Boolean
+                    get() = canScroll
+            },
             overscrollEffect = controller
         )
 
@@ -186,15 +193,19 @@
             up()
         }
 
-        val lastControlledConsumed = rule.runOnIdle {
+        rule.runOnIdle {
             val slop = viewConfig.touchSlop
             // since we consume 1/10 of the delta in the pre scroll during overscroll, expect 9/10
             assertThat(abs(acummulatedScroll - 1000f * 9 / 10)).isWithin(0.1f)
 
-            assertThat(controller.preScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
+            assertThat(controller.lastPreScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
             assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Drag)
-            controller.isEnabled = false
-            controller.preScrollDelta
+            controller.lastPreScrollDelta = Offset.Zero
+        }
+
+        // Inform scrollable that we cannot scroll anymore
+        rule.runOnIdle {
+            canScroll = false
         }
 
         rule.onNodeWithTag(boxTag).performTouchInput {
@@ -204,8 +215,8 @@
         }
 
         rule.runOnIdle {
-            // still there because we are disabled
-            assertThat(controller.preScrollDelta).isEqualTo(lastControlledConsumed)
+            // Scrollable should not have dispatched any new deltas
+            assertThat(controller.lastPreScrollDelta).isEqualTo(Offset.Zero)
         }
     }
 
@@ -231,7 +242,6 @@
         )
 
         rule.runOnIdle {
-            // we passed isContentScrolls = 1, so initial draw must occur
             assertThat(controller.drawCallsCount).isEqualTo(1)
         }
 
@@ -355,7 +365,6 @@
         }
 
         rule.runOnIdle {
-            controller.isEnabled = true
             val offset = Offset(0f, 5f)
             controller.consumePostScroll(
                 initialDragDelta = offset,
@@ -450,7 +459,6 @@
         )
 
         rule.runOnIdle {
-            // we passed isContentScrolls = 1, so initial draw must occur
             assertThat(controller.drawCallsCount).isEqualTo(1)
         }
 
@@ -467,7 +475,7 @@
         rule.runOnIdle {
             with(controller) {
                 // presented on consume pre scroll
-                assertSingleAxisValue(preScrollDelta.x, preScrollDelta.y)
+                assertSingleAxisValue(lastPreScrollDelta.x, lastPreScrollDelta.y)
 
                 // presented on consume post scroll
                 assertSingleAxisValue(lastOverscrollDelta.x, lastOverscrollDelta.y)
@@ -493,7 +501,6 @@
         )
 
         rule.runOnIdle {
-            // we passed isContentScrolls = 1, so initial draw must occur
             assertThat(controller.drawCallsCount).isEqualTo(1)
         }
 
@@ -510,7 +517,7 @@
         rule.runOnIdle {
             with(controller) {
                 // presented on consume pre scroll
-                assertSingleAxisValue(preScrollDelta.y, preScrollDelta.x)
+                assertSingleAxisValue(lastPreScrollDelta.y, lastPreScrollDelta.x)
 
                 // presented on consume post scroll
                 assertSingleAxisValue(lastOverscrollDelta.y, lastOverscrollDelta.x)
@@ -630,14 +637,13 @@
     ) : OverscrollEffect {
         var drawCallsCount = 0
         var isInProgressCallCount = 0
-        var isContentScrolls = true
 
         var lastVelocity = Velocity.Zero
         var lastInitialDragDelta = Offset.Zero
         var lastOverscrollDelta = Offset.Zero
         var lastNestedScrollSource: NestedScrollSource? = null
 
-        var preScrollDelta = Offset.Zero
+        var lastPreScrollDelta = Offset.Zero
         var preScrollSource: NestedScrollSource? = null
 
         var preFlingVelocity = Velocity.Zero
@@ -646,7 +652,7 @@
             scrollDelta: Offset,
             source: NestedScrollSource
         ): Offset {
-            preScrollDelta = scrollDelta
+            lastPreScrollDelta = scrollDelta
             preScrollSource = source
 
             return if (consumePreCycles) scrollDelta / 10f else Offset.Zero
@@ -677,12 +683,6 @@
                 return animationRunning
             }
 
-        override var isEnabled: Boolean
-            get() = isContentScrolls
-            set(value) {
-                isContentScrolls = value
-            }
-
         override val effectModifier: Modifier = Modifier.drawBehind { drawCallsCount += 1 }
     }
 
@@ -703,7 +703,6 @@
         )
 
         rule.runOnIdle {
-            // we passed isContentScrolls = 1, so initial draw must occur
             assertThat(controller.drawCallsCount).isEqualTo(1)
         }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index 3ca102f..1496765 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -2565,8 +2565,6 @@
 
     override suspend fun consumePostFling(velocity: Velocity) {}
 
-    override var isEnabled: Boolean = false
-
     override val isInProgress: Boolean
         get() = false
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
index 179cc7d..2428fea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
 import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.list.BaseLazyListTestWithOrientation
@@ -30,6 +31,8 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -40,10 +43,14 @@
 import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.test.swipeWithVelocity
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.LargeTest
 import kotlin.math.abs
+import kotlin.math.absoluteValue
 import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -229,6 +236,130 @@
         }
     }
 
+    @Test
+    fun performFling_shouldPropagateVelocityIfHitEdges() {
+        var stepSize = 0f
+        var latestAvailableVelocity = Velocity.Zero
+        lateinit var lazyListState: LazyListState
+        val inspectingNestedScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                latestAvailableVelocity = available
+                return Velocity.Zero
+            }
+        }
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            lazyListState = rememberLazyListState(180) // almost at the end
+            stepSize = with(density) { ItemSize.toPx() }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .nestedScroll(inspectingNestedScrollConnection)
+            ) {
+                MainLayout(state = lazyListState)
+            }
+        }
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                30000f
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+
+        // arrange
+        rule.runOnIdle {
+            runBlocking {
+                lazyListState.scrollToItem(20) // almost at the start
+            }
+        }
+
+        latestAvailableVelocity = Velocity.Zero
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                -1.5f * stepSize,
+                30000f
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+    }
+
+    @Test
+    fun performFling_shouldConsumeAllVelocityIfInTheMiddleOfTheList() {
+        var stepSize = 0f
+        var latestAvailableVelocity = Velocity.Zero
+        lateinit var lazyListState: LazyListState
+        val inspectingNestedScrollConnection = object : NestedScrollConnection {
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                latestAvailableVelocity = available
+                return Velocity.Zero
+            }
+        }
+
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            lazyListState = rememberLazyListState(100) // middle of the list
+            stepSize = with(density) { ItemSize.toPx() }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .nestedScroll(inspectingNestedScrollConnection)
+            ) {
+                MainLayout(state = lazyListState)
+            }
+        }
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                10000f // use a not so high velocity
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+
+        // arrange
+        rule.runOnIdle {
+            runBlocking {
+                lazyListState.scrollToItem(100) // return to the middle
+            }
+        }
+
+        latestAvailableVelocity = Velocity.Zero
+
+        // act
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                -1.5f * stepSize,
+                10000f // use a not so high velocity
+            )
+        }
+
+        // assert
+        rule.runOnIdle {
+            assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+        }
+    }
+
     private fun onMainList() = rule.onNodeWithTag(TestTag)
 
     @Composable
@@ -290,6 +421,10 @@
         )
     }
 
+    private fun Velocity.toAbsoluteFloat(): Float {
+        return (if (orientation == Orientation.Vertical) y else x).absoluteValue
+    }
+
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
index 0c409ef..a09f72b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -67,6 +67,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import kotlin.test.assertNotEquals
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -111,7 +112,7 @@
     }
 
     @Test
-    fun performFling_afterSnappingVelocity_shouldReturnNoVelocity() {
+    fun performFling_afterSnappingVelocity_everythingWasConsumed_shouldReturnNoVelocity() {
         val testLayoutInfoProvider = TestLayoutInfoProvider()
         var afterFlingVelocity = 0f
         rule.setContent {
@@ -133,6 +134,29 @@
     }
 
     @Test
+    fun performFling_afterSnappingVelocity_didNotConsumeAllScroll_shouldReturnRemainingVelocity() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        var afterFlingVelocity = 0f
+        rule.setContent {
+            // Consume only half
+            val scrollableState = rememberScrollableState(consumeScrollDelta = { it / 2f })
+            val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+
+            LaunchedEffect(Unit) {
+                scrollableState.scroll {
+                    afterFlingVelocity = with(testFlingBehavior) {
+                        performFling(50000f)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotEquals(NoVelocity, afterFlingVelocity)
+        }
+    }
+
+    @Test
     fun findClosestOffset_noFlingDirection_shouldReturnAbsoluteDistance() {
         val testLayoutInfoProvider = TestLayoutInfoProvider()
         val offset = findClosestOffset(0f, testLayoutInfoProvider, density)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
index 5d6408a..dd1e23b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -38,6 +38,7 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -250,6 +251,7 @@
         assertThat(state.canScrollBackward).isFalse()
     }
 
+    @Ignore("b/259608530")
     @Test
     fun canScrollBackward() = runBlocking {
         withContext(Dispatchers.Main + AutoTestFrameClock()) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
index c34d2b2..3df360d 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
@@ -29,6 +29,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
@@ -73,6 +75,18 @@
         )
     }
 
+    internal fun axisSize(crossAxis: Int, mainAxis: Int): IntSize =
+        IntSize(
+            if (orientation == Orientation.Vertical) crossAxis else mainAxis,
+            if (orientation == Orientation.Vertical) mainAxis else crossAxis,
+        )
+
+    internal fun axisOffset(crossAxis: Int, mainAxis: Int): IntOffset =
+        IntOffset(
+            if (orientation == Orientation.Vertical) crossAxis else mainAxis,
+            if (orientation == Orientation.Vertical) mainAxis else crossAxis,
+        )
+
     @Composable
     internal fun LazyStaggeredGrid(
         cells: StaggeredGridCells,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
index e55a4d1..c718b43 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.ui.Modifier
@@ -310,4 +309,34 @@
             .assertMainAxisSizeIsEqualTo(20.dp)
             .assertCrossAxisSizeIsEqualTo(itemSizeDp * 2)
     }
+
+    @Test
+    fun scrollsCorrectlyWithKeyAndLargeMainAxisContentPadding() {
+        state = LazyStaggeredGridState(initialFirstVisibleItemIndex = 0)
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 2,
+                modifier = Modifier
+                    .testTag(LazyStaggeredGrid)
+                    .axisSize(itemSizeDp * 2, itemSizeDp * 5),
+                contentPadding = PaddingValues(
+                    mainAxis = itemSizeDp * 2,
+                    crossAxis = 0.dp
+                ),
+                state = state
+            ) {
+                items(1000, key = { it }) {
+                    Spacer(Modifier.mainAxisSize(itemSizeDp).testTag("$it"))
+                }
+            }
+        }
+
+        repeat(10) {
+            state.scrollBy(itemSizeDp * 20)
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(400)
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index a9dc3a2..7eec8ab 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -95,6 +95,8 @@
                 "Visible items MUST BE sorted: ${state.layoutInfo.visibleItemsInfo}",
                 isSorted
             )
+
+            assertThat(state.layoutInfo.orientation == orientation)
         }
     }
 
@@ -286,6 +288,53 @@
     }
 
     @Test
+    fun itemSizeInLayoutInfo() {
+        rule.setContent {
+            state = rememberLazyStaggeredGridState()
+            LazyStaggeredGrid(
+                lanes = 3,
+                state = state,
+                modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+            ) {
+                items(6) {
+                    Spacer(
+                        Modifier
+                            .axisSize(
+                                crossAxis = itemSizeDp,
+                                mainAxis = itemSizeDp * (it + 1)
+                            )
+                            .testTag("$it")
+                            .debugBorder()
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSizeDp * 3)
+
+        val items = state.layoutInfo.visibleItemsInfo
+
+        assertThat(items.size).isEqualTo(3)
+        with(items[0]) {
+            assertThat(index).isEqualTo(3)
+            assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 4))
+            assertThat(offset).isEqualTo(axisOffset(0, -itemSizePx * 2))
+        }
+
+        with(items[1]) {
+            assertThat(index).isEqualTo(4)
+            assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 5))
+            assertThat(offset).isEqualTo(axisOffset(itemSizePx, -itemSizePx))
+        }
+
+        with(items[2]) {
+            assertThat(index).isEqualTo(5)
+            assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 6))
+            assertThat(offset).isEqualTo(axisOffset(itemSizePx * 2, 0))
+        }
+    }
+
+    @Test
     fun itemCanEmitZeroNodes() {
         rule.setContent {
             state = rememberLazyStaggeredGridState()
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
index 32047cf..b651632 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
@@ -252,21 +252,6 @@
 
     private var containerSize = Size.Zero
 
-    private val isEnabledState = mutableStateOf(false)
-    override var isEnabled: Boolean = false
-        get() = isEnabledState.value
-        set(value) {
-            // Use field instead of isEnabledState to avoid a state read
-            val enabledChanged = field != value
-            isEnabledState.value = value
-            field = value
-
-            if (enabledChanged) {
-                scrollCycleInProgress = false
-                animateToRelease()
-            }
-        }
-
     override val isInProgress: Boolean
         get() {
             return allEffects.fastAny { it.distanceCompat != 0f }
@@ -523,8 +508,6 @@
 
     override suspend fun consumePostFling(velocity: Velocity) {}
 
-    override var isEnabled: Boolean = false
-
     override val isInProgress: Boolean
         get() = false
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
index bea29df..b56d000 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
@@ -96,12 +96,6 @@
     suspend fun consumePostFling(velocity: Velocity)
 
     /**
-     * Whether the overscroll effect is enabled or not. If it is not enabled, [scrollable] won't
-     * send the events to this effect.
-     */
-    var isEnabled: Boolean
-
-    /**
      * Whether over scroll within this controller is currently on progress or not, e.g. if the
      * overscroll effect is playing animation or shown/interactable in any other way.
      *
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 9c1374a..76621b48 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -302,7 +302,7 @@
             overscrollEffect = overscrollEffect
         )
         val layout =
-            ScrollingLayoutModifier(state, reverseScrolling, isVertical, overscrollEffect)
+            ScrollingLayoutModifier(state, reverseScrolling, isVertical)
         semantics
             .clipScrollableContainer(orientation)
             .overscroll(overscrollEffect)
@@ -319,12 +319,10 @@
     }
 )
 
-@OptIn(ExperimentalFoundationApi::class)
 private data class ScrollingLayoutModifier(
     val scrollerState: ScrollState,
     val isReversed: Boolean,
-    val isVertical: Boolean,
-    val overscrollEffect: OverscrollEffect
+    val isVertical: Boolean
 ) : LayoutModifier {
     override fun MeasureScope.measure(
         measurable: Measurable,
@@ -345,7 +343,6 @@
         val scrollHeight = placeable.height - height
         val scrollWidth = placeable.width - width
         val side = if (isVertical) scrollHeight else scrollWidth
-        overscrollEffect.isEnabled = side != 0
         // The max value must be updated before returning from the measure block so that any other
         // chained RemeasurementModifiers that try to perform scrolling based on the new
         // measurements inside onRemeasured are able to scroll to the new max based on the newly-
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 7fa7897..3876703 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -390,11 +390,14 @@
         return overscrollPreConsumed + preConsumedByParent + axisConsumed + parentConsumed
     }
 
+    private val shouldDispatchOverscroll
+        get() = scrollableState.canScrollForward || scrollableState.canScrollBackward
+
     fun overscrollPreConsumeDelta(
         scrollDelta: Offset,
         source: NestedScrollSource
     ): Offset {
-        return if (overscrollEffect != null && overscrollEffect.isEnabled) {
+        return if (overscrollEffect != null && shouldDispatchOverscroll) {
             overscrollEffect.consumePreScroll(scrollDelta, source)
         } else {
             Offset.Zero
@@ -406,7 +409,7 @@
         availableForOverscroll: Offset,
         source: NestedScrollSource
     ) {
-        if (overscrollEffect != null && overscrollEffect.isEnabled) {
+        if (overscrollEffect != null && shouldDispatchOverscroll) {
             overscrollEffect.consumePostScroll(
                 consumedByChain,
                 availableForOverscroll,
@@ -430,7 +433,7 @@
 
         val availableVelocity = initialVelocity.singleAxisVelocity()
         val preOverscrollConsumed =
-            if (overscrollEffect != null && overscrollEffect.isEnabled) {
+            if (overscrollEffect != null && shouldDispatchOverscroll) {
                 overscrollEffect.consumePreFling(availableVelocity)
             } else {
                 Velocity.Zero
@@ -446,7 +449,7 @@
                 velocityLeft
             )
         val totalLeft = velocityLeft - consumedPost
-        if (overscrollEffect != null && overscrollEffect.isEnabled) {
+        if (overscrollEffect != null && shouldDispatchOverscroll) {
             overscrollEffect.consumePostFling(totalLeft)
         }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index b1e05f7..59b76f4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -95,24 +95,32 @@
 
     override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
         // If snapping from scroll (short snap) or fling (long snap)
-        withContext(motionScaleDuration) {
+        val (remainingOffset, remainingState) = withContext(motionScaleDuration) {
             if (abs(initialVelocity) <= abs(velocityThreshold)) {
                 shortSnap(initialVelocity)
             } else {
                 longSnap(initialVelocity)
             }
         }
-        return NoVelocity
+        debugLog { "Post Settling Offset=$remainingOffset" }
+        // No remaining offset means we've used everything, no need to propagate velocity. Otherwise
+        // we couldn't use everything (probably because we have hit the min/max bounds of the
+        // containing layout) we should propagate the offset.
+        return if (remainingOffset == 0f) NoVelocity else remainingState.velocity
     }
 
-    private suspend fun ScrollScope.shortSnap(velocity: Float) {
+    private suspend fun ScrollScope.shortSnap(
+        velocity: Float
+    ): AnimationResult<Float, AnimationVector1D> {
         debugLog { "Short Snapping" }
         val closestOffset = findClosestOffset(0f, snapLayoutInfoProvider, density)
         val animationState = AnimationState(NoDistance, velocity)
-        animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
+        return animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
     }
 
-    private suspend fun ScrollScope.longSnap(initialVelocity: Float) {
+    private suspend fun ScrollScope.longSnap(
+        initialVelocity: Float
+    ): AnimationResult<Float, AnimationVector1D> {
         debugLog { "Long Snapping" }
         val initialOffset =
             with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
@@ -123,7 +131,7 @@
 
         debugLog { "Settling Final Bound=$remainingOffset" }
 
-        animateSnap(
+        return animateSnap(
             remainingOffset,
             remainingOffset,
             animationState.copy(value = 0f),
@@ -134,7 +142,7 @@
     private suspend fun ScrollScope.runApproach(
         initialTargetOffset: Float,
         initialVelocity: Float
-    ): ApproachStepResult {
+    ): AnimationResult<Float, AnimationVector1D> {
 
         val animation =
             if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) {
@@ -237,22 +245,28 @@
     animation: ApproachAnimation<Float, AnimationVector1D>,
     snapLayoutInfoProvider: SnapLayoutInfoProvider,
     density: Density
-): ApproachStepResult {
+): AnimationResult<Float, AnimationVector1D> {
 
-    val currentAnimationState =
-        animation.approachAnimation(this, initialTargetOffset, initialVelocity)
+    val (_, currentAnimationState) = animation.approachAnimation(
+        this,
+        initialTargetOffset,
+        initialVelocity
+    )
 
     val remainingOffset =
         findClosestOffset(currentAnimationState.velocity, snapLayoutInfoProvider, density)
 
     // will snap the remainder
-    return ApproachStepResult(remainingOffset, currentAnimationState)
+    return AnimationResult(remainingOffset, currentAnimationState)
 }
 
-private data class ApproachStepResult(
-    val remainingOffset: Float,
-    val currentAnimationState: AnimationState<Float, AnimationVector1D>
-)
+private class AnimationResult<T, V : AnimationVector>(
+    val remainingOffset: T,
+    val currentAnimationState: AnimationState<T, V>
+) {
+    operator fun component1(): T = remainingOffset
+    operator fun component2(): AnimationState<T, V> = currentAnimationState
+}
 
 /**
  * Finds the closest offset to snap to given the Fling Direction.
@@ -309,7 +323,7 @@
     targetOffset: Float,
     animationState: AnimationState<Float, AnimationVector1D>,
     decayAnimationSpec: DecayAnimationSpec<Float>
-): AnimationState<Float, AnimationVector1D> {
+): AnimationResult<Float, AnimationVector1D> {
     var previousValue = 0f
 
     fun AnimationScope<Float, AnimationVector1D>.consumeDelta(delta: Float) {
@@ -332,7 +346,10 @@
             previousValue = value
         }
     }
-    return animationState
+    return AnimationResult(
+        targetOffset - previousValue,
+        animationState
+    )
 }
 
 /**
@@ -344,7 +361,7 @@
     cancelOffset: Float,
     animationState: AnimationState<Float, AnimationVector1D>,
     snapAnimationSpec: AnimationSpec<Float>
-): AnimationState<Float, AnimationVector1D> {
+): AnimationResult<Float, AnimationVector1D> {
     var consumedUpToNow = 0f
     val initialVelocity = animationState.velocity
     animationState.animateTo(
@@ -359,12 +376,15 @@
         if (abs(delta - consumed) > 0.5f || realValue != value) {
             cancelAnimation()
         }
-        consumedUpToNow += delta
+        consumedUpToNow += consumed
     }
 
     // Always course correct velocity so they don't become too large.
     val finalVelocity = animationState.velocity.coerceToTarget(initialVelocity)
-    return animationState.copy(velocity = finalVelocity)
+    return AnimationResult(
+        targetOffset - consumedUpToNow,
+        animationState.copy(velocity = finalVelocity)
+    )
 }
 
 private fun Float.coerceToTarget(target: Float): Float {
@@ -380,7 +400,7 @@
         scope: ScrollScope,
         offset: T,
         velocity: T
-    ): AnimationState<T, V>
+    ): AnimationResult<T, V>
 }
 
 @OptIn(ExperimentalFoundationApi::class)
@@ -393,7 +413,7 @@
         scope: ScrollScope,
         offset: Float,
         velocity: Float
-    ): AnimationState<Float, AnimationVector1D> {
+    ): AnimationResult<Float, AnimationVector1D> {
         val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
         val targetOffset =
             (abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign(
@@ -417,7 +437,7 @@
         scope: ScrollScope,
         offset: Float,
         velocity: Float
-    ): AnimationState<Float, AnimationVector1D> {
+    ): AnimationResult<Float, AnimationVector1D> {
         val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
         return with(scope) {
             animateDecay(offset, animationState, decayAnimationSpec)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index a28a5e4..ec2d8b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation.lazy
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.OverscrollEffect
 import androidx.compose.foundation.checkScrollableContainerConstraints
 import androidx.compose.foundation.clipScrollableContainer
 import androidx.compose.foundation.gestures.FlingBehavior
@@ -91,7 +90,6 @@
         itemProvider,
         state,
         beyondBoundsInfo,
-        overscrollEffect,
         contentPadding,
         reverseLayout,
         isVertical,
@@ -160,8 +158,6 @@
     state: LazyListState,
     /** Keeps track of the number of items we measure and place that are beyond visible bounds. */
     beyondBoundsInfo: LazyListBeyondBoundsInfo,
-    /** The overscroll controller. */
-    overscrollEffect: OverscrollEffect,
     /** The inner padding to be added for the whole content(nor for each individual item) */
     contentPadding: PaddingValues,
     /** reverse the direction of scrolling and layout */
@@ -183,7 +179,6 @@
 ) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
     state,
     beyondBoundsInfo,
-    overscrollEffect,
     contentPadding,
     reverseLayout,
     isVertical,
@@ -332,19 +327,6 @@
             }
         ).also {
             state.applyMeasureResult(it)
-            refreshOverscrollInfo(overscrollEffect, it)
         }
     }
 }
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun refreshOverscrollInfo(
-    overscrollEffect: OverscrollEffect,
-    result: LazyListMeasureResult
-) {
-    val canScrollForward = result.canScrollForward
-    val canScrollBackward = (result.firstVisibleItem?.index ?: 0) != 0 ||
-        result.firstVisibleItemScrollOffset != 0
-
-    overscrollEffect.isEnabled = canScrollForward || canScrollBackward
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 79c1683..83ec3f9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation.lazy.grid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.OverscrollEffect
 import androidx.compose.foundation.checkScrollableContainerConstraints
 import androidx.compose.foundation.clipScrollableContainer
 import androidx.compose.foundation.gestures.FlingBehavior
@@ -89,7 +88,6 @@
     val measurePolicy = rememberLazyGridMeasurePolicy(
         itemProvider,
         state,
-        overscrollEffect,
         slotSizesSums,
         contentPadding,
         reverseLayout,
@@ -154,8 +152,6 @@
     itemProvider: LazyGridItemProvider,
     /** The state of the list. */
     state: LazyGridState,
-    /** The overscroll controller. */
-    overscrollEffect: OverscrollEffect,
     /** Prefix sums of cross axis sizes of slots of the grid. */
     slotSizesSums: Density.(Constraints) -> List<Int>,
     /** The inner padding to be added for the whole content(nor for each individual item) */
@@ -172,7 +168,6 @@
     placementAnimator: LazyGridItemPlacementAnimator
 ) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
     state,
-    overscrollEffect,
     slotSizesSums,
     contentPadding,
     reverseLayout,
@@ -360,19 +355,6 @@
             }
         ).also {
             state.applyMeasureResult(it)
-            refreshOverscrollInfo(overscrollEffect, it)
         }
     }
 }
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun refreshOverscrollInfo(
-    overscrollEffect: OverscrollEffect,
-    result: LazyGridMeasureResult
-) {
-    val canScrollForward = result.canScrollForward
-    val canScrollBackward = (result.firstVisibleLine?.items?.firstOrNull() ?: 0) != 0 ||
-        result.firstVisibleLineScrollOffset != 0
-
-    overscrollEffect.isEnabled = canScrollForward || canScrollBackward
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
index 7c11f76..2630e7d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
@@ -72,8 +72,7 @@
         orientation,
         verticalArrangement,
         horizontalArrangement,
-        slotSizesSums,
-        overscrollEffect
+        slotSizesSums
     )
     val semanticState = rememberLazyStaggeredGridSemanticState(state, itemProvider, reverseLayout)
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index e0f601d..138d229 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -177,13 +177,14 @@
                 measureResult = layout(constraints.minWidth, constraints.minHeight) {},
                 canScrollForward = false,
                 canScrollBackward = false,
+                isVertical = isVertical,
                 visibleItemsInfo = emptyList(),
                 totalItemsCount = itemCount,
                 viewportSize = IntSize(constraints.minWidth, constraints.minHeight),
                 viewportStartOffset = -beforeContentPadding,
                 viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
                 beforeContentPadding = beforeContentPadding,
-                afterContentPadding = afterContentPadding
+                afterContentPadding = afterContentPadding,
             )
         }
 
@@ -560,6 +561,7 @@
             },
             canScrollForward = canScrollForward,
             canScrollBackward = canScrollBackward,
+            isVertical = isVertical,
             visibleItemsInfo = positionedItems.asMutableList(),
             totalItemsCount = itemCount,
             viewportSize = IntSize(layoutWidth, layoutHeight),
@@ -713,7 +715,11 @@
             lane = lane,
             index = index,
             key = key,
-            size = IntSize(sizeWithSpacings, crossAxisSize),
+            size = if (isVertical) {
+                IntSize(crossAxisSize, sizeWithSpacings)
+            } else {
+                IntSize(sizeWithSpacings, crossAxisSize)
+            },
             placeables = placeables,
             contentOffset = contentOffset,
             isVertical = isVertical
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
index 4b9097f..a645632 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation.lazy.staggeredgrid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.OverscrollEffect
 import androidx.compose.foundation.checkScrollableContainerConstraints
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
@@ -46,8 +45,7 @@
     orientation: Orientation,
     verticalArrangement: Arrangement.Vertical,
     horizontalArrangement: Arrangement.Horizontal,
-    slotSizesSums: Density.(Constraints) -> IntArray,
-    overscrollEffect: OverscrollEffect
+    slotSizesSums: Density.(Constraints) -> IntArray
 ): LazyLayoutMeasureScope.(Constraints) -> LazyStaggeredGridMeasureResult = remember(
     state,
     itemProvider,
@@ -56,8 +54,7 @@
     orientation,
     verticalArrangement,
     horizontalArrangement,
-    slotSizesSums,
-    overscrollEffect,
+    slotSizesSums
 ) {
     { constraints ->
         checkScrollableContainerConstraints(
@@ -125,7 +122,6 @@
             afterContentPadding = afterContentPadding,
         ).also {
             state.applyMeasureResult(it)
-            overscrollEffect.isEnabled = it.canScrollForward || it.canScrollBackward
         }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
index e63287a..352a762 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.lazy.staggeredgrid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -63,6 +64,11 @@
 @ExperimentalFoundationApi
 sealed interface LazyStaggeredGridLayoutInfo {
     /**
+     * Orientation of the staggered grid.
+     */
+    val orientation: Orientation
+
+    /**
      * The list of [LazyStaggeredGridItemInfo] per each visible item ordered by index.
      */
     val visibleItemsInfo: List<LazyStaggeredGridItemInfo>
@@ -129,6 +135,7 @@
     val measureResult: MeasureResult,
     val canScrollForward: Boolean,
     val canScrollBackward: Boolean,
+    val isVertical: Boolean,
     override val totalItemsCount: Int,
     override val visibleItemsInfo: List<LazyStaggeredGridItemInfo>,
     override val viewportSize: IntSize,
@@ -136,7 +143,10 @@
     override val viewportEndOffset: Int,
     override val beforeContentPadding: Int,
     override val afterContentPadding: Int
-) : LazyStaggeredGridLayoutInfo, MeasureResult by measureResult
+) : LazyStaggeredGridLayoutInfo, MeasureResult by measureResult {
+    override val orientation: Orientation =
+        if (isVertical) Orientation.Vertical else Orientation.Horizontal
+}
 
 @OptIn(ExperimentalFoundationApi::class)
 internal object EmptyLazyStaggeredGridLayoutInfo : LazyStaggeredGridLayoutInfo {
@@ -147,4 +157,5 @@
     override val viewportEndOffset: Int = 0
     override val beforeContentPadding: Int = 0
     override val afterContentPadding: Int = 0
+    override val orientation: Orientation = Orientation.Vertical
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
index b1a3529..5d5fe33 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
@@ -23,6 +23,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.util.fastFirstOrNull
 
 @ExperimentalFoundationApi
 internal class LazyStaggeredGridScrollPosition(
@@ -42,7 +43,13 @@
      * Updates the current scroll position based on the results of the last measurement.
      */
     fun updateFromMeasureResult(measureResult: LazyStaggeredGridMeasureResult) {
-        lastKnownFirstItemKey = measureResult.visibleItemsInfo.firstOrNull()?.key
+        val firstVisibleIndex = measureResult.firstVisibleItemIndices
+            .minBy { if (it == -1) Int.MAX_VALUE else it }
+            .let { if (it == Int.MAX_VALUE) 0 else it }
+
+        lastKnownFirstItemKey = measureResult.visibleItemsInfo
+            .fastFirstOrNull { it.index == firstVisibleIndex }
+            ?.key
         // we ignore the index and offset from measureResult until we get at least one
         // measurement with real items. otherwise the initial index and scroll passed to the
         // state would be lost and overridden with zeros.
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
index 3c53ec5..75bbcfc 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
@@ -48,7 +48,6 @@
 
     override suspend fun consumePostFling(velocity: Velocity) {}
 
-    override var isEnabled = false
     override val isInProgress = false
     override val effectModifier = Modifier
 }
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
index e883852..8081212 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
@@ -16,24 +16,20 @@
 
 package androidx.compose.integration.demos
 
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.integration.demos.common.Demo
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Close
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
 import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBarScrollBehavior
 import androidx.compose.runtime.Composable
@@ -44,7 +40,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.withStyle
@@ -58,9 +53,8 @@
     val filteredDemos = launchableDemos
         .filter { it.title.contains(filterText, ignoreCase = true) }
         .sortedBy { it.title }
-    // TODO: migrate to LazyColumn after b/175671850
-    Column(Modifier.verticalScroll(rememberScrollState())) {
-        filteredDemos.forEach { demo ->
+    LazyColumn {
+        items(filteredDemos) { demo ->
             FilteredDemoListItem(
                 demo,
                 filterText = filterText,
@@ -99,23 +93,20 @@
 }
 
 /**
- * [BasicTextField] that edits the current [filterText], providing [onFilter] when edited.
+ * [TextField] that edits the current [filterText], providing [onFilter] when edited.
  */
 @Composable
-@OptIn(ExperimentalFoundationApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
 private fun FilterField(
     filterText: String,
     onFilter: (String) -> Unit,
     modifier: Modifier = Modifier
 ) {
     val focusRequester = remember { FocusRequester() }
-    // TODO: replace with Material text field when available
-    BasicTextField(
+    TextField(
         modifier = modifier.focusRequester(focusRequester),
         value = filterText,
-        onValueChange = onFilter,
-        textStyle = LocalTextStyle.current,
-        cursorBrush = SolidColor(LocalContentColor.current)
+        onValueChange = onFilter
     )
     DisposableEffect(focusRequester) {
         focusRequester.requestFocus()
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 1b36239..e77373f 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -112,11 +112,3 @@
 android {
     namespace "androidx.compose.material.ripple"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index ec09563..c553c4b 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -383,8 +383,8 @@
     property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
-    property @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.material.ExperimentalMaterialApi public final Float? offset;
-    property @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
+    property @androidx.compose.material.ExperimentalMaterialApi public final Float? offset;
+    property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
   }
 
@@ -416,7 +416,7 @@
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterialApi {
   }
 
-  @androidx.compose.material.ExperimentalMaterialApi public interface ExposedDropdownMenuBoxScope {
+  @androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface ExposedDropdownMenuBoxScope {
     method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method public androidx.compose.ui.Modifier exposedDropdownSize(androidx.compose.ui.Modifier, optional boolean matchTextFieldWidth);
   }
@@ -844,7 +844,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError);
   }
 
-  @androidx.compose.material.ExperimentalMaterialApi public interface TextFieldColorsWithIcons extends androidx.compose.material.TextFieldColors {
+  @androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface TextFieldColorsWithIcons extends androidx.compose.material.TextFieldColors {
     method @androidx.compose.runtime.Composable public default androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
     method @androidx.compose.runtime.Composable public default androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
   }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
new file mode 100644
index 0000000..1cfa222
--- /dev/null
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.material.pullrefresh
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.testutils.assertPixelColor
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterialApi::class)
+class PullRefreshIndicatorTransformTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    // Convert from floats to DP to avoid rounding issues later
+
+    private val IndicatorSize get() = with(rule.density) { 200f.toDp() }
+    // Make the box large enough so that when the indicator is offset when not shown, it is still
+    // offset to within the bounds of the containing box
+    private val ContainingBoxSize = with(rule.density) { 800f.toDp() }
+    private val BoxTag = "Box"
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun indicatorClippedWhenNotDisplayed() {
+        rule.setContent {
+            val state = rememberPullRefreshState(refreshing = false, {})
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.White)
+                    .wrapContentSize(Alignment.Center)
+                    // Set a larger size so that when offset the indicator will still appear
+                    .size(ContainingBoxSize)
+                    .testTag(BoxTag),
+                contentAlignment = Alignment.Center
+            ) {
+                Box(Modifier
+                    .pullRefreshIndicatorTransform(state)
+                    .size(IndicatorSize)
+                    .background(Color.Black)
+                )
+            }
+        }
+        // The indicator should be fully clipped and invisible
+        rule.onNodeWithTag(BoxTag).captureToImage().assertPixels { Color.White }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun indicatorPartiallyClippedWhenPartiallyDisplayed() {
+        lateinit var state: PullRefreshState
+        rule.setContent {
+            // Pull down by 100 pixels (the actual position delta is half of this because the state
+            // applies a multiplier)
+            state = rememberPullRefreshState(refreshing = false, onRefresh = {}).apply {
+                onPull(100f)
+            }
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.White)
+                    .wrapContentSize(Alignment.Center)
+                    // Set a larger size so that when offset the indicator will still appear
+                    .size(ContainingBoxSize)
+                    .testTag(BoxTag),
+                contentAlignment = Alignment.Center
+            ) {
+                Box(Modifier
+                    .pullRefreshIndicatorTransform(state)
+                    .size(IndicatorSize)
+                    .background(Color.Black)
+                )
+            }
+        }
+        // The indicator should be partially clipped
+        rule.onNodeWithTag(BoxTag).captureToImage().run {
+            val indicatorStart = with(rule.density) { width / 2 - IndicatorSize.toPx() / 2 }.toInt()
+            val indicatorXRange = with(rule.density) {
+                indicatorStart until (indicatorStart + IndicatorSize.toPx()).toInt()
+            }
+
+            val indicatorTop = with(rule.density) { height / 2 - IndicatorSize.toPx() / 2 }.toInt()
+            val indicatorYRange = indicatorTop until indicatorTop + state.position.toInt()
+
+            val pixel = toPixelMap()
+            for (x in 0 until width) {
+                for (y in 0 until height) {
+                    val expectedColor = if (x in indicatorXRange && y in indicatorYRange) {
+                        Color.Black
+                    } else {
+                        Color.White
+                    }
+                    pixel.assertPixelColor(expectedColor, x, y)
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun indicatorNotClippedWhenFullyDisplayed() {
+        lateinit var state: PullRefreshState
+        rule.setContent {
+            // Set refreshing and set the refreshing offset to match the indicator size -
+            // this means that the indicator will start to show at an offset of 0 from its normal
+            // layout position, since by default it is negatively offset by its height
+            state = rememberPullRefreshState(
+                refreshing = true,
+                onRefresh = {},
+                refreshingOffset = IndicatorSize
+            )
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.White)
+                    .wrapContentSize(Alignment.Center)
+                    // Set a larger size so that when offset the indicator will still appear
+                    .size(ContainingBoxSize)
+                    .testTag(BoxTag),
+                contentAlignment = Alignment.Center
+            ) {
+                Box(Modifier
+                    .pullRefreshIndicatorTransform(state)
+                    .size(IndicatorSize)
+                    .background(Color.Black)
+                )
+            }
+        }
+        // The indicator should be fully visible
+        rule.onNodeWithTag(BoxTag).captureToImage().run {
+            val indicatorStart = with(rule.density) { width / 2 - IndicatorSize.toPx() / 2 }.toInt()
+            val indicatorXRange = with(rule.density) {
+                indicatorStart until (indicatorStart + IndicatorSize.toPx()).toInt()
+            }
+
+            val indicatorTop = with(rule.density) { height / 2 - IndicatorSize.toPx() / 2 }.toInt()
+            val indicatorYRange = with(rule.density) {
+                indicatorTop until (indicatorTop + IndicatorSize.toPx()).toInt()
+            }
+
+            val pixel = toPixelMap()
+            for (x in 0 until width) {
+                for (y in 0 until height) {
+                    val expectedColor = if (x in indicatorXRange && y in indicatorYRange) {
+                        Color.Black
+                    } else {
+                        Color.White
+                    }
+                    pixel.assertPixelColor(expectedColor, x, y)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
index 63c1027..fe4190a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
@@ -79,13 +79,14 @@
 
     @Test
     fun pullLessThanOrEqualToThreshold_doesNot_triggerRefresh() {
+        lateinit var state: PullRefreshState
         var refreshCount = 0
         var touchSlop = 0f
         val threshold = 400f
 
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            val state = rememberPullRefreshState(
+            state = rememberPullRefreshState(
                 refreshing = false,
                 onRefresh = { refreshCount++ },
                 refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
@@ -110,7 +111,12 @@
         // Equal to threshold
         pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop) }
 
-        rule.runOnIdle { assertThat(refreshCount).isEqualTo(0) }
+        rule.runOnIdle {
+            assertThat(refreshCount).isEqualTo(0)
+            // Since onRefresh was not called, we should reset the position back to 0
+            assertThat(state.progress).isEqualTo(0f)
+            assertThat(state.position).isEqualTo(0f)
+        }
     }
 
     @Test
@@ -309,6 +315,43 @@
         }
     }
 
+    @Test
+    fun pullBeyondThreshold_refreshingNotChangedToTrue_animatePositionBackToZero() {
+        lateinit var state: PullRefreshState
+        var refreshCount = 0
+        var touchSlop = 0f
+        val threshold = 400f
+
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { refreshCount++ },
+                refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
+            )
+
+            Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+                LazyColumn {
+                    items(100) {
+                        Text("item $it")
+                    }
+                }
+            }
+        }
+
+        // Account for PullModifier - pull down twice the threshold value.
+        pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop + 1f) }
+
+        rule.runOnIdle {
+            // onRefresh should be called
+            assertThat(refreshCount).isEqualTo(1)
+            // Since onRefresh did not change refreshing, we should have reset the position back to
+            // 0
+            assertThat(state.progress).isEqualTo(0f)
+            assertThat(state.position).isEqualTo(0f)
+        }
+    }
+
     /**
      * Taken from the private function of the same name in [PullRefreshState].
      */
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
index cfaa288..4a46e56 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
@@ -190,6 +190,7 @@
     }
 }
 
+@JvmDefaultWithCompatibility
 /**
  * Scope for [ExposedDropdownMenuBox].
  */
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index 9ee4ffb..21d797a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -63,7 +63,7 @@
  *
  * @param state The associated [SwipeableV2State].
  * @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
  * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
  * swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
  * @param interactionSource Optional [MutableInteractionSource] that will passed on to
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
index 975361d..dc0a4da 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
@@ -141,6 +141,7 @@
     fun cursorColor(isError: Boolean): State<Color>
 }
 
+@JvmDefaultWithCompatibility
 /**
  * Temporary experimental interface, to expose interactionSource to
  * leadingIconColor and trailingIconColor.
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
index 7ca9609..ad97176 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
@@ -18,15 +18,12 @@
 
 import androidx.compose.animation.core.LinearOutSlowInEasing
 import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.drawscope.clipRect
 import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.platform.inspectable
 
 /**
  * A modifier for translating the position and scaling the size of a pull-to-refresh indicator
@@ -42,17 +39,30 @@
 fun Modifier.pullRefreshIndicatorTransform(
     state: PullRefreshState,
     scale: Boolean = false,
-) = composed(inspectorInfo = debugInspectorInfo {
+) = inspectable(inspectorInfo = debugInspectorInfo {
     name = "pullRefreshIndicatorTransform"
     properties["state"] = state
     properties["scale"] = scale
 }) {
-    var height by remember { mutableStateOf(0) }
-
     Modifier
-        .onSizeChanged { height = it.height }
+        // Essentially we only want to clip the at the top, so the indicator will not appear when
+        // the position is 0. It is preferable to clip the indicator as opposed to the layout that
+        // contains the indicator, as this would also end up clipping shadows drawn by items in a
+        // list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
+        // for the other dimensions to allow for more room for elevation / arbitrary indicators - we
+        // only ever really want to clip at the top edge.
+        .drawWithContent {
+            clipRect(
+                top = 0f,
+                left = -Float.MAX_VALUE,
+                right = Float.MAX_VALUE,
+                bottom = Float.MAX_VALUE
+            ) {
+                this@drawWithContent.drawContent()
+            }
+        }
         .graphicsLayer {
-            translationY = state.position - height
+            translationY = state.position - size.height
 
             if (scale && !state.refreshing) {
                 val scaleFraction = LinearOutSlowInEasing
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
index e673400..b425a38 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material.pullrefresh
 
 import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatorMutex
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.SideEffect
@@ -136,9 +137,8 @@
         if (!this._refreshing) {
             if (adjustedDistancePulled > threshold) {
                 onRefreshState.value()
-            } else {
-                animateIndicatorTo(0f)
             }
+            animateIndicatorTo(0f)
         }
         distancePulled = 0f
     }
@@ -151,9 +151,16 @@
         }
     }
 
+    // Make sure to cancel any existing animations when we launch a new one. We use this instead of
+    // Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
+    // overhead of running through the animation pipeline instead of directly mutating the state.
+    private val mutatorMutex = MutatorMutex()
+
     private fun animateIndicatorTo(offset: Float) = animationScope.launch {
-        animate(initialValue = _position, targetValue = offset) { value, _ ->
-            _position = value
+        mutatorMutex.mutate {
+            animate(initialValue = _position, targetValue = offset) { value, _ ->
+                _position = value
+            }
         }
     }
 
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index 5f1fc46..ad2abb0 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -113,11 +113,3 @@
 android {
     namespace "androidx.compose.material3.windowsizeclass"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 8230350..d92a1b2 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -326,8 +326,8 @@
     property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
-    property @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.runtime.State<java.lang.Float> offset;
-    property @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.material3.DrawerValue targetValue;
+    property @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.runtime.State<java.lang.Float> offset;
+    property @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.material3.DrawerValue targetValue;
     field public static final androidx.compose.material3.DrawerState.Companion Companion;
   }
 
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 7a79167..fb360a4 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -160,11 +160,3 @@
             project.rootDir.absolutePath + "/../../golden/compose/material3/material3"
     namespace "androidx.compose.material3"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/build.gradle b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
index 189003f..28da77e 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
@@ -46,11 +46,3 @@
 android {
     namespace "androidx.compose.material3.catalog.library"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index 4849ec9..021de9a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -35,8 +35,15 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
@@ -46,19 +53,19 @@
 import kotlinx.coroutines.launch
 
 /**
- * Enable swipe gestures between a set of predefined states.
+ * Enable swipe gestures between a set of predefined values.
  *
  * When a swipe is detected, the offset of the [SwipeableV2State] will be updated with the swipe
  * delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
  * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableV2State] will also be updated to the state corresponding to
+ * reached, the value of the [SwipeableV2State] will also be updated to the value corresponding to
  * the new anchor.
  *
  * Swiping is constrained between the minimum and maximum anchors.
  *
  * @param state The associated [SwipeableV2State].
  * @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
  * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
  * swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
  * @param interactionSource Optional [MutableInteractionSource] that will passed on to
@@ -86,36 +93,46 @@
  * the state with them.
  *
  * @param state The associated [SwipeableV2State]
- * @param possibleStates All possible states the [SwipeableV2State] could be in.
+ * @param possibleValues All possible values the [SwipeableV2State] could be in.
  * @param anchorsChanged A callback to be invoked when the anchors have changed, `null` by default.
  * Components with custom reconciliation logic should implement this callback, i.e. to re-target an
  * in-progress animation.
  * @param calculateAnchor This method will be invoked to calculate the position of all
- * [possibleStates], given this node's layout size. Return the anchor's offset from the initial
- * anchor, or `null` to indicate that a state does not exist.
+ * [possibleValues], given this node's layout size. Return the anchor's offset from the initial
+ * anchor, or `null` to indicate that a value does not have an anchor.
  */
 @ExperimentalMaterial3Api
 internal fun <T> Modifier.swipeAnchors(
     state: SwipeableV2State<T>,
-    possibleStates: Set<T>,
+    possibleValues: Set<T>,
     anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
-    calculateAnchor: (state: T, layoutSize: IntSize) -> Float?,
-) = onSizeChanged { layoutSize ->
-    val previousAnchors = state.anchors
-    val newAnchors = mutableMapOf<T, Float>()
-    possibleStates.forEach {
-        val anchorValue = calculateAnchor(it, layoutSize)
-        if (anchorValue != null) {
-            newAnchors[it] = anchorValue
+    calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
+) = this.then(SwipeAnchorsModifier(
+    onDensityChanged = { state.density = it },
+    onSizeChanged = { layoutSize ->
+        val previousAnchors = state.anchors
+        val newAnchors = mutableMapOf<T, Float>()
+        possibleValues.forEach {
+            val anchorValue = calculateAnchor(it, layoutSize)
+            if (anchorValue != null) {
+                newAnchors[it] = anchorValue
+            }
         }
+        if (previousAnchors != newAnchors) {
+            state.updateAnchors(newAnchors)
+            if (previousAnchors.isNotEmpty()) {
+                anchorsChanged?.invoke(previousAnchors, newAnchors)
+            }
+        }
+    },
+    inspectorInfo = debugInspectorInfo {
+        name = "swipeAnchors"
+        properties["state"] = state
+        properties["possibleValues"] = possibleValues
+        properties["anchorsChanged"] = anchorsChanged
+        properties["calculateAnchor"] = calculateAnchor
     }
-    if (previousAnchors == newAnchors) return@onSizeChanged
-    state.updateAnchors(newAnchors)
-
-    if (previousAnchors.isNotEmpty()) {
-        anchorsChanged?.invoke(previousAnchors, newAnchors)
-    }
-}
+))
 
 /**
  * State of the [swipeableV2] modifier.
@@ -124,10 +141,9 @@
  * to change the state either immediately or by starting an animation. To create and remember a
  * [SwipeableV2State] use [rememberSwipeableV2State].
  *
- * @param initialState The initial value of the state.
- * @param density The density used to convert thresholds from px to dp.
+ * @param initialValue The initial value of the state.
  * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
  * @param positionalThreshold The positional threshold to be used when calculating the target state
  * while a swipe is in progress and when settling after the swipe ends. This is the distance from
  * the start of a transition. It will be, depending on the direction of the interaction, added or
@@ -140,31 +156,30 @@
 @Stable
 @ExperimentalMaterial3Api
 internal class SwipeableV2State<T>(
-    initialState: T,
-    internal val density: Density,
+    initialValue: T,
     internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
-    internal val confirmStateChange: (newValue: T) -> Boolean = { true },
+    internal val confirmValueChange: (newValue: T) -> Boolean = { true },
     internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
         SwipeableV2Defaults.PositionalThreshold,
     internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
 ) {
 
     /**
-     * The current state of the [SwipeableV2State].
+     * The current value of the [SwipeableV2State].
      */
-    var currentState: T by mutableStateOf(initialState)
+    var currentValue: T by mutableStateOf(initialValue)
         private set
 
     /**
-     * The target state. This is the closest state to the current offset (taking into account
+     * The target value. This is the closest value to the current offset (taking into account
      * positional thresholds). If no interactions like animations or drags are in progress, this
-     * will be the current state.
+     * will be the current value.
      */
-    val targetState: T by derivedStateOf {
+    val targetValue: T by derivedStateOf {
         val currentOffset = offset
         if (currentOffset != null) {
-            computeTarget(currentOffset, currentState, velocity = 0f)
-        } else currentState
+            computeTarget(currentOffset, currentValue, velocity = 0f)
+        } else currentValue
     }
 
     /**
@@ -180,6 +195,7 @@
      *
      * To guarantee stricter semantics, consider using [requireOffset].
      */
+    @get:Suppress("AutoBoxing")
     val offset: Float? by derivedStateOf {
         dragPosition?.coerceIn(minBound, maxBound)
     }
@@ -201,12 +217,13 @@
         private set
 
     /**
-     * The fraction of the progress going from currentState to targetState, within [0f..1f] bounds.
+     * The fraction of the progress going from [currentValue] to [targetValue], within [0f..1f]
+     * bounds.
      */
     /*@FloatRange(from = 0f, to = 1f)*/
     val progress: Float by derivedStateOf {
-        val a = anchors[currentState] ?: 0f
-        val b = anchors[targetState] ?: 0f
+        val a = anchors[currentValue] ?: 0f
+        val b = anchors[targetValue] ?: 0f
         val distance = abs(b - a)
         if (distance > 1e-6f) {
             val progress = (this.requireOffset() - a) / (b - a)
@@ -229,57 +246,57 @@
     private val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
     private val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
 
-    private val velocityThresholdPx = with(density) { velocityThreshold.toPx() }
-
     internal val draggableState = DraggableState {
         dragPosition = (dragPosition ?: 0f) + it
     }
 
     internal var anchors by mutableStateOf(emptyMap<T, Float>())
 
+    internal var density: Density? = null
+
     internal fun updateAnchors(newAnchors: Map<T, Float>) {
         val previousAnchorsEmpty = anchors.isEmpty()
         anchors = newAnchors
         if (previousAnchorsEmpty) {
-            dragPosition = anchors.requireAnchor(this.currentState)
+            dragPosition = anchors.requireAnchor(this.currentValue)
         }
     }
 
     /**
-     * Whether the [state] has an anchor associated with it.
+     * Whether the [value] has an anchor associated with it.
      */
-    fun hasAnchorForState(state: T): Boolean = anchors.containsKey(state)
+    fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
 
     /**
-     * Snap to a [targetState] without any animation.
+     * Snap to a [targetValue] without any animation.
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
      *
-     * @param targetState The target state of the animation
+     * @param targetValue The target value of the animation
      */
-    suspend fun snapTo(targetState: T) {
-        val targetOffset = anchors.requireAnchor(targetState)
+    suspend fun snapTo(targetValue: T) {
+        val targetOffset = anchors.requireAnchor(targetValue)
         draggableState.drag {
             dragBy(targetOffset - requireOffset())
         }
-        this.currentState = targetState
+        this.currentValue = targetValue
     }
 
     /**
-     * Animate to a [targetState].
+     * Animate to a [targetValue].
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
      *
-     * @param targetState The target state of the animation
+     * @param targetValue The target value of the animation
      * @param velocity The velocity the animation should start with, [lastVelocity] by default
      */
     suspend fun animateTo(
-        targetState: T,
+        targetValue: T,
         velocity: Float = lastVelocity,
     ) {
-        val targetOffset = anchors.requireAnchor(targetState)
+        val targetOffset = anchors.requireAnchor(targetValue)
         try {
             draggableState.drag {
                 isAnimationRunning = true
@@ -304,7 +321,7 @@
                 .entries
                 .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
                 ?.key
-            this.currentState = endState ?: currentState
+            this.currentValue = endState ?: currentValue
         }
     }
 
@@ -312,17 +329,17 @@
      * Find the closest anchor taking into account the velocity and settle at it with an animation.
      */
     suspend fun settle(velocity: Float) {
-        val previousState = this.currentState
-        val targetState = computeTarget(
+        val previousValue = this.currentValue
+        val targetValue = computeTarget(
             offset = requireOffset(),
-            currentState = previousState,
+            currentValue = previousValue,
             velocity = velocity
         )
-        if (confirmStateChange(targetState)) {
-            animateTo(targetState, velocity)
+        if (confirmValueChange(targetValue)) {
+            animateTo(targetValue, velocity)
         } else {
             // If the user vetoed the state change, rollback to the previous state.
-            animateTo(previousState, velocity)
+            animateTo(previousValue, velocity)
         }
     }
 
@@ -344,42 +361,49 @@
 
     private fun computeTarget(
         offset: Float,
-        currentState: T,
+        currentValue: T,
         velocity: Float
     ): T {
         val currentAnchors = anchors
-        val currentAnchor = currentAnchors.requireAnchor(currentState)
+        val currentAnchor = currentAnchors.requireAnchor(currentValue)
+        val currentDensity = requireDensity()
+        val velocityThresholdPx = with(currentDensity) { velocityThreshold.toPx() }
         return if (currentAnchor <= offset) {
             // Swiping from lower to upper (positive).
             if (velocity >= velocityThresholdPx) {
-                currentAnchors.closestState(offset, true)
+                currentAnchors.closestAnchor(offset, true)
             } else {
-                val upper = currentAnchors.closestState(offset, true)
+                val upper = currentAnchors.closestAnchor(offset, true)
                 val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
-                val relativeThreshold = abs(positionalThreshold(density, distance))
+                val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
                 val absoluteThreshold = abs(currentAnchor + relativeThreshold)
-                if (offset < absoluteThreshold) currentState else upper
+                if (offset < absoluteThreshold) currentValue else upper
             }
         } else {
             // Swiping from upper to lower (negative).
             if (velocity <= -velocityThresholdPx) {
-                currentAnchors.closestState(offset, false)
+                currentAnchors.closestAnchor(offset, false)
             } else {
-                val lower = currentAnchors.closestState(offset, false)
+                val lower = currentAnchors.closestAnchor(offset, false)
                 val distance = abs(currentAnchor - currentAnchors.getValue(lower))
-                val relativeThreshold = abs(positionalThreshold(density, distance))
+                val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
                 val absoluteThreshold = abs(currentAnchor - relativeThreshold)
                 if (offset < 0) {
                     // For negative offsets, larger absolute thresholds are closer to lower anchors
                     // than smaller ones.
-                    if (abs(offset) < absoluteThreshold) currentState else lower
+                    if (abs(offset) < absoluteThreshold) currentValue else lower
                 } else {
-                    if (offset > absoluteThreshold) currentState else lower
+                    if (offset > absoluteThreshold) currentValue else lower
                 }
             }
         }
     }
 
+    private fun requireDensity() = requireNotNull(density) {
+        "SwipeableState did not have a density attached. Are you using Modifier.swipeable with " +
+            "this=$this SwipeableState?"
+    }
+
     companion object {
         /**
          * The default [Saver] implementation for [SwipeableV2State].
@@ -387,20 +411,18 @@
         @ExperimentalMaterial3Api
         fun <T : Any> Saver(
             animationSpec: AnimationSpec<Float>,
-            confirmStateChange: (T) -> Boolean,
+            confirmValueChange: (T) -> Boolean,
             positionalThreshold: Density.(distance: Float) -> Float,
-            velocityThreshold: Dp,
-            density: Density
+            velocityThreshold: Dp
         ) = Saver<SwipeableV2State<T>, T>(
-            save = { it.currentState },
+            save = { it.currentValue },
             restore = {
                 SwipeableV2State(
-                    initialState = it,
+                    initialValue = it,
                     animationSpec = animationSpec,
-                    confirmStateChange = confirmStateChange,
+                    confirmValueChange = confirmValueChange,
                     positionalThreshold = positionalThreshold,
-                    velocityThreshold = velocityThreshold,
-                    density = density
+                    velocityThreshold = velocityThreshold
                 )
             }
         )
@@ -410,35 +432,32 @@
 /**
  * Create and remember a [SwipeableV2State].
  *
- * @param initialState The initial state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param initialValue The initial value.
+ * @param animationSpec The default animation that will be used to animate to a new value.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending value change.
  */
 @Composable
 @ExperimentalMaterial3Api
 internal fun <T : Any> rememberSwipeableV2State(
-    initialState: T,
+    initialValue: T,
     animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
-    confirmStateChange: (newValue: T) -> Boolean = { true }
+    confirmValueChange: (newValue: T) -> Boolean = { true }
 ): SwipeableV2State<T> {
-    val density = LocalDensity.current
     return rememberSaveable(
-        initialState, animationSpec, confirmStateChange, density,
+        initialValue, animationSpec, confirmValueChange,
         saver = SwipeableV2State.Saver(
             animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange,
+            confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
-            velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
-            density = density
+            velocityThreshold = SwipeableV2Defaults.VelocityThreshold
         ),
     ) {
         SwipeableV2State(
-            initialState = initialState,
+            initialValue = initialValue,
             animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange,
+            confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
-            velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
-            density = density
+            velocityThreshold = SwipeableV2Defaults.VelocityThreshold
         )
     }
 }
@@ -492,11 +511,42 @@
         fixedPositionalThreshold(56.dp)
 }
 
-private fun <T> Map<T, Float>.closestState(
+@Stable
+private class SwipeAnchorsModifier(
+    private val onDensityChanged: (density: Density) -> Unit,
+    private val onSizeChanged: (layoutSize: IntSize) -> Unit,
+    inspectorInfo: InspectorInfo.() -> Unit,
+) : LayoutModifier, OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
+
+    private var lastDensity: Float = -1f
+    private var lastFontScale: Float = -1f
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        if (density != lastDensity || fontScale != lastFontScale) {
+            onDensityChanged(Density(density, fontScale))
+            lastDensity = density
+            lastFontScale = fontScale
+        }
+        val placeable = measurable.measure(constraints)
+        return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+    }
+
+    override fun onRemeasured(size: IntSize) {
+        onSizeChanged(size)
+    }
+
+    override fun toString() = "SwipeAnchorsModifierImpl(updateDensity=$onDensityChanged, " +
+        "onSizeChanged=$onSizeChanged)"
+}
+
+private fun <T> Map<T, Float>.closestAnchor(
     offset: Float = 0f,
     searchUpwards: Boolean = false
 ): T {
-    require(isNotEmpty()) { "The anchors were empty when trying to find the closest state" }
+    require(isNotEmpty()) { "The anchors were empty when trying to find the closest anchor" }
     return minBy { (_, anchor) ->
         val delta = if (searchUpwards) anchor - offset else offset - anchor
         if (delta < 0) Float.POSITIVE_INFINITY else delta
@@ -505,6 +555,6 @@
 
 private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
 private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(state: T) = requireNotNull(this[state]) {
-    "Required anchor $state was not found in anchors. Current anchors: ${this.toMap()}"
+private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
+    "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
 }
diff --git a/compose/runtime/runtime-livedata/build.gradle b/compose/runtime/runtime-livedata/build.gradle
index bfc0bbb..692ea89 100644
--- a/compose/runtime/runtime-livedata/build.gradle
+++ b/compose/runtime/runtime-livedata/build.gradle
@@ -54,11 +54,3 @@
 android {
     namespace "androidx.compose.runtime.livedata"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/runtime/runtime-rxjava2/build.gradle b/compose/runtime/runtime-rxjava2/build.gradle
index ecf556a8..341374c 100644
--- a/compose/runtime/runtime-rxjava2/build.gradle
+++ b/compose/runtime/runtime-rxjava2/build.gradle
@@ -53,11 +53,3 @@
 android {
     namespace "androidx.compose.runtime.rxjava2"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/runtime/runtime-rxjava3/build.gradle b/compose/runtime/runtime-rxjava3/build.gradle
index bc209c1..d6ca101 100644
--- a/compose/runtime/runtime-rxjava3/build.gradle
+++ b/compose/runtime/runtime-rxjava3/build.gradle
@@ -53,11 +53,3 @@
 android {
     namespace "androidx.compose.runtime.rxjava3"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index cf8f78a..2874e46 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -130,11 +130,3 @@
 android {
     namespace "androidx.compose.runtime.saveable"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/runtime/runtime-tracing/build.gradle b/compose/runtime/runtime-tracing/build.gradle
index 5977424..dffd973 100644
--- a/compose/runtime/runtime-tracing/build.gradle
+++ b/compose/runtime/runtime-tracing/build.gradle
@@ -50,11 +50,3 @@
     inceptionYear = "2022"
     description = "Additional tracing in Compose"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 69ceccb..0818d02 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -122,11 +122,3 @@
     description = "Tree composition support for code generated by the Compose compiler plugin and corresponding public API"
     legacyDisableKotlinStrictApiMode = true
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all"
-        ]
-    }
-}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index e637a6a..79d6d48 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -17,16 +17,22 @@
 package androidx.compose.runtime
 
 import androidx.compose.runtime.collection.IdentityArraySet
+import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
 import androidx.compose.runtime.snapshots.MutableSnapshot
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.SnapshotApplyResult
+import androidx.compose.runtime.snapshots.fastAny
 import androidx.compose.runtime.snapshots.fastForEach
+import androidx.compose.runtime.snapshots.fastGroupBy
 import androidx.compose.runtime.snapshots.fastMap
 import androidx.compose.runtime.snapshots.fastMapNotNull
 import androidx.compose.runtime.tooling.CompositionData
-import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
-import androidx.compose.runtime.snapshots.fastAny
-import androidx.compose.runtime.snapshots.fastGroupBy
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.resume
+import kotlin.native.concurrent.ThreadLocal
 import kotlinx.coroutines.CancellableContinuation
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
@@ -44,12 +50,6 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.coroutineContext
-import kotlin.coroutines.resume
-import kotlin.native.concurrent.ThreadLocal
 
 // TODO: Can we use rootKey for this since all compositions will have an eventual Recomposer parent?
 private const val RecomposerCompoundHashKey = 1000
@@ -412,8 +412,14 @@
     private fun recordComposerModificationsLocked() {
         val changes = snapshotInvalidations
         if (changes.isNotEmpty()) {
-            knownCompositions.fastForEach { composition ->
-                composition.recordModificationsOf(changes)
+            run {
+                knownCompositions.fastForEach { composition ->
+                    composition.recordModificationsOf(changes)
+
+                    // Avoid using knownCompositions if recording modification detected a
+                    // shutdown of the recomposer.
+                    if (_state.value <= State.ShuttingDown) return@run
+                }
             }
             snapshotInvalidations = mutableSetOf()
             if (deriveStateLocked() != null) {
@@ -627,6 +633,13 @@
                     synchronized(stateLock) {
                         deriveStateLocked()
                     }
+
+                    // Ensure any state objects that were written during apply changes, e.g. nodes
+                    // with state-backed properties, get sent apply notifications to invalidate
+                    // anything observing the nodes. Call this method instead of
+                    // sendApplyNotifications to ensure that objects that were _created_ in this
+                    // snapshot are also considered changed after this point.
+                    Snapshot.notifyObjectsInitialized()
                 }
             }
 
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 285838a..9c76b09 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -38,10 +38,12 @@
 import androidx.compose.runtime.mock.contact
 import androidx.compose.runtime.mock.expectChanges
 import androidx.compose.runtime.mock.expectNoChanges
+import androidx.compose.runtime.mock.frameDelayMillis
 import androidx.compose.runtime.mock.revalidate
 import androidx.compose.runtime.mock.skip
 import androidx.compose.runtime.mock.validate
 import androidx.compose.runtime.snapshots.Snapshot
+import kotlin.coroutines.CoroutineContext
 import kotlin.random.Random
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -53,7 +55,9 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
 
 @Composable
 fun Container(content: @Composable () -> Unit) = content()
@@ -3009,7 +3013,8 @@
                 assertEquals(1, applier.insertCount, "expected setup node not inserted")
                 shouldEmitNode = false
                 Snapshot.sendApplyNotifications()
-                testScheduler.advanceUntilIdle()
+                // Only advance one frame since the next frame will be automatically scheduled.
+                testScheduler.advanceTimeByFrame(coroutineContext)
                 assertEquals(1, stateMutatedOnRemove.value, "observable removals performed")
                 // Only two composition passes should have been performed by this point; a state
                 // invalidation in the applier should not be picked up or acted upon until after
@@ -3436,18 +3441,32 @@
 internal suspend fun <R> localRecomposerTest(
     block: CoroutineScope.(Recomposer) -> R
 ) = coroutineScope {
-    val contextWithClock = coroutineContext + TestMonotonicFrameClock(this)
-    val recomposer = Recomposer(contextWithClock)
-    launch(contextWithClock) {
-        recomposer.runRecomposeAndApplyChanges()
+    withContext(TestMonotonicFrameClock(this)) {
+        val recomposer = Recomposer(coroutineContext)
+        launch {
+            recomposer.runRecomposeAndApplyChanges()
+        }
+        block(recomposer)
+        // This call doesn't need to be in a finally; everything it does will be torn down
+        // in exceptional cases by the coroutineScope failure
+        recomposer.cancel()
     }
-    block(recomposer)
-    // This call doesn't need to be in a finally; everything it does will be torn down
-    // in exceptional cases by the coroutineScope failure
-    recomposer.cancel()
 }
 
-@Composable fun Wrap(content: @Composable () -> Unit) {
+/**
+ * Advances this scheduler by exactly one frame, as defined by the [TestMonotonicFrameClock] in
+ * [context].
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+private fun TestCoroutineScheduler.advanceTimeByFrame(context: CoroutineContext) {
+    val testClock = context[MonotonicFrameClock] as TestMonotonicFrameClock
+    advanceTimeBy(testClock.frameDelayMillis)
+    // advanceTimeBy doesn't run tasks at exactly frameDelayMillis.
+    runCurrent()
+}
+
+@Composable
+fun Wrap(content: @Composable () -> Unit) {
     content()
 }
 
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt
index 6f594fb..02c15a2 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt
@@ -692,13 +692,16 @@
         }
         validate(0)
         enabler = true
-        expectChanges()
-        validate(0)
+        // First recomposition here will emit state=0 and create the DisposableEffect. When the
+        // effect runs, it will set state=1, which will invalidate the composition and immediately
+        // trigger another recomposition to emit state=1.
+        assertEquals(2, advanceCount())
+        validate(1)
         advance()
-        expectChanges()
+        assertEquals(1, advanceCount())
         validate(2)
         advance()
-        expectChanges()
+        assertEquals(1, advanceCount())
         validate(3)
     }
 }
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
index 65078df..36f7425 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
@@ -22,21 +22,23 @@
 import androidx.compose.runtime.mock.compositionTest
 import androidx.compose.runtime.mock.expectNoChanges
 import androidx.compose.runtime.snapshots.Snapshot
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class RecomposerTests {
@@ -309,6 +311,112 @@
     fun constructRecomposerWithCancelledJob() {
         Recomposer(Job().apply { cancel() })
     }
+
+    @Test // regression test for b/243862703
+    fun cancelWithPendingInvalidations() {
+        val dispatcher = StandardTestDispatcher()
+        runTest(dispatcher) {
+            val testClock = TestMonotonicFrameClock(this)
+            withContext(testClock) {
+
+                val recomposer = Recomposer(coroutineContext)
+                var launched = false
+                val runner = launch {
+                    launched = true
+                    recomposer.runRecomposeAndApplyChanges()
+                }
+                val compositionOne = Composition(UnitApplier(), recomposer)
+                val compositionTwo = Composition(UnitApplier(), recomposer)
+                var state by mutableStateOf(0)
+                var lastCompositionOneState = -1
+                var lastCompositionTwoState = -1
+                compositionOne.setContent {
+                    lastCompositionOneState = state
+                    LaunchedEffect(Unit) {
+                        delay(1_000)
+                    }
+                }
+                compositionTwo.setContent {
+                    lastCompositionTwoState = state
+                    LaunchedEffect(Unit) {
+                        delay(1_000)
+                    }
+                }
+
+                assertEquals(0, lastCompositionOneState, "initial composition")
+                assertEquals(0, lastCompositionTwoState, "initial composition")
+
+                dispatcher.scheduler.runCurrent()
+
+                assertNotNull(
+                    withTimeoutOrNull(3_000) { recomposer.awaitIdle() },
+                    "timed out waiting for recomposer idle for recomposition"
+                )
+
+                dispatcher.scheduler.runCurrent()
+
+                assertTrue(launched, "Recomposer was never started")
+
+                Snapshot.withMutableSnapshot {
+                    state = 1
+                }
+
+                recomposer.cancel()
+
+                assertNotNull(
+                    withTimeoutOrNull(3_000) { recomposer.awaitIdle() },
+                    "timed out waiting for recomposer idle for recomposition"
+                )
+
+                assertNotNull(
+                    withTimeoutOrNull(3_000) { runner.join() },
+                    "timed out waiting for recomposer runner job"
+                )
+            }
+        }
+    }
+
+    @Test
+    fun stateChangesDuringApplyChangesAreNotifiedBeforeFrameFinished() = compositionTest {
+        val count = mutableStateOf(0)
+        val countFromEffect = mutableStateOf(0)
+        val applications = mutableListOf<Set<Any>>()
+        var recompositions = 0
+
+        @Composable
+        fun CountRecorder(count: Int) {
+            SideEffect {
+                countFromEffect.value = count
+            }
+        }
+
+        compose {
+            recompositions++
+            CountRecorder(count.value)
+        }
+
+        assertEquals(0, countFromEffect.value)
+        assertEquals(1, recompositions)
+
+        // Change the count and send the apply notification to invalidate the composition.
+        count.value = 1
+
+        // Register the apply observer after changing state to invalidate composition, but
+        // before actually allowing the recomposition to happen.
+        Snapshot.registerApplyObserver { applied, _ ->
+            applications += applied
+        }
+        assertTrue(applications.isEmpty())
+
+        assertEquals(1, advanceCount())
+
+        // Make sure we actually recomposed.
+        assertEquals(2, recompositions)
+
+        // The Recomposer should have received notification for the node's state.
+        @Suppress("RemoveExplicitTypeArguments")
+        assertEquals<List<Set<Any>>>(listOf(setOf(countFromEffect)), applications)
+    }
 }
 
 class UnitApplier : Applier<Unit> {
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 99ccb0c..5b5d26c 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -122,11 +122,3 @@
 android {
     namespace "androidx.compose.testutils"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-geometry/build.gradle b/compose/ui/ui-geometry/build.gradle
index f0c8326..e9cef5f 100644
--- a/compose/ui/ui-geometry/build.gradle
+++ b/compose/ui/ui-geometry/build.gradle
@@ -88,11 +88,3 @@
 android {
     namespace "androidx.compose.ui.geometry"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 5a1dee5..9d729af 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -167,11 +167,3 @@
         systemProperties["GOLDEN_PATH"] = project.rootDir.absolutePath + "/../../golden"
     }
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index 690e0fd..f817241 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -103,11 +103,3 @@
 inspection {
     name = "compose-ui-inspection.jar"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index d7e2dfb..b7ba299 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -157,11 +157,3 @@
     description = "Compose testing integration with JUnit4"
     legacyDisableKotlinStrictApiMode = true
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-test-manifest/build.gradle b/compose/ui/ui-test-manifest/build.gradle
index 620f03f..6d5972c 100644
--- a/compose/ui/ui-test-manifest/build.gradle
+++ b/compose/ui/ui-test-manifest/build.gradle
@@ -39,11 +39,3 @@
 android {
     namespace "androidx.compose.ui.test.manifest"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 0863884..641350f 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -166,11 +166,3 @@
     description = "Compose testing library"
     legacyDisableKotlinStrictApiMode = true
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-text-google-fonts/build.gradle b/compose/ui/ui-text-google-fonts/build.gradle
index 4fb85a7..74702ad 100644
--- a/compose/ui/ui-text-google-fonts/build.gradle
+++ b/compose/ui/ui-text-google-fonts/build.gradle
@@ -52,11 +52,3 @@
 android {
     namespace "androidx.compose.ui.text.googlefonts"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index a27a205..68fe0b6 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -1230,6 +1230,9 @@
   public final class AndroidTextPaint_androidKt {
   }
 
+  public final class EmojiCompatStatusKt {
+  }
+
   public final class Synchronization_jvmKt {
   }
 
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index c7f1c59..8a1820d 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -370,10 +370,10 @@
     method public androidx.compose.ui.text.style.TextGeometricTransform? getTextGeometricTransform();
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.SpanStyle merge(optional androidx.compose.ui.text.SpanStyle? other);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.SpanStyle plus(androidx.compose.ui.text.SpanStyle other);
-    property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
     property public final long background;
     property public final androidx.compose.ui.text.style.BaselineShift? baselineShift;
-    property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
     property public final long color;
     property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle;
     property public final androidx.compose.ui.text.font.FontFamily? fontFamily;
@@ -573,21 +573,21 @@
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.TextStyle plus(androidx.compose.ui.text.SpanStyle other);
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle toParagraphStyle();
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.SpanStyle toSpanStyle();
-    property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
     property public final long background;
     property public final androidx.compose.ui.text.style.BaselineShift? baselineShift;
-    property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
     property public final long color;
-    property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle;
     property public final androidx.compose.ui.text.font.FontFamily? fontFamily;
     property public final String? fontFeatureSettings;
     property public final long fontSize;
     property public final androidx.compose.ui.text.font.FontStyle? fontStyle;
     property public final androidx.compose.ui.text.font.FontSynthesis? fontSynthesis;
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
-    property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.Hyphens? hyphens;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.Hyphens? hyphens;
     property public final long letterSpacing;
-    property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.intl.LocaleList? localeList;
@@ -1345,6 +1345,9 @@
   public final class AndroidTextPaint_androidKt {
   }
 
+  public final class EmojiCompatStatusKt {
+  }
+
   public final class Synchronization_jvmKt {
   }
 
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index a27a205..68fe0b6 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -1230,6 +1230,9 @@
   public final class AndroidTextPaint_androidKt {
   }
 
+  public final class EmojiCompatStatusKt {
+  }
+
   public final class Synchronization_jvmKt {
   }
 
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index 49606fa..cc5ecad 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -47,6 +47,7 @@
         implementation(libs.kotlinStdlib)
         implementation("androidx.core:core:1.7.0")
         implementation('androidx.collection:collection:1.0.0')
+        implementation("androidx.emoji2:emoji2:1.2.0")
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
@@ -130,6 +131,7 @@
             androidMain.dependencies {
                 api("androidx.annotation:annotation:1.1.0")
                 implementation("androidx.core:core:1.7.0")
+                implementation("androidx.emoji2:emoji2:1.2.0")
                 implementation('androidx.collection:collection:1.0.0')
             }
 
@@ -191,11 +193,3 @@
 android {
     namespace "androidx.compose.ui.text"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt
new file mode 100644
index 0000000..83f7477
--- /dev/null
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.text.platform
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.unit.Density
+import androidx.emoji2.text.EmojiCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class AndroidParagraphIntrinsicsTest {
+
+    val context = InstrumentationRegistry.getInstrumentation().context
+
+    @After
+    fun cleanup() {
+        EmojiCompat.reset(null)
+        EmojiCompatStatus.setDelegateForTesting(null)
+    }
+
+    @Test
+    fun whenEmojiCompatLoads_hasStaleFontsIsTrue() {
+        val fontState = mutableStateOf(false)
+        EmojiCompatStatus.setDelegateForTesting(object : EmojiCompatStatusDelegate {
+            override val fontLoaded: State<Boolean>
+                get() = fontState
+        })
+
+        val subject = ActualParagraphIntrinsics(
+            "text",
+            TextStyle.Default,
+            listOf(),
+            listOf(),
+            Density(1f),
+            createFontFamilyResolver(context)
+        )
+
+        assertThat(subject.hasStaleResolvedFonts).isFalse()
+        fontState.value = true
+        assertThat(subject.hasStaleResolvedFonts).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt
new file mode 100644
index 0000000..3d15326
--- /dev/null
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.text.platform
+
+import android.graphics.Typeface
+import androidx.compose.runtime.State
+import androidx.emoji2.text.EmojiCompat
+import androidx.emoji2.text.EmojiCompat.LOAD_STRATEGY_MANUAL
+import androidx.emoji2.text.EmojiCompat.MetadataRepoLoader
+import androidx.emoji2.text.MetadataRepo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EmojiCompatStatusTest {
+
+    @Before
+    fun reset() {
+        EmojiCompat.reset(null)
+        EmojiCompatStatus.setDelegateForTesting(null)
+    }
+
+    @After
+    fun clean() {
+        EmojiCompat.reset(null)
+        EmojiCompatStatus.setDelegateForTesting(null)
+    }
+
+    @Test
+    fun nonConfiguredEc_isNotLoaded() {
+        EmojiCompat.reset(null)
+        assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+    }
+
+    @Test
+    fun default_isNotLoaded() {
+        val (config, deferred) = makeEmojiConfig()
+        EmojiCompat.init(config)
+        assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+        deferred.complete(null)
+    }
+
+    @Test
+    fun loading_isNotLoaded() {
+        val (config, deferred) = makeEmojiConfig()
+        val ec = EmojiCompat.init(config)
+        ec.load()
+        assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+        deferred.complete(null)
+    }
+
+    @Test
+    fun error_isNotLoaded() {
+        val (config, deferred) = makeEmojiConfig()
+        val ec = EmojiCompat.init(config)
+        ec.load()
+        deferred.complete(null)
+        assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+    }
+
+    @Test
+    fun loaded_isLoaded() {
+        val (config, deferred) = makeEmojiConfig()
+        val ec = EmojiCompat.init(config)
+        deferred.complete(MetadataRepo.create(Typeface.DEFAULT))
+        ec.load()
+        // query now, after init EC
+        EmojiCompatStatus.setDelegateForTesting(null)
+        EmojiCompatStatus.fontLoaded.assertTrue()
+    }
+
+    @Test
+    fun nonLoaded_toLoaded_updatesReturnState() {
+        val (config, deferred) = makeEmojiConfig()
+        val ec = EmojiCompat.init(config)
+        val state = EmojiCompatStatus.fontLoaded
+        assertThat(state.value).isFalse()
+
+        deferred.complete(MetadataRepo.create(Typeface.DEFAULT))
+        ec.load()
+
+        state.assertTrue()
+    }
+
+    @Test
+    fun nonConfigured_canLoadLater() {
+        EmojiCompat.reset(null)
+        val initialFontLoad = EmojiCompatStatus.fontLoaded
+        assertThat(initialFontLoad.value).isFalse()
+
+        val (config, deferred) = makeEmojiConfig()
+        val ec = EmojiCompat.init(config)
+        deferred.complete(MetadataRepo.create(Typeface.DEFAULT))
+        ec.load()
+
+        EmojiCompatStatus.fontLoaded.assertTrue()
+    }
+
+    private fun State<Boolean>.assertTrue() {
+        // there's too many async actors to do anything reasonable and non-flaky here. tie up the
+        // test thread until main posts the value
+        runBlocking {
+            withTimeout(1000) {
+                while (!value)
+                    delay(0)
+            }
+            assertThat(value).isTrue()
+        }
+    }
+
+    private fun makeEmojiConfig(): Pair<EmojiCompat.Config, CompletableDeferred<MetadataRepo?>> {
+        val deferred = CompletableDeferred<MetadataRepo?>(null)
+        val loader = MetadataRepoLoader { cb ->
+            CoroutineScope(Dispatchers.Default).launch {
+                val result = deferred.await()
+                if (result != null) {
+                    cb.onLoaded(result)
+                } else {
+                    cb.onFailed(IllegalStateException("No"))
+                }
+            }
+        }
+        val config = object : EmojiCompat.Config(loader) {}
+        config.setMetadataLoadStrategy(LOAD_STRATEGY_MANUAL)
+        return config to deferred
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
index 9c943d0..3bcd60e 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.text.platform
 
 import android.graphics.Typeface
+import android.text.Spannable
 import android.text.SpannableString
 import android.text.TextPaint
 import android.text.style.CharacterStyle
@@ -41,6 +42,7 @@
 import androidx.compose.ui.text.style.TextIndent
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.isUnspecified
+import androidx.emoji2.text.EmojiCompat
 
 @OptIn(InternalPlatformTextApi::class, ExperimentalTextApi::class)
 internal fun createCharSequence(
@@ -51,16 +53,28 @@
     placeholders: List<AnnotatedString.Range<Placeholder>>,
     density: Density,
     resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface,
+    useEmojiCompat: Boolean,
 ): CharSequence {
+
+    val currentText = if (useEmojiCompat && EmojiCompat.isConfigured()) {
+        EmojiCompat.get().process(text)!!
+    } else {
+        text
+    }
+
     if (spanStyles.isEmpty() &&
         placeholders.isEmpty() &&
         contextTextStyle.textIndent == TextIndent.None &&
         contextTextStyle.lineHeight.isUnspecified
     ) {
-        return text
+        return currentText
     }
 
-    val spannableString = SpannableString(text)
+    val spannableString = if (currentText is Spannable) {
+        currentText
+    } else {
+        SpannableString(currentText)
+    }
 
     // b/199939617
     // Due to a bug in the platform's native drawText stack, some CJK characters cause a bolder
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
index 8c9a75c..74dd3ce 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
@@ -65,8 +65,17 @@
 
     private val resolvedTypefaces: MutableList<TypefaceDirtyTracker> = mutableListOf()
 
+    /**
+     * If emojiCompat is used in the making of this Paragraph
+     *
+     * This value will never change
+     */
+    private val emojiCompatProcessed: Boolean = EmojiCompatStatus.fontLoaded.value
+
     override val hasStaleResolvedFonts: Boolean
-        get() = resolvedTypefaces.fastAny { it.isStaleResolvedFont }
+        get() = resolvedTypefaces.fastAny { it.isStaleResolvedFont } ||
+            (!emojiCompatProcessed &&
+                /* short-circuit this state read */ EmojiCompatStatus.fontLoaded.value)
 
     internal val textDirectionHeuristic = resolveTextDirectionHeuristics(
         style.textDirection,
@@ -74,18 +83,18 @@
     )
 
     init {
-        val resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface = {
-                fontFamily, fontWeight, fontStyle, fontSynthesis ->
-            val result = fontFamilyResolver.resolve(
-                fontFamily,
-                fontWeight,
-                fontStyle,
-                fontSynthesis
-            )
-            val holder = TypefaceDirtyTracker(result)
-            resolvedTypefaces.add(holder)
-            holder.typeface
-        }
+        val resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface =
+            { fontFamily, fontWeight, fontStyle, fontSynthesis ->
+                val result = fontFamilyResolver.resolve(
+                    fontFamily,
+                    fontWeight,
+                    fontStyle,
+                    fontSynthesis
+                )
+                val holder = TypefaceDirtyTracker(result)
+                resolvedTypefaces.add(holder)
+                holder.typeface
+            }
 
         val notAppliedStyle = textPaint.applySpanStyle(
             style = style.toSpanStyle(),
@@ -109,6 +118,7 @@
             placeholders = placeholders,
             density = density,
             resolveTypeface = resolveTypeface,
+            useEmojiCompat = emojiCompatProcessed
         )
 
         layoutIntrinsics = LayoutIntrinsics(charSequence, textPaint, textDirectionHeuristic)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.kt
new file mode 100644
index 0000000..b17a3a8
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.text.platform
+
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.emoji2.text.EmojiCompat
+
+/**
+ * Tests may provide alternative global implementations for [EmojiCompatStatus] using this delegate.
+ */
+internal interface EmojiCompatStatusDelegate {
+    val fontLoaded: State<Boolean>
+}
+
+/**
+ * Used for observing emojicompat font loading status from compose.
+ */
+internal object EmojiCompatStatus : EmojiCompatStatusDelegate {
+    private var delegate: EmojiCompatStatusDelegate = DefaultImpl()
+
+    /**
+     * True if the emoji2 font is currently loaded and processing will be successful
+     *
+     * False when emoji2 may complete loading in the future.
+     */
+    override val fontLoaded: State<Boolean>
+        get() = delegate.fontLoaded
+
+    /**
+     * Do not call.
+     *
+     * This is for tests that want to control EmojiCompatStatus behavior.
+     */
+    @VisibleForTesting
+    internal fun setDelegateForTesting(newDelegate: EmojiCompatStatusDelegate?) {
+        delegate = newDelegate ?: DefaultImpl()
+    }
+}
+
+/**
+ * is-a state, but doesn't cause an observation when read
+ */
+private class ImmutableBool(override val value: Boolean) : State<Boolean>
+private val Falsey = ImmutableBool(false)
+
+private class DefaultImpl : EmojiCompatStatusDelegate {
+
+    private var loadState: State<Boolean>?
+
+    init {
+        loadState = if (EmojiCompat.isConfigured()) {
+            getFontLoadState()
+        } else {
+            // EC isn't configured yet, will check again in getter
+            null
+        }
+    }
+
+    override val fontLoaded: State<Boolean>
+        get() = if (loadState != null) {
+            loadState!!
+        } else {
+            // EC wasn't configured last time, check again and update loadState if it's ready
+            if (EmojiCompat.isConfigured()) {
+                loadState = getFontLoadState()
+                loadState!!
+            } else {
+                // ec disabled path
+                // no observations allowed, this is pre init
+                Falsey
+            }
+        }
+
+    private fun getFontLoadState(): State<Boolean> {
+        val ec = EmojiCompat.get()
+        return if (ec.loadState == EmojiCompat.LOAD_STATE_SUCCEEDED) {
+            ImmutableBool(true)
+        } else {
+            val mutableLoaded = mutableStateOf(false)
+            val initCallback = object : EmojiCompat.InitCallback() {
+                override fun onInitialized() {
+                    mutableLoaded.value = true // update previous observers
+                    loadState = ImmutableBool(true) // never observe again
+                }
+
+                override fun onFailed(throwable: Throwable?) {
+                    loadState = Falsey // never observe again
+                }
+            }
+            ec.registerInitCallback(initCallback)
+            mutableLoaded
+        }
+    }
+}
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index ec92f46..46505a8 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -131,11 +131,3 @@
 android {
     namespace "androidx.compose.ui.tooling.data"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index 86c9465..97820f5 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -79,11 +79,3 @@
 android {
     namespace "androidx.compose.ui.tooling.preview"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index f8affb0..dba7455 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -139,11 +139,3 @@
 android {
     namespace "androidx.compose.ui.tooling"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-unit/build.gradle b/compose/ui/ui-unit/build.gradle
index 60360f3..9dbad26 100644
--- a/compose/ui/ui-unit/build.gradle
+++ b/compose/ui/ui-unit/build.gradle
@@ -117,11 +117,3 @@
 android {
     namespace "androidx.compose.ui.unit"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index ec838b5..765a47a 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -96,11 +96,3 @@
 android {
     namespace "androidx.compose.ui.util"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui-viewbinding/build.gradle b/compose/ui/ui-viewbinding/build.gradle
index dca93c8..723ef55 100644
--- a/compose/ui/ui-viewbinding/build.gradle
+++ b/compose/ui/ui-viewbinding/build.gradle
@@ -55,11 +55,3 @@
 android {
     namespace "androidx.compose.ui.viewbinding"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 3dd72e6..69ffa60 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -473,8 +473,8 @@
     property public abstract boolean canFocus;
     property public default androidx.compose.ui.focus.FocusRequester down;
     property public default androidx.compose.ui.focus.FocusRequester end;
-    property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> enter;
-    property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> exit;
+    property @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> enter;
+    property @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> exit;
     property public default androidx.compose.ui.focus.FocusRequester left;
     property public default androidx.compose.ui.focus.FocusRequester next;
     property public default androidx.compose.ui.focus.FocusRequester previous;
@@ -1817,12 +1817,12 @@
     method public long getUptimeMillis();
     method public boolean isConsumed();
     property @Deprecated public final androidx.compose.ui.input.pointer.ConsumedData consumed;
-    property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public final java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical;
+    property @androidx.compose.ui.ExperimentalComposeUiApi public final java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical;
     property public final long id;
     property public final boolean isConsumed;
     property public final long position;
     property public final boolean pressed;
-    property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public final float pressure;
+    property @androidx.compose.ui.ExperimentalComposeUiApi public final float pressure;
     property public final long previousPosition;
     property public final boolean previousPressed;
     property public final long previousUptimeMillis;
@@ -1842,7 +1842,7 @@
     method public abstract void onCancel();
     method public abstract void onPointerEvent(androidx.compose.ui.input.pointer.PointerEvent pointerEvent, androidx.compose.ui.input.pointer.PointerEventPass pass, long bounds);
     property public boolean interceptOutOfBoundsChildEvents;
-    property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public boolean shareWithSiblings;
+    property @androidx.compose.ui.ExperimentalComposeUiApi public boolean shareWithSiblings;
     property public final long size;
   }
 
@@ -2277,7 +2277,7 @@
     method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
-    property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
+    property @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
     property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection;
     property protected abstract int parentWidth;
   }
diff --git a/compose/ui/ui/benchmark/build.gradle b/compose/ui/ui/benchmark/build.gradle
index 0b58476..5cc4f13 100644
--- a/compose/ui/ui/benchmark/build.gradle
+++ b/compose/ui/ui/benchmark/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
 
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.5.1")
     androidTestImplementation("androidx.activity:activity-compose:1.3.1")
     androidTestImplementation(project(":benchmark:benchmark-junit4"))
     androidTestImplementation(project(":compose:foundation:foundation"))
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
index ef060ab..082292b 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
@@ -24,8 +24,7 @@
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.platform.createLifecycleAwareWindowRecomposer
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -49,23 +48,14 @@
     @UiThreadTest
     fun createRecomposer() {
         val rootView = rule.activityTestRule.activity.window.decorView.rootView
-        val lifecycle = object : Lifecycle() {
-            override fun addObserver(observer: LifecycleObserver) {
-                if (observer is LifecycleEventObserver) {
-                    observer.onStateChanged({ this }, Event.ON_CREATE)
-                }
-            }
-
-            override fun removeObserver(observer: LifecycleObserver) {}
-            override fun getCurrentState(): State = State.CREATED
-        }
+        val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.CREATED)
         var view: View? = null
         rule.benchmarkRule.measureRepeated {
             runWithTimingDisabled {
                 view = View(rule.activityTestRule.activity)
                 (rootView as ViewGroup).addView(view)
             }
-            view!!.createLifecycleAwareWindowRecomposer(lifecycle = lifecycle)
+            view!!.createLifecycleAwareWindowRecomposer(lifecycle = lifecycleOwner.lifecycle)
             runWithTimingDisabled {
                 (rootView as ViewGroup).removeAllViews()
                 view = null
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 4944ff2..e44eb1f 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -276,14 +276,6 @@
     }
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-            "-Xjvm-default=all"
-        ]
-    }
-}
-
 android {
     testOptions.unitTests.includeAndroidResources = true
     buildTypes.all {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
index 2d5bf83..ee4701d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
@@ -48,6 +48,7 @@
 import androidx.test.espresso.action.Press
 import androidx.test.espresso.action.Swipe
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -985,6 +986,7 @@
         }
     }
 
+    @FlakyTest(bugId = 259725902)
     @Test
     fun nestedScroll_movingTarget_velocityShouldRespectSign() {
         var lastVelocity = Velocity.Zero
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt
new file mode 100644
index 0000000..36a3ef8
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt
@@ -0,0 +1,557 @@
+/*
+ * 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.layout
+
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Copies of most of the tests in [RemeasureWithIntrinsicsTest] but without using
+ * TestMonotonicFrameClock, since it does layout passes slightly differently than in production
+ * and this has bitten us in the past (see b/222093277).
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class RemeasureWithIntrinsicsRealClockTest {
+
+    @get:Rule
+    val rule = ActivityScenarioRule(TestActivity::class.java)
+
+    @get:Rule
+    val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
+
+    private val testLatch = CountDownLatch(1)
+
+    @Test
+    fun remeasuringChildWhenParentUsedIntrinsicSizes() {
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var childSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics {
+                    LayoutWithIntrinsics(
+                        intrinsicWidth,
+                        intrinsicHeight,
+                        Modifier.onSizeChanged { childSize = it }
+                    )
+                }
+            },
+            test = {
+                assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+                withFrameNanos {}
+
+                assertThat(childSize).isEqualTo(IntSize(30, 20))
+            }
+        )
+    }
+
+    @Test
+    fun updatingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var childSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics {
+                    Box(
+                        Modifier
+                            .onSizeChanged { childSize = it }
+                            .withIntrinsics(intrinsicWidth, intrinsicHeight)
+                    )
+                }
+            },
+            test = {
+                assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+                withFrameNanos {}
+
+                assertThat(childSize).isEqualTo(IntSize(30, 20))
+            }
+        )
+    }
+
+    @Test
+    fun remeasuringGrandChildWhenGrandParentUsedIntrinsicSizes() {
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var childSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics {
+                    Box(propagateMinConstraints = true) {
+                        LayoutWithIntrinsics(
+                            intrinsicWidth,
+                            intrinsicHeight,
+                            Modifier.onSizeChanged { childSize = it }
+                        )
+                    }
+                }
+            },
+            test = {
+                assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+                withFrameNanos {}
+
+                assertThat(childSize).isEqualTo(IntSize(30, 20))
+            }
+        )
+    }
+
+    @Test
+    fun updatingGrandChildIntrinsicsViaModifierWhenGrandParentUsedIntrinsicSizes() {
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var childSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics {
+                    Box(propagateMinConstraints = true) {
+                        Box(
+                            Modifier
+                                .onSizeChanged { childSize = it }
+                                .withIntrinsics(intrinsicWidth, intrinsicHeight)
+                        )
+                    }
+                }
+            },
+            test = {
+                assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+                withFrameNanos {}
+
+                assertThat(childSize).isEqualTo(IntSize(30, 20))
+            }
+        )
+    }
+
+    @Test
+    fun nodeDoesNotCauseRemeasureOfAncestor_whenItsIntrinsicsAreUnused() {
+        var measures = 0
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var parentSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics(
+                    onMeasure = { ++measures },
+                    modifier = Modifier.onSizeChanged { parentSize = it }
+                ) {
+                    LayoutWithIntrinsics(20, 20) {
+                        LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+                    }
+                }
+            },
+            test = {
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+
+                withFrameNanos {}
+                assertThat(measures).isEqualTo(1)
+                assertThat(parentSize).isEqualTo(IntSize(20, 20))
+            }
+        )
+    }
+
+    @Test
+    fun causesRemeasureOfAllDependantAncestors() {
+        var measures1 = 0
+        var measures2 = 0
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics(
+                    onMeasure = { ++measures1 }
+                ) {
+                    Box {
+                        LayoutUsingIntrinsics(
+                            onMeasure = { ++measures2 }
+                        ) {
+                            Box {
+                                LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+                            }
+                        }
+                    }
+                }
+            },
+            test = {
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+
+                withFrameNanos {}
+                assertThat(measures1).isEqualTo(2)
+                assertThat(measures2).isEqualTo(2)
+
+                // Shouldn't remeasure any more.
+                withFrameNanos {}
+                assertThat(measures1).isEqualTo(2)
+                assertThat(measures2).isEqualTo(2)
+            }
+        )
+    }
+
+    @Test
+    fun whenConnectionFromOwnerDoesNotQueryAnymore() {
+        var measures = 0
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var connectionModifier by mutableStateOf(Modifier as Modifier)
+
+        val parentLayoutPolicy = MeasurePolicy { measurables, constraints ->
+            ++measures
+            val measurable = measurables.first()
+            // Query intrinsics but do not size child to them, to make sure we are
+            // remeasured when the connectionModifier is added.
+            measurable.maxIntrinsicWidth(constraints.maxHeight)
+            measurable.maxIntrinsicHeight(constraints.maxWidth)
+            val placeable = measurable.measure(constraints)
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                placeable.place(0, 0)
+            }
+        }
+
+        setTestContent(
+            content = {
+                Layout(
+                    {
+                        Box(modifier = connectionModifier) {
+                            LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+                        }
+                    },
+                    measurePolicy = parentLayoutPolicy
+                )
+            },
+            test = {
+                assertThat(measures).isEqualTo(1)
+                connectionModifier = Modifier.size(10.toDp())
+
+                withFrameNanos {}
+                assertThat(measures).isEqualTo(2)
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+
+                withFrameNanos {}
+                assertThat(measures).isEqualTo(2)
+            }
+        )
+    }
+
+    @Test
+    fun whenQueriedFromModifier() {
+        var parentMeasures = 0
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var boxSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutMaybeUsingIntrinsics({ false }, onMeasure = { ++parentMeasures }) {
+                    // Box used to fast return intrinsics and do not remeasure when the size
+                    // of the inner Box is changing after the intrinsics change.
+                    Box(Modifier.requiredSize(100.toDp())) {
+                        Box(
+                            Modifier
+                                .onSizeChanged { boxSize = it }
+                                .then(ModifierUsingIntrinsics)
+                        ) {
+                            LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+                        }
+                    }
+                }
+            },
+            test = {
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+
+                withFrameNanos {}
+                assertThat(parentMeasures).isEqualTo(1)
+                assertThat(boxSize).isEqualTo(IntSize(30, 20))
+            }
+        )
+    }
+
+    @Test
+    fun whenQueriedFromModifier_andAParentQueriesAbove() {
+        var parentMeasures = 0
+        var intrinsicWidth by mutableStateOf(40)
+        var intrinsicHeight by mutableStateOf(50)
+        var boxSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics(onMeasure = { ++parentMeasures }) {
+                    // Box used to fast return intrinsics and do not remeasure when the size
+                    // of the inner Box is changing after the intrinsics change.
+                    Box(Modifier.requiredSize(100.toDp())) {
+                        Box(
+                            Modifier
+                                .onSizeChanged { boxSize = it }
+                                .then(ModifierUsingIntrinsics)
+                        ) {
+                            LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+                        }
+                    }
+                }
+            },
+            test = {
+                intrinsicWidth = 30
+                intrinsicHeight = 20
+
+                withFrameNanos {}
+                assertThat(parentMeasures).isEqualTo(1)
+                assertThat(boxSize).isEqualTo(IntSize(30, 20))
+            }
+        )
+    }
+
+    @Test
+    fun introducingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+        var childModifier by mutableStateOf(Modifier as Modifier)
+        var childSize: IntSize? = null
+
+        setTestContent(
+            content = {
+                LayoutUsingIntrinsics {
+                    Box(
+                        Modifier
+                            .onSizeChanged { childSize = it }
+                            .then(childModifier)
+                    )
+                }
+            },
+            test = {
+                assertThat(childSize).isEqualTo(IntSize.Zero)
+
+                childModifier = Modifier.withIntrinsics(30, 20)
+
+                withFrameNanos {}
+                assertThat(childSize).isEqualTo(IntSize(30, 20))
+            }
+        )
+    }
+
+    private fun setTestContent(
+        content: @Composable Density.() -> Unit,
+        test: suspend Density.() -> Unit
+    ) {
+        rule.withActivity {
+            setContent {
+                val density = LocalDensity.current
+                content(density)
+                LaunchedEffect(Unit) {
+                    // Wait for the first layout pass to finish.
+                    withFrameNanos {}
+                    test(density)
+                    testLatch.countDown()
+                }
+            }
+        }
+        testLatch.await(3, TimeUnit.SECONDS)
+    }
+
+    @Composable
+    private fun LayoutWithIntrinsics(
+        width: Int,
+        height: Int,
+        modifier: Modifier = Modifier,
+        onMeasure: () -> Unit = {},
+        content: @Composable () -> Unit = {}
+    ) {
+        Layout(
+            content = content,
+            modifier = modifier,
+            measurePolicy = object : MeasurePolicy {
+                override fun MeasureScope.measure(
+                    measurables: List<Measurable>,
+                    constraints: Constraints
+                ): MeasureResult {
+                    onMeasure()
+                    return layout(constraints.minWidth, constraints.minHeight) {}
+                }
+
+                override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+                    measurables: List<IntrinsicMeasurable>,
+                    height: Int
+                ): Int = width
+
+                override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+                    measurables: List<IntrinsicMeasurable>,
+                    width: Int
+                ): Int = height
+            }
+        )
+    }
+
+    @Composable
+    private fun LayoutMaybeUsingIntrinsics(
+        useIntrinsics: () -> Boolean,
+        modifier: Modifier = Modifier,
+        onMeasure: () -> Unit = {},
+        content: @Composable () -> Unit
+    ) {
+        val measurePolicy = remember {
+            object : MeasurePolicy {
+                override fun MeasureScope.measure(
+                    measurables: List<Measurable>,
+                    constraints: Constraints
+                ): MeasureResult {
+                    require(measurables.size == 1)
+                    onMeasure()
+                    val childConstraints = if (useIntrinsics()) {
+                        val width = measurables.first().maxIntrinsicWidth(constraints.maxHeight)
+                        val height = measurables.first().maxIntrinsicHeight(constraints.maxWidth)
+                        Constraints.fixed(width, height)
+                    } else {
+                        constraints
+                    }
+                    val placeable = measurables.first().measure(childConstraints)
+                    return layout(placeable.width, placeable.height) {
+                        placeable.place(0, 0)
+                    }
+                }
+
+                override fun IntrinsicMeasureScope.minIntrinsicWidth(
+                    measurables: List<IntrinsicMeasurable>,
+                    height: Int
+                ) = measurables.first().minIntrinsicWidth(height)
+
+                override fun IntrinsicMeasureScope.minIntrinsicHeight(
+                    measurables: List<IntrinsicMeasurable>,
+                    width: Int
+                ) = measurables.first().minIntrinsicHeight(width)
+
+                override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+                    measurables: List<IntrinsicMeasurable>,
+                    height: Int
+                ) = measurables.first().maxIntrinsicWidth(height)
+
+                override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+                    measurables: List<IntrinsicMeasurable>,
+                    width: Int
+                ) = measurables.first().maxIntrinsicHeight(width)
+            }
+        }
+        Layout(content, modifier, measurePolicy)
+    }
+
+    @Composable
+    private fun LayoutUsingIntrinsics(
+        modifier: Modifier = Modifier,
+        onMeasure: () -> Unit = {},
+        content: @Composable () -> Unit
+    ) = LayoutMaybeUsingIntrinsics({ true }, modifier, onMeasure, content)
+
+    private val ModifierUsingIntrinsics = object : LayoutModifier {
+        override fun MeasureScope.measure(
+            measurable: Measurable,
+            constraints: Constraints
+        ): MeasureResult {
+            val width = measurable.maxIntrinsicWidth(constraints.maxHeight)
+            val height = measurable.maxIntrinsicHeight(constraints.maxWidth)
+            val placeable = measurable.measure(Constraints.fixed(width, height))
+            return layout(placeable.width, placeable.height) {
+                placeable.place(0, 0)
+            }
+        }
+
+        override fun IntrinsicMeasureScope.minIntrinsicWidth(
+            measurable: IntrinsicMeasurable,
+            height: Int
+        ) = measurable.minIntrinsicWidth(height)
+
+        override fun IntrinsicMeasureScope.minIntrinsicHeight(
+            measurable: IntrinsicMeasurable,
+            width: Int
+        ) = measurable.minIntrinsicHeight(width)
+
+        override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+            measurable: IntrinsicMeasurable,
+            height: Int
+        ) = measurable.maxIntrinsicWidth(height)
+
+        override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+            measurable: IntrinsicMeasurable,
+            width: Int
+        ) = measurable.maxIntrinsicHeight(width)
+    }
+
+    private fun Modifier.withIntrinsics(width: Int, height: Int): Modifier {
+        return this.then(object : LayoutModifier {
+            override fun MeasureScope.measure(
+                measurable: Measurable,
+                constraints: Constraints
+            ): MeasureResult {
+                val placeable = measurable.measure(constraints)
+                return layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
+            }
+
+            override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+                measurable: IntrinsicMeasurable,
+                height: Int
+            ): Int = width
+
+            override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+                measurable: IntrinsicMeasurable,
+                width: Int
+            ): Int = height
+        })
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
index 86a6f47..5cd91ca 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
@@ -433,154 +433,154 @@
             .assertWidthIsEqualTo(30.dp)
             .assertHeightIsEqualTo(20.dp)
     }
-}
 
-@Composable
-private fun LayoutWithIntrinsics(
-    width: Dp,
-    height: Dp,
-    modifier: Modifier = Modifier,
-    onMeasure: () -> Unit = {},
-    content: @Composable () -> Unit = {}
-) {
-    Layout(
-        content = content,
-        modifier = modifier,
-        measurePolicy = object : MeasurePolicy {
-            override fun MeasureScope.measure(
-                measurables: List<Measurable>,
-                constraints: Constraints
-            ): MeasureResult {
-                onMeasure()
-                return layout(constraints.minWidth, constraints.minHeight) {}
-            }
-
-            override fun IntrinsicMeasureScope.maxIntrinsicWidth(
-                measurables: List<IntrinsicMeasurable>,
-                height: Int
-            ): Int = width.roundToPx()
-
-            override fun IntrinsicMeasureScope.maxIntrinsicHeight(
-                measurables: List<IntrinsicMeasurable>,
-                width: Int
-            ): Int = height.roundToPx()
-        }
-    )
-}
-
-@Composable
-private fun LayoutMaybeUsingIntrinsics(
-    useIntrinsics: () -> Boolean,
-    modifier: Modifier = Modifier,
-    onMeasure: () -> Unit = {},
-    content: @Composable () -> Unit
-) {
-    val measurePolicy = remember {
-        object : MeasurePolicy {
-            override fun MeasureScope.measure(
-                measurables: List<Measurable>,
-                constraints: Constraints
-            ): MeasureResult {
-                require(measurables.size == 1)
-                onMeasure()
-                val childConstraints = if (useIntrinsics()) {
-                    val width = measurables.first().maxIntrinsicWidth(constraints.maxHeight)
-                    val height = measurables.first().maxIntrinsicHeight(constraints.maxWidth)
-                    Constraints.fixed(width, height)
-                } else {
-                    constraints
+    @Composable
+    private fun LayoutWithIntrinsics(
+        width: Dp,
+        height: Dp,
+        modifier: Modifier = Modifier,
+        onMeasure: () -> Unit = {},
+        content: @Composable () -> Unit = {}
+    ) {
+        Layout(
+            content = content,
+            modifier = modifier,
+            measurePolicy = object : MeasurePolicy {
+                override fun MeasureScope.measure(
+                    measurables: List<Measurable>,
+                    constraints: Constraints
+                ): MeasureResult {
+                    onMeasure()
+                    return layout(constraints.minWidth, constraints.minHeight) {}
                 }
-                val placeable = measurables.first().measure(childConstraints)
-                return layout(placeable.width, placeable.height) {
-                    placeable.place(0, 0)
-                }
+
+                override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+                    measurables: List<IntrinsicMeasurable>,
+                    height: Int
+                ): Int = width.roundToPx()
+
+                override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+                    measurables: List<IntrinsicMeasurable>,
+                    width: Int
+                ): Int = height.roundToPx()
             }
-
-            override fun IntrinsicMeasureScope.minIntrinsicWidth(
-                measurables: List<IntrinsicMeasurable>,
-                height: Int
-            ) = measurables.first().minIntrinsicWidth(height)
-
-            override fun IntrinsicMeasureScope.minIntrinsicHeight(
-                measurables: List<IntrinsicMeasurable>,
-                width: Int
-            ) = measurables.first().minIntrinsicHeight(width)
-
-            override fun IntrinsicMeasureScope.maxIntrinsicWidth(
-                measurables: List<IntrinsicMeasurable>,
-                height: Int
-            ) = measurables.first().maxIntrinsicWidth(height)
-
-            override fun IntrinsicMeasureScope.maxIntrinsicHeight(
-                measurables: List<IntrinsicMeasurable>,
-                width: Int
-            ) = measurables.first().maxIntrinsicHeight(width)
-        }
-    }
-    Layout(content, modifier, measurePolicy)
-}
-
-@Composable
-private fun LayoutUsingIntrinsics(
-    modifier: Modifier = Modifier,
-    onMeasure: () -> Unit = {},
-    content: @Composable () -> Unit
-) = LayoutMaybeUsingIntrinsics({ true }, modifier, onMeasure, content)
-
-private val ModifierUsingIntrinsics = object : LayoutModifier {
-    override fun MeasureScope.measure(
-        measurable: Measurable,
-        constraints: Constraints
-    ): MeasureResult {
-        val width = measurable.maxIntrinsicWidth(constraints.maxHeight)
-        val height = measurable.maxIntrinsicHeight(constraints.maxWidth)
-        val placeable = measurable.measure(Constraints.fixed(width, height))
-        return layout(placeable.width, placeable.height) {
-            placeable.place(0, 0)
-        }
+        )
     }
 
-    override fun IntrinsicMeasureScope.minIntrinsicWidth(
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ) = measurable.minIntrinsicWidth(height)
+    @Composable
+    private fun LayoutMaybeUsingIntrinsics(
+        useIntrinsics: () -> Boolean,
+        modifier: Modifier = Modifier,
+        onMeasure: () -> Unit = {},
+        content: @Composable () -> Unit
+    ) {
+        val measurePolicy = remember {
+            object : MeasurePolicy {
+                override fun MeasureScope.measure(
+                    measurables: List<Measurable>,
+                    constraints: Constraints
+                ): MeasureResult {
+                    require(measurables.size == 1)
+                    onMeasure()
+                    val childConstraints = if (useIntrinsics()) {
+                        val width = measurables.first().maxIntrinsicWidth(constraints.maxHeight)
+                        val height = measurables.first().maxIntrinsicHeight(constraints.maxWidth)
+                        Constraints.fixed(width, height)
+                    } else {
+                        constraints
+                    }
+                    val placeable = measurables.first().measure(childConstraints)
+                    return layout(placeable.width, placeable.height) {
+                        placeable.place(0, 0)
+                    }
+                }
 
-    override fun IntrinsicMeasureScope.minIntrinsicHeight(
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ) = measurable.minIntrinsicHeight(width)
+                override fun IntrinsicMeasureScope.minIntrinsicWidth(
+                    measurables: List<IntrinsicMeasurable>,
+                    height: Int
+                ) = measurables.first().minIntrinsicWidth(height)
 
-    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ) = measurable.maxIntrinsicWidth(height)
+                override fun IntrinsicMeasureScope.minIntrinsicHeight(
+                    measurables: List<IntrinsicMeasurable>,
+                    width: Int
+                ) = measurables.first().minIntrinsicHeight(width)
 
-    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ) = measurable.maxIntrinsicHeight(width)
-}
+                override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+                    measurables: List<IntrinsicMeasurable>,
+                    height: Int
+                ) = measurables.first().maxIntrinsicWidth(height)
 
-private fun Modifier.withIntrinsics(width: Dp, height: Dp): Modifier {
-    return this.then(object : LayoutModifier {
+                override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+                    measurables: List<IntrinsicMeasurable>,
+                    width: Int
+                ) = measurables.first().maxIntrinsicHeight(width)
+            }
+        }
+        Layout(content, modifier, measurePolicy)
+    }
+
+    @Composable
+    private fun LayoutUsingIntrinsics(
+        modifier: Modifier = Modifier,
+        onMeasure: () -> Unit = {},
+        content: @Composable () -> Unit
+    ) = LayoutMaybeUsingIntrinsics({ true }, modifier, onMeasure, content)
+
+    private val ModifierUsingIntrinsics = object : LayoutModifier {
         override fun MeasureScope.measure(
             measurable: Measurable,
             constraints: Constraints
         ): MeasureResult {
-            val placeable = measurable.measure(constraints)
+            val width = measurable.maxIntrinsicWidth(constraints.maxHeight)
+            val height = measurable.maxIntrinsicHeight(constraints.maxWidth)
+            val placeable = measurable.measure(Constraints.fixed(width, height))
             return layout(placeable.width, placeable.height) {
                 placeable.place(0, 0)
             }
         }
 
+        override fun IntrinsicMeasureScope.minIntrinsicWidth(
+            measurable: IntrinsicMeasurable,
+            height: Int
+        ) = measurable.minIntrinsicWidth(height)
+
+        override fun IntrinsicMeasureScope.minIntrinsicHeight(
+            measurable: IntrinsicMeasurable,
+            width: Int
+        ) = measurable.minIntrinsicHeight(width)
+
         override fun IntrinsicMeasureScope.maxIntrinsicWidth(
             measurable: IntrinsicMeasurable,
             height: Int
-        ): Int = width.roundToPx()
+        ) = measurable.maxIntrinsicWidth(height)
 
         override fun IntrinsicMeasureScope.maxIntrinsicHeight(
             measurable: IntrinsicMeasurable,
             width: Int
-        ): Int = height.roundToPx()
-    })
+        ) = measurable.maxIntrinsicHeight(width)
+    }
+
+    private fun Modifier.withIntrinsics(width: Dp, height: Dp): Modifier {
+        return this.then(object : LayoutModifier {
+            override fun MeasureScope.measure(
+                measurable: Measurable,
+                constraints: Constraints
+            ): MeasureResult {
+                val placeable = measurable.measure(constraints)
+                return layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
+            }
+
+            override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+                measurable: IntrinsicMeasurable,
+                height: Int
+            ): Int = width.roundToPx()
+
+            override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+                measurable: IntrinsicMeasurable,
+                width: Int
+            ): Int = height.roundToPx()
+        })
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index 02c2c59..65fe4f0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -558,6 +558,7 @@
         assertNotNull("test did not run", result?.getOrThrow())
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollVerticallyDown_returnsTrue_onlyAfterDownEventInScrollable() {
         lateinit var composeView: View
@@ -583,6 +584,7 @@
         }
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollVerticallyUp_returnsTrue_onlyAfterDownEventInScrollable() {
         lateinit var composeView: View
@@ -610,6 +612,7 @@
         }
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollVertically_returnsFalse_afterDownEventOutsideScrollable() {
         lateinit var composeView: View
@@ -633,6 +636,7 @@
         }
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollHorizontallyRight_returnsTrue_onlyAfterDownEventInScrollable() {
         lateinit var composeView: View
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 0c83242..ef66a60 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.constraintlayout.compose {
 
-  public interface BaselineAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
   }
 
@@ -241,7 +241,7 @@
     property public abstract String constraintLayoutTag;
   }
 
-  @androidx.compose.runtime.Immutable public interface ConstraintSet {
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
     method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
     method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
     method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
@@ -322,7 +322,7 @@
     property public final androidx.constraintlayout.compose.HorizontalAlign Start;
   }
 
-  public interface HorizontalAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
@@ -466,7 +466,7 @@
     property public final androidx.constraintlayout.compose.VerticalAlign Top;
   }
 
-  public interface VerticalAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index 96daef8..dbaa5af 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -32,7 +32,7 @@
     property protected final androidx.constraintlayout.core.parser.CLArray framesContainer;
   }
 
-  public interface BaselineAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
   }
 
@@ -272,7 +272,7 @@
     property public abstract String constraintLayoutTag;
   }
 
-  @androidx.compose.runtime.Immutable public interface ConstraintSet {
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
     method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
     method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
     method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
@@ -391,7 +391,7 @@
     property public final androidx.constraintlayout.compose.HorizontalAlign Start;
   }
 
-  public interface HorizontalAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
@@ -887,7 +887,7 @@
     property public final androidx.constraintlayout.compose.VerticalAlign Top;
   }
 
-  public interface VerticalAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index d13a833..1295ede 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.constraintlayout.compose {
 
-  public interface BaselineAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
   }
 
@@ -245,7 +245,7 @@
     property public abstract String constraintLayoutTag;
   }
 
-  @androidx.compose.runtime.Immutable public interface ConstraintSet {
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
     method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
     method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
     method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
@@ -347,7 +347,7 @@
     property public final androidx.constraintlayout.compose.HorizontalAlign Start;
   }
 
-  public interface HorizontalAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
@@ -625,7 +625,7 @@
     property public final androidx.constraintlayout.compose.VerticalAlign Top;
   }
 
-  public interface VerticalAnchorable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index c93b13f..e54edbd 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -424,7 +424,7 @@
         val boxSize = 100
         val offset = 150
 
-        val position = Array(3) { Ref<Offset>() }
+        val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
 
         rule.setContent {
             ConstraintLayout(
@@ -441,7 +441,7 @@
                         }
                         .size(boxSize.toDp(), boxSize.toDp())
                         .onGloballyPositioned {
-                            position[0].value = it.positionInRoot()
+                            position[0] = it.positionInRoot().round()
                         }
                 )
                 val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
@@ -453,7 +453,7 @@
                         }
                         .size(boxSize.toDp(), boxSize.toDp())
                         .onGloballyPositioned {
-                            position[1].value = it.positionInRoot()
+                            position[1] = it.positionInRoot().round()
                         }
                 )
                 Box(
@@ -464,7 +464,7 @@
                         }
                         .size(boxSize.toDp(), boxSize.toDp())
                         .onGloballyPositioned {
-                            position[2].value = it.positionInRoot()
+                            position[2] = it.positionInRoot().round()
                         }
                 )
             }
@@ -476,24 +476,24 @@
         rule.runOnIdle {
             assertEquals(
                 Offset(
-                    ((displayWidth - boxSize) / 2).toFloat(),
-                    ((displayHeight - boxSize) / 2).toFloat()
-                ),
-                position[0].value
+                    ((displayWidth - boxSize) / 2f),
+                    ((displayHeight - boxSize) / 2f)
+                ).round(),
+                position[0]
             )
             assertEquals(
                 Offset(
-                    (displayWidth / 2 + offset).toFloat(),
-                    ((displayHeight - boxSize) / 2 - boxSize).toFloat()
-                ),
-                position[1].value
+                    (displayWidth / 2f + offset),
+                    ((displayHeight - boxSize) / 2f - boxSize)
+                ).round(),
+                position[1]
             )
             assertEquals(
-                Offset(
-                    offset.toFloat(),
-                    (displayHeight - boxSize - offset).toFloat()
+                IntOffset(
+                    offset,
+                    (displayHeight - boxSize - offset)
                 ),
-                position[2].value
+                position[2]
             )
         }
     }
@@ -504,7 +504,7 @@
         val boxSize = 100
         val offset = 150
 
-        val position = Array(3) { Ref<Offset>() }
+        val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
 
         rule.setContent {
             ConstraintLayout(
@@ -540,7 +540,7 @@
                             .layoutId("box$i")
                             .size(boxSize.toDp(), boxSize.toDp())
                             .onGloballyPositioned {
-                                position[i].value = it.positionInRoot()
+                                position[i] = it.positionInRoot().round()
                             }
                     )
                 }
@@ -555,22 +555,22 @@
                 Offset(
                     (displayWidth - boxSize) / 2f,
                     (displayHeight - boxSize) / 2f
-                ),
-                position[0].value
+                ).round(),
+                position[0]
             )
             assertEquals(
                 Offset(
                     (displayWidth / 2f + offset),
-                    ((displayHeight - boxSize) / 2 - boxSize).toFloat()
-                ),
-                position[1].value
+                    ((displayHeight - boxSize) / 2f - boxSize)
+                ).round(),
+                position[1]
             )
             assertEquals(
-                Offset(
-                    offset.toFloat(),
-                    (displayHeight - boxSize - offset).toFloat()
+                IntOffset(
+                    offset,
+                    (displayHeight - boxSize - offset)
                 ),
-                position[2].value
+                position[2]
             )
         }
     }
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
index c97c9ae..c4fe3fa 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
@@ -23,6 +23,7 @@
 import androidx.constraintlayout.compose.AnchorFunctions.verticalAnchorFunctions
 import androidx.constraintlayout.core.state.ConstraintReference
 
+@JvmDefaultWithCompatibility
 /**
  * Represents a vertical side of a layout (i.e start and end) that can be anchored using
  * [linkTo] in their `Modifier.constrainAs` blocks.
@@ -38,6 +39,7 @@
     )
 }
 
+@JvmDefaultWithCompatibility
 /**
  * Represents a horizontal side of a layout (i.e top and bottom) that can be anchored using
  * [linkTo] in their `Modifier.constrainAs` blocks.
@@ -53,6 +55,7 @@
     )
 }
 
+@JvmDefaultWithCompatibility
 /**
  * Represents the [FirstBaseline] of a layout that can be anchored
  * using [linkTo] in their `Modifier.constrainAs` blocks.
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
index c21e716..ff210be 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
@@ -20,6 +20,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.constraintlayout.core.state.Transition
 
+@JvmDefaultWithCompatibility
 /**
  * Immutable description of the constraints used to layout the children of a [ConstraintLayout].
  */
@@ -38,6 +39,7 @@
     fun isDirty(measurables: List<Measurable>): Boolean = true
 }
 
+@JvmDefaultWithCompatibility
 @Immutable
 internal interface DerivedConstraintSet : ConstraintSet {
     /**
diff --git a/core/core-performance/api/current.txt b/core/core-performance/api/current.txt
index 6715c6c..406c06f 100644
--- a/core/core-performance/api/current.txt
+++ b/core/core-performance/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.core.performance {
 
-  public interface DevicePerformance {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
     method public default static androidx.core.performance.DevicePerformance create(android.content.Context context);
     method public int getMediaPerformanceClass();
     property public abstract int mediaPerformanceClass;
diff --git a/core/core-performance/api/public_plus_experimental_current.txt b/core/core-performance/api/public_plus_experimental_current.txt
index 6715c6c..406c06f 100644
--- a/core/core-performance/api/public_plus_experimental_current.txt
+++ b/core/core-performance/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.core.performance {
 
-  public interface DevicePerformance {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
     method public default static androidx.core.performance.DevicePerformance create(android.content.Context context);
     method public int getMediaPerformanceClass();
     property public abstract int mediaPerformanceClass;
diff --git a/core/core-performance/api/restricted_current.txt b/core/core-performance/api/restricted_current.txt
index 6715c6c..406c06f 100644
--- a/core/core-performance/api/restricted_current.txt
+++ b/core/core-performance/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.core.performance {
 
-  public interface DevicePerformance {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
     method public default static androidx.core.performance.DevicePerformance create(android.content.Context context);
     method public int getMediaPerformanceClass();
     property public abstract int mediaPerformanceClass;
diff --git a/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt b/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt
index 874d323..ae4e414 100644
--- a/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt
+++ b/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.os.Build
 
+@JvmDefaultWithCompatibility
 /**
  * Reports the media performance class of the device.
  *
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 52d84ae..2e29520 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -44,6 +44,7 @@
 
 android {
     defaultConfig {
+        minSdkVersion 33
         multiDexEnabled = true
     }
 
diff --git a/core/uwb/uwb/api/current.txt b/core/uwb/uwb/api/current.txt
index e7d77f6..34d2568 100644
--- a/core/uwb/uwb/api/current.txt
+++ b/core/uwb/uwb/api/current.txt
@@ -123,7 +123,7 @@
     method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
   }
 
-  public interface UwbManager {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager {
     method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbClientSessionScope>);
     method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControleeSessionScope>);
     method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControllerSessionScope>);
diff --git a/core/uwb/uwb/api/public_plus_experimental_current.txt b/core/uwb/uwb/api/public_plus_experimental_current.txt
index e7d77f6..34d2568 100644
--- a/core/uwb/uwb/api/public_plus_experimental_current.txt
+++ b/core/uwb/uwb/api/public_plus_experimental_current.txt
@@ -123,7 +123,7 @@
     method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
   }
 
-  public interface UwbManager {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager {
     method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbClientSessionScope>);
     method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControleeSessionScope>);
     method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControllerSessionScope>);
diff --git a/core/uwb/uwb/api/restricted_current.txt b/core/uwb/uwb/api/restricted_current.txt
index e7d77f6..34d2568 100644
--- a/core/uwb/uwb/api/restricted_current.txt
+++ b/core/uwb/uwb/api/restricted_current.txt
@@ -123,7 +123,7 @@
     method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
   }
 
-  public interface UwbManager {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager {
     method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbClientSessionScope>);
     method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControleeSessionScope>);
     method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControllerSessionScope>);
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 85d148ce..24a6baa 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -55,6 +55,7 @@
 android {
     namespace "androidx.core.uwb"
     defaultConfig {
+        minSdkVersion 33
         multiDexEnabled = true
     }
 }
diff --git a/core/uwb/uwb/src/main/AndroidManifest.xml b/core/uwb/uwb/src/main/AndroidManifest.xml
index ef5bd56..fd303e0 100644
--- a/core/uwb/uwb/src/main/AndroidManifest.xml
+++ b/core/uwb/uwb/src/main/AndroidManifest.xml
@@ -15,5 +15,7 @@
   limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
-</manifest>
\ No newline at end of file
+    <queries>
+        <package android:name="androidx.core.uwb.backend" />
+    </queries>
+</manifest>
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
index 62e938d..4167e093 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
@@ -22,4 +22,10 @@
  * @property channel the current channel for the device.
  * @property preambleIndex the current preamble index for the device.
  */
-class UwbComplexChannel(val channel: Int, val preambleIndex: Int)
\ No newline at end of file
+class UwbComplexChannel(val channel: Int, val preambleIndex: Int) {
+
+    /** Returns the string format of [UwbComplexChannel]. */
+    override fun toString(): String {
+        return "UwbComplexChannel(channel=$channel, preambleIndex=$preambleIndex)"
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
index e666470..8a0305e 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import androidx.core.uwb.impl.UwbManagerImpl
 
+@JvmDefaultWithCompatibility
 /**
  * Interface for getting UWB capabilities and interacting with nearby UWB devices to perform
  * ranging.
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
new file mode 100644
index 0000000..2094c06
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * 
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingSessionCallback
+ *
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal", "CallbackMethodName"})
+public interface IRangingSessionCallback extends android.os.IInterface
+{
+    /**
+     * The version of this interface that the caller is built against.
+     * This might be different from what {@link #getInterfaceVersion()
+     * getInterfaceVersion} returns as that is the version of the interface
+     * that the remote object is implementing.
+     */
+    public static final int VERSION = 1;
+    public static final String HASH = "notfrozen";
+    /** Default implementation for IRangingSessionCallback. */
+    public static class Default implements androidx.core.uwb.backend.IRangingSessionCallback
+    {
+        @Override public void onRangingInitialized(@NonNull UwbDevice device) throws android.os.RemoteException
+        {
+        }
+        @Override public void onRangingResult(@NonNull UwbDevice device,
+                @NonNull RangingPosition position) throws android.os.RemoteException
+        {
+        }
+        @Override public void onRangingSuspended(@NonNull UwbDevice device, int reason) throws android.os.RemoteException
+        {
+        }
+        @Override
+        public int getInterfaceVersion() {
+            return 0;
+        }
+
+        @NonNull
+        @Override
+        public String getInterfaceHash() {
+            return "";
+        }
+        @Override
+        @SuppressLint("MissingNullability")
+        public android.os.IBinder asBinder() {
+            return null;
+        }
+    }
+    /** Local-side IPC implementation stub class. */
+    @SuppressLint("RawAidl")
+    public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IRangingSessionCallback
+    {
+        /** Construct the stub at attach it to the interface. */
+        public Stub()
+        {
+            this.attachInterface(this, DESCRIPTOR);
+        }
+        /**
+         * Cast an IBinder object into an androidx.core.uwb.backend.IRangingSessionCallback interface,
+         * generating a proxy if needed.
+         */
+        @Nullable
+        public static androidx.core.uwb.backend.IRangingSessionCallback asInterface(
+                @Nullable IBinder obj)
+        {
+            if ((obj==null)) {
+                return null;
+            }
+            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+            if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IRangingSessionCallback))) {
+                return ((androidx.core.uwb.backend.IRangingSessionCallback)iin);
+            }
+            return new androidx.core.uwb.backend.IRangingSessionCallback.Stub.Proxy(obj);
+        }
+        @SuppressLint("MissingNullability")
+        @Override public android.os.IBinder asBinder()
+        {
+            return this;
+        }
+        @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+                int flags) throws android.os.RemoteException
+        {
+            java.lang.String descriptor = DESCRIPTOR;
+            if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+                data.enforceInterface(descriptor);
+            }
+            switch (code)
+            {
+                case INTERFACE_TRANSACTION:
+                {
+                    reply.writeString(descriptor);
+                    return true;
+                }
+                case TRANSACTION_getInterfaceVersion:
+                {
+                    reply.writeNoException();
+                    reply.writeInt(getInterfaceVersion());
+                    return true;
+                }
+                case TRANSACTION_getInterfaceHash:
+                {
+                    reply.writeNoException();
+                    reply.writeString(getInterfaceHash());
+                    return true;
+                }
+            }
+            switch (code)
+            {
+                case TRANSACTION_onRangingInitialized:
+                {
+                    androidx.core.uwb.backend.UwbDevice _arg0;
+                    _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+                    data.enforceNoDataAvail();
+                    this.onRangingInitialized(_arg0);
+                    break;
+                }
+                case TRANSACTION_onRangingResult:
+                {
+                    androidx.core.uwb.backend.UwbDevice _arg0;
+                    _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+                    androidx.core.uwb.backend.RangingPosition _arg1;
+                    _arg1 = data.readTypedObject(androidx.core.uwb.backend.RangingPosition.CREATOR);
+                    data.enforceNoDataAvail();
+                    this.onRangingResult(_arg0, _arg1);
+                    break;
+                }
+                case TRANSACTION_onRangingSuspended:
+                {
+                    androidx.core.uwb.backend.UwbDevice _arg0;
+                    _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+                    int _arg1;
+                    _arg1 = data.readInt();
+                    data.enforceNoDataAvail();
+                    this.onRangingSuspended(_arg0, _arg1);
+                    break;
+                }
+                default:
+                {
+                    return super.onTransact(code, data, reply, flags);
+                }
+            }
+            return true;
+        }
+        private static class Proxy implements androidx.core.uwb.backend.IRangingSessionCallback
+        {
+            private android.os.IBinder mRemote;
+            Proxy(android.os.IBinder remote)
+            {
+                mRemote = remote;
+            }
+            private int mCachedVersion = -1;
+            private String mCachedHash = "-1";
+            @Override public android.os.IBinder asBinder()
+            {
+                return mRemote;
+            }
+            @SuppressLint("UnusedMethod")
+            public java.lang.String getInterfaceDescriptor()
+            {
+                return DESCRIPTOR;
+            }
+            @Override public void onRangingInitialized(@NonNull androidx.core.uwb.backend.UwbDevice device) throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeTypedObject(device, 0);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingInitialized, _data, null, android.os.IBinder.FLAG_ONEWAY);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method onRangingInitialized is unimplemented.");
+                    }
+                }
+                finally {
+                    _data.recycle();
+                }
+            }
+            @Override public void onRangingResult(@NonNull androidx.core.uwb.backend.UwbDevice device, @NonNull androidx.core.uwb.backend.RangingPosition position) throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeTypedObject(device, 0);
+                    _data.writeTypedObject(position, 0);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingResult, _data, null, android.os.IBinder.FLAG_ONEWAY);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method onRangingResult is unimplemented.");
+                    }
+                }
+                finally {
+                    _data.recycle();
+                }
+            }
+            @Override public void onRangingSuspended(@NonNull androidx.core.uwb.backend.UwbDevice device, int reason) throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeTypedObject(device, 0);
+                    _data.writeInt(reason);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingSuspended, _data, null, android.os.IBinder.FLAG_ONEWAY);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method onRangingSuspended is unimplemented.");
+                    }
+                }
+                finally {
+                    _data.recycle();
+                }
+            }
+            @Override
+            @SuppressLint("UnusedVariable")
+            public int getInterfaceVersion() throws android.os.RemoteException {
+                if (mCachedVersion == -1) {
+                    android.os.Parcel data = android.os.Parcel.obtain();
+                    android.os.Parcel reply = android.os.Parcel.obtain();
+                    try {
+                        data.writeInterfaceToken(DESCRIPTOR);
+                        boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+                        reply.readException();
+                        mCachedVersion = reply.readInt();
+                    } finally {
+                        reply.recycle();
+                        data.recycle();
+                    }
+                }
+                return mCachedVersion;
+            }
+            @NonNull
+            @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+            @Override
+            public synchronized String getInterfaceHash() throws android.os.RemoteException {
+                if ("-1".equals(mCachedHash)) {
+                    android.os.Parcel data = android.os.Parcel.obtain();
+                    android.os.Parcel reply = android.os.Parcel.obtain();
+                    try {
+                        data.writeInterfaceToken(DESCRIPTOR);
+                        boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+                        reply.readException();
+                        mCachedHash = reply.readString();
+                    } finally {
+                        reply.recycle();
+                        data.recycle();
+                    }
+                }
+                return mCachedHash;
+            }
+        }
+        static final int TRANSACTION_onRangingInitialized = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+        static final int TRANSACTION_onRangingResult = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+        static final int TRANSACTION_onRangingSuspended = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+        static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+        static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+    }
+    public static final java.lang.String DESCRIPTOR = "androidx"
+            + ".core.uwb.backend.IRangingSessionCallback";
+    /** Reasons for suspend */
+    public static final int UNKNOWN = 0;
+    public static final int WRONG_PARAMETERS = 1;
+    public static final int FAILED_TO_START = 2;
+    public static final int STOPPED_BY_PEER = 3;
+    public static final int STOP_RANGING_CALLED = 4;
+    @SuppressLint("MinMaxConstant")
+    public static final int MAX_RANGING_ROUND_RETRY_REACHED = 5;
+    public void onRangingInitialized(@NonNull UwbDevice device) throws android.os.RemoteException;
+    public void onRangingResult(@NonNull UwbDevice device, @NonNull RangingPosition position) throws android.os.RemoteException;
+    public void onRangingSuspended(@NonNull UwbDevice device, int reason) throws android.os.RemoteException;
+    @SuppressLint("CallbackMethodName")
+    public int getInterfaceVersion() throws android.os.RemoteException;
+
+    @NonNull
+    @SuppressLint("CallbackMethodName")
+    public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
new file mode 100644
index 0000000..5008f91
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+ /**
+  * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal"})
+public interface IUwb extends android.os.IInterface
+{
+    /**
+     * The version of this interface that the caller is built against.
+     * This might be different from what {@link #getInterfaceVersion()
+     * getInterfaceVersion} returns as that is the version of the interface
+     * that the remote object is implementing.
+     */
+    public static final int VERSION = 1;
+    public static final String HASH = "notfrozen";
+    /** Default implementation for IUwb. */
+    public static class Default implements androidx.core.uwb.backend.IUwb
+    {
+        @NonNull
+        @Override
+        public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException
+        {
+            return null;
+        }
+
+        @NonNull
+        @Override
+        public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException
+        {
+            return null;
+        }
+        @Override
+        public int getInterfaceVersion() {
+            return 0;
+        }
+
+        @NonNull
+        @Override
+        public String getInterfaceHash() {
+            return "";
+        }
+        @Override
+        @SuppressLint("MissingNullability")
+        public android.os.IBinder asBinder() {
+            return null;
+        }
+    }
+    /** Local-side IPC implementation stub class. */
+    @SuppressLint("RawAidl")
+    public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IUwb
+    {
+        /** Construct the stub at attach it to the interface. */
+        public Stub()
+        {
+            this.attachInterface(this, DESCRIPTOR);
+        }
+        /**
+         * Cast an IBinder object into an androidx.core.uwb.backend.IUwb interface,
+         * generating a proxy if needed.
+         */
+        @Nullable
+        public static androidx.core.uwb.backend.IUwb asInterface(@Nullable IBinder obj)
+        {
+            if ((obj==null)) {
+                return null;
+            }
+            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+            if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IUwb))) {
+                return ((androidx.core.uwb.backend.IUwb)iin);
+            }
+            return new androidx.core.uwb.backend.IUwb.Stub.Proxy(obj);
+        }
+        @SuppressLint("MissingNullability")
+        @Override public android.os.IBinder asBinder()
+        {
+            return this;
+        }
+        @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+                int flags) throws android.os.RemoteException
+        {
+            java.lang.String descriptor = DESCRIPTOR;
+            if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+                data.enforceInterface(descriptor);
+            }
+            switch (code)
+            {
+                case INTERFACE_TRANSACTION:
+                {
+                    reply.writeString(descriptor);
+                    return true;
+                }
+                case TRANSACTION_getInterfaceVersion:
+                {
+                    reply.writeNoException();
+                    reply.writeInt(getInterfaceVersion());
+                    return true;
+                }
+                case TRANSACTION_getInterfaceHash:
+                {
+                    reply.writeNoException();
+                    reply.writeString(getInterfaceHash());
+                    return true;
+                }
+            }
+            switch (code)
+            {
+                case TRANSACTION_getControleeClient:
+                {
+                    androidx.core.uwb.backend.IUwbClient _result = this.getControleeClient();
+                    reply.writeNoException();
+                    reply.writeStrongInterface(_result);
+                    break;
+                }
+                case TRANSACTION_getControllerClient:
+                {
+                    androidx.core.uwb.backend.IUwbClient _result = this.getControllerClient();
+                    reply.writeNoException();
+                    reply.writeStrongInterface(_result);
+                    break;
+                }
+                default:
+                {
+                    return super.onTransact(code, data, reply, flags);
+                }
+            }
+            return true;
+        }
+        private static class Proxy implements androidx.core.uwb.backend.IUwb
+        {
+            private android.os.IBinder mRemote;
+            Proxy(android.os.IBinder remote)
+            {
+                mRemote = remote;
+            }
+            private int mCachedVersion = -1;
+            private String mCachedHash = "-1";
+            @Override public android.os.IBinder asBinder()
+            {
+                return mRemote;
+            }
+            @SuppressLint("UnusedMethod")
+            public java.lang.String getInterfaceDescriptor()
+            {
+                return DESCRIPTOR;
+            }
+            @NonNull
+            @Override public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                androidx.core.uwb.backend.IUwbClient _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_getControleeClient, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method getControleeClient is unimplemented.");
+                    }
+                    _reply.readException();
+                    _result = androidx.core.uwb.backend.IUwbClient.Stub.asInterface(_reply.readStrongBinder());
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+            @NonNull
+            @Override public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                androidx.core.uwb.backend.IUwbClient _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_getControllerClient, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method getControllerClient is unimplemented.");
+                    }
+                    _reply.readException();
+                    _result = androidx.core.uwb.backend.IUwbClient.Stub.asInterface(_reply.readStrongBinder());
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+            @Override
+            @SuppressLint("UnusedVariable")
+            public int getInterfaceVersion() throws android.os.RemoteException {
+                if (mCachedVersion == -1) {
+                    android.os.Parcel data = android.os.Parcel.obtain();
+                    android.os.Parcel reply = android.os.Parcel.obtain();
+                    try {
+                        data.writeInterfaceToken(DESCRIPTOR);
+                        boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+                        reply.readException();
+                        mCachedVersion = reply.readInt();
+                    } finally {
+                        reply.recycle();
+                        data.recycle();
+                    }
+                }
+                return mCachedVersion;
+            }
+            @NonNull
+            @Override
+            @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+            public synchronized String getInterfaceHash() throws android.os.RemoteException {
+                if ("-1".equals(mCachedHash)) {
+                    android.os.Parcel data = android.os.Parcel.obtain();
+                    android.os.Parcel reply = android.os.Parcel.obtain();
+                    try {
+                        data.writeInterfaceToken(DESCRIPTOR);
+                        boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+                        reply.readException();
+                        mCachedHash = reply.readString();
+                    } finally {
+                        reply.recycle();
+                        data.recycle();
+                    }
+                }
+                return mCachedHash;
+            }
+        }
+        static final int TRANSACTION_getControleeClient = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+        static final int TRANSACTION_getControllerClient = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+        static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+        static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+    }
+    public static final java.lang.String DESCRIPTOR =
+            "androidx.core.uwb.backend.IUwb";
+
+    @NonNull
+    public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException;
+
+    @NonNull
+    public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException;
+    public int getInterfaceVersion() throws android.os.RemoteException;
+
+    @NonNull
+    public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
new file mode 100644
index 0000000..e9f53ea
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbClient
+ *
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal", "ExecutorRegistration"})
+public interface IUwbClient extends android.os.IInterface
+{
+    /**
+     * The version of this interface that the caller is built against.
+     * This might be different from what {@link #getInterfaceVersion()
+     * getInterfaceVersion} returns as that is the version of the interface
+     * that the remote object is implementing.
+     */
+    public static final int VERSION = 1;
+    public static final String HASH = "notfrozen";
+    /** Default implementation for IUwbClient. */
+    public static class Default implements androidx.core.uwb.backend.IUwbClient
+    {
+        @Override public boolean isAvailable() throws android.os.RemoteException
+        {
+            return false;
+        }
+
+        @Nullable
+        @Override
+        public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException
+        {
+            return null;
+        }
+        @Nullable
+        @Override public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException
+        {
+            return null;
+        }
+        @Nullable
+        @Override public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException
+        {
+            return null;
+        }
+        @SuppressLint("ExecutorRegistration")
+        @Override public void startRanging(@NonNull RangingParameters parameters,
+                @NonNull IRangingSessionCallback callback) throws android.os.RemoteException
+        {
+        }
+        @SuppressLint("ExecutorRegistration")
+        @Override public void stopRanging(@NonNull IRangingSessionCallback callback) throws android.os.RemoteException
+        {
+        }
+        @Override public void addControlee(@NonNull UwbAddress address) throws android.os.RemoteException
+        {
+        }
+        @Override public void removeControlee(@NonNull UwbAddress address) throws android.os.RemoteException
+        {
+        }
+        @Override
+        public int getInterfaceVersion() {
+            return 0;
+        }
+
+        @NonNull
+        @Override
+        public String getInterfaceHash() {
+            return "";
+        }
+        @Override
+        @SuppressLint("MissingNullability")
+        public android.os.IBinder asBinder() {
+            return null;
+        }
+    }
+    /** Local-side IPC implementation stub class. */
+    @SuppressLint("RawAidl")
+    public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IUwbClient
+    {
+        /** Construct the stub at attach it to the interface. */
+        public Stub()
+        {
+            this.attachInterface(this, DESCRIPTOR);
+        }
+        /**
+         * Cast an IBinder object into an androidx.core.uwb.backend.IUwbClient interface,
+         * generating a proxy if needed.
+         */
+        @Nullable
+        public static androidx.core.uwb.backend.IUwbClient asInterface(@Nullable IBinder obj)
+        {
+            if ((obj==null)) {
+                return null;
+            }
+            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+            if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IUwbClient))) {
+                return ((androidx.core.uwb.backend.IUwbClient)iin);
+            }
+            return new androidx.core.uwb.backend.IUwbClient.Stub.Proxy(obj);
+        }
+        @SuppressLint("MissingNullability")
+        @Override public android.os.IBinder asBinder()
+        {
+            return this;
+        }
+        @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+                int flags) throws android.os.RemoteException
+        {
+            java.lang.String descriptor = DESCRIPTOR;
+            if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+                data.enforceInterface(descriptor);
+            }
+            switch (code)
+            {
+                case INTERFACE_TRANSACTION:
+                {
+                    reply.writeString(descriptor);
+                    return true;
+                }
+                case TRANSACTION_getInterfaceVersion:
+                {
+                    reply.writeNoException();
+                    reply.writeInt(getInterfaceVersion());
+                    return true;
+                }
+                case TRANSACTION_getInterfaceHash:
+                {
+                    reply.writeNoException();
+                    reply.writeString(getInterfaceHash());
+                    return true;
+                }
+            }
+            switch (code)
+            {
+                case TRANSACTION_isAvailable:
+                {
+                    boolean _result = this.isAvailable();
+                    reply.writeNoException();
+                    reply.writeBoolean(_result);
+                    break;
+                }
+                case TRANSACTION_getRangingCapabilities:
+                {
+                    androidx.core.uwb.backend.RangingCapabilities _result = this.getRangingCapabilities();
+                    reply.writeNoException();
+                    reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+                    break;
+                }
+                case TRANSACTION_getLocalAddress:
+                {
+                    androidx.core.uwb.backend.UwbAddress _result = this.getLocalAddress();
+                    reply.writeNoException();
+                    reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+                    break;
+                }
+                case TRANSACTION_getComplexChannel:
+                {
+                    androidx.core.uwb.backend.UwbComplexChannel _result = this.getComplexChannel();
+                    reply.writeNoException();
+                    reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+                    break;
+                }
+                case TRANSACTION_startRanging:
+                {
+                    androidx.core.uwb.backend.RangingParameters _arg0;
+                    _arg0 = data.readTypedObject(androidx.core.uwb.backend.RangingParameters.CREATOR);
+                    androidx.core.uwb.backend.IRangingSessionCallback _arg1;
+                    _arg1 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
+                    data.enforceNoDataAvail();
+                    this.startRanging(_arg0, _arg1);
+                    reply.writeNoException();
+                    break;
+                }
+                case TRANSACTION_stopRanging:
+                {
+                    androidx.core.uwb.backend.IRangingSessionCallback _arg0;
+                    _arg0 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
+                    data.enforceNoDataAvail();
+                    this.stopRanging(_arg0);
+                    reply.writeNoException();
+                    break;
+                }
+                case TRANSACTION_addControlee:
+                {
+                    androidx.core.uwb.backend.UwbAddress _arg0;
+                    _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+                    data.enforceNoDataAvail();
+                    this.addControlee(_arg0);
+                    reply.writeNoException();
+                    break;
+                }
+                case TRANSACTION_removeControlee:
+                {
+                    androidx.core.uwb.backend.UwbAddress _arg0;
+                    _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+                    data.enforceNoDataAvail();
+                    this.removeControlee(_arg0);
+                    reply.writeNoException();
+                    break;
+                }
+                default:
+                {
+                    return super.onTransact(code, data, reply, flags);
+                }
+            }
+            return true;
+        }
+        private static class Proxy implements androidx.core.uwb.backend.IUwbClient
+        {
+            private android.os.IBinder mRemote;
+            Proxy(android.os.IBinder remote)
+            {
+                mRemote = remote;
+            }
+            private int mCachedVersion = -1;
+            private String mCachedHash = "-1";
+            @Override public android.os.IBinder asBinder()
+            {
+                return mRemote;
+            }
+            @SuppressLint("UnusedMethod")
+            public java.lang.String getInterfaceDescriptor()
+            {
+                return DESCRIPTOR;
+            }
+            @Override public boolean isAvailable() throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                boolean _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_isAvailable, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method isAvailable is unimplemented.");
+                    }
+                    _reply.readException();
+                    _result = _reply.readBoolean();
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+            @NonNull
+            @Override public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                androidx.core.uwb.backend.RangingCapabilities _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_getRangingCapabilities, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method getRangingCapabilities is unimplemented.");
+                    }
+                    _reply.readException();
+                    _result = _reply.readTypedObject(androidx.core.uwb.backend.RangingCapabilities.CREATOR);
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+            @Override public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                androidx.core.uwb.backend.UwbAddress _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_getLocalAddress, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method getLocalAddress is unimplemented.");
+                    }
+                    _reply.readException();
+                    _result = _reply.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+            @Override public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                androidx.core.uwb.backend.UwbComplexChannel _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_getComplexChannel, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method getComplexChannel is unimplemented.");
+                    }
+                    _reply.readException();
+                    _result = _reply.readTypedObject(androidx.core.uwb.backend.UwbComplexChannel.CREATOR);
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+            @Override public void startRanging(@NonNull androidx.core.uwb.backend.RangingParameters parameters, @NonNull androidx.core.uwb.backend.IRangingSessionCallback callback) throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeTypedObject(parameters, 0);
+                    _data.writeStrongInterface(callback);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_startRanging, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method startRanging is unimplemented.");
+                    }
+                    _reply.readException();
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
+            @Override public void stopRanging(@NonNull androidx.core.uwb.backend.IRangingSessionCallback callback) throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeStrongInterface(callback);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_stopRanging, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method stopRanging is unimplemented.");
+                    }
+                    _reply.readException();
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
+            @Override public void addControlee(@NonNull androidx.core.uwb.backend.UwbAddress address) throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeTypedObject(address, 0);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_addControlee, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method addControlee is unimplemented.");
+                    }
+                    _reply.readException();
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
+            @Override public void removeControlee(@NonNull androidx.core.uwb.backend.UwbAddress address) throws android.os.RemoteException
+            {
+                android.os.Parcel _data = android.os.Parcel.obtain();
+                android.os.Parcel _reply = android.os.Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeTypedObject(address, 0);
+                    boolean _status = mRemote.transact(Stub.TRANSACTION_removeControlee, _data, _reply, 0);
+                    if (!_status) {
+                        throw new android.os.RemoteException("Method removeControlee is unimplemented.");
+                    }
+                    _reply.readException();
+                }
+                finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
+            @Override
+            @SuppressLint("UnusedVariable")
+            public int getInterfaceVersion() throws android.os.RemoteException {
+                if (mCachedVersion == -1) {
+                    android.os.Parcel data = android.os.Parcel.obtain();
+                    android.os.Parcel reply = android.os.Parcel.obtain();
+                    try {
+                        data.writeInterfaceToken(DESCRIPTOR);
+                        boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+                        reply.readException();
+                        mCachedVersion = reply.readInt();
+                    } finally {
+                        reply.recycle();
+                        data.recycle();
+                    }
+                }
+                return mCachedVersion;
+            }
+            @NonNull
+            @Override
+            @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+            public synchronized String getInterfaceHash() throws android.os.RemoteException {
+                if ("-1".equals(mCachedHash)) {
+                    android.os.Parcel data = android.os.Parcel.obtain();
+                    android.os.Parcel reply = android.os.Parcel.obtain();
+                    try {
+                        data.writeInterfaceToken(DESCRIPTOR);
+                        boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+                        reply.readException();
+                        mCachedHash = reply.readString();
+                    } finally {
+                        reply.recycle();
+                        data.recycle();
+                    }
+                }
+                return mCachedHash;
+            }
+        }
+        static final int TRANSACTION_isAvailable = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+        static final int TRANSACTION_getRangingCapabilities = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+        static final int TRANSACTION_getLocalAddress = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+        static final int TRANSACTION_getComplexChannel = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
+        static final int TRANSACTION_startRanging = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
+        static final int TRANSACTION_stopRanging = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5);
+        static final int TRANSACTION_addControlee = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6);
+        static final int TRANSACTION_removeControlee = (android.os.IBinder.FIRST_CALL_TRANSACTION + 7);
+        static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+        static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+    }
+    public static final java.lang.String DESCRIPTOR =
+            "androidx.core.uwb.backend.IUwbClient";
+    public boolean isAvailable() throws android.os.RemoteException;
+
+    @Nullable
+    public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException;
+
+    @Nullable
+    public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException;
+
+    @Nullable
+    public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException;
+    @SuppressLint("ExecutorRegistration")
+    public void startRanging(@NonNull RangingParameters parameters,
+            @NonNull IRangingSessionCallback callback) throws android.os.RemoteException;
+    @SuppressLint("ExecutorRegistration")
+    public void stopRanging(@NonNull IRangingSessionCallback callback) throws android.os.RemoteException;
+    public void addControlee(@NonNull UwbAddress address) throws android.os.RemoteException;
+    public void removeControlee(@NonNull UwbAddress address) throws android.os.RemoteException;
+    public int getInterfaceVersion() throws android.os.RemoteException;
+
+    @NonNull
+    public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
new file mode 100644
index 0000000..6da98f1
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * 
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingCapabilities
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingCapabilities implements android.os.Parcelable
+{
+    @SuppressLint("MutableBareField")
+    public boolean supportsDistance = false;
+    @SuppressLint("MutableBareField")
+    public boolean supportsAzimuthalAngle = false;
+    @SuppressLint("MutableBareField")
+    public boolean supportsElevationAngle = false;
+    @NonNull
+    public static final android.os.Parcelable.Creator<RangingCapabilities> CREATOR = new android.os.Parcelable.Creator<RangingCapabilities>() {
+        @Override
+        public RangingCapabilities createFromParcel(android.os.Parcel _aidl_source) {
+            RangingCapabilities _aidl_out = new RangingCapabilities();
+            _aidl_out.readFromParcel(_aidl_source);
+            return _aidl_out;
+        }
+        @Override
+        public RangingCapabilities[] newArray(int _aidl_size) {
+            return new RangingCapabilities[_aidl_size];
+        }
+    };
+    @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.writeInt(0);
+        _aidl_parcel.writeBoolean(supportsDistance);
+        _aidl_parcel.writeBoolean(supportsAzimuthalAngle);
+        _aidl_parcel.writeBoolean(supportsElevationAngle);
+        int _aidl_end_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.setDataPosition(_aidl_start_pos);
+        _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+        _aidl_parcel.setDataPosition(_aidl_end_pos);
+    }
+    @SuppressLint("Finally")
+    public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        int _aidl_parcelable_size = _aidl_parcel.readInt();
+        try {
+            if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            supportsDistance = _aidl_parcel.readBoolean();
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            supportsAzimuthalAngle = _aidl_parcel.readBoolean();
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            supportsElevationAngle = _aidl_parcel.readBoolean();
+        } finally {
+            if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+                throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+            }
+            _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+        }
+    }
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
new file mode 100644
index 0000000..5b02460
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingMeasurement
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingMeasurement implements android.os.Parcelable
+{
+    @SuppressLint("MutableBareField")
+    public int confidence = 0;
+    @SuppressLint("MutableBareField")
+    public float value = 0.000000f;
+    @NonNull
+    public static final android.os.Parcelable.Creator<RangingMeasurement> CREATOR = new android.os.Parcelable.Creator<RangingMeasurement>() {
+        @Override
+        public RangingMeasurement createFromParcel(android.os.Parcel _aidl_source) {
+            RangingMeasurement _aidl_out = new RangingMeasurement();
+            _aidl_out.readFromParcel(_aidl_source);
+            return _aidl_out;
+        }
+        @Override
+        public RangingMeasurement[] newArray(int _aidl_size) {
+            return new RangingMeasurement[_aidl_size];
+        }
+    };
+    @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.writeInt(0);
+        _aidl_parcel.writeInt(confidence);
+        _aidl_parcel.writeFloat(value);
+        int _aidl_end_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.setDataPosition(_aidl_start_pos);
+        _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+        _aidl_parcel.setDataPosition(_aidl_end_pos);
+    }
+    @SuppressLint("Finally")
+    public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        int _aidl_parcelable_size = _aidl_parcel.readInt();
+        try {
+            if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            confidence = _aidl_parcel.readInt();
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            value = _aidl_parcel.readFloat();
+        } finally {
+            if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+                throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+            }
+            _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+        }
+    }
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
new file mode 100644
index 0000000..966830c
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingParameters
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingParameters implements android.os.Parcelable
+{
+    @SuppressLint("MutableBareField")
+    public int uwbConfigId;
+    @SuppressLint("MutableBareField")
+    public int sessionId = 0;
+    @Nullable
+    @SuppressLint("MutableBareField")
+    public byte[] sessionKeyInfo;
+    @Nullable
+    @SuppressLint("MutableBareField")
+    public androidx.core.uwb.backend.UwbComplexChannel complexChannel;
+    @Nullable
+    @SuppressLint({"MutableBareField", "NullableCollection"})
+    public java.util.List<androidx.core.uwb.backend.UwbDevice> peerDevices;
+    @SuppressLint("MutableBareField")
+    public int rangingUpdateRate = 0;
+    @NonNull
+    public static final android.os.Parcelable.Creator<RangingParameters> CREATOR = new android.os.Parcelable.Creator<RangingParameters>() {
+        @Override
+        public RangingParameters createFromParcel(android.os.Parcel _aidl_source) {
+            RangingParameters _aidl_out = new RangingParameters();
+            _aidl_out.readFromParcel(_aidl_source);
+            return _aidl_out;
+        }
+        @Override
+        public RangingParameters[] newArray(int _aidl_size) {
+            return new RangingParameters[_aidl_size];
+        }
+    };
+    @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.writeInt(0);
+        _aidl_parcel.writeInt(uwbConfigId);
+        _aidl_parcel.writeInt(sessionId);
+        _aidl_parcel.writeByteArray(sessionKeyInfo);
+        _aidl_parcel.writeTypedObject(complexChannel, _aidl_flag);
+        _aidl_parcel.writeTypedList(peerDevices);
+        _aidl_parcel.writeInt(rangingUpdateRate);
+        int _aidl_end_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.setDataPosition(_aidl_start_pos);
+        _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+        _aidl_parcel.setDataPosition(_aidl_end_pos);
+    }
+    @SuppressLint("Finally")
+    public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        int _aidl_parcelable_size = _aidl_parcel.readInt();
+        try {
+            if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            uwbConfigId = _aidl_parcel.readInt();
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            sessionId = _aidl_parcel.readInt();
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            sessionKeyInfo = _aidl_parcel.createByteArray();
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            complexChannel = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.UwbComplexChannel.CREATOR);
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            peerDevices = _aidl_parcel.createTypedArrayList(androidx.core.uwb.backend.UwbDevice.CREATOR);
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            rangingUpdateRate = _aidl_parcel.readInt();
+        } finally {
+            if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+                throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+            }
+            _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+        }
+    }
+    @Override
+    public int describeContents() {
+        int _mask = 0;
+        _mask |= describeContents(complexChannel);
+        _mask |= describeContents(peerDevices);
+        return _mask;
+    }
+    private int describeContents(Object _v) {
+        if (_v == null) return 0;
+        if (_v instanceof java.util.Collection) {
+            int _mask = 0;
+            for (Object o : (java.util.Collection) _v) {
+                _mask |= describeContents(o);
+            }
+            return _mask;
+        }
+        if (_v instanceof android.os.Parcelable) {
+            return ((android.os.Parcelable) _v).describeContents();
+        }
+        return 0;
+    }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
new file mode 100644
index 0000000..1173b81
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingPosition
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingPosition implements android.os.Parcelable
+{
+    @Nullable
+    @SuppressLint("MutableBareField")
+    public androidx.core.uwb.backend.RangingMeasurement distance;
+    @Nullable
+    @SuppressLint("MutableBareField")
+    public androidx.core.uwb.backend.RangingMeasurement azimuth;
+    @Nullable
+    @SuppressLint("MutableBareField")
+    public androidx.core.uwb.backend.RangingMeasurement elevation;
+    @SuppressLint("MutableBareField")
+    public long elapsedRealtimeNanos = 0L;
+    @NonNull
+    public static final android.os.Parcelable.Creator<RangingPosition> CREATOR = new android.os.Parcelable.Creator<RangingPosition>() {
+        @Override
+        public RangingPosition createFromParcel(android.os.Parcel _aidl_source) {
+            RangingPosition _aidl_out = new RangingPosition();
+            _aidl_out.readFromParcel(_aidl_source);
+            return _aidl_out;
+        }
+        @Override
+        public RangingPosition[] newArray(int _aidl_size) {
+            return new RangingPosition[_aidl_size];
+        }
+    };
+    @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.writeInt(0);
+        _aidl_parcel.writeTypedObject(distance, _aidl_flag);
+        _aidl_parcel.writeTypedObject(azimuth, _aidl_flag);
+        _aidl_parcel.writeTypedObject(elevation, _aidl_flag);
+        _aidl_parcel.writeLong(elapsedRealtimeNanos);
+        int _aidl_end_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.setDataPosition(_aidl_start_pos);
+        _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+        _aidl_parcel.setDataPosition(_aidl_end_pos);
+    }
+    @SuppressLint("Finally")
+    public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        int _aidl_parcelable_size = _aidl_parcel.readInt();
+        try {
+            if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            distance = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            azimuth = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            elevation = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            elapsedRealtimeNanos = _aidl_parcel.readLong();
+        } finally {
+            if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+                throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+            }
+            _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+        }
+    }
+    @Override
+    public int describeContents() {
+        int _mask = 0;
+        _mask |= describeContents(distance);
+        _mask |= describeContents(azimuth);
+        _mask |= describeContents(elevation);
+        return _mask;
+    }
+    private int describeContents(Object _v) {
+        if (_v == null) return 0;
+        if (_v instanceof android.os.Parcelable) {
+            return ((android.os.Parcelable) _v).describeContents();
+        }
+        return 0;
+    }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
new file mode 100644
index 0000000..e076cd1
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbAddress
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbAddress implements android.os.Parcelable
+{
+    @Nullable
+    @SuppressLint("MutableBareField")
+    public byte[] address;
+    @NonNull
+    public static final android.os.Parcelable.Creator<UwbAddress> CREATOR = new android.os.Parcelable.Creator<UwbAddress>() {
+        @Override
+        public UwbAddress createFromParcel(android.os.Parcel _aidl_source) {
+            UwbAddress _aidl_out = new UwbAddress();
+            _aidl_out.readFromParcel(_aidl_source);
+            return _aidl_out;
+        }
+        @Override
+        public UwbAddress[] newArray(int _aidl_size) {
+            return new UwbAddress[_aidl_size];
+        }
+    };
+    @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.writeInt(0);
+        _aidl_parcel.writeByteArray(address);
+        int _aidl_end_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.setDataPosition(_aidl_start_pos);
+        _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+        _aidl_parcel.setDataPosition(_aidl_end_pos);
+    }
+    @SuppressLint("Finally")
+    public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        int _aidl_parcelable_size = _aidl_parcel.readInt();
+        try {
+            if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            address = _aidl_parcel.createByteArray();
+        } finally {
+            if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+                throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+            }
+            _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+        }
+    }
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
new file mode 100644
index 0000000..24d001a
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbComplexChannel
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbComplexChannel implements android.os.Parcelable
+{
+    @SuppressLint("MutableBareField")
+    public int channel = 0;
+    @SuppressLint("MutableBareField")
+    public int preambleIndex = 0;
+    @NonNull
+    public static final android.os.Parcelable.Creator<UwbComplexChannel> CREATOR = new android.os.Parcelable.Creator<UwbComplexChannel>() {
+        @Override
+        public UwbComplexChannel createFromParcel(android.os.Parcel _aidl_source) {
+            UwbComplexChannel _aidl_out = new UwbComplexChannel();
+            _aidl_out.readFromParcel(_aidl_source);
+            return _aidl_out;
+        }
+        @Override
+        public UwbComplexChannel[] newArray(int _aidl_size) {
+            return new UwbComplexChannel[_aidl_size];
+        }
+    };
+    @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.writeInt(0);
+        _aidl_parcel.writeInt(channel);
+        _aidl_parcel.writeInt(preambleIndex);
+        int _aidl_end_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.setDataPosition(_aidl_start_pos);
+        _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+        _aidl_parcel.setDataPosition(_aidl_end_pos);
+    }
+    @SuppressLint("Finally")
+    public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        int _aidl_parcelable_size = _aidl_parcel.readInt();
+        try {
+            if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            channel = _aidl_parcel.readInt();
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            preambleIndex = _aidl_parcel.readInt();
+        } finally {
+            if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+                throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+            }
+            _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+        }
+    }
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
new file mode 100644
index 0000000..054aad7
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file is auto-generated.  DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbDevice
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbDevice implements android.os.Parcelable
+{
+    @Nullable
+    @SuppressLint("MutableBareField")
+    public androidx.core.uwb.backend.UwbAddress address;
+    @NonNull
+    public static final android.os.Parcelable.Creator<UwbDevice> CREATOR = new android.os.Parcelable.Creator<UwbDevice>() {
+        @Override
+        public UwbDevice createFromParcel(android.os.Parcel _aidl_source) {
+            UwbDevice _aidl_out = new UwbDevice();
+            _aidl_out.readFromParcel(_aidl_source);
+            return _aidl_out;
+        }
+        @Override
+        public UwbDevice[] newArray(int _aidl_size) {
+            return new UwbDevice[_aidl_size];
+        }
+    };
+    @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.writeInt(0);
+        _aidl_parcel.writeTypedObject(address, _aidl_flag);
+        int _aidl_end_pos = _aidl_parcel.dataPosition();
+        _aidl_parcel.setDataPosition(_aidl_start_pos);
+        _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+        _aidl_parcel.setDataPosition(_aidl_end_pos);
+    }
+    @SuppressLint("Finally")
+    public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+    {
+        int _aidl_start_pos = _aidl_parcel.dataPosition();
+        int _aidl_parcelable_size = _aidl_parcel.readInt();
+        try {
+            if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+            if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+            address = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+        } finally {
+            if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+                throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+            }
+            _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+        }
+    }
+    @Override
+    public int describeContents() {
+        int _mask = 0;
+        _mask |= describeContents(address);
+        return _mask;
+    }
+    private int describeContents(Object _v) {
+        if (_v == null) return 0;
+        if (_v instanceof android.os.Parcelable) {
+            return ((android.os.Parcelable) _v).describeContents();
+        }
+        return 0;
+    }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt
new file mode 100644
index 0000000..fea81c2
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.core.uwb.impl
+
+import android.util.Log
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingMeasurement
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControleeSessionScope
+import androidx.core.uwb.backend.IRangingSessionCallback
+import androidx.core.uwb.backend.IUwbClient
+import androidx.core.uwb.backend.RangingPosition
+import androidx.core.uwb.backend.UwbComplexChannel
+import androidx.core.uwb.backend.UwbDevice
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+internal class UwbClientSessionScopeAospImpl(
+    private val uwbClient: IUwbClient,
+    override val rangingCapabilities: RangingCapabilities,
+    override val localAddress: UwbAddress,
+) : UwbControleeSessionScope {
+    companion object {
+        private const val TAG = "UwbClientSessionScope"
+    }
+    private var sessionStarted = false
+
+    override fun prepareSession(parameters: RangingParameters) = callbackFlow {
+        if (sessionStarted) {
+            throw IllegalStateException("Ranging has already started. To initiate " +
+                "a new ranging session, create a new client session scope.")
+        }
+
+        val parametersBuilder1 = androidx.core.uwb.backend.RangingParameters()
+        parametersBuilder1.uwbConfigId = when (parameters.uwbConfigType) {
+            RangingParameters.UWB_CONFIG_ID_1 -> RangingParameters.UWB_CONFIG_ID_1
+            RangingParameters.UWB_CONFIG_ID_3 -> RangingParameters.UWB_CONFIG_ID_3
+            else -> throw IllegalArgumentException("The selected UWB Config Id is not a valid id.")
+        }
+        parametersBuilder1.rangingUpdateRate = when (parameters.updateRateType) {
+            RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC ->
+                RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC
+            RangingParameters.RANGING_UPDATE_RATE_FREQUENT ->
+                RangingParameters.RANGING_UPDATE_RATE_FREQUENT
+            RangingParameters.RANGING_UPDATE_RATE_INFREQUENT ->
+                RangingParameters.RANGING_UPDATE_RATE_INFREQUENT
+            else -> throw IllegalArgumentException(
+                "The selected ranging update rate is not a valid update rate.")
+        }
+        parametersBuilder1.sessionId = parameters.sessionId
+        parametersBuilder1.sessionKeyInfo = parameters.sessionKeyInfo
+        if (parameters.complexChannel != null) {
+            val channel = UwbComplexChannel()
+            channel.channel = parameters.complexChannel.channel
+            channel.preambleIndex = parameters.complexChannel.preambleIndex
+            parametersBuilder1.complexChannel = channel
+        }
+
+        val peerList = ArrayList<UwbDevice>()
+        for (peer in parameters.peerDevices) {
+            val device = UwbDevice()
+            val address = androidx.core.uwb.backend.UwbAddress()
+            address.address = peer.address.address
+            device.address = address
+            peerList.add(device)
+        }
+        parametersBuilder1.peerDevices = peerList
+        val callback =
+            object : IRangingSessionCallback.Stub() {
+                override fun onRangingInitialized(device: UwbDevice) {
+                    Log.i(TAG, "Started UWB ranging.")
+                }
+
+                override fun onRangingResult(device: UwbDevice, position: RangingPosition) {
+                    trySend(
+                        RangingResult.RangingResultPosition(
+                            androidx.core.uwb.UwbDevice(UwbAddress(device.address?.address!!)),
+                            androidx.core.uwb.RangingPosition(
+                                position.distance?.let { RangingMeasurement(it.value) },
+                                position.azimuth?.let {
+                                    RangingMeasurement(it.value)
+                                },
+                                position.elevation?.let {
+                                    RangingMeasurement(it.value)
+                                },
+                                position.elapsedRealtimeNanos
+                            )
+                        )
+                    )
+                }
+
+                override fun onRangingSuspended(device: UwbDevice, reason: Int) {
+                    trySend(
+                        RangingResult.RangingResultPeerDisconnected(
+                            androidx.core.uwb.UwbDevice(UwbAddress(device.address?.address!!))
+                        )
+                    )
+                }
+
+                override fun getInterfaceVersion(): Int {
+                    return 0
+                }
+
+                override fun getInterfaceHash(): String {
+                    return ""
+                }
+            }
+
+        try {
+            uwbClient.startRanging(parametersBuilder1, callback)
+            sessionStarted = true
+        } catch (e: Exception) {
+            throw(e)
+        }
+
+        awaitClose {
+            CoroutineScope(Dispatchers.Main.immediate).launch {
+                try {
+                    uwbClient.stopRanging(callback)
+                } catch (e: Exception) {
+                    throw(e)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt
new file mode 100644
index 0000000..e39bed3
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.core.uwb.impl
+
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControleeSessionScope
+import androidx.core.uwb.backend.IUwbClient
+import kotlinx.coroutines.flow.Flow
+
+internal class UwbControleeSessionScopeAospImpl(
+    uwbClient: IUwbClient,
+    override val rangingCapabilities: RangingCapabilities,
+    override val localAddress: UwbAddress
+) : UwbControleeSessionScope {
+    private val uwbClientSessionScope =
+        UwbClientSessionScopeAospImpl(uwbClient, rangingCapabilities, localAddress)
+
+    override fun prepareSession(parameters: RangingParameters): Flow<RangingResult> {
+        return uwbClientSessionScope.prepareSession(parameters)
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt
new file mode 100644
index 0000000..0630d41
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.core.uwb.impl
+
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbComplexChannel
+import androidx.core.uwb.UwbControllerSessionScope
+import androidx.core.uwb.backend.IUwbClient
+import kotlinx.coroutines.flow.Flow
+
+internal class UwbControllerSessionScopeAospImpl(
+    private val uwbClient: IUwbClient,
+    override val rangingCapabilities: RangingCapabilities,
+    override val localAddress: UwbAddress,
+    override val uwbComplexChannel: UwbComplexChannel
+) : UwbControllerSessionScope {
+    private val uwbClientSessionScope =
+        UwbClientSessionScopeAospImpl(uwbClient, rangingCapabilities, localAddress)
+    override suspend fun addControlee(address: UwbAddress) {
+        val uwbAddress = androidx.core.uwb.backend.UwbAddress()
+        uwbAddress.address = address.address
+        try {
+            uwbClient.addControlee(uwbAddress)
+        } catch (e: Exception) {
+            throw(e)
+        }
+    }
+
+    override suspend fun removeControlee(address: UwbAddress) {
+        val uwbAddress = androidx.core.uwb.backend.UwbAddress()
+        uwbAddress.address = address.address
+        try {
+            uwbClient.removeControlee(uwbAddress)
+        } catch (e: Exception) {
+            throw(e)
+        }
+    }
+
+    override fun prepareSession(parameters: RangingParameters): Flow<RangingResult> {
+        return uwbClientSessionScope.prepareSession(parameters)
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
index bbf9cec..84297cf 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
@@ -16,7 +16,12 @@
 
 package androidx.core.uwb.impl
 
+import android.content.ComponentName
 import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
 import androidx.core.uwb.RangingCapabilities
 import androidx.core.uwb.UwbAddress
 import androidx.core.uwb.UwbClientSessionScope
@@ -24,13 +29,35 @@
 import androidx.core.uwb.UwbControleeSessionScope
 import androidx.core.uwb.UwbControllerSessionScope
 import androidx.core.uwb.UwbManager
+import androidx.core.uwb.backend.IUwb
 import com.google.android.gms.common.api.ApiException
 import com.google.android.gms.nearby.Nearby
 import kotlinx.coroutines.tasks.await
 import androidx.core.uwb.helper.checkSystemFeature
 import androidx.core.uwb.helper.handleApiException
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
 
 internal class UwbManagerImpl(private val context: Context) : UwbManager {
+    companion object {
+        const val TAG = "UwbMangerImpl"
+        var iUwb: IUwb? = null
+    }
+
+    init {
+        val connection = object : ServiceConnection {
+            override fun onServiceConnected(className: ComponentName, service: IBinder) {
+                iUwb = IUwb.Stub.asInterface(service)
+                Log.i(TAG, "iUwb service created successfully.")
+            }
+            override fun onServiceDisconnected(p0: ComponentName?) {
+                iUwb = null
+            }
+        }
+        val intent = Intent("androidx.core.uwb.backend.service")
+        intent.setPackage("androidx.core.uwb.backend")
+        context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+    }
     @Deprecated("Renamed to controleeSessionScope")
     override suspend fun clientSessionScope(): UwbClientSessionScope {
         return createClientSessionScope(false)
@@ -46,6 +73,14 @@
 
     private suspend fun createClientSessionScope(isController: Boolean): UwbClientSessionScope {
         checkSystemFeature(context)
+        val hasGmsCore = GoogleApiAvailability.getInstance()
+            .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+        return if (hasGmsCore) createGmsClientSessionScope(isController)
+        else createAospClientSessionScope(isController)
+    }
+
+    private suspend fun createGmsClientSessionScope(isController: Boolean): UwbClientSessionScope {
+        Log.i(TAG, "Creating Gms Client session scope")
         val uwbClient = if (isController)
             Nearby.getUwbControllerClient(context) else Nearby.getUwbControleeClient(context)
         try {
@@ -77,4 +112,41 @@
                 "up-to-date with the service backend.")
         }
     }
+
+    private fun createAospClientSessionScope(isController: Boolean): UwbClientSessionScope {
+        Log.i(TAG, "Creating Aosp Client session scope")
+        val uwbClient = if (isController)
+            iUwb?.controllerClient else iUwb?.controleeClient
+        if (uwbClient == null) {
+            Log.e(TAG, "Failed to get UwbClient. AOSP backend is not available.")
+        }
+        try {
+            val aospLocalAddress = uwbClient!!.localAddress
+            val aospRangingCapabilities = uwbClient.rangingCapabilities
+            val localAddress = aospLocalAddress?.address?.let { UwbAddress(it) }
+            val rangingCapabilities = aospRangingCapabilities?.let {
+                RangingCapabilities(
+                    it.supportsDistance,
+                    it.supportsAzimuthalAngle,
+                    it.supportsElevationAngle)
+            }
+            return if (isController) {
+                val uwbComplexChannel = uwbClient.complexChannel
+                UwbControllerSessionScopeAospImpl(
+                    uwbClient,
+                    rangingCapabilities!!,
+                    localAddress!!,
+                    UwbComplexChannel(uwbComplexChannel!!.channel, uwbComplexChannel.preambleIndex)
+                )
+            } else {
+                UwbControleeSessionScopeAospImpl(
+                    uwbClient,
+                    rangingCapabilities!!,
+                    localAddress!!
+                )
+            }
+        } catch (e: Exception) {
+            throw e
+        }
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index a3e6a53..e5e59bf 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -32,6 +32,7 @@
 import androidx.credentials.exceptions.CreateCredentialUnknownException
 import androidx.credentials.exceptions.GetCredentialException
 import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
 import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
 import java.util.concurrent.Executor
 
@@ -61,9 +62,13 @@
         if (cancellationReviewer(fragmentManager, cancellationSignal)) {
             return
         }
-        TODO("Not yet implemented")
+        // TODO("Manage Fragment Lifecycle and Fragment Manager Properly")
+        CredentialProviderBeginSignInController.getInstance(fragmentManager).invokePlayServices(
+            request, callback, executor
+        )
     }
 
+    @SuppressWarnings("deprecated")
     override fun onCreateCredential(
         request: CreateCredentialRequest,
         activity: Activity?,
@@ -88,12 +93,15 @@
                 callback,
                 executor)
         } else if (request is CreatePublicKeyCredentialRequest) {
-            TODO("Not yet implemented")
+            // TODO("Add in")
         } else {
             throw UnsupportedOperationException(
                 "Unsupported request; not password or publickeycredential")
         }
     }
+    override fun isAvailableOnDevice(): Boolean {
+        TODO("Not yet implemented")
+    }
 
     @SuppressLint("ClassVerificationFailure", "NewApi")
     private fun cancellationReviewer(
@@ -116,10 +124,6 @@
         return false
     }
 
-    override fun isAvailableOnDevice(): Boolean {
-        TODO("Not yet implemented")
-    }
-
     companion object {
         private val TAG = CredentialProviderPlayServicesImpl::class.java.name
     }
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
new file mode 100644
index 0000000..83f8738
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.credentials.playservices.controllers.BeginSignIn
+
+/*
+ * 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.
+ */
+
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetPasswordOption
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+
+/**
+ * A utility class to handle logic for the begin sign in controller.
+ *
+ * @hide
+ */
+class BeginSignInControllerUtility {
+
+    companion object {
+        internal fun constructBeginSignInRequest(request: GetCredentialRequest):
+            BeginSignInRequest {
+            val requestBuilder = BeginSignInRequest.Builder()
+            for (option in request.getCredentialOptions) {
+                if (option is GetPasswordOption) {
+                    requestBuilder.setPasswordRequestOptions(
+                        BeginSignInRequest.PasswordRequestOptions.Builder()
+                            .setSupported(true)
+                            .build()
+                    )
+                }
+                // TODO("Add GoogleIDToken version and passkey version")
+            }
+            return requestBuilder
+                .setAutoSelectEnabled(request.isAutoSelectAllowed)
+                .build()
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
index e38cca7..1c9f3c1 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
@@ -16,15 +16,29 @@
 
 package androidx.credentials.playservices.controllers.BeginSignIn
 
+import android.annotation.SuppressLint
+import android.app.Activity
 import android.content.Intent
+import android.content.IntentSender
+import android.os.Bundle
 import android.util.Log
+import androidx.credentials.Credential
 import androidx.credentials.CredentialManagerCallback
 import androidx.credentials.GetCredentialRequest
 import androidx.credentials.GetCredentialResponse
+import androidx.credentials.PasswordCredential
+import androidx.credentials.exceptions.GetCredentialCanceledException
 import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.playservices.controllers.BeginSignIn.BeginSignInControllerUtility.Companion.constructBeginSignInRequest
 import androidx.credentials.playservices.controllers.CredentialProviderController
 import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.BeginSignInResult
+import com.google.android.gms.auth.api.identity.Identity
 import com.google.android.gms.auth.api.identity.SignInCredential
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.CommonStatusCodes
 import java.util.concurrent.Executor
 
 /**
@@ -50,12 +64,54 @@
      */
     private lateinit var executor: Executor
 
+    @SuppressLint("ClassVerificationFailure", "NewApi")
     override fun invokePlayServices(
         request: GetCredentialRequest,
         callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
         executor: Executor
     ) {
-        TODO("Not yet implemented")
+        this.callback = callback
+        this.executor = executor
+        val convertedRequest: BeginSignInRequest = this.convertRequestToPlayServices(request)
+        Identity.getSignInClient(activity)
+            .beginSignIn(convertedRequest)
+            .addOnSuccessListener { result: BeginSignInResult ->
+                try {
+                    startIntentSenderForResult(
+                        result.pendingIntent.intentSender,
+                        REQUEST_CODE_BEGIN_SIGN_IN,
+                        null, /* fillInIntent= */
+                        0, /* flagsMask= */
+                        0, /* flagsValue= */
+                        0, /* extraFlags= */
+                        null /* options= */
+                    )
+                } catch (e: IntentSender.SendIntentException) {
+                    Log.e(TAG, "Couldn't start One Tap UI in beginSignIn: " +
+                        e.localizedMessage
+                    )
+                    val exception: GetCredentialException = GetCredentialUnknownException(
+                        e.localizedMessage)
+                    executor.execute { ->
+                        callback.onError(exception)
+                    }
+                }
+            }
+            .addOnFailureListener { e: Exception ->
+                // No saved credentials found. Launch the One Tap sign-up flow, or
+                // do nothing and continue presenting the signed-out UI.
+                Log.i(TAG, "Failure in begin sign in call")
+                if (e.localizedMessage != null) { Log.i(TAG, e.localizedMessage!!) }
+                var exception: GetCredentialException = GetCredentialUnknownException()
+                if (e is ApiException && e.statusCode in this.retryables) {
+                    exception = GetCredentialInterruptedException(e.localizedMessage)
+                }
+                executor.execute { ->
+                    callback.onError(
+                        exception
+                    )
+                }
+            }
     }
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -64,17 +120,68 @@
     }
 
     private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
-        Log.i(TAG, "$uniqueRequestCode $resultCode $data")
-        TODO("Not yet implemented")
+        if (uniqueRequestCode != REQUEST_CODE_BEGIN_SIGN_IN) {
+            Log.i(TAG, "returned request code does not match what was given")
+            return
+        }
+        if (resultCode != Activity.RESULT_OK) {
+            var exception: GetCredentialException = GetCredentialUnknownException()
+            if (resultCode == Activity.RESULT_CANCELED) {
+                exception = GetCredentialCanceledException()
+            }
+            this.executor.execute { -> this.callback.onError(exception) }
+            return
+        }
+        try {
+            val signInCredential = Identity.getSignInClient(activity as Activity)
+                .getSignInCredentialFromIntent(data)
+            Log.i(TAG, "Credential returned : " + signInCredential.googleIdToken + " , " +
+                signInCredential.id + ", " + signInCredential.password)
+            val response = convertResponseToCredentialManager(signInCredential)
+            Log.i(TAG, "Credential : " + response.credential.toString())
+            this.executor.execute { this.callback.onResult(response) }
+        } catch (e: ApiException) {
+            var exception: GetCredentialException = GetCredentialUnknownException()
+            if (e.statusCode == CommonStatusCodes.CANCELED) {
+                Log.i(TAG, "User cancelled the prompt!")
+                exception = GetCredentialCanceledException()
+            } else if (e.statusCode in this.retryables) {
+                exception = GetCredentialInterruptedException()
+            }
+            executor.execute { ->
+                callback.onError(
+                    exception
+                )
+            }
+            return
+        }
     }
 
-    override fun convertRequestToPlayServices(request: GetCredentialRequest): BeginSignInRequest {
-        TODO("Not yet implemented")
+    override fun convertRequestToPlayServices(request: GetCredentialRequest):
+        BeginSignInRequest {
+        return constructBeginSignInRequest(request)
     }
 
     override fun convertResponseToCredentialManager(response: SignInCredential):
         GetCredentialResponse {
-        TODO("Not yet implemented")
+        var cred: Credential? = null
+        if (response.password != null) {
+            cred = PasswordCredential(response.id, response.password!!)
+        } else if (response.googleIdToken != null) {
+            TODO(" Implement GoogleIdTokenVersion")
+        }
+        // TODO("Implement PublicKeyCredential Version")
+        else {
+            Log.i(TAG, "Credential returned but no google Id or password or passkey found")
+        }
+        if (cred == null) {
+            executor.execute { callback.onError(
+                GetCredentialUnknownException(
+                    "cred should not be null")
+            ) }
+            return GetCredentialResponse(Credential("ERROR", Bundle.EMPTY))
+        }
+        return GetCredentialResponse(cred)
     }
 
     companion object {
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
index 2261081..be6ba81 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
@@ -16,14 +16,27 @@
 
 package androidx.credentials.playservices.controllers.CreatePassword
 
+import android.annotation.SuppressLint
+import android.app.Activity
 import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import androidx.credentials.CreateCredentialResponse
 import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePasswordResponse
 import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.exceptions.CreateCredentialCanceledException
 import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialInterruptedException
+import androidx.credentials.exceptions.CreateCredentialUnknownException
 import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.auth.api.identity.Identity
 import com.google.android.gms.auth.api.identity.SavePasswordRequest
+import com.google.android.gms.auth.api.identity.SavePasswordResult
+import com.google.android.gms.auth.api.identity.SignInPassword
+import com.google.android.gms.common.api.ApiException
 import java.util.concurrent.Executor
 
 /**
@@ -50,37 +63,97 @@
      */
     private lateinit var executor: Executor
 
+    @SuppressLint("ClassVerificationFailure")
     override fun invokePlayServices(
         request: CreatePasswordRequest,
         callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
         executor: Executor
     ) {
-        TODO("Not yet implemented")
+        this.callback = callback
+        this.executor = executor
+        val convertedRequest: SavePasswordRequest = this.convertRequestToPlayServices(request)
+        Identity.getCredentialSavingClient(activity)
+            .savePassword(convertedRequest)
+            .addOnSuccessListener { result: SavePasswordResult ->
+                try {
+                    if (Build.VERSION.SDK_INT >= 24) {
+                        startIntentSenderForResult(
+                            result.pendingIntent.intentSender,
+                            REQUEST_CODE_GIS_SAVE_PASSWORD,
+                            null, /* fillInIntent= */
+                            0, /* flagsMask= */
+                            0, /* flagsValue= */
+                            0, /* extraFlags= */
+                            null /* options= */
+                        )
+                    }
+                } catch (e: IntentSender.SendIntentException) {
+                    Log.i(
+                        TAG, "Failed to send pending intent for savePassword" +
+                            " : " + e.message
+                    )
+                    val exception: CreateCredentialException = CreateCredentialUnknownException()
+                    executor.execute { ->
+                        callback.onError(
+                            exception
+                        )
+                    }
+                }
+            }
+            .addOnFailureListener { e: Exception ->
+                Log.i(TAG, "CreatePassword failed with : " + e.message)
+                var exception: CreateCredentialException = CreateCredentialUnknownException()
+                if (e is ApiException && e.statusCode in this.retryables) {
+                    exception = CreateCredentialInterruptedException()
+                }
+                executor.execute { ->
+                    callback.onError(
+                        exception
+                    )
+                }
+            }
     }
-
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         super.onActivityResult(requestCode, resultCode, data)
         handleResponse(requestCode, resultCode, data)
     }
 
     private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
-        Log.i(TAG, "$uniqueRequestCode $resultCode $data")
-        TODO("Not yet implemented")
+        Log.i(TAG, "$data - the intent back - is un-used.")
+        if (uniqueRequestCode != REQUEST_CODE_GIS_SAVE_PASSWORD) {
+            return
+        }
+        if (resultCode != Activity.RESULT_OK) {
+            var exception: CreateCredentialException = CreateCredentialUnknownException()
+            if (resultCode == Activity.RESULT_CANCELED) {
+                exception = CreateCredentialCanceledException()
+            }
+            this.executor.execute { -> this.callback.onError(exception) }
+            return
+        }
+        Log.i(TAG, "Saving password succeeded")
+        val response: CreateCredentialResponse = convertResponseToCredentialManager(Unit)
+        this.executor.execute { -> this.callback.onResult(response) }
     }
 
-    override fun convertRequestToPlayServices(request: CreatePasswordRequest): SavePasswordRequest {
-        TODO("Not yet implemented")
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public override fun convertRequestToPlayServices(request: CreatePasswordRequest):
+        SavePasswordRequest {
+        return SavePasswordRequest.builder().setSignInPassword(
+            SignInPassword(request.id, request.password)
+        ).build()
     }
 
-    override fun convertResponseToCredentialManager(response: Unit): CreateCredentialResponse {
-        TODO("Not yet implemented")
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public override fun convertResponseToCredentialManager(response: Unit):
+        CreateCredentialResponse {
+        return CreatePasswordResponse()
     }
 
     companion object {
         private val TAG = CredentialProviderCreatePasswordController::class.java.name
         private const val REQUEST_CODE_GIS_SAVE_PASSWORD: Int = 1
         // TODO("Ensure this works with the lifecycle")
-
         /**
          * This finds a past version of the
          * [CredentialProviderCreatePasswordController] if it exists, otherwise
@@ -120,4 +193,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
index ccaf643..6843502 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
@@ -37,8 +37,7 @@
  */
 @Suppress("deprecation")
 abstract class CredentialProviderController<T1 : Any, T2 : Any, R2 : Any, R1 : Any,
-    E1 : Any> : android.app
-        .Fragment() {
+    E1 : Any> : android.app.Fragment() {
 
     protected var retryables: Set<Int> = setOf(INTERNAL_ERROR,
         NETWORK_ERROR)
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index 6bb5a52..3d7dde9 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -53,11 +53,3 @@
     inceptionYear = "2022"
     description = "Android Credentials Library"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/datastore/datastore-rxjava2/api/current.txt b/datastore/datastore-rxjava2/api/current.txt
index 433238b..dc394de 100644
--- a/datastore/datastore-rxjava2/api/current.txt
+++ b/datastore/datastore-rxjava2/api/current.txt
@@ -27,7 +27,7 @@
     method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava2.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.Scheduler scheduler);
   }
 
-  public interface RxSharedPreferencesMigration<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
     method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
     method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
   }
diff --git a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
index e662ed7..fb412f3 100644
--- a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
@@ -29,7 +29,7 @@
     method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava2.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.Scheduler scheduler);
   }
 
-  public interface RxSharedPreferencesMigration<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
     method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
     method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
   }
diff --git a/datastore/datastore-rxjava2/api/restricted_current.txt b/datastore/datastore-rxjava2/api/restricted_current.txt
index 433238b..dc394de 100644
--- a/datastore/datastore-rxjava2/api/restricted_current.txt
+++ b/datastore/datastore-rxjava2/api/restricted_current.txt
@@ -27,7 +27,7 @@
     method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava2.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.Scheduler scheduler);
   }
 
-  public interface RxSharedPreferencesMigration<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
     method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
     method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
   }
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
index a24bcfa..ae5de44c 100644
--- a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
@@ -24,6 +24,7 @@
 import io.reactivex.Single
 import kotlinx.coroutines.rx2.await
 
+@JvmDefaultWithCompatibility
 /**
  * Client implemented migration interface.
  **/
diff --git a/datastore/datastore-rxjava3/api/current.txt b/datastore/datastore-rxjava3/api/current.txt
index 1033671..218d1e9 100644
--- a/datastore/datastore-rxjava3/api/current.txt
+++ b/datastore/datastore-rxjava3/api/current.txt
@@ -27,7 +27,7 @@
     method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava3.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.rxjava3.core.Scheduler scheduler);
   }
 
-  public interface RxSharedPreferencesMigration<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
     method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
     method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
   }
diff --git a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
index 05fa237..025e898 100644
--- a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
@@ -29,7 +29,7 @@
     method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava3.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.rxjava3.core.Scheduler scheduler);
   }
 
-  public interface RxSharedPreferencesMigration<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
     method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
     method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
   }
diff --git a/datastore/datastore-rxjava3/api/restricted_current.txt b/datastore/datastore-rxjava3/api/restricted_current.txt
index 1033671..218d1e9 100644
--- a/datastore/datastore-rxjava3/api/restricted_current.txt
+++ b/datastore/datastore-rxjava3/api/restricted_current.txt
@@ -27,7 +27,7 @@
     method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava3.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.rxjava3.core.Scheduler scheduler);
   }
 
-  public interface RxSharedPreferencesMigration<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
     method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
     method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
   }
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
index d53b48d..b159b92 100644
--- a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
@@ -24,6 +24,7 @@
 import io.reactivex.rxjava3.core.Single
 import kotlinx.coroutines.rx3.await
 
+@JvmDefaultWithCompatibility
 /**
  * Client implemented migration interface.
  **/
diff --git a/development/build_log_processor.sh b/development/build_log_processor.sh
index a9b931a..b55ad12 100755
--- a/development/build_log_processor.sh
+++ b/development/build_log_processor.sh
@@ -21,9 +21,6 @@
   echo
   echo "Executes <command> <arguments> and then runs build_log_simplifier.py against its output"
   echo
-  echo "-Pandroidx.summarizeStderr"
-  echo "  Run build_log_simplifier.py on failure to produce a summary of the build output"
-  echo
   echo "-Pandroidx.validateNoUnrecognizedMessages"
   echo "  Run build_log_simplifier.py --validate on success to confirm that the build generated no unrecognized messages"
   exit 1
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index add01af..8cd2b88 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -792,3 +792,5 @@
 xcframework successfully .*
 # iOS benchmark invocation
 /usr/bin/xcodebuild
+# > Configure project :internal-testutils-ktx
+WARNING:The option setting 'android\.r8\.maxWorkers=[0-9]+' is experimental\.
diff --git a/emoji2/emoji2-emojipicker/api/current.txt b/emoji2/emoji2-emojipicker/api/current.txt
index aab9e2c..f759566 100644
--- a/emoji2/emoji2-emojipicker/api/current.txt
+++ b/emoji2/emoji2-emojipicker/api/current.txt
@@ -8,9 +8,18 @@
     method public float getEmojiGridRows();
     method public void setEmojiGridColumns(int);
     method public void setEmojiGridRows(float);
+    method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
     property public final int emojiGridColumns;
     property public final float emojiGridRows;
   }
 
+  public final class EmojiViewItem {
+    ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+    method public String getEmoji();
+    method public java.util.List<java.lang.String> getVariants();
+    property public final String emoji;
+    property public final java.util.List<java.lang.String> variants;
+  }
+
 }
 
diff --git a/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt b/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
index aab9e2c..f759566 100644
--- a/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
+++ b/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
@@ -8,9 +8,18 @@
     method public float getEmojiGridRows();
     method public void setEmojiGridColumns(int);
     method public void setEmojiGridRows(float);
+    method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
     property public final int emojiGridColumns;
     property public final float emojiGridRows;
   }
 
+  public final class EmojiViewItem {
+    ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+    method public String getEmoji();
+    method public java.util.List<java.lang.String> getVariants();
+    property public final String emoji;
+    property public final java.util.List<java.lang.String> variants;
+  }
+
 }
 
diff --git a/emoji2/emoji2-emojipicker/api/restricted_current.txt b/emoji2/emoji2-emojipicker/api/restricted_current.txt
index aab9e2c..f759566 100644
--- a/emoji2/emoji2-emojipicker/api/restricted_current.txt
+++ b/emoji2/emoji2-emojipicker/api/restricted_current.txt
@@ -8,9 +8,18 @@
     method public float getEmojiGridRows();
     method public void setEmojiGridColumns(int);
     method public void setEmojiGridRows(float);
+    method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
     property public final int emojiGridColumns;
     property public final float emojiGridRows;
   }
 
+  public final class EmojiViewItem {
+    ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+    method public String getEmoji();
+    method public java.util.List<java.lang.String> getVariants();
+    property public final String emoji;
+    property public final java.util.List<java.lang.String> variants;
+  }
+
 }
 
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index dd40d3a..bcc3091 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -33,6 +33,8 @@
     implementation 'androidx.tracing:tracing-ktx:1.0.0'
     implementation 'androidx.test.ext:junit-ktx:1.1.3'
 
+    androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
+    androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
index 8a8ebeb..baf6c45 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
+++ b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
@@ -17,11 +17,24 @@
 package androidx.emoji2.emojipicker.samples
 
 import android.os.Bundle
+import android.util.Log
 import androidx.appcompat.app.AppCompatActivity
+import androidx.emoji2.emojipicker.EmojiPickerView
+import androidx.emoji2.emojipicker.EmojiViewItem
 
 class MainActivity : AppCompatActivity() {
+    companion object {
+        internal const val LOG_TAG = "SAMPLE_APP"
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.main)
+
+        val view = findViewById<EmojiPickerView>(R.id.emoji_picker)
+        view.setOnEmojiPickedListener { emoji: EmojiViewItem ->
+            Log.d(LOG_TAG, String.format("emoji: %s", emoji.emoji))
+            Log.d(LOG_TAG, String.format("variants: %s", emoji.variants.toString()))
+        }
     }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
index a63f725..b9b80a5 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
@@ -16,16 +16,34 @@
 
 package androidx.emoji2.emojipicker
 
+import androidx.emoji2.emojipicker.R as EmojiPickerViewR
+import androidx.test.espresso.Espresso.onView
+import org.hamcrest.Description
 import android.app.Activity
 import android.content.Context
 import android.os.Bundle
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
 import androidx.core.view.isVisible
 import androidx.emoji2.emojipicker.test.R
+import androidx.recyclerview.widget.RecyclerView
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.BoundedMatcher
+import androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams
+import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -61,4 +79,112 @@
             assertEquals(mEmojiPickerView.emojiGridColumns, 10)
         }
     }
+
+    @Test
+    fun testCustomEmojiPickerView_noVariant() {
+        mActivityTestRule.scenario.onActivity {
+            val targetView = findViewByEmoji(
+                it.findViewById(R.id.emojiPickerTest),
+                "\uD83D\uDE00"
+            )
+            // No variant indicator
+            assertEquals(
+                (targetView.parent as FrameLayout).findViewById<ImageView>(
+                    EmojiPickerViewR.id.variant_availability_indicator
+                ).visibility,
+                GONE
+            )
+            // Not long-clickable
+            assertEquals(targetView.isLongClickable, false)
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 24)
+    fun testCustomEmojiPickerView_hasVariant() {
+        // 👃 has variants
+        val noseEmoji = "\uD83D\uDC43"
+        lateinit var bodyView: EmojiPickerBodyView
+        mActivityTestRule.scenario.onActivity {
+            bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
+                .findViewById(EmojiPickerViewR.id.emoji_picker_body)
+        }
+        onView(withId(EmojiPickerViewR.id.emoji_picker_body))
+            .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
+        val targetView = findViewByEmoji(bodyView, noseEmoji)
+        // Variant indicator visible
+        assertEquals(
+            (targetView.parent as FrameLayout).findViewById<ImageView>(
+                EmojiPickerViewR.id.variant_availability_indicator
+            ).visibility, VISIBLE
+        )
+        // Long-clickable
+        assertEquals(targetView.isLongClickable, true)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 24)
+    fun testStickyVariant_displayAndSaved() {
+        val noseEmoji = "\uD83D\uDC43"
+        val noseEmojiDark = "\uD83D\uDC43\uD83C\uDFFF"
+        lateinit var bodyView: EmojiPickerBodyView
+        mActivityTestRule.scenario.onActivity {
+            bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
+                .findViewById(EmojiPickerViewR.id.emoji_picker_body)
+        }
+        // Scroll to the nose emoji, long click then select nose in dark skin tone
+        onView(withId(EmojiPickerViewR.id.emoji_picker_body))
+            .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
+        onView(createEmojiViewMatcher(noseEmoji)).perform(longClick())
+        onView(createEmojiViewMatcher(noseEmojiDark))
+            .inRoot(hasWindowLayoutParams())
+            .perform(click())
+        assertNotNull(findViewByEmoji(bodyView, noseEmojiDark))
+        // Switch back to clear saved preference
+        onView(createEmojiViewMatcher(noseEmojiDark)).perform(longClick())
+        onView(createEmojiViewMatcher(noseEmoji))
+            .inRoot(hasWindowLayoutParams())
+            .perform(click())
+        assertNotNull(findViewByEmoji(bodyView, noseEmoji))
+    }
+
+    private fun findViewByEmoji(root: View, emoji: String) =
+        mutableListOf<View>().apply {
+            findViewsById(
+                root,
+                EmojiPickerViewR.id.emoji_view, this
+            )
+        }.first { (it as EmojiView).emoji == emoji }
+
+    private fun findViewsById(root: View, id: Int, output: MutableList<View>) {
+        if (root !is ViewGroup) {
+            return
+        }
+        for (i in 0 until root.childCount) {
+            root.getChildAt(i).apply {
+                if (this.id == id) {
+                    output.add(this)
+                }
+            }.also {
+                findViewsById(it, id, output)
+            }
+        }
+    }
+
+    private fun createEmojiViewHolderMatcher(emoji: String) =
+        object :
+            BoundedMatcher<RecyclerView.ViewHolder, EmojiViewHolder>(EmojiViewHolder::class.java) {
+            override fun describeTo(description: Description) {}
+            override fun matchesSafely(item: EmojiViewHolder) =
+                (item.itemView as FrameLayout)
+                    .findViewById<EmojiView>(EmojiPickerViewR.id.emoji_view)
+                    .emoji == emoji
+        }
+
+    private fun createEmojiViewMatcher(emoji: String) =
+        object :
+            BoundedMatcher<View, EmojiView>(EmojiView::class.java) {
+            override fun describeTo(description: Description) {}
+            override fun matchesSafely(item: EmojiView) = item.emoji == emoji
+        }
 }
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
index 3a4e7ed..57136d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -33,7 +33,7 @@
  * @property categorizedEmojiData: a list that holds bundled emoji separated by category, filtered
  * by renderability check. This is the data source for EmojiPickerView.
  *
- * @property emojiVariantsLookup: a map of emoji variants in bundled emoji, keyed by the primary
+ * @property emojiVariantsLookup: a map of emoji variants in bundled emoji, keyed by the base
  * emoji. This allows faster variants lookup.
  */
 internal object BundledEmojiListLoader {
@@ -64,7 +64,7 @@
         emojiVariantsLookup ?: getCategorizedEmojiData()
             .flatMap { it.emojiDataList }
             .filter { it.variants.isNotEmpty() }
-            .associate { it.primary to it.variants }
+            .associate { it.emoji to it.variants }
             .also { emojiVariantsLookup = it }
 
     private suspend fun loadEmojiAsync(
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index 9679844..8356d6c 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -22,24 +22,24 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams
-import androidx.annotation.IntRange
 import androidx.annotation.UiThread
-import androidx.annotation.VisibleForTesting
 import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.util.Consumer
 import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import androidx.tracing.Trace
+import androidx.emoji2.emojipicker.EmojiPickerConstants.RECENT_CATEGORY_INDEX
 
 /** RecyclerView adapter for emoji body.  */
 internal class EmojiPickerBodyAdapter(
-    context: Context,
+    private val context: Context,
     private val emojiGridColumns: Int,
     private val emojiGridRows: Float,
-    private val categoryNames: Array<String>
+    private val categoryNames: Array<String>,
+    private val stickyVariantProvider: StickyVariantProvider,
+    private val onEmojiPickedListener: Consumer<EmojiViewItem>?
 ) : Adapter<ViewHolder>() {
     private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
-    private val context = context
-
     private var flattenSource: ItemViewDataFlatList
 
     init {
@@ -58,47 +58,63 @@
         Trace.beginSection("EmojiPickerBodyAdapter.onCreateViewHolder")
         return try {
             val view: View
-            if (viewType == CategorySeparatorViewData.TYPE) {
-                view = layoutInflater.inflate(
-                    R.layout.category_text_view,
-                    parent,
-                    /* attachToRoot= */ false
-                )
-                view.layoutParams =
-                    LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
-            } else if (viewType == EmptyCategoryViewData.TYPE) {
-                view = layoutInflater.inflate(
-                    R.layout.emoji_picker_empty_category_text_view,
-                    parent,
-                    /* attachToRoot= */ false
-                )
-                view.layoutParams =
-                    LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
-                view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
-            } else if (viewType == EmojiViewData.TYPE) {
-                return EmojiViewHolder(
-                    parent,
-                    layoutInflater,
-                    getParentWidth(parent) / emojiGridColumns,
-                    (parent.measuredHeight / emojiGridRows).toInt(),
-                )
-            } else if (viewType == DummyViewData.TYPE) {
-                view = View(context)
-                view.layoutParams = LayoutParams(
-                    getParentWidth(parent) / emojiGridColumns,
-                    (parent.measuredHeight / emojiGridRows).toInt()
-                )
-            } else {
-                Log.e(
-                    "EmojiPickerBodyAdapter",
-                    "EmojiPickerBodyAdapter gets unsupported view type."
-                )
-                view = View(context)
-                view.layoutParams =
-                    LayoutParams(
+            when (viewType) {
+                CategorySeparatorViewData.TYPE -> {
+                    view = layoutInflater.inflate(
+                        R.layout.category_text_view,
+                        parent,
+                        /* attachToRoot = */ false
+                    )
+                    view.layoutParams =
+                        LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+                }
+
+                EmptyCategoryViewData.TYPE -> {
+                    view = layoutInflater.inflate(
+                        R.layout.emoji_picker_empty_category_text_view,
+                        parent,
+                        /* attachToRoot = */ false
+                    )
+                    view.layoutParams =
+                        LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+                    view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
+                }
+
+                EmojiViewData.TYPE -> {
+                    return EmojiViewHolder(
+                        parent,
+                        layoutInflater,
+                        getParentWidth(parent) / emojiGridColumns,
+                        (parent.measuredHeight / emojiGridRows).toInt(),
+                        stickyVariantProvider,
+                        onEmojiPickedListener,
+                        onEmojiPickedFromPopupListener = { emoji ->
+                            (flattenSource[bindingAdapterPosition] as EmojiViewData).primary = emoji
+                            notifyItemChanged(bindingAdapterPosition)
+                        }
+                    )
+                }
+
+                DummyViewData.TYPE -> {
+                    view = View(context)
+                    view.layoutParams = LayoutParams(
                         getParentWidth(parent) / emojiGridColumns,
                         (parent.measuredHeight / emojiGridRows).toInt()
                     )
+                }
+
+                else -> {
+                    Log.e(
+                        "EmojiPickerBodyAdapter",
+                        "EmojiPickerBodyAdapter gets unsupported view type."
+                    )
+                    view = View(context)
+                    view.layoutParams =
+                        LayoutParams(
+                            getParentWidth(parent) / emojiGridColumns,
+                            (parent.measuredHeight / emojiGridRows).toInt()
+                        )
+                }
             }
             object : ViewHolder(view) {}
         } finally {
@@ -116,13 +132,19 @@
             if (categoryName.isEmpty()) {
                 categoryName = categoryNames[categoryIndex]
             }
+
             // Show category label.
             val categoryLabel = view.findViewById<AppCompatTextView>(R.id.category_name)
-            if (categoryName.isEmpty()) {
-                categoryLabel.visibility = View.GONE
-            } else {
-                categoryLabel.text = categoryName
+            if (categoryIndex == RECENT_CATEGORY_INDEX) {
+                categoryLabel.text = context.getString(R.string.emoji_category_recent)
                 categoryLabel.visibility = View.VISIBLE
+            } else {
+                if (categoryName.isEmpty()) {
+                    categoryLabel.visibility = View.GONE
+                } else {
+                    categoryLabel.text = categoryName
+                    categoryLabel.visibility = View.VISIBLE
+                }
             }
         } else if (viewType == EmptyCategoryViewData.TYPE) {
             // Show empty category description.
@@ -131,9 +153,9 @@
             val item = flattenSource[position] as EmptyCategoryViewData
             var content = item.description
             if (content.isEmpty()) {
-                val categoryIndex: Int = getCategoryIndex(position)
+                val categoryIndex = flattenSource.getCategoryIndex(position)
                 content = context.getString(
-                    if (categoryIndex == EmojiPickerConstants.RECENT_CATEGORY_INDEX)
+                    if (categoryIndex == RECENT_CATEGORY_INDEX)
                         R.string.emoji_empty_recent_category
                     else R.string.emoji_empty_non_recent_category
                 )
@@ -159,16 +181,6 @@
         return flattenSource[position].type
     }
 
-    @IntRange(from = 0)
-    fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
-        return getFlattenSource().getCategoryIndex(position)
-    }
-
-    @VisibleForTesting
-    fun getFlattenSource(): ItemViewDataFlatList {
-        return flattenSource
-    }
-
     private fun getParentWidth(parent: ViewGroup): Int {
         return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
     }
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index 178c0cb..2abfc99 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -21,6 +21,7 @@
 import android.os.Trace
 import android.util.AttributeSet
 import android.widget.FrameLayout
+import androidx.core.util.Consumer
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import kotlinx.coroutines.CoroutineScope
@@ -60,8 +61,11 @@
             field = if (value > 0) value else EmojiPickerConstants.DEFAULT_BODY_COLUMNS
         }
 
+    private val stickyVariantProvider = StickyVariantProvider(context)
+
     private lateinit var headerView: RecyclerView
     private lateinit var bodyView: RecyclerView
+    private var onEmojiPickedListener: Consumer<EmojiViewItem>? = null
 
     init {
         val typedArray: TypedArray =
@@ -84,25 +88,36 @@
         }
     }
 
-    private fun getEmojiPickerBodyAdapter(
+    private fun createEmojiPickerBodyAdapter(
         context: Context,
         emojiGridColumns: Int,
         emojiGridRows: Float,
-        categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>
+        categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>,
+        onEmojiPickedListener: Consumer<EmojiViewItem>?
     ): EmojiPickerBodyAdapter {
         val categoryNames = mutableListOf<String>()
         val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
+        // add recent category as the first row
+        categoryNames.add(resources.getString(R.string.emoji_category_recent))
+        categorizedEmojis.add(mutableListOf())
+
         for (i in categorizedEmojiData.indices) {
             categoryNames.add(categorizedEmojiData[i].categoryName)
             categorizedEmojis.add(
-                categorizedEmojiData[i].emojiDataList.toMutableList()
+                categorizedEmojiData[i].emojiDataList.map {
+                    stickyVariantProvider.stickyVariantMap[it.emoji]?.let { stickyVariant ->
+                        EmojiViewItem(stickyVariant, it.variants)
+                    } ?: it
+                }.toMutableList()
             )
         }
         val adapter = EmojiPickerBodyAdapter(
             context,
             emojiGridColumns,
             emojiGridRows,
-            categoryNames.toTypedArray()
+            categoryNames.toTypedArray(),
+            stickyVariantProvider,
+            onEmojiPickedListener
         )
         adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
 
@@ -121,7 +136,7 @@
                         EmojiViewData(
                             categoryIndex,
                             idInCategory,
-                            eachEmoji.primary,
+                            eachEmoji.emoji,
                             eachEmoji.variants.toTypedArray()
                         )
                     )
@@ -144,15 +159,28 @@
             LinearLayoutManager(
                 context,
                 LinearLayoutManager.HORIZONTAL,
-                /* reverseLayout= */ false
+                /* reverseLayout = */ false
             )
         headerView.adapter = EmojiPickerHeaderAdapter(context)
 
         // set bodyView
         bodyView = emojiPicker.findViewById(R.id.emoji_picker_body)
         val categorizedEmojiData = BundledEmojiListLoader.getCategorizedEmojiData()
-        bodyView.adapter = getEmojiPickerBodyAdapter(
-            context, emojiGridColumns, emojiGridRows, categorizedEmojiData
-        )
+        bodyView.adapter =
+            createEmojiPickerBodyAdapter(
+                context,
+                emojiGridColumns,
+                emojiGridRows,
+                categorizedEmojiData,
+                onEmojiPickedListener
+            )
     }
-}
\ No newline at end of file
+
+    /**
+     * This function is used to set the custom behavior after clicking on an emoji icon. Clients
+     * could specify their own behavior inside this function.
+     */
+    fun setOnEmojiPickedListener(onEmojiPickedListener: Consumer<EmojiViewItem>?) {
+        this.onEmojiPickedListener = onEmojiPickedListener
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
index bba8664..1e12a47 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
@@ -80,6 +80,7 @@
             if (value != null) {
                 post {
                     drawEmoji(value)
+                    contentDescription = value
                     invalidate()
                 }
             }
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
index aace300..3c34b4d 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
@@ -30,8 +30,9 @@
     /** The id of this emoji view in the category, usually is the position of the emoji.  */
     private val idInCategory: Int
 
-    /** Primary key which is used for labeling and for PRESS action.  */
-    val primary: String
+    /** Primary key which is used for labeling and for PRESS action. This value could be updated
+     * to one of its variants. */
+    var primary: String
 
     /** Secondary keys which are used for LONG_PRESS action.  */
     val secondaries: Array<String>
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
index 4dc5cd7..cbe6e27 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
@@ -16,32 +16,134 @@
 
 package androidx.emoji2.emojipicker
 
+import android.view.Gravity
 import android.view.LayoutInflater
+import android.view.View.GONE
+import android.view.View.OnClickListener
+import android.view.View.OnLongClickListener
+import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.GridLayout
+import android.widget.ImageView
+import android.widget.PopupWindow
+import androidx.core.util.Consumer
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import kotlin.math.roundToInt
 
 /** A [ViewHolder] containing an emoji view and emoji data.  */
 internal class EmojiViewHolder(
     parent: ViewGroup,
     layoutInflater: LayoutInflater,
     width: Int,
-    height: Int
+    height: Int,
+    stickyVariantProvider: StickyVariantProvider,
+    onEmojiPickedListener: Consumer<EmojiViewItem>?,
+    onEmojiPickedFromPopupListener: EmojiViewHolder.(String) -> Unit
 ) : ViewHolder(
     layoutInflater
-        .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot= */false)
+        .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot = */false)
 ) {
+    private val onEmojiClickListener: OnClickListener = OnClickListener {
+        // TODO(scduan): Add other on click events (e.g, add to recent)
+        onEmojiPickedListener?.accept(emojiViewItem)
+    }
+
+    private val onEmojiLongClickListener: OnLongClickListener = OnLongClickListener {
+        val variants = emojiViewItem.variants
+        val popupView = layoutInflater
+            .inflate(R.layout.variant_popup, null, false)
+            .findViewById<GridLayout>(R.id.variant_popup)
+            .apply {
+                // Show 6 emojis in one row at most
+                this.columnCount = minOf(6, variants.size)
+                this.rowCount =
+                    variants.size / this.columnCount +
+                        if (variants.size % this.columnCount == 0) 0 else 1
+                this.orientation = GridLayout.HORIZONTAL
+            }
+        val popupWindow = showPopupWindow(emojiView, popupView)
+        for (v in variants) {
+            // Add variant emoji view to the popup view
+            layoutInflater
+                .inflate(R.layout.emoji_view_holder, null, false).apply {
+                    this as FrameLayout
+                    (getChildAt(0) as EmojiView).emoji = v
+                    setOnClickListener {
+                        onEmojiPickedFromPopupListener(this@EmojiViewHolder, v)
+                        onEmojiClickListener.onClick(it)
+                        // variants[0] is always the base (i.e., primary) emoji
+                        stickyVariantProvider.update(variants[0], v)
+                        popupWindow.dismiss()
+                        // Hover on the base emoji after popup dismissed
+                        emojiView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+                    }
+                }.also {
+                    popupView.addView(it)
+                    it.layoutParams.width = emojiView.measuredWidth
+                    it.layoutParams.height = emojiView.measuredHeight
+                }
+        }
+        popupView.post {
+            // Hover on the first emoji in the popup
+            (popupView.getChildAt(0) as FrameLayout).getChildAt(0)
+                .sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+        }
+        true
+    }
+
     private val emojiView: EmojiView
+    private val indicator: ImageView
+    private lateinit var emojiViewItem: EmojiViewItem
 
     init {
         itemView.layoutParams = LayoutParams(width, height)
         emojiView = itemView.findViewById(R.id.emoji_view)
         emojiView.isClickable = true
+        emojiView.setOnClickListener(onEmojiClickListener)
+        indicator = itemView.findViewById(R.id.variant_availability_indicator)
     }
 
     fun bindEmoji(
-        emojiViewItem: EmojiViewItem
+        emojiViewItem: EmojiViewItem,
     ) {
-        emojiView.emoji = emojiViewItem.primary
+        emojiView.emoji = emojiViewItem.emoji
+        this.emojiViewItem = emojiViewItem
+
+        if (emojiViewItem.variants.isNotEmpty()) {
+            indicator.visibility = VISIBLE
+            emojiView.setOnLongClickListener(onEmojiLongClickListener)
+            emojiView.isLongClickable = true
+        } else {
+            indicator.visibility = GONE
+            emojiView.setOnLongClickListener(null)
+            emojiView.isLongClickable = false
+        }
+    }
+
+    private fun showPopupWindow(
+        parent: EmojiView,
+        popupView: GridLayout
+    ): PopupWindow {
+        val location = IntArray(2)
+        parent.getLocationInWindow(location)
+        // Make the popup view center align with the target emoji view.
+        val x =
+            location[0] + parent.width / 2f - popupView.columnCount * parent.width / 2f
+        val y = location[1] - popupView.rowCount * parent.height
+        return PopupWindow(
+            popupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, false
+        ).also {
+            it.isOutsideTouchable = true
+            it.isTouchable = true
+            it.showAtLocation(
+                parent,
+                Gravity.NO_GRAVITY,
+                x.roundToInt(),
+                y
+            )
+        }
     }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
index 2f79955..e6121f1 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
@@ -16,4 +16,10 @@
 
 package androidx.emoji2.emojipicker
 
-internal class EmojiViewItem(val primary: String, val variants: List<String>)
\ No newline at end of file
+/**
+ * [EmojiViewItem] is a class holding the displayed emoji and its emoji variants
+ *
+ * @param emoji Used to represent the displayed emoji of the [EmojiViewItem].
+ * @param variants Used to represent the corresponding emoji variants of this base emoji.
+ */
+class EmojiViewItem(val emoji: String, val variants: List<String>)
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt
new file mode 100644
index 0000000..7c45cf6
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+
+/**
+ * A class that handles user's emoji variant selection using SharedPreferences.
+ */
+internal class StickyVariantProvider(context: Context) {
+    companion object {
+        const val PREFERENCES_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
+        const val STICKY_VARIANT_PROVIDER_KEY = "pref_key_sticky_variant"
+        const val KEY_VALUE_DELIMITER = "="
+        const val ENTRY_DELIMITER = "|"
+    }
+
+    private val sharedPreferences =
+        context.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE)
+
+    internal val stickyVariantMap: Map<String, String> by lazy {
+        sharedPreferences.getString(STICKY_VARIANT_PROVIDER_KEY, null)?.split(ENTRY_DELIMITER)
+            ?.associate { entry ->
+                entry.split(KEY_VALUE_DELIMITER, limit = 2).takeIf { it.size == 2 }
+                    ?.let { it[0] to it[1] } ?: ("" to "")
+            } ?: mapOf()
+    }
+
+    internal fun update(baseEmoji: String, variantClicked: String) {
+        stickyVariantMap.toMutableMap().apply {
+            if (baseEmoji == variantClicked) {
+                this.remove(baseEmoji)
+            } else {
+                this[baseEmoji] = variantClicked
+            }
+            sharedPreferences.edit()
+                .putString(
+                    STICKY_VARIANT_PROVIDER_KEY,
+                    entries.joinToString(ENTRY_DELIMITER)
+                ).commit()
+        }
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
index 7b1bdaf..b724ba9 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
@@ -83,7 +83,7 @@
         targetFile.bufferedWriter()
             .use { out ->
                 for (emoji in data) {
-                    out.write(emoji.primary)
+                    out.write(emoji.emoji)
                     emoji.variants.forEach { out.write(",$it") }
                     out.newLine()
                 }
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
new file mode 100644
index 0000000..1715a56
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:tint="?attr/colorControlNormal">
+    <corners android:radius="@dimen/emoji_picker_popup_view_holder_corner_radius"/>
+</shape>
\ No newline at end of file
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
new file mode 100644
index 0000000..e27fe2e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M2,22h20V2L2,22z"/>
+</vector>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
index 0fa8c9c..76fe2b9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
@@ -15,7 +15,6 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="0dp"
     android:layout_height="0dp">
@@ -28,4 +27,11 @@
         android:importantForAccessibility="yes"
         android:textSize="30dp"
         tools:ignore="SpUsage" />
+    <ImageView
+        android:id="@+id/variant_availability_indicator"
+        android:visibility="gone"
+        android:src="@drawable/variant_availability_indicator"
+        android:layout_width="5dp"
+        android:layout_height="5dp"
+        android:layout_gravity="bottom|end"/>
 </FrameLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
new file mode 100644
index 0000000..d61780e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<GridLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/variant_popup"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    style="@style/EmojiPickerStylePopupViewHolder">
+</GridLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
index cc79c54..207125d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -24,4 +24,11 @@
     <dimen name="emoji_picker_header_icon_underline_height">2dp</dimen>
     <dimen name="emoji_picker_header_height">50dp</dimen>
     <dimen name="emoji_picker_header_padding">8dp</dimen>
+    <dimen name="variant_availability_indicator_height">5dp</dimen>
+    <dimen name="variant_availability_indicator_width">5dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_padding_vertical">3dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_padding_start">3dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_padding_end">5dp</dimen>
+    <dimen name="emoji_picker_popup_view_elevation">2dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_corner_radius">10dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
index 472702f..c883441 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
@@ -15,6 +15,8 @@
   -->
 
 <resources>
+    <!-- Emoji recent sub category. -->
+    <string name="emoji_category_recent">RECENTLY USED</string>
     <!-- Emoji keyboard's smileys and emotions sub category. -->
     <string name="emoji_category_emotions">SMILEYS AND EMOTIONS</string>
     <!-- Emoji keyboard's people sub category. -->
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
index 0e6d24e..d715e58 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
@@ -31,4 +31,13 @@
     <style name="EmojiPickerStyleCategoryLabelText">
         <item name="android:textColor">?attr/EmojiPickerColorCategoryLabelText</item>
     </style>
+
+    <style name="EmojiPickerStylePopupViewHolder">
+        <item name="android:padding">@dimen/emoji_picker_popup_view_holder_padding_vertical</item>
+        <item name="android:paddingStart">@dimen/emoji_picker_popup_view_holder_padding_start</item>
+        <item name="android:paddingEnd">@dimen/emoji_picker_popup_view_holder_padding_end</item>
+        <item name="android:background">@drawable/popup_view_rounded_background</item>
+        <item name="android:elevation">@dimen/emoji_picker_popup_view_elevation</item>
+    </style>
+
 </resources>
diff --git a/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt b/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt
index 59134a1..d986e5e 100644
--- a/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt
+++ b/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt
@@ -27,6 +27,7 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.hamcrest.CoreMatchers.not
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,6 +37,8 @@
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class FragmentScenarioDialogFragmentTest {
+
+    @Ignore // b/259726188
     @Test
     fun launchFragment() {
         with(launchFragment<SimpleDialogFragment>()) {
@@ -48,6 +51,7 @@
         }
     }
 
+    @Ignore // b/259727355
     @Test
     fun launchFragmentInContainer() {
         with(launchFragmentInContainer<SimpleDialogFragment>()) {
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index ff192ef..0fe45ab 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -254,7 +254,7 @@
     method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyListScope {
+  @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
     method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
     field public static final androidx.glance.appwidget.lazy.LazyListScope.Companion Companion;
@@ -276,7 +276,7 @@
     method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+  @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyVerticalGridScope {
     method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
     method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
     field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index cd1190b..68e9f6c 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -268,7 +268,7 @@
     method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyListScope {
+  @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
     method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
     field public static final androidx.glance.appwidget.lazy.LazyListScope.Companion Companion;
@@ -290,7 +290,7 @@
     method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+  @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyVerticalGridScope {
     method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
     method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
     field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index ff192ef..0fe45ab 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -254,7 +254,7 @@
     method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyListScope {
+  @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
     method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
     field public static final androidx.glance.appwidget.lazy.LazyListScope.Companion Companion;
@@ -276,7 +276,7 @@
     method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+  @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyVerticalGridScope {
     method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
     method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
     field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
diff --git a/glance/glance-appwidget/integration-tests/template-demos/build.gradle b/glance/glance-appwidget/integration-tests/template-demos/build.gradle
index 6f5933f..ec43ef6 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/build.gradle
+++ b/glance/glance-appwidget/integration-tests/template-demos/build.gradle
@@ -27,6 +27,8 @@
     implementation(libs.kotlinStdlib)
     implementation(project(":glance:glance"))
     implementation(project(":glance:glance-appwidget"))
+    implementation(project(":glance:glance-material"))
+    implementation(project(":glance:glance-material3"))
     implementation("androidx.activity:activity:1.4.0")
     implementation("androidx.activity:activity-compose:1.4.0")
     implementation("androidx.compose.material:material:1.1.0-beta02")
@@ -36,6 +38,7 @@
     implementation("androidx.compose.material:material:1.1.0-beta02")
     implementation("androidx.datastore:datastore-preferences-core:1.0.0")
     implementation("androidx.datastore:datastore-preferences:1.0.0-rc02")
+    implementation "androidx.compose.material3:material3:1.0.0"
 }
 
 android {
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
index 189229d..0036785 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
@@ -91,7 +91,7 @@
         </receiver>
 
         <receiver
-            android:name="androidx.glance.appwidget.template.demos.FullHeaderListReceiver"
+            android:name="androidx.glance.appwidget.template.demos.FullHeaderThemedListReceiver"
             android:enabled="@bool/glance_appwidget_available"
             android:exported="false"
             android:label="@string/list_style_with_header">
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
index 464d336..fd1b18a 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
@@ -64,11 +64,11 @@
 
 /**
  * List demo with list items in full details and list header without action button using data and
- * list template from [BaseListDemoWidget].
+ * list template from [BaseListDemoWidget] with custom theme.
  */
-class FullHeaderListDemoWidget : BaseListDemoWidget() {
+class FullHeaderListThemedDemoWidget : BaseListDemoWidget() {
     @Composable
-    override fun TemplateContent() = ListTemplateContent(ListStyle.Full, true)
+    override fun TemplateContent() = ListTemplateContent(ListStyle.Full, true, true)
 }
 
 /**
@@ -93,8 +93,8 @@
     override val glanceAppWidget: GlanceAppWidget = FullHeaderActionListDemoWidget()
 }
 
-class FullHeaderListReceiver : GlanceAppWidgetReceiver() {
-    override val glanceAppWidget: GlanceAppWidget = FullHeaderListDemoWidget()
+class FullHeaderThemedListReceiver : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget = FullHeaderListThemedDemoWidget()
 }
 
 class NoHeaderListReceiver : GlanceAppWidgetReceiver() {
@@ -124,14 +124,16 @@
      * @param listStyle styling the list by [ListStyle] based data details
      * @param initialNumItems initial number of list items to generate in the demo
      * @param showHeader whether to show list header as a whole
+     * @param customTheme whether to override Glance system theme with custom theme
      */
     @Composable
     internal fun ListTemplateContent(
         listStyle: ListStyle,
         showHeader: Boolean = false,
+        customTheme: Boolean = false,
         initialNumItems: Int = MAX_ITEMS,
     ) {
-        GlanceTheme {
+        GlanceTheme(if (customTheme) PalmLeafScheme.colors else GlanceTheme.colors) {
             val state = currentState<Preferences>()
             val content = mutableListOf<ListTemplateItem>()
             for (i in 1..(state[CountKey] ?: initialNumItems)) {
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt
new file mode 100644
index 0000000..b5a4029
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.glance.appwidget.template.demos
+
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.graphics.Color
+import androidx.glance.material3.ColorProviders
+
+/**
+ * Custom Color theme for the Palm Leaf themed demo templates
+ */
+object PalmLeafScheme {
+    val md_theme_light_primary = Color(0xFF026E00)
+    val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+    val md_theme_light_primaryContainer = Color(0xFFD7E8CD)
+    val md_theme_light_onPrimaryContainer = Color(0xFF002200)
+    val md_theme_light_secondary = Color(0xFFA900A9)
+    val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+    val md_theme_light_secondaryContainer = Color(0xFFFFD7F5)
+    val md_theme_light_onSecondaryContainer = Color(0xFF380038)
+    val md_theme_light_tertiary = Color(0xFF006A6A)
+    val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+    val md_theme_light_tertiaryContainer = Color(0xFF00FBFB)
+    val md_theme_light_onTertiaryContainer = Color(0xFF002020)
+    val md_theme_light_error = Color(0xFFBA1A1A)
+    val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+    val md_theme_light_onError = Color(0xFFFFFFFF)
+    val md_theme_light_onErrorContainer = Color(0xFF410002)
+    val md_theme_light_background = Color(0xFFFFFBFF)
+    val md_theme_light_onBackground = Color(0xFF1E1C00)
+    val md_theme_light_surface = Color(0xFFFFFBFF)
+    val md_theme_light_onSurface = Color(0xFF1E1C00)
+    val md_theme_light_surfaceVariant = Color(0xFFDFE4D7)
+    val md_theme_light_onSurfaceVariant = Color(0xFF43483F)
+    val md_theme_light_outline = Color(0xFF73796E)
+    val md_theme_light_inverseOnSurface = Color(0xFFFFF565)
+    val md_theme_light_inverseSurface = Color(0xFF353200)
+    val md_theme_light_inversePrimary = Color(0xFF02E600)
+    val md_theme_light_shadow = Color(0xFF000000)
+    val md_theme_light_surfaceTint = Color(0xFF026E00)
+
+    val md_theme_dark_primary = Color(0xFF02E600)
+    val md_theme_dark_onPrimary = Color(0xFF013A00)
+    val md_theme_dark_primaryContainer = Color(0xFFD7E8CD)
+    val md_theme_dark_onPrimaryContainer = Color(0xFF77FF61)
+    val md_theme_dark_secondary = Color(0xFFFFABF3)
+    val md_theme_dark_onSecondary = Color(0xFF5B005B)
+    val md_theme_dark_secondaryContainer = Color(0xFF810081)
+    val md_theme_dark_onSecondaryContainer = Color(0xFFFFD7F5)
+    val md_theme_dark_tertiary = Color(0xFF00DDDD)
+    val md_theme_dark_onTertiary = Color(0xFF003737)
+    val md_theme_dark_tertiaryContainer = Color(0xFF004F4F)
+    val md_theme_dark_onTertiaryContainer = Color(0xFF00FBFB)
+    val md_theme_dark_error = Color(0xFFFFB4AB)
+    val md_theme_dark_errorContainer = Color(0xFF93000A)
+    val md_theme_dark_onError = Color(0xFF690005)
+    val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+    val md_theme_dark_background = Color(0xFF1E1C00)
+    val md_theme_dark_onBackground = Color(0xFFF2E720)
+    val md_theme_dark_surface = Color(0xFF1E1C00)
+    val md_theme_dark_onSurface = Color(0xFFF2E720)
+    val md_theme_dark_surfaceVariant = Color(0xFF43483F)
+    val md_theme_dark_onSurfaceVariant = Color(0xFFC3C8BC)
+    val md_theme_dark_outline = Color(0xFF8D9387)
+    val md_theme_dark_inverseOnSurface = Color(0xFF1E1C00)
+    val md_theme_dark_inverseSurface = Color(0xFFF2E720)
+    val md_theme_dark_inversePrimary = Color(0xFF026E00)
+    val md_theme_dark_shadow = Color(0xFF000000)
+    val md_theme_dark_surfaceTint = Color(0xFF02E600)
+
+    val seed = Color(0xFF00FF00)
+
+    val LightColors = lightColorScheme(
+        primary = md_theme_light_primary,
+        onPrimary = md_theme_light_onPrimary,
+        primaryContainer = md_theme_light_primaryContainer,
+        onPrimaryContainer = md_theme_light_onPrimaryContainer,
+        secondary = md_theme_light_secondary,
+        onSecondary = md_theme_light_onSecondary,
+        secondaryContainer = md_theme_light_secondaryContainer,
+        onSecondaryContainer = md_theme_light_onSecondaryContainer,
+        tertiary = md_theme_light_tertiary,
+        onTertiary = md_theme_light_onTertiary,
+        tertiaryContainer = md_theme_light_tertiaryContainer,
+        onTertiaryContainer = md_theme_light_onTertiaryContainer,
+        error = md_theme_light_error,
+        onError = md_theme_light_onError,
+        errorContainer = md_theme_light_errorContainer,
+        onErrorContainer = md_theme_light_onErrorContainer,
+        background = md_theme_light_background,
+        onBackground = md_theme_light_onBackground,
+        surface = md_theme_light_surface,
+        onSurface = md_theme_light_onSurface,
+        surfaceVariant = md_theme_light_surfaceVariant,
+        onSurfaceVariant = md_theme_light_onSurfaceVariant,
+        outline = md_theme_light_outline,
+        inverseSurface = md_theme_light_inverseSurface,
+        inverseOnSurface = md_theme_light_inverseOnSurface,
+        inversePrimary = md_theme_light_inversePrimary,
+        surfaceTint = md_theme_light_surfaceTint,
+    )
+
+    val DarkColors = darkColorScheme(
+        primary = md_theme_dark_primary,
+        onPrimary = md_theme_dark_onPrimary,
+        primaryContainer = md_theme_dark_primaryContainer,
+        onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+        secondary = md_theme_dark_secondary,
+        onSecondary = md_theme_dark_onSecondary,
+        secondaryContainer = md_theme_dark_secondaryContainer,
+        onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+        tertiary = md_theme_dark_tertiary,
+        onTertiary = md_theme_dark_onTertiary,
+        tertiaryContainer = md_theme_dark_tertiaryContainer,
+        onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+        error = md_theme_dark_error,
+        onError = md_theme_dark_onError,
+        errorContainer = md_theme_dark_errorContainer,
+        onErrorContainer = md_theme_dark_onErrorContainer,
+        background = md_theme_dark_background,
+        onBackground = md_theme_dark_onBackground,
+        surface = md_theme_dark_surface,
+        onSurface = md_theme_dark_onSurface,
+        surfaceVariant = md_theme_dark_surfaceVariant,
+        onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+        outline = md_theme_dark_outline,
+        inverseSurface = md_theme_dark_inverseSurface,
+        inverseOnSurface = md_theme_dark_inverseOnSurface,
+        inversePrimary = md_theme_dark_inversePrimary,
+        surfaceTint = md_theme_dark_surfaceTint,
+    )
+
+    val colors = ColorProviders(
+        light = LightColors,
+        dark = DarkColors
+    )
+}
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
index 4ee5288..0c49cae 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
@@ -27,7 +27,7 @@
 
     <!-- Styles of the List Template widgets -->
     <string name="list_style_with_header_action">List Template with header and action demo</string>
-    <string name="list_style_with_header">List Template with header demo</string>
+    <string name="list_style_with_header">List Template with header and custom theme demo</string>
     <string name="list_style_no_header">List Template with no header demo</string>
     <string name="list_style_brief">List Template with no header demo in brief info</string>
 
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 8e5d0a2..4b49d42 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -102,6 +102,7 @@
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Rule
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -755,6 +756,7 @@
         }
     }
 
+    @Ignore // b/259718271
     @Test
     fun compoundButtonAction() {
         val checkbox = "checkbox"
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
index ff7adf8..d68d7e7 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
@@ -124,6 +124,7 @@
 @LazyScopeMarker
 interface LazyItemScope
 
+@JvmDefaultWithCompatibility
 /**
  * Receiver scope which is used by [LazyColumn].
  */
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
index 8ab2849..cf37b7e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
@@ -116,6 +116,7 @@
     )
 }
 
+@JvmDefaultWithCompatibility
 /**
  * Receiver scope which is used by [LazyColumn].
  */
diff --git a/glance/glance-wear-tiles/api/current.txt b/glance/glance-wear-tiles/api/current.txt
index ee79bb24..3a8208e 100644
--- a/glance/glance-wear-tiles/api/current.txt
+++ b/glance/glance-wear-tiles/api/current.txt
@@ -110,7 +110,7 @@
     method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.glance.GlanceModifier modifier, optional float anchorDegrees, optional int anchorType, optional int radialAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.CurvedScope,kotlin.Unit> content);
   }
 
-  @androidx.glance.wear.tiles.curved.CurvedScopeMarker public interface CurvedScope {
+  @androidx.glance.wear.tiles.curved.CurvedScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface CurvedScope {
     method public void curvedComposable(optional boolean rotateContent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public void curvedLine(androidx.glance.unit.ColorProvider color, optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
     method public void curvedSpacer(optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
@@ -132,7 +132,7 @@
     property public final androidx.glance.text.FontWeight? fontWeight;
   }
 
-  @androidx.compose.runtime.Stable public interface GlanceCurvedModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceCurvedModifier {
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
@@ -148,7 +148,7 @@
     method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? super R,? extends R> operation);
   }
 
-  public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
     method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
diff --git a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
index d6febbf..39c1be1 100644
--- a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
+++ b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
@@ -122,7 +122,7 @@
     method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.glance.GlanceModifier modifier, optional float anchorDegrees, optional int anchorType, optional int radialAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.CurvedScope,kotlin.Unit> content);
   }
 
-  @androidx.glance.wear.tiles.curved.CurvedScopeMarker public interface CurvedScope {
+  @androidx.glance.wear.tiles.curved.CurvedScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface CurvedScope {
     method public void curvedComposable(optional boolean rotateContent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public void curvedLine(androidx.glance.unit.ColorProvider color, optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
     method public void curvedSpacer(optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
@@ -144,7 +144,7 @@
     property public final androidx.glance.text.FontWeight? fontWeight;
   }
 
-  @androidx.compose.runtime.Stable public interface GlanceCurvedModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceCurvedModifier {
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
@@ -160,7 +160,7 @@
     method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? super R,? extends R> operation);
   }
 
-  public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
     method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
diff --git a/glance/glance-wear-tiles/api/restricted_current.txt b/glance/glance-wear-tiles/api/restricted_current.txt
index ee79bb24..3a8208e 100644
--- a/glance/glance-wear-tiles/api/restricted_current.txt
+++ b/glance/glance-wear-tiles/api/restricted_current.txt
@@ -110,7 +110,7 @@
     method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.glance.GlanceModifier modifier, optional float anchorDegrees, optional int anchorType, optional int radialAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.CurvedScope,kotlin.Unit> content);
   }
 
-  @androidx.glance.wear.tiles.curved.CurvedScopeMarker public interface CurvedScope {
+  @androidx.glance.wear.tiles.curved.CurvedScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface CurvedScope {
     method public void curvedComposable(optional boolean rotateContent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public void curvedLine(androidx.glance.unit.ColorProvider color, optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
     method public void curvedSpacer(optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
@@ -132,7 +132,7 @@
     property public final androidx.glance.text.FontWeight? fontWeight;
   }
 
-  @androidx.compose.runtime.Stable public interface GlanceCurvedModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceCurvedModifier {
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
@@ -148,7 +148,7 @@
     method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? super R,? extends R> operation);
   }
 
-  public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
     method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
     method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt
index f82054c..c84930b 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt
@@ -244,6 +244,7 @@
 @CurvedScopeMarker
 interface CurvedChildScope
 
+@JvmDefaultWithCompatibility
 /** A scope for elements which can only be contained within a [CurvedRow]. */
 @CurvedScopeMarker
 interface CurvedScope {
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt
index 796b93c..dd08bc9 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt
@@ -23,6 +23,7 @@
 import androidx.glance.semantics.SemanticsConfiguration
 import androidx.glance.semantics.SemanticsPropertyReceiver
 
+@JvmDefaultWithCompatibility
 /**
  * An ordered, immutable, collection of modifier element that works with curved components in the
  * Glance library.
@@ -73,6 +74,7 @@
         if (other === GlanceCurvedModifier) this
         else CombinedGlanceCurvedModifier(this, other)
 
+    @JvmDefaultWithCompatibility
     /**
      * A single element contained within a [GlanceCurvedModifier] chain.
      */
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 222ec6d..56a9a5b 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -48,7 +48,7 @@
   public interface GlanceId {
   }
 
-  @androidx.compose.runtime.Stable public interface GlanceModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceModifier {
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
@@ -64,7 +64,7 @@
     method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.GlanceModifier.Element,? super R,? extends R> operation);
   }
 
-  public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
     method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 222ec6d..56a9a5b 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -48,7 +48,7 @@
   public interface GlanceId {
   }
 
-  @androidx.compose.runtime.Stable public interface GlanceModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceModifier {
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
@@ -64,7 +64,7 @@
     method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.GlanceModifier.Element,? super R,? extends R> operation);
   }
 
-  public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
     method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 222ec6d..56a9a5b 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -48,7 +48,7 @@
   public interface GlanceId {
   }
 
-  @androidx.compose.runtime.Stable public interface GlanceModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceModifier {
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
@@ -64,7 +64,7 @@
     method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.GlanceModifier.Element,? super R,? extends R> operation);
   }
 
-  public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
     method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
     method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt
index bdafdf3..bf8ffc9 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt
@@ -17,6 +17,7 @@
 
 import androidx.compose.runtime.Stable
 
+@JvmDefaultWithCompatibility
 /**
  * An ordered, immutable, collection of modifier element for the Glance library.
  *
@@ -65,6 +66,7 @@
     infix fun then(other: GlanceModifier): GlanceModifier =
         if (other === GlanceModifier) this else CombinedGlanceModifier(this, other)
 
+    @JvmDefaultWithCompatibility
     /**
      * A single element contained within a [GlanceModifier] chain.
      */
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
index cb4c94b..d14b141 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
@@ -29,6 +29,7 @@
 import androidx.work.workDataOf
 import java.util.concurrent.TimeUnit
 
+@JvmDefaultWithCompatibility
 /**
  * [SessionManager] is the entrypoint for Glance surfaces to start a session worker that will handle
  * their composition.
diff --git a/gradle.properties b/gradle.properties
index c6660ac..c2082a6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -44,6 +44,8 @@
 android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline
 # Workaround for b/162074215
 android.includeDependencyInfoInApks=false
+# Allow multiple r8 tasks at once because otherwise they can make the critical path longer: b/256187923
+android.r8.maxWorkers=2
 
 kotlin.stdlib.default.dependency=false
 # mac targets cannot be built on linux, suppress the warning.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0546b9e..11e4050 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -35,7 +35,7 @@
 incap = "0.2"
 jcodec = "0.2.5"
 kotlin = "1.7.21"
-kotlinBenchmark = "0.4.5"
+kotlinBenchmark = "0.4.6"
 kotlinNative = "1.7.21"
 kotlinCompileTesting = "1.4.9"
 kotlinCoroutines = "1.6.4"
@@ -43,7 +43,7 @@
 ksp = "1.7.21-1.0.8"
 ktlint = "0.46.0-20220520.192227-74"
 leakcanary = "2.8.1"
-metalava = "1.0.0-alpha06"
+metalava = "1.0.0-alpha07"
 mockito = "2.25.0"
 moshi = "1.13.0"
 protobuf = "3.21.8"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 764b8b2e..25322cf 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -809,17 +809,17 @@
             <sha256 value="4df94aaeee8d900be431386e31ef44e82a66e57c3ae30866aec2875aff01fe70" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
-         <artifact name="kotlinx-benchmark-plugin-0.4.5.jar">
-            <sha256 value="64042e32a7c23040980d8b7cb9c2dc2281bf6e19da511d054639f44ba5eaac66" origin="Generated by Gradle because artifact wasn't signed"/>
+      <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.6">
+         <artifact name="kotlinx-benchmark-plugin-0.4.6.jar">
+            <sha256 value="9806179c613658456da5a53cb945138ee0983ecccc9185de22ece2be9c14cfc7" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
-         <artifact name="kotlinx-benchmark-plugin-0.4.5.pom">
-            <sha256 value="33a06eec241b0a20bc3f37b32574d1aaed34905005daba892a85cd832de14700" origin="Generated by Gradle because artifact wasn't signed"/>
+         <artifact name="kotlinx-benchmark-plugin-0.4.6.module">
+            <sha256 value="b2bcac512573b085113a2ad74ed339b7cdefc49094b342a463b5a51b07ef5e1e" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
       </component>
-      <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
-         <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.5.pom">
-            <sha256 value="b13fc9975c2f5cfb992f423c60be042252a289e0f40390e7da2122ac2dec08a5" origin="Generated by Gradle because artifact wasn't signed"/>
+      <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.6">
+         <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.6.pom">
+            <sha256 value="3f739c3504155f5568c26c5c013aa62dc254daa7fe627d7b43b3448c79d36447" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
       </component>
       <component group="org.ow2" name="ow2" version="1.5" androidx:reason="https://gitlab.ow2.org/asm/asm/-/merge_requests/354">
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index bf036fb..7000b35 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -43,7 +43,7 @@
     field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
   }
 
-  public static interface GLFrontBufferedRenderer.Callback<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
     method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
     method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
     method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
@@ -116,7 +116,7 @@
     method @WorkerThread public void onEGLContextDestroyed(androidx.graphics.opengl.egl.EGLManager eglManager);
   }
 
-  public static interface GLRenderer.RenderCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLRenderer.RenderCallback {
     method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
     method @WorkerThread public default android.opengl.EGLSurface? onSurfaceCreated(androidx.graphics.opengl.egl.EGLSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
   }
@@ -199,7 +199,7 @@
   public static final class EGLManager.Companion {
   }
 
-  public interface EGLSpec {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface EGLSpec {
     method public int eglClientWaitSyncKHR(androidx.opengl.EGLSyncKHR sync, int flags, long timeoutNanos);
     method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
     method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.opengl.EGLImageKHR? eglCreateImageFromHardwareBuffer(android.hardware.HardwareBuffer hardwareBuffer);
@@ -336,6 +336,7 @@
     method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
     field public static final androidx.opengl.EGLExt.Companion Companion;
+    field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
     field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
     field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
     field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
index bf036fb..7000b35 100644
--- a/graphics/graphics-core/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -43,7 +43,7 @@
     field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
   }
 
-  public static interface GLFrontBufferedRenderer.Callback<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
     method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
     method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
     method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
@@ -116,7 +116,7 @@
     method @WorkerThread public void onEGLContextDestroyed(androidx.graphics.opengl.egl.EGLManager eglManager);
   }
 
-  public static interface GLRenderer.RenderCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLRenderer.RenderCallback {
     method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
     method @WorkerThread public default android.opengl.EGLSurface? onSurfaceCreated(androidx.graphics.opengl.egl.EGLSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
   }
@@ -199,7 +199,7 @@
   public static final class EGLManager.Companion {
   }
 
-  public interface EGLSpec {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface EGLSpec {
     method public int eglClientWaitSyncKHR(androidx.opengl.EGLSyncKHR sync, int flags, long timeoutNanos);
     method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
     method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.opengl.EGLImageKHR? eglCreateImageFromHardwareBuffer(android.hardware.HardwareBuffer hardwareBuffer);
@@ -336,6 +336,7 @@
     method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
     field public static final androidx.opengl.EGLExt.Companion Companion;
+    field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
     field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
     field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
     field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index 8ec6a0b..0ee95a4 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -43,7 +43,7 @@
     field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
   }
 
-  public static interface GLFrontBufferedRenderer.Callback<T> {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
     method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
     method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
     method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
@@ -116,7 +116,7 @@
     method @WorkerThread public void onEGLContextDestroyed(androidx.graphics.opengl.egl.EGLManager eglManager);
   }
 
-  public static interface GLRenderer.RenderCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLRenderer.RenderCallback {
     method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
     method @WorkerThread public default android.opengl.EGLSurface? onSurfaceCreated(androidx.graphics.opengl.egl.EGLSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
   }
@@ -200,7 +200,7 @@
   public static final class EGLManager.Companion {
   }
 
-  public interface EGLSpec {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface EGLSpec {
     method public int eglClientWaitSyncKHR(androidx.opengl.EGLSyncKHR sync, int flags, long timeoutNanos);
     method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
     method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.opengl.EGLImageKHR? eglCreateImageFromHardwareBuffer(android.hardware.HardwareBuffer hardwareBuffer);
@@ -337,6 +337,7 @@
     method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
     field public static final androidx.opengl.EGLExt.Companion Companion;
+    field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
     field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
     field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
     field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
index b7c29c1..2e54e92 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
@@ -30,6 +30,7 @@
 import androidx.annotation.RequiresApi
 import androidx.opengl.EGLBindings
 import androidx.opengl.EGLExt
+import androidx.opengl.EGLExt.Companion.EGL_ANDROID_CLIENT_BUFFER
 import androidx.opengl.EGLExt.Companion.EGL_SYNC_CONDITION_KHR
 import androidx.opengl.EGLExt.Companion.EGL_SYNC_FENCE_KHR
 import androidx.opengl.EGLExt.Companion.EGL_SYNC_NATIVE_FENCE_ANDROID
@@ -399,9 +400,14 @@
                 isExtensionSupported(EGL_KHR_IMAGE_BASE)
             val androidImageNativeBufferSupported =
                 isExtensionSupported(EGL_ANDROID_IMAGE_NATIVE_BUFFER)
+            val eglClientBufferSupported =
+                isExtensionSupported(EGL_ANDROID_CLIENT_BUFFER)
             // According to EGL spec both these extensions are required in order to support
             // eglGetNativeClientBufferAndroid
-            if (khrImageBaseSupported && androidImageNativeBufferSupported) {
+            if (khrImageBaseSupported &&
+                androidImageNativeBufferSupported &&
+                eglClientBufferSupported
+            ) {
                 assertTrue(EGLBindings.nSupportsEglGetNativeClientBufferAndroid())
             }
         }
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index b191913..59f43ce 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -613,6 +613,7 @@
             }
     }
 
+    @JvmDefaultWithCompatibility
     /**
      * Provides callbacks for consumers to draw into the front and double buffered layers as well as
      * provide opportunities to synchronize [SurfaceControlCompat.Transaction]s to submit the layers
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index a13a2a9..fbdaf57 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -19,6 +19,7 @@
 import androidx.graphics.opengl.GLRenderer
 import androidx.graphics.surface.SurfaceControlCompat
 
+@JvmDefaultWithCompatibility
 /**
  * Interface used to define a parent for rendering front and double buffered layers.
  * This provides the following facilities:
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
index a23a80a..ed18a5b 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
@@ -299,6 +299,7 @@
         fun onEGLContextDestroyed(eglManager: EGLManager)
     }
 
+    @JvmDefaultWithCompatibility
     /**
      * Interface used for creating an [EGLSurface] with a user defined configuration
      * from the provided surface as well as a callback used to render content into the surface
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
index 09f4584..7efd824 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
@@ -39,6 +39,7 @@
 import androidx.opengl.EGLImageKHR
 import androidx.opengl.EGLSyncKHR
 
+@JvmDefaultWithCompatibility
 /**
  * Interface for accessing various EGL facilities independent of EGL versions.
  * That is each EGL version implements this specification.
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
index 0a8272c..a2ef757 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
@@ -79,6 +79,7 @@
         fun build(): SurfaceControlImpl
     }
 
+    @JvmDefaultWithCompatibility
     @RequiresApi(Build.VERSION_CODES.KITKAT)
     interface Transaction : AutoCloseable {
 
diff --git a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
index 0f48ee3..a2015a1 100644
--- a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
+++ b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
@@ -184,6 +184,14 @@
         const val EGL_KHR_IMAGE_BASE = "EGL_KHR_image_base"
 
         /**
+         * Extension that allows creating an EGLClientBuffer from an Android [HardwareBuffer]
+         * object which can later be used to create an [EGLImageKHR] instance.
+         * See:
+         * https://registry.khronos.org/EGL/extensions/ANDROID/EGL_ANDROID_get_native_client_buffer.txt
+         */
+        const val EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer"
+
+        /**
          * Extension that defines a new EGL resource type that is suitable for
          * sharing 2D arrays of image data between client APIs, the EGLImage,
          * and allows creating EGLImages from EGL native pixmaps.
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index bff896d..0349123 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.connect.client {
 
-  public interface HealthConnectClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
     method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
     method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
     method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
@@ -31,7 +31,7 @@
     method public boolean isProviderAvailable(android.content.Context context);
   }
 
-  public interface PermissionController {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
     method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
     method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
     method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index bff896d..0349123 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.connect.client {
 
-  public interface HealthConnectClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
     method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
     method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
     method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
@@ -31,7 +31,7 @@
     method public boolean isProviderAvailable(android.content.Context context);
   }
 
-  public interface PermissionController {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
     method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
     method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
     method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 0bf9f664..3be3faa 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.connect.client {
 
-  public interface HealthConnectClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
     method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
     method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
     method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
@@ -31,7 +31,7 @@
     method public boolean isProviderAvailable(android.content.Context context);
   }
 
-  public interface PermissionController {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
     method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
     method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
     method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 4bcab1f..c6f6c51 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -43,6 +43,7 @@
 import java.io.IOException
 import kotlin.reflect.KClass
 
+@JvmDefaultWithCompatibility
 /** Interface to access health and fitness records. */
 interface HealthConnectClient {
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index a8fd057..e5aa0f7 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -22,6 +22,7 @@
 import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal
 import androidx.health.connect.client.permission.HealthPermission
 
+@JvmDefaultWithCompatibility
 /** Interface for operations related to permissions. */
 interface PermissionController {
 
diff --git a/health/health-services-client/api/1.0.0-beta02.txt b/health/health-services-client/api/1.0.0-beta02.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/1.0.0-beta02.txt
+++ b/health/health-services-client/api/1.0.0-beta02.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.services.client {
 
-  public interface ExerciseClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
     property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
   }
 
-  public interface MeasureCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
     method public default void onRegistered();
@@ -78,7 +78,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
-  public interface PassiveListenerCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
     method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
     ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
   }
 
-  public interface Availability {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
     method public int getId();
     property public abstract int id;
     field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.services.client {
 
-  public interface ExerciseClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
     property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
   }
 
-  public interface MeasureCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
     method public default void onRegistered();
@@ -78,7 +78,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
-  public interface PassiveListenerCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
     method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
     ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
   }
 
-  public interface Availability {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
     method public int getId();
     property public abstract int id;
     field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.services.client {
 
-  public interface ExerciseClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
     property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
   }
 
-  public interface MeasureCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
     method public default void onRegistered();
@@ -78,7 +78,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
-  public interface PassiveListenerCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
     method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
     ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
   }
 
-  public interface Availability {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
     method public int getId();
     property public abstract int id;
     field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.services.client {
 
-  public interface ExerciseClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
     property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
   }
 
-  public interface MeasureCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
     method public default void onRegistered();
@@ -78,7 +78,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
-  public interface PassiveListenerCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
     method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
     ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
   }
 
-  public interface Availability {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
     method public int getId();
     property public abstract int id;
     field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/restricted_1.0.0-beta02.txt b/health/health-services-client/api/restricted_1.0.0-beta02.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta02.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta02.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.services.client {
 
-  public interface ExerciseClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
     property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
   }
 
-  public interface MeasureCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
     method public default void onRegistered();
@@ -78,7 +78,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
-  public interface PassiveListenerCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
     method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
     ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
   }
 
-  public interface Availability {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
     method public int getId();
     property public abstract int id;
     field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.health.services.client {
 
-  public interface ExerciseClient {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
     property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
   }
 
-  public interface MeasureCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
     method public default void onRegistered();
@@ -78,7 +78,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
-  public interface PassiveListenerCallback {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
     method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
     ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
   }
 
-  public interface Availability {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
     method public int getId();
     property public abstract int id;
     field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
index 311cdcd..948a4d9 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
@@ -31,6 +31,7 @@
 import com.google.common.util.concurrent.ListenableFuture
 import java.util.concurrent.Executor
 
+@JvmDefaultWithCompatibility
 /** Client which provides a way to subscribe to the health data of a device during an exercise. */
 public interface ExerciseClient {
     /**
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt
index e648778..0397fca 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt
@@ -22,6 +22,7 @@
 import androidx.health.services.client.data.DataType
 import androidx.health.services.client.data.DeltaDataType
 
+@JvmDefaultWithCompatibility
 /** Callback for [MeasureClient.registerMeasureCallback]. */
 public interface MeasureCallback {
 
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt
index b202724..0efdfc9 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt
@@ -22,6 +22,7 @@
 import androidx.health.services.client.data.PassiveGoal
 import androidx.health.services.client.data.UserActivityInfo
 
+@JvmDefaultWithCompatibility
 /** A callback for receiving passive monitoring updates. */
 public interface PassiveListenerCallback {
 
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
index f4438ca..4b3f682 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
@@ -21,6 +21,7 @@
 import androidx.health.services.client.proto.DataProto.Availability.AvailabilityCase
 import androidx.health.services.client.proto.DataProto.Availability.DataTypeAvailability as DataTypeAvailabilityProto
 
+@JvmDefaultWithCompatibility
 /** Availability of a [DataType]. */
 public interface Availability {
     public val id: Int
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index af40a5a..b8e3752 100644
--- a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -53,6 +53,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -157,6 +158,7 @@
         }
     }
 
+    @Ignore // b/239415930
     @Test
     @LargeTest
     public void testInputBuffer_NoGrid_NoHandler() throws Throwable {
@@ -166,6 +168,7 @@
         doTestForVariousNumberImages(builder);
     }
 
+    @Ignore // b/239415930
     @Test
     @LargeTest
     public void testInputBuffer_Grid_NoHandler() throws Throwable {
@@ -175,6 +178,7 @@
         doTestForVariousNumberImages(builder);
     }
 
+    @Ignore // b/239415930
     @Test
     @LargeTest
     public void testInputBuffer_NoGrid_Handler() throws Throwable {
@@ -184,6 +188,7 @@
         doTestForVariousNumberImages(builder);
     }
 
+    @Ignore // b/239415930
     @Test
     @LargeTest
     public void testInputBuffer_Grid_Handler() throws Throwable {
diff --git a/libraryversions.toml b/libraryversions.toml
index c5f8065..23d4145 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -135,6 +135,7 @@
 VIEWPAGER2 = "1.2.0-alpha01"
 WEAR = "1.3.0-alpha04"
 WEAR_COMPOSE = "1.2.0-alpha01"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha01"
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha01"
diff --git a/lifecycle/integration-tests/incrementality/build.gradle b/lifecycle/integration-tests/incrementality/build.gradle
index f048361..21dcdf0 100644
--- a/lifecycle/integration-tests/incrementality/build.gradle
+++ b/lifecycle/integration-tests/incrementality/build.gradle
@@ -32,7 +32,11 @@
 SdkResourceGenerator.generateForHostTest(project)
 
 // lifecycle-common and annotation are the dependencies of lifecycle-compiler
-tasks.findByPath("test").dependsOn(tasks.findByPath(":lifecycle:lifecycle-compiler:publish"),
-        tasks.findByPath(":lifecycle:lifecycle-common:publish"),
-        tasks.findByPath(":lifecycle:lifecycle-runtime:publish"),
-        tasks.findByPath(":annotation:annotation:publish"))
+tasks.findByPath("test").configure {
+    dependsOn(":lifecycle:lifecycle-compiler:publish")
+    dependsOn(":lifecycle:lifecycle-common:publish")
+    dependsOn(":lifecycle:lifecycle-runtime:publish")
+    dependsOn(":annotation:annotation:publish")
+    dependsOn(":arch:core:core-common:publish")
+    dependsOn(":arch:core:core-runtime:publish")
+}
diff --git a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
index 43a6a72..5b4d60d 100644
--- a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
+++ b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
@@ -36,7 +36,7 @@
     fun initialization() {
         val owner = TestLifecycleOwner(Lifecycle.State.INITIALIZED, UnconfinedTestDispatcher())
         val scope = owner.lifecycleScope
-        assertThat(owner.lifecycle.mInternalScopeRef.get()).isSameInstanceAs(scope)
+        assertThat(owner.lifecycle.internalScopeRef.get()).isSameInstanceAs(scope)
         val scope2 = owner.lifecycleScope
         assertThat(scope).isSameInstanceAs(scope2)
         runBlocking(Dispatchers.Main) {
diff --git a/lifecycle/lifecycle-common/api/current.txt b/lifecycle/lifecycle-common/api/current.txt
index 53075b6..a339f50 100644
--- a/lifecycle/lifecycle-common/api/current.txt
+++ b/lifecycle/lifecycle-common/api/current.txt
@@ -12,17 +12,21 @@
 
   public abstract class Lifecycle {
     ctor public Lifecycle();
-    method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver);
+    method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
     method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
-    method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver);
+    method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+    property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
   }
 
   public enum Lifecycle.Event {
-    method public static androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State);
-    method public static androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State);
-    method public androidx.lifecycle.Lifecycle.State getTargetState();
-    method public static androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State);
-    method public static androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State);
+    method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+    method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+    method public final androidx.lifecycle.Lifecycle.State getTargetState();
+    method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+    method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+    method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.lifecycle.Lifecycle.Event[] values();
+    property public final androidx.lifecycle.Lifecycle.State targetState;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
@@ -30,10 +34,20 @@
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+    field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+  }
+
+  public static final class Lifecycle.Event.Companion {
+    method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
   }
 
   public enum Lifecycle.State {
-    method public boolean isAtLeast(androidx.lifecycle.Lifecycle.State);
+    method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+    method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.lifecycle.Lifecycle.State[] values();
     enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
     enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
     enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
diff --git a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
index 53075b6..a339f50 100644
--- a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
@@ -12,17 +12,21 @@
 
   public abstract class Lifecycle {
     ctor public Lifecycle();
-    method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver);
+    method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
     method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
-    method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver);
+    method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+    property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
   }
 
   public enum Lifecycle.Event {
-    method public static androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State);
-    method public static androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State);
-    method public androidx.lifecycle.Lifecycle.State getTargetState();
-    method public static androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State);
-    method public static androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State);
+    method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+    method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+    method public final androidx.lifecycle.Lifecycle.State getTargetState();
+    method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+    method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+    method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.lifecycle.Lifecycle.Event[] values();
+    property public final androidx.lifecycle.Lifecycle.State targetState;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
@@ -30,10 +34,20 @@
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+    field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+  }
+
+  public static final class Lifecycle.Event.Companion {
+    method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
   }
 
   public enum Lifecycle.State {
-    method public boolean isAtLeast(androidx.lifecycle.Lifecycle.State);
+    method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+    method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.lifecycle.Lifecycle.State[] values();
     enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
     enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
     enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
diff --git a/lifecycle/lifecycle-common/api/restricted_current.txt b/lifecycle/lifecycle-common/api/restricted_current.txt
index 86f017b..04b9c90 100644
--- a/lifecycle/lifecycle-common/api/restricted_current.txt
+++ b/lifecycle/lifecycle-common/api/restricted_current.txt
@@ -19,17 +19,21 @@
 
   public abstract class Lifecycle {
     ctor public Lifecycle();
-    method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver);
+    method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
     method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
-    method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver);
+    method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+    property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
   }
 
   public enum Lifecycle.Event {
-    method public static androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State);
-    method public static androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State);
-    method public androidx.lifecycle.Lifecycle.State getTargetState();
-    method public static androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State);
-    method public static androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State);
+    method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+    method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+    method public final androidx.lifecycle.Lifecycle.State getTargetState();
+    method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+    method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+    method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.lifecycle.Lifecycle.Event[] values();
+    property public final androidx.lifecycle.Lifecycle.State targetState;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
@@ -37,10 +41,20 @@
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
     enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+    field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+  }
+
+  public static final class Lifecycle.Event.Companion {
+    method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+    method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
   }
 
   public enum Lifecycle.State {
-    method public boolean isAtLeast(androidx.lifecycle.Lifecycle.State);
+    method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+    method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.lifecycle.Lifecycle.State[] values();
     enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
     enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
     enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
@@ -59,13 +73,14 @@
     method public androidx.lifecycle.Lifecycle getLifecycle();
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Lifecycling {
-    method public static String getAdapterName(String);
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Lifecycling {
+    method public static String getAdapterName(String className);
+    method public static androidx.lifecycle.LifecycleEventObserver lifecycleEventObserver(Object object);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MethodCallsLogger {
     ctor public MethodCallsLogger();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean approveCall(String, int);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean approveCall(String name, int type);
   }
 
   @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface OnLifecycleEvent {
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index 71c6829..061f3b0 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -44,11 +44,3 @@
     inceptionYear = "2017"
     description = "Android Lifecycle-Common"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.java
deleted file mode 100644
index 0a5efd9..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.java
+++ /dev/null
@@ -1,298 +0,0 @@
-/*
- * Copyright (C) 2017 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.lifecycle;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Defines an object that has an Android Lifecycle. {@link androidx.fragment.app.Fragment Fragment}
- * and {@link androidx.fragment.app.FragmentActivity FragmentActivity} classes implement
- * {@link LifecycleOwner} interface which has the {@link LifecycleOwner#getLifecycle()
- * getLifecycle} method to access the Lifecycle. You can also implement {@link LifecycleOwner}
- * in your own classes.
- * <p>
- * {@link Event#ON_CREATE}, {@link Event#ON_START}, {@link Event#ON_RESUME} events in this class
- * are dispatched <b>after</b> the {@link LifecycleOwner}'s related method returns.
- * {@link Event#ON_PAUSE}, {@link Event#ON_STOP}, {@link Event#ON_DESTROY} events in this class
- * are dispatched <b>before</b> the {@link LifecycleOwner}'s related method is called.
- * For instance, {@link Event#ON_START} will be dispatched after
- * {@link android.app.Activity#onStart onStart} returns, {@link Event#ON_STOP} will be dispatched
- * before {@link android.app.Activity#onStop onStop} is called.
- * This gives you certain guarantees on which state the owner is in.
- * <p>
- * To observe lifecycle events call {@link #addObserver(LifecycleObserver)} passing an object
- * that implements either {@link DefaultLifecycleObserver} or {@link LifecycleEventObserver}.
- */
-public abstract class Lifecycle {
-
-    /**
-     * Lifecycle coroutines extensions stashes the CoroutineScope into this field.
-     *
-     * @hide used by lifecycle-common-ktx
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    AtomicReference<Object> mInternalScopeRef = new AtomicReference<>();
-
-    /**
-     * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
-     * state.
-     * <p>
-     * The given observer will be brought to the current state of the LifecycleOwner.
-     * For example, if the LifecycleOwner is in {@link State#STARTED} state, the given observer
-     * will receive {@link Event#ON_CREATE}, {@link Event#ON_START} events.
-     *
-     * @param observer The observer to notify.
-     */
-    @MainThread
-    public abstract void addObserver(@NonNull LifecycleObserver observer);
-
-    /**
-     * Removes the given observer from the observers list.
-     * <p>
-     * If this method is called while a state change is being dispatched,
-     * <ul>
-     * <li>If the given observer has not yet received that event, it will not receive it.
-     * <li>If the given observer has more than 1 method that observes the currently dispatched
-     * event and at least one of them received the event, all of them will receive the event and
-     * the removal will happen afterwards.
-     * </ul>
-     *
-     * @param observer The observer to be removed.
-     */
-    @MainThread
-    public abstract void removeObserver(@NonNull LifecycleObserver observer);
-
-    /**
-     * Returns the current state of the Lifecycle.
-     *
-     * @return The current state of the Lifecycle.
-     */
-    @MainThread
-    @NonNull
-    public abstract State getCurrentState();
-
-    @SuppressWarnings("WeakerAccess")
-    public enum Event {
-        /**
-         * Constant for onCreate event of the {@link LifecycleOwner}.
-         */
-        ON_CREATE,
-        /**
-         * Constant for onStart event of the {@link LifecycleOwner}.
-         */
-        ON_START,
-        /**
-         * Constant for onResume event of the {@link LifecycleOwner}.
-         */
-        ON_RESUME,
-        /**
-         * Constant for onPause event of the {@link LifecycleOwner}.
-         */
-        ON_PAUSE,
-        /**
-         * Constant for onStop event of the {@link LifecycleOwner}.
-         */
-        ON_STOP,
-        /**
-         * Constant for onDestroy event of the {@link LifecycleOwner}.
-         */
-        ON_DESTROY,
-        /**
-         * An {@link Event Event} constant that can be used to match all events.
-         */
-        ON_ANY;
-
-        /**
-         * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
-         * leaving the specified {@link Lifecycle.State} to a lower state, or {@code null}
-         * if there is no valid event that can move down from the given state.
-         *
-         * @param state the higher state that the returned event will transition down from
-         * @return the event moving down the lifecycle phases from state
-         */
-        @Nullable
-        public static Event downFrom(@NonNull State state) {
-            switch (state) {
-                case CREATED:
-                    return ON_DESTROY;
-                case STARTED:
-                    return ON_STOP;
-                case RESUMED:
-                    return ON_PAUSE;
-                default:
-                    return null;
-            }
-        }
-
-        /**
-         * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
-         * entering the specified {@link Lifecycle.State} from a higher state, or {@code null}
-         * if there is no valid event that can move down to the given state.
-         *
-         * @param state the lower state that the returned event will transition down to
-         * @return the event moving down the lifecycle phases to state
-         */
-        @Nullable
-        public static Event downTo(@NonNull State state) {
-            switch (state) {
-                case DESTROYED:
-                    return ON_DESTROY;
-                case CREATED:
-                    return ON_STOP;
-                case STARTED:
-                    return ON_PAUSE;
-                default:
-                    return null;
-            }
-        }
-
-        /**
-         * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
-         * leaving the specified {@link Lifecycle.State} to a higher state, or {@code null}
-         * if there is no valid event that can move up from the given state.
-         *
-         * @param state the lower state that the returned event will transition up from
-         * @return the event moving up the lifecycle phases from state
-         */
-        @Nullable
-        public static Event upFrom(@NonNull State state) {
-            switch (state) {
-                case INITIALIZED:
-                    return ON_CREATE;
-                case CREATED:
-                    return ON_START;
-                case STARTED:
-                    return ON_RESUME;
-                default:
-                    return null;
-            }
-        }
-
-        /**
-         * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
-         * entering the specified {@link Lifecycle.State} from a lower state, or {@code null}
-         * if there is no valid event that can move up to the given state.
-         *
-         * @param state the higher state that the returned event will transition up to
-         * @return the event moving up the lifecycle phases to state
-         */
-        @Nullable
-        public static Event upTo(@NonNull State state) {
-            switch (state) {
-                case CREATED:
-                    return ON_CREATE;
-                case STARTED:
-                    return ON_START;
-                case RESUMED:
-                    return ON_RESUME;
-                default:
-                    return null;
-            }
-        }
-
-        /**
-         * Returns the new {@link Lifecycle.State} of a {@link Lifecycle} that just reported
-         * this {@link Lifecycle.Event}.
-         *
-         * Throws {@link IllegalArgumentException} if called on {@link #ON_ANY}, as it is a special
-         * value used by {@link OnLifecycleEvent} and not a real lifecycle event.
-         *
-         * @return the state that will result from this event
-         */
-        @NonNull
-        public State getTargetState() {
-            switch (this) {
-                case ON_CREATE:
-                case ON_STOP:
-                    return State.CREATED;
-                case ON_START:
-                case ON_PAUSE:
-                    return State.STARTED;
-                case ON_RESUME:
-                    return State.RESUMED;
-                case ON_DESTROY:
-                    return State.DESTROYED;
-                case ON_ANY:
-                    break;
-            }
-            throw new IllegalArgumentException(this + " has no target state");
-        }
-    }
-
-    /**
-     * Lifecycle states. You can consider the states as the nodes in a graph and
-     * {@link Event}s as the edges between these nodes.
-     */
-    @SuppressWarnings("WeakerAccess")
-    public enum State {
-        /**
-         * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
-         * any more events. For instance, for an {@link android.app.Activity}, this state is reached
-         * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
-         */
-        DESTROYED,
-
-        /**
-         * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
-         * the state when it is constructed but has not received
-         * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
-         */
-        INITIALIZED,
-
-        /**
-         * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
-         * is reached in two cases:
-         * <ul>
-         *     <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
-         *     <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
-         * </ul>
-         */
-        CREATED,
-
-        /**
-         * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
-         * is reached in two cases:
-         * <ul>
-         *     <li>after {@link android.app.Activity#onStart() onStart} call;
-         *     <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
-         * </ul>
-         */
-        STARTED,
-
-        /**
-         * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
-         * is reached after {@link android.app.Activity#onResume() onResume} is called.
-         */
-        RESUMED;
-
-        /**
-         * Compares if this State is greater or equal to the given {@code state}.
-         *
-         * @param state State to compare with
-         * @return true if this State is greater or equal to the given {@code state}
-         */
-        public boolean isAtLeast(@NonNull State state) {
-            return compareTo(state) >= 0;
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
new file mode 100644
index 0000000..a0e6648
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2017 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.lifecycle
+
+import androidx.annotation.MainThread
+import androidx.annotation.RestrictTo
+import androidx.lifecycle.Lifecycle.Event
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Defines an object that has an Android Lifecycle. [Fragment][androidx.fragment.app.Fragment]
+ * and [FragmentActivity][androidx.fragment.app.FragmentActivity] classes implement
+ * [LifecycleOwner] interface which has the [ getLifecycle][LifecycleOwner.getLifecycle] method to access the Lifecycle. You can also implement [LifecycleOwner]
+ * in your own classes.
+ *
+ * [Event.ON_CREATE], [Event.ON_START], [Event.ON_RESUME] events in this class
+ * are dispatched **after** the [LifecycleOwner]'s related method returns.
+ * [Event.ON_PAUSE], [Event.ON_STOP], [Event.ON_DESTROY] events in this class
+ * are dispatched **before** the [LifecycleOwner]'s related method is called.
+ * For instance, [Event.ON_START] will be dispatched after
+ * [onStart][android.app.Activity.onStart] returns, [Event.ON_STOP] will be dispatched
+ * before [onStop][android.app.Activity.onStop] is called.
+ * This gives you certain guarantees on which state the owner is in.
+ *
+ * To observe lifecycle events call [.addObserver] passing an object
+ * that implements either [DefaultLifecycleObserver] or [LifecycleEventObserver].
+ */
+public abstract class Lifecycle {
+    /**
+     * Lifecycle coroutines extensions stashes the CoroutineScope into this field.
+     *
+     * @hide used by lifecycle-common-ktx
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public var internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()
+
+    /**
+     * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
+     * state.
+     *
+     * The given observer will be brought to the current state of the LifecycleOwner.
+     * For example, if the LifecycleOwner is in [State.STARTED] state, the given observer
+     * will receive [Event.ON_CREATE], [Event.ON_START] events.
+     *
+     * @param observer The observer to notify.
+     */
+    @MainThread
+    public abstract fun addObserver(observer: LifecycleObserver)
+
+    /**
+     * Removes the given observer from the observers list.
+     *
+     * If this method is called while a state change is being dispatched,
+     *
+     *  * If the given observer has not yet received that event, it will not receive it.
+     *  * If the given observer has more than 1 method that observes the currently dispatched
+     * event and at least one of them received the event, all of them will receive the event and
+     * the removal will happen afterwards.
+     *
+     *
+     * @param observer The observer to be removed.
+     */
+    @MainThread
+    public abstract fun removeObserver(observer: LifecycleObserver)
+
+    /**
+     * Returns the current state of the Lifecycle.
+     *
+     * @return The current state of the Lifecycle.
+     */
+    @get:MainThread
+    public abstract val currentState: State
+
+    public enum class Event {
+        /**
+         * Constant for onCreate event of the [LifecycleOwner].
+         */
+        ON_CREATE,
+
+        /**
+         * Constant for onStart event of the [LifecycleOwner].
+         */
+        ON_START,
+
+        /**
+         * Constant for onResume event of the [LifecycleOwner].
+         */
+        ON_RESUME,
+
+        /**
+         * Constant for onPause event of the [LifecycleOwner].
+         */
+        ON_PAUSE,
+
+        /**
+         * Constant for onStop event of the [LifecycleOwner].
+         */
+        ON_STOP,
+
+        /**
+         * Constant for onDestroy event of the [LifecycleOwner].
+         */
+        ON_DESTROY,
+
+        /**
+         * An [Event] constant that can be used to match all events.
+         */
+        ON_ANY;
+
+        /**
+         * Returns the new [Lifecycle.State] of a [Lifecycle] that just reported
+         * this [Lifecycle.Event].
+         *
+         * Throws [IllegalArgumentException] if called on [.ON_ANY], as it is a special
+         * value used by [OnLifecycleEvent] and not a real lifecycle event.
+         *
+         * @return the state that will result from this event
+         */
+        public val targetState: State
+            get() {
+                when (this) {
+                    ON_CREATE, ON_STOP -> return State.CREATED
+                    ON_START, ON_PAUSE -> return State.STARTED
+                    ON_RESUME -> return State.RESUMED
+                    ON_DESTROY -> return State.DESTROYED
+                    ON_ANY -> {}
+                }
+                throw IllegalArgumentException("$this has no target state")
+            }
+
+        public companion object {
+            /**
+             * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+             * leaving the specified [Lifecycle.State] to a lower state, or `null`
+             * if there is no valid event that can move down from the given state.
+             *
+             * @param state the higher state that the returned event will transition down from
+             * @return the event moving down the lifecycle phases from state
+             */
+            @JvmStatic
+            public fun downFrom(state: State): Event? {
+                return when (state) {
+                    State.CREATED -> ON_DESTROY
+                    State.STARTED -> ON_STOP
+                    State.RESUMED -> ON_PAUSE
+                    else -> null
+                }
+            }
+
+            /**
+             * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+             * entering the specified [Lifecycle.State] from a higher state, or `null`
+             * if there is no valid event that can move down to the given state.
+             *
+             * @param state the lower state that the returned event will transition down to
+             * @return the event moving down the lifecycle phases to state
+             */
+            @JvmStatic
+            public fun downTo(state: State): Event? {
+                return when (state) {
+                    State.DESTROYED -> ON_DESTROY
+                    State.CREATED -> ON_STOP
+                    State.STARTED -> ON_PAUSE
+                    else -> null
+                }
+            }
+
+            /**
+             * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+             * leaving the specified [Lifecycle.State] to a higher state, or `null`
+             * if there is no valid event that can move up from the given state.
+             *
+             * @param state the lower state that the returned event will transition up from
+             * @return the event moving up the lifecycle phases from state
+             */
+            @JvmStatic
+            public fun upFrom(state: State): Event? {
+                return when (state) {
+                    State.INITIALIZED -> ON_CREATE
+                    State.CREATED -> ON_START
+                    State.STARTED -> ON_RESUME
+                    else -> null
+                }
+            }
+
+            /**
+             * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+             * entering the specified [Lifecycle.State] from a lower state, or `null`
+             * if there is no valid event that can move up to the given state.
+             *
+             * @param state the higher state that the returned event will transition up to
+             * @return the event moving up the lifecycle phases to state
+             */
+            @JvmStatic
+            public fun upTo(state: State): Event? {
+                return when (state) {
+                    State.CREATED -> ON_CREATE
+                    State.STARTED -> ON_START
+                    State.RESUMED -> ON_RESUME
+                    else -> null
+                }
+            }
+        }
+    }
+
+    /**
+     * Lifecycle states. You can consider the states as the nodes in a graph and
+     * [Event]s as the edges between these nodes.
+     */
+    public enum class State {
+        /**
+         * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
+         * any more events. For instance, for an [android.app.Activity], this state is reached
+         * **right before** Activity's [onDestroy][android.app.Activity.onDestroy] call.
+         */
+        DESTROYED,
+
+        /**
+         * Initialized state for a LifecycleOwner. For an [android.app.Activity], this is
+         * the state when it is constructed but has not received
+         * [onCreate][android.app.Activity.onCreate] yet.
+         */
+        INITIALIZED,
+
+        /**
+         * Created state for a LifecycleOwner. For an [android.app.Activity], this state
+         * is reached in two cases:
+         *
+         *  * after [onCreate][android.app.Activity.onCreate] call;
+         *  * **right before** [onStop][android.app.Activity.onStop] call.
+         *
+         */
+        CREATED,
+
+        /**
+         * Started state for a LifecycleOwner. For an [android.app.Activity], this state
+         * is reached in two cases:
+         *
+         *  * after [onStart][android.app.Activity.onStart] call;
+         *  * **right before** [onPause][android.app.Activity.onPause] call.
+         *
+         */
+        STARTED,
+
+        /**
+         * Resumed state for a LifecycleOwner. For an [android.app.Activity], this state
+         * is reached after [onResume][android.app.Activity.onResume] is called.
+         */
+        RESUMED;
+
+        /**
+         * Compares if this State is greater or equal to the given `state`.
+         *
+         * @param state State to compare with
+         * @return true if this State is greater or equal to the given `state`
+         */
+        public fun isAtLeast(state: State): Boolean {
+            return compareTo(state) >= 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java
deleted file mode 100644
index 722f0bf..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright (C) 2017 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.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Internal class to handle lifecycle conversion etc.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class Lifecycling {
-
-    private static final int REFLECTIVE_CALLBACK = 1;
-    private static final int GENERATED_CALLBACK = 2;
-
-    private static Map<Class<?>, Integer> sCallbackCache = new HashMap<>();
-    private static Map<Class<?>, List<Constructor<? extends GeneratedAdapter>>> sClassToAdapters =
-            new HashMap<>();
-
-    // Left for binary compatibility when lifecycle-common goes up 2.1 as transitive dep
-    // but lifecycle-runtime stays 2.0
-
-    /**
-     * @deprecated Left for compatibility with lifecycle-runtime:2.0
-     */
-    @SuppressWarnings("deprecation")
-    @Deprecated
-    @NonNull
-    static GenericLifecycleObserver getCallback(final Object object) {
-        final LifecycleEventObserver observer = lifecycleEventObserver(object);
-        return new GenericLifecycleObserver() {
-            @Override
-            public void onStateChanged(@NonNull LifecycleOwner source,
-                    @NonNull Lifecycle.Event event) {
-                observer.onStateChanged(source, event);
-            }
-        };
-    }
-
-    @NonNull
-    @SuppressWarnings("deprecation")
-    static LifecycleEventObserver lifecycleEventObserver(Object object) {
-        boolean isLifecycleEventObserver = object instanceof LifecycleEventObserver;
-        boolean isDefaultLifecycleObserver = object instanceof DefaultLifecycleObserver;
-        if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
-            return new DefaultLifecycleObserverAdapter((DefaultLifecycleObserver) object,
-                    (LifecycleEventObserver) object);
-        }
-        if (isDefaultLifecycleObserver) {
-            return new DefaultLifecycleObserverAdapter((DefaultLifecycleObserver) object, null);
-        }
-
-        if (isLifecycleEventObserver) {
-            return (LifecycleEventObserver) object;
-        }
-
-        final Class<?> klass = object.getClass();
-        int type = getObserverConstructorType(klass);
-        if (type == GENERATED_CALLBACK) {
-            List<Constructor<? extends GeneratedAdapter>> constructors =
-                    sClassToAdapters.get(klass);
-            if (constructors.size() == 1) {
-                GeneratedAdapter generatedAdapter = createGeneratedAdapter(
-                        constructors.get(0), object);
-                return new SingleGeneratedAdapterObserver(generatedAdapter);
-            }
-            GeneratedAdapter[] adapters = new GeneratedAdapter[constructors.size()];
-            for (int i = 0; i < constructors.size(); i++) {
-                adapters[i] = createGeneratedAdapter(constructors.get(i), object);
-            }
-            return new CompositeGeneratedAdaptersObserver(adapters);
-        }
-        return new ReflectiveGenericLifecycleObserver(object);
-    }
-
-    private static GeneratedAdapter createGeneratedAdapter(
-            Constructor<? extends GeneratedAdapter> constructor, Object object) {
-        //noinspection TryWithIdenticalCatches
-        try {
-            return constructor.newInstance(object);
-        } catch (IllegalAccessException e) {
-            throw new RuntimeException(e);
-        } catch (InstantiationException e) {
-            throw new RuntimeException(e);
-        } catch (InvocationTargetException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    @SuppressWarnings("deprecation")
-    @Nullable
-    private static Constructor<? extends GeneratedAdapter> generatedConstructor(Class<?> klass) {
-        try {
-            Package aPackage = klass.getPackage();
-            String name = klass.getCanonicalName();
-            final String fullPackage = aPackage != null ? aPackage.getName() : "";
-            final String adapterName = getAdapterName(fullPackage.isEmpty() ? name :
-                    name.substring(fullPackage.length() + 1));
-
-            @SuppressWarnings("unchecked") final Class<? extends GeneratedAdapter> aClass =
-                    (Class<? extends GeneratedAdapter>) Class.forName(
-                            fullPackage.isEmpty() ? adapterName : fullPackage + "." + adapterName);
-            Constructor<? extends GeneratedAdapter> constructor =
-                    aClass.getDeclaredConstructor(klass);
-            if (!constructor.isAccessible()) {
-                constructor.setAccessible(true);
-            }
-            return constructor;
-        } catch (ClassNotFoundException e) {
-            return null;
-        } catch (NoSuchMethodException e) {
-            // this should not happen
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static int getObserverConstructorType(Class<?> klass) {
-        Integer callbackCache = sCallbackCache.get(klass);
-        if (callbackCache != null) {
-            return callbackCache;
-        }
-        int type = resolveObserverCallbackType(klass);
-        sCallbackCache.put(klass, type);
-        return type;
-    }
-
-    private static int resolveObserverCallbackType(Class<?> klass) {
-        // anonymous class bug:35073837
-        if (klass.getCanonicalName() == null) {
-            return REFLECTIVE_CALLBACK;
-        }
-
-        Constructor<? extends GeneratedAdapter> constructor = generatedConstructor(klass);
-        if (constructor != null) {
-            sClassToAdapters.put(klass, Collections
-                    .<Constructor<? extends GeneratedAdapter>>singletonList(constructor));
-            return GENERATED_CALLBACK;
-        }
-
-        @SuppressWarnings("deprecation")
-        boolean hasLifecycleMethods = ClassesInfoCache.sInstance.hasLifecycleMethods(klass);
-        if (hasLifecycleMethods) {
-            return REFLECTIVE_CALLBACK;
-        }
-
-        Class<?> superclass = klass.getSuperclass();
-        List<Constructor<? extends GeneratedAdapter>> adapterConstructors = null;
-        if (isLifecycleParent(superclass)) {
-            if (getObserverConstructorType(superclass) == REFLECTIVE_CALLBACK) {
-                return REFLECTIVE_CALLBACK;
-            }
-            adapterConstructors = new ArrayList<>(sClassToAdapters.get(superclass));
-        }
-
-        for (Class<?> intrface : klass.getInterfaces()) {
-            if (!isLifecycleParent(intrface)) {
-                continue;
-            }
-            if (getObserverConstructorType(intrface) == REFLECTIVE_CALLBACK) {
-                return REFLECTIVE_CALLBACK;
-            }
-            if (adapterConstructors == null) {
-                adapterConstructors = new ArrayList<>();
-            }
-            adapterConstructors.addAll(sClassToAdapters.get(intrface));
-        }
-        if (adapterConstructors != null) {
-            sClassToAdapters.put(klass, adapterConstructors);
-            return GENERATED_CALLBACK;
-        }
-
-        return REFLECTIVE_CALLBACK;
-    }
-
-    private static boolean isLifecycleParent(Class<?> klass) {
-        return klass != null && LifecycleObserver.class.isAssignableFrom(klass);
-    }
-
-    /**
-     * Create a name for an adapter class.
-     */
-    @NonNull
-    public static String getAdapterName(@NonNull String className) {
-        return className.replace(".", "_") + "_LifecycleAdapter";
-    }
-
-    private Lifecycling() {
-    }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
new file mode 100644
index 0000000..f3a1b9f
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2017 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.lifecycle
+
+import androidx.annotation.RestrictTo
+import java.lang.reflect.Constructor
+import java.lang.reflect.InvocationTargetException
+
+/**
+ * Internal class to handle lifecycle conversion etc.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object Lifecycling {
+    private const val REFLECTIVE_CALLBACK = 1
+    private const val GENERATED_CALLBACK = 2
+    private val callbackCache: MutableMap<Class<*>, Int> = HashMap()
+    private val classToAdapters: MutableMap<Class<*>, List<Constructor<out GeneratedAdapter>>> =
+        HashMap()
+
+    @JvmStatic
+    @Suppress("DEPRECATION")
+    public fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
+        val isLifecycleEventObserver = `object` is LifecycleEventObserver
+        val isDefaultLifecycleObserver = `object` is DefaultLifecycleObserver
+        if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
+            return DefaultLifecycleObserverAdapter(
+                `object` as DefaultLifecycleObserver,
+                `object` as LifecycleEventObserver
+            )
+        }
+        if (isDefaultLifecycleObserver) {
+            return DefaultLifecycleObserverAdapter(`object` as DefaultLifecycleObserver, null)
+        }
+        if (isLifecycleEventObserver) {
+            return `object` as LifecycleEventObserver
+        }
+        val klass: Class<*> = `object`.javaClass
+        val type = getObserverConstructorType(klass)
+        if (type == GENERATED_CALLBACK) {
+            val constructors = classToAdapters[klass]!!
+            if (constructors.size == 1) {
+                val generatedAdapter = createGeneratedAdapter(
+                    constructors[0], `object`
+                )
+                return SingleGeneratedAdapterObserver(generatedAdapter)
+            }
+            val adapters: Array<GeneratedAdapter> = Array(constructors.size) { i ->
+                createGeneratedAdapter(constructors[i], `object`)
+            }
+            return CompositeGeneratedAdaptersObserver(adapters)
+        }
+        return ReflectiveGenericLifecycleObserver(`object`)
+    }
+
+    private fun createGeneratedAdapter(
+        constructor: Constructor<out GeneratedAdapter>,
+        `object`: Any
+    ): GeneratedAdapter {
+        return try {
+            constructor.newInstance(`object`)
+        } catch (e: IllegalAccessException) {
+            throw RuntimeException(e)
+        } catch (e: InstantiationException) {
+            throw RuntimeException(e)
+        } catch (e: InvocationTargetException) {
+            throw RuntimeException(e)
+        }
+    }
+
+    @Suppress("DEPRECATION")
+    private fun generatedConstructor(klass: Class<*>): Constructor<out GeneratedAdapter>? {
+        return try {
+            val aPackage = klass.getPackage()
+            val name = klass.canonicalName
+            val fullPackage = if (aPackage != null) aPackage.name else ""
+            val adapterName =
+                getAdapterName(
+                    if (fullPackage.isEmpty()) name
+                    else name.substring(fullPackage.length + 1)
+                )
+            @Suppress("UNCHECKED_CAST")
+            val aClass = Class.forName(
+                if (fullPackage.isEmpty()) adapterName else "$fullPackage.$adapterName"
+            ) as Class<out GeneratedAdapter>
+            val constructor = aClass.getDeclaredConstructor(klass)
+            if (!constructor.isAccessible) {
+                constructor.isAccessible = true
+            }
+            constructor
+        } catch (e: ClassNotFoundException) {
+            null
+        } catch (e: NoSuchMethodException) {
+            // this should not happen
+            throw RuntimeException(e)
+        }
+    }
+
+    private fun getObserverConstructorType(klass: Class<*>): Int {
+        val callbackCache = callbackCache[klass]
+        if (callbackCache != null) {
+            return callbackCache
+        }
+        val type = resolveObserverCallbackType(klass)
+        this.callbackCache[klass] = type
+        return type
+    }
+
+    private fun resolveObserverCallbackType(klass: Class<*>): Int {
+        // anonymous class bug:35073837
+        if (klass.canonicalName == null) {
+            return REFLECTIVE_CALLBACK
+        }
+        val constructor = generatedConstructor(klass)
+        if (constructor != null) {
+            classToAdapters[klass] = listOf(constructor)
+            return GENERATED_CALLBACK
+        }
+        @Suppress("DEPRECATION")
+        val hasLifecycleMethods = ClassesInfoCache.sInstance.hasLifecycleMethods(klass)
+        if (hasLifecycleMethods) {
+            return REFLECTIVE_CALLBACK
+        }
+        val superclass = klass.superclass
+        var adapterConstructors: MutableList<Constructor<out GeneratedAdapter>>? = null
+        if (isLifecycleParent(superclass)) {
+            if (getObserverConstructorType(superclass) == REFLECTIVE_CALLBACK) {
+                return REFLECTIVE_CALLBACK
+            }
+            adapterConstructors = ArrayList(
+                classToAdapters[superclass]!!
+            )
+        }
+        for (intrface in klass.interfaces) {
+            if (!isLifecycleParent(intrface)) {
+                continue
+            }
+            if (getObserverConstructorType(intrface) == REFLECTIVE_CALLBACK) {
+                return REFLECTIVE_CALLBACK
+            }
+            if (adapterConstructors == null) {
+                adapterConstructors = ArrayList()
+            }
+            adapterConstructors.addAll(classToAdapters[intrface]!!)
+        }
+        if (adapterConstructors != null) {
+            classToAdapters[klass] = adapterConstructors
+            return GENERATED_CALLBACK
+        }
+        return REFLECTIVE_CALLBACK
+    }
+
+    private fun isLifecycleParent(klass: Class<*>?): Boolean {
+        return klass != null && LifecycleObserver::class.java.isAssignableFrom(klass)
+    }
+
+    /**
+     * Create a name for an adapter class.
+     */
+    @JvmStatic
+    public fun getAdapterName(className: String): String {
+        return className.replace(".", "_") + "_LifecycleAdapter"
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.java
deleted file mode 100644
index d72a3ac..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2017 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.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class MethodCallsLogger {
-    private Map<String, Integer> mCalledMethods = new HashMap<>();
-
-    /**
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    public boolean approveCall(@NonNull String name, int type) {
-        Integer nullableMask = mCalledMethods.get(name);
-        int mask = nullableMask != null ? nullableMask : 0;
-        boolean wasCalled = (mask & type) != 0;
-        mCalledMethods.put(name, mask | type);
-        return !wasCalled;
-    }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt
new file mode 100644
index 0000000..c14c15e
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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.lifecycle
+
+import androidx.annotation.RestrictTo
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public open class MethodCallsLogger() {
+    private val calledMethods: MutableMap<String, Int> = HashMap()
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public open fun approveCall(name: String, type: Int): Boolean {
+        val nullableMask = calledMethods[name]
+        val mask = nullableMask ?: 0
+        val wasCalled = mask and type != 0
+        calledMethods[name] = mask or type
+        return !wasCalled
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java
index 0fc7f0e..44e7ee5 100644
--- a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java
+++ b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java
@@ -101,20 +101,6 @@
         assertThat(observer, is(observer));
     }
 
-    // MUST BE HERE TILL Lifecycle 3.0.0 release for back-compatibility with other modules
-    @SuppressWarnings("deprecation")
-    @Test
-    public void testDeprecatedLifecyclingCallback() {
-        GenericLifecycleObserver genericLifecycleObserver = new GenericLifecycleObserver() {
-            @Override
-            public void onStateChanged(@NonNull LifecycleOwner source,
-                    @NonNull Lifecycle.Event event) {
-            }
-        };
-        LifecycleEventObserver observer = Lifecycling.getCallback(genericLifecycleObserver);
-        assertThat(observer, is(observer));
-    }
-
     @Test
     public void defaultLifecycleObserverAndAnnotations() {
         class AnnotatedFullLifecycleObserver implements DefaultLifecycleObserver {
diff --git a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt
index 1bb682c..028af8a 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt
@@ -35,7 +35,7 @@
 public val Lifecycle.coroutineScope: LifecycleCoroutineScope
     get() {
         while (true) {
-            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
+            val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
             if (existing != null) {
                 return existing
             }
@@ -43,7 +43,7 @@
                 this,
                 SupervisorJob() + Dispatchers.Main.immediate
             )
-            if (mInternalScopeRef.compareAndSet(null, newScope)) {
+            if (internalScopeRef.compareAndSet(null, newScope)) {
                 newScope.register()
                 return newScope
             }
diff --git a/lifecycle/lifecycle-runtime/api/current.txt b/lifecycle/lifecycle-runtime/api/current.txt
index 5bbe95f..c7da237 100644
--- a/lifecycle/lifecycle-runtime/api/current.txt
+++ b/lifecycle/lifecycle-runtime/api/current.txt
@@ -2,15 +2,22 @@
 package androidx.lifecycle {
 
   public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
-    ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner);
-    method public void addObserver(androidx.lifecycle.LifecycleObserver);
-    method @VisibleForTesting public static androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner);
+    ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+    method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+    method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
     method public androidx.lifecycle.Lifecycle.State getCurrentState();
     method public int getObserverCount();
-    method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event);
-    method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State);
-    method public void removeObserver(androidx.lifecycle.LifecycleObserver);
-    method @MainThread public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+    method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+    method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+    method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+    method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+    property public androidx.lifecycle.Lifecycle.State currentState;
+    property public int observerCount;
+    field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+  }
+
+  public static final class LifecycleRegistry.Companion {
+    method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
   }
 
   @Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt
index 5bbe95f..c7da237 100644
--- a/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt
@@ -2,15 +2,22 @@
 package androidx.lifecycle {
 
   public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
-    ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner);
-    method public void addObserver(androidx.lifecycle.LifecycleObserver);
-    method @VisibleForTesting public static androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner);
+    ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+    method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+    method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
     method public androidx.lifecycle.Lifecycle.State getCurrentState();
     method public int getObserverCount();
-    method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event);
-    method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State);
-    method public void removeObserver(androidx.lifecycle.LifecycleObserver);
-    method @MainThread public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+    method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+    method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+    method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+    method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+    property public androidx.lifecycle.Lifecycle.State currentState;
+    property public int observerCount;
+    field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+  }
+
+  public static final class LifecycleRegistry.Companion {
+    method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
   }
 
   @Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/api/restricted_current.txt b/lifecycle/lifecycle-runtime/api/restricted_current.txt
index 74af488..4602a21 100644
--- a/lifecycle/lifecycle-runtime/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime/api/restricted_current.txt
@@ -2,15 +2,22 @@
 package androidx.lifecycle {
 
   public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
-    ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner);
-    method public void addObserver(androidx.lifecycle.LifecycleObserver);
-    method @VisibleForTesting public static androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner);
+    ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+    method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+    method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
     method public androidx.lifecycle.Lifecycle.State getCurrentState();
     method public int getObserverCount();
-    method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event);
-    method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State);
-    method public void removeObserver(androidx.lifecycle.LifecycleObserver);
-    method @MainThread public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+    method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+    method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+    method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+    method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+    property public androidx.lifecycle.Lifecycle.State currentState;
+    property public int observerCount;
+    field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+  }
+
+  public static final class LifecycleRegistry.Companion {
+    method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
   }
 
   @Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index bd48528..c4f8f49 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -3,6 +3,7 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("org.jetbrains.kotlin.android")
 }
 
 android {
@@ -13,12 +14,13 @@
 }
 
 dependencies {
+    api(libs.kotlinStdlib)
     api(project(":lifecycle:lifecycle-common"))
 
-    api("androidx.arch.core:core-common:2.1.0")
+    api(projectOrArtifact(":arch:core:core-common"))
     // necessary for IJ to resolve dependencies.
     api("androidx.annotation:annotation:1.1.0")
-    implementation("androidx.arch.core:core-runtime:2.1.0")
+    implementation(projectOrArtifact(":arch:core:core-runtime"))
 
     testImplementation(libs.junit)
     testImplementation(libs.mockitoCore4)
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
deleted file mode 100644
index 5279d4c..0000000
--- a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
+++ /dev/null
@@ -1,365 +0,0 @@
-/*
- * Copyright (C) 2017 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.lifecycle;
-
-import static androidx.lifecycle.Lifecycle.State.DESTROYED;
-import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
-
-import android.annotation.SuppressLint;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.arch.core.internal.FastSafeIterableMap;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * An implementation of {@link Lifecycle} that can handle multiple observers.
- * <p>
- * It is used by Fragments and Support Library Activities. You can also directly use it if you have
- * a custom LifecycleOwner.
- */
-public class LifecycleRegistry extends Lifecycle {
-
-    /**
-     * Custom list that keeps observers and can handle removals / additions during traversal.
-     *
-     * Invariant: at any moment of time for observer1 & observer2:
-     * if addition_order(observer1) < addition_order(observer2), then
-     * state(observer1) >= state(observer2),
-     */
-    private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap =
-            new FastSafeIterableMap<>();
-    /**
-     * Current state
-     */
-    private State mState;
-    /**
-     * The provider that owns this Lifecycle.
-     * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
-     * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
-     * because it keeps strong references on all other listeners, so you'll leak all of them as
-     * well.
-     */
-    private final WeakReference<LifecycleOwner> mLifecycleOwner;
-
-    private int mAddingObserverCounter = 0;
-
-    private boolean mHandlingEvent = false;
-    private boolean mNewEventOccurred = false;
-
-    // we have to keep it for cases:
-    // void onStart() {
-    //     mRegistry.removeObserver(this);
-    //     mRegistry.add(newObserver);
-    // }
-    // newObserver should be brought only to CREATED state during the execution of
-    // this onStart method. our invariant with mObserverMap doesn't help, because parent observer
-    // is no longer in the map.
-    private ArrayList<State> mParentStates = new ArrayList<>();
-    private final boolean mEnforceMainThread;
-
-    /**
-     * Creates a new LifecycleRegistry for the given provider.
-     * <p>
-     * You should usually create this inside your LifecycleOwner class's constructor and hold
-     * onto the same instance.
-     *
-     * @param provider The owner LifecycleOwner
-     */
-    public LifecycleRegistry(@NonNull LifecycleOwner provider) {
-        this(provider, true);
-    }
-
-    private LifecycleRegistry(@NonNull LifecycleOwner provider, boolean enforceMainThread) {
-        mLifecycleOwner = new WeakReference<>(provider);
-        mState = INITIALIZED;
-        mEnforceMainThread = enforceMainThread;
-    }
-
-    /**
-     * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
-     *
-     * @param state new state
-     * @deprecated Use {@link #setCurrentState(State)}.
-     */
-    @Deprecated
-    @MainThread
-    public void markState(@NonNull State state) {
-        enforceMainThreadIfNeeded("markState");
-        setCurrentState(state);
-    }
-
-    /**
-     * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
-     *
-     * @param state new state
-     */
-    @MainThread
-    public void setCurrentState(@NonNull State state) {
-        enforceMainThreadIfNeeded("setCurrentState");
-        moveToState(state);
-    }
-
-    /**
-     * Sets the current state and notifies the observers.
-     * <p>
-     * Note that if the {@code currentState} is the same state as the last call to this method,
-     * calling this method has no effect.
-     *
-     * @param event The event that was received
-     */
-    public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
-        enforceMainThreadIfNeeded("handleLifecycleEvent");
-        moveToState(event.getTargetState());
-    }
-
-    private void moveToState(State next) {
-        if (mState == next) {
-            return;
-        }
-        if (mState == INITIALIZED && next == DESTROYED) {
-            throw new IllegalStateException(
-                    "no event down from " + mState + " in component " + mLifecycleOwner.get());
-        }
-        mState = next;
-        if (mHandlingEvent || mAddingObserverCounter != 0) {
-            mNewEventOccurred = true;
-            // we will figure out what to do on upper level.
-            return;
-        }
-        mHandlingEvent = true;
-        sync();
-        mHandlingEvent = false;
-        if (mState == DESTROYED) {
-            mObserverMap = new FastSafeIterableMap<>();
-        }
-    }
-
-    private boolean isSynced() {
-        if (mObserverMap.size() == 0) {
-            return true;
-        }
-        State eldestObserverState = mObserverMap.eldest().getValue().mState;
-        State newestObserverState = mObserverMap.newest().getValue().mState;
-        return eldestObserverState == newestObserverState && mState == newestObserverState;
-    }
-
-    private State calculateTargetState(LifecycleObserver observer) {
-        Map.Entry<LifecycleObserver, ObserverWithState> previous = mObserverMap.ceil(observer);
-
-        State siblingState = previous != null ? previous.getValue().mState : null;
-        State parentState = !mParentStates.isEmpty() ? mParentStates.get(mParentStates.size() - 1)
-                : null;
-        return min(min(mState, siblingState), parentState);
-    }
-
-    @Override
-    public void addObserver(@NonNull LifecycleObserver observer) {
-        enforceMainThreadIfNeeded("addObserver");
-        State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
-        ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
-        ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
-
-        if (previous != null) {
-            return;
-        }
-        LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
-        if (lifecycleOwner == null) {
-            // it is null we should be destroyed. Fallback quickly
-            return;
-        }
-
-        boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
-        State targetState = calculateTargetState(observer);
-        mAddingObserverCounter++;
-        while ((statefulObserver.mState.compareTo(targetState) < 0
-                && mObserverMap.contains(observer))) {
-            pushParentState(statefulObserver.mState);
-            final Event event = Event.upFrom(statefulObserver.mState);
-            if (event == null) {
-                throw new IllegalStateException("no event up from " + statefulObserver.mState);
-            }
-            statefulObserver.dispatchEvent(lifecycleOwner, event);
-            popParentState();
-            // mState / subling may have been changed recalculate
-            targetState = calculateTargetState(observer);
-        }
-
-        if (!isReentrance) {
-            // we do sync only on the top level.
-            sync();
-        }
-        mAddingObserverCounter--;
-    }
-
-    private void popParentState() {
-        mParentStates.remove(mParentStates.size() - 1);
-    }
-
-    private void pushParentState(State state) {
-        mParentStates.add(state);
-    }
-
-    @Override
-    public void removeObserver(@NonNull LifecycleObserver observer) {
-        enforceMainThreadIfNeeded("removeObserver");
-        // we consciously decided not to send destruction events here in opposition to addObserver.
-        // Our reasons for that:
-        // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
-        // actually occurred but earlier.
-        // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
-        // event. If removeObserver method sends destruction events, then a clean up routine becomes
-        // more cumbersome. More specific example of that is: your LifecycleObserver listens for
-        // a web connection, in the usual routine in OnStop method you report to a server that a
-        // session has just ended and you close the connection. Now let's assume now that you
-        // lost an internet and as a result you removed this observer. If you get destruction
-        // events in removeObserver, you should have a special case in your onStop method that
-        // checks if your web connection died and you shouldn't try to report anything to a server.
-        mObserverMap.remove(observer);
-    }
-
-    /**
-     * The number of observers.
-     *
-     * @return The number of observers.
-     */
-    @SuppressWarnings("WeakerAccess")
-    public int getObserverCount() {
-        enforceMainThreadIfNeeded("getObserverCount");
-        return mObserverMap.size();
-    }
-
-    @NonNull
-    @Override
-    public State getCurrentState() {
-        return mState;
-    }
-
-    private void forwardPass(LifecycleOwner lifecycleOwner) {
-        Iterator<Map.Entry<LifecycleObserver, ObserverWithState>> ascendingIterator =
-                mObserverMap.iteratorWithAdditions();
-        while (ascendingIterator.hasNext() && !mNewEventOccurred) {
-            Map.Entry<LifecycleObserver, ObserverWithState> entry = ascendingIterator.next();
-            ObserverWithState observer = entry.getValue();
-            while ((observer.mState.compareTo(mState) < 0 && !mNewEventOccurred
-                    && mObserverMap.contains(entry.getKey()))) {
-                pushParentState(observer.mState);
-                final Event event = Event.upFrom(observer.mState);
-                if (event == null) {
-                    throw new IllegalStateException("no event up from " + observer.mState);
-                }
-                observer.dispatchEvent(lifecycleOwner, event);
-                popParentState();
-            }
-        }
-    }
-
-    private void backwardPass(LifecycleOwner lifecycleOwner) {
-        Iterator<Map.Entry<LifecycleObserver, ObserverWithState>> descendingIterator =
-                mObserverMap.descendingIterator();
-        while (descendingIterator.hasNext() && !mNewEventOccurred) {
-            Map.Entry<LifecycleObserver, ObserverWithState> entry = descendingIterator.next();
-            ObserverWithState observer = entry.getValue();
-            while ((observer.mState.compareTo(mState) > 0 && !mNewEventOccurred
-                    && mObserverMap.contains(entry.getKey()))) {
-                Event event = Event.downFrom(observer.mState);
-                if (event == null) {
-                    throw new IllegalStateException("no event down from " + observer.mState);
-                }
-                pushParentState(event.getTargetState());
-                observer.dispatchEvent(lifecycleOwner, event);
-                popParentState();
-            }
-        }
-    }
-
-    // happens only on the top of stack (never in reentrance),
-    // so it doesn't have to take in account parents
-    private void sync() {
-        LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
-        if (lifecycleOwner == null) {
-            throw new IllegalStateException("LifecycleOwner of this LifecycleRegistry is already"
-                    + "garbage collected. It is too late to change lifecycle state.");
-        }
-        while (!isSynced()) {
-            mNewEventOccurred = false;
-            // no need to check eldest for nullability, because isSynced does it for us.
-            if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) {
-                backwardPass(lifecycleOwner);
-            }
-            Map.Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest();
-            if (!mNewEventOccurred && newest != null
-                    && mState.compareTo(newest.getValue().mState) > 0) {
-                forwardPass(lifecycleOwner);
-            }
-        }
-        mNewEventOccurred = false;
-    }
-
-    @SuppressLint("RestrictedApi")
-    private void enforceMainThreadIfNeeded(String methodName) {
-        if (mEnforceMainThread) {
-            if (!ArchTaskExecutor.getInstance().isMainThread()) {
-                throw new IllegalStateException("Method " + methodName + " must be called on the "
-                        + "main thread");
-            }
-        }
-    }
-
-    /**
-     * Creates a new LifecycleRegistry for the given provider, that doesn't check
-     * that its methods are called on the threads other than main.
-     * <p>
-     * LifecycleRegistry is not synchronized: if multiple threads access this {@code
-     * LifecycleRegistry}, it must be synchronized externally.
-     * <p>
-     * Another possible use-case for this method is JVM testing, when main thread is not present.
-     */
-    @VisibleForTesting
-    @NonNull
-    public static LifecycleRegistry createUnsafe(@NonNull LifecycleOwner owner) {
-        return new LifecycleRegistry(owner, false);
-    }
-
-    static State min(@NonNull State state1, @Nullable State state2) {
-        return state2 != null && state2.compareTo(state1) < 0 ? state2 : state1;
-    }
-
-    static class ObserverWithState {
-        State mState;
-        LifecycleEventObserver mLifecycleObserver;
-
-        ObserverWithState(LifecycleObserver observer, State initialState) {
-            mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
-            mState = initialState;
-        }
-
-        void dispatchEvent(LifecycleOwner owner, Event event) {
-            State newState = event.getTargetState();
-            mState = min(mState, newState);
-            mLifecycleObserver.onStateChanged(owner, event);
-            mState = newState;
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.kt b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.kt
new file mode 100644
index 0000000..8da7e1e
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.kt
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2017 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.lifecycle
+
+import android.annotation.SuppressLint
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.internal.FastSafeIterableMap
+import java.lang.ref.WeakReference
+
+/**
+ * An implementation of [Lifecycle] that can handle multiple observers.
+ *
+ * It is used by Fragments and Support Library Activities. You can also directly use it if you have
+ * a custom LifecycleOwner.
+ */
+open class LifecycleRegistry private constructor(
+    provider: LifecycleOwner,
+    private val enforceMainThread: Boolean
+) : Lifecycle() {
+    /**
+     * Custom list that keeps observers and can handle removals / additions during traversal.
+     *
+     * Invariant: at any moment of time for observer1 & observer2:
+     * if addition_order(observer1) < addition_order(observer2), then
+     * state(observer1) >= state(observer2),
+     */
+    private var observerMap = FastSafeIterableMap<LifecycleObserver, ObserverWithState>()
+
+    /**
+     * Current state
+     */
+    private var state: State = State.INITIALIZED
+
+    /**
+     * The provider that owns this Lifecycle.
+     * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
+     * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
+     * because it keeps strong references on all other listeners, so you'll leak all of them as
+     * well.
+     */
+    private val lifecycleOwner: WeakReference<LifecycleOwner>
+    private var addingObserverCounter = 0
+    private var handlingEvent = false
+    private var newEventOccurred = false
+
+    // we have to keep it for cases:
+    // void onStart() {
+    //     mRegistry.removeObserver(this);
+    //     mRegistry.add(newObserver);
+    // }
+    // newObserver should be brought only to CREATED state during the execution of
+    // this onStart method. our invariant with observerMap doesn't help, because parent observer
+    // is no longer in the map.
+    private var parentStates = ArrayList<State>()
+
+    /**
+     * Creates a new LifecycleRegistry for the given provider.
+     *
+     * You should usually create this inside your LifecycleOwner class's constructor and hold
+     * onto the same instance.
+     *
+     * @param provider The owner LifecycleOwner
+     */
+    constructor(provider: LifecycleOwner) : this(provider, true)
+
+    init {
+        lifecycleOwner = WeakReference(provider)
+    }
+
+    /**
+     * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
+     *
+     * @param state new state
+     */
+    @MainThread
+    @Deprecated("Override [currentState].")
+    open fun markState(state: State) {
+        enforceMainThreadIfNeeded("markState")
+        currentState = state
+    }
+
+    override var currentState: State
+        get() = state
+        /**
+         * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
+         *
+         * @param state new state
+         */
+        set(state) {
+            enforceMainThreadIfNeeded("setCurrentState")
+            moveToState(state)
+        }
+
+    /**
+     * Sets the current state and notifies the observers.
+     *
+     * Note that if the `currentState` is the same state as the last call to this method,
+     * calling this method has no effect.
+     *
+     * @param event The event that was received
+     */
+    open fun handleLifecycleEvent(event: Event) {
+        enforceMainThreadIfNeeded("handleLifecycleEvent")
+        moveToState(event.targetState)
+    }
+
+    private fun moveToState(next: State) {
+        if (state == next) {
+            return
+        }
+        check(!(state == State.INITIALIZED && next == State.DESTROYED)) {
+            "no event down from $state in component ${lifecycleOwner.get()}"
+        }
+        state = next
+        if (handlingEvent || addingObserverCounter != 0) {
+            newEventOccurred = true
+            // we will figure out what to do on upper level.
+            return
+        }
+        handlingEvent = true
+        sync()
+        handlingEvent = false
+        if (state == State.DESTROYED) {
+            observerMap = FastSafeIterableMap()
+        }
+    }
+
+    private val isSynced: Boolean
+        get() {
+            if (observerMap.size() == 0) {
+                return true
+            }
+            val eldestObserverState = observerMap.eldest()!!.value.state
+            val newestObserverState = observerMap.newest()!!.value.state
+            return eldestObserverState == newestObserverState && state == newestObserverState
+        }
+
+    private fun calculateTargetState(observer: LifecycleObserver): State {
+        val map = observerMap.ceil(observer)
+        val siblingState = map?.value?.state
+        val parentState =
+            if (parentStates.isNotEmpty()) parentStates[parentStates.size - 1] else null
+        return min(min(state, siblingState), parentState)
+    }
+
+    /**
+     * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
+     * state.
+     *
+     * The given observer will be brought to the current state of the LifecycleOwner.
+     * For example, if the LifecycleOwner is in [Lifecycle.State.STARTED] state, the given observer
+     * will receive [Lifecycle.Event.ON_CREATE], [Lifecycle.Event.ON_START] events.
+     *
+     * @param observer The observer to notify.
+     *
+     * @throws IllegalStateException if no event up from observer's initial state
+     */
+    override fun addObserver(observer: LifecycleObserver) {
+        enforceMainThreadIfNeeded("addObserver")
+        val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
+        val statefulObserver = ObserverWithState(observer, initialState)
+        val previous = observerMap.putIfAbsent(observer, statefulObserver)
+        if (previous != null) {
+            return
+        }
+        val lifecycleOwner = lifecycleOwner.get()
+            ?: // it is null we should be destroyed. Fallback quickly
+            return
+        val isReentrance = addingObserverCounter != 0 || handlingEvent
+        var targetState = calculateTargetState(observer)
+        addingObserverCounter++
+        while (statefulObserver.state < targetState && observerMap.contains(observer)
+        ) {
+            pushParentState(statefulObserver.state)
+            val event = Event.upFrom(statefulObserver.state)
+                ?: throw IllegalStateException("no event up from ${statefulObserver.state}")
+            statefulObserver.dispatchEvent(lifecycleOwner, event)
+            popParentState()
+            // mState / subling may have been changed recalculate
+            targetState = calculateTargetState(observer)
+        }
+        if (!isReentrance) {
+            // we do sync only on the top level.
+            sync()
+        }
+        addingObserverCounter--
+    }
+
+    private fun popParentState() {
+        parentStates.removeAt(parentStates.size - 1)
+    }
+
+    private fun pushParentState(state: State) {
+        parentStates.add(state)
+    }
+
+    override fun removeObserver(observer: LifecycleObserver) {
+        enforceMainThreadIfNeeded("removeObserver")
+        // we consciously decided not to send destruction events here in opposition to addObserver.
+        // Our reasons for that:
+        // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
+        // actually occurred but earlier.
+        // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
+        // event. If removeObserver method sends destruction events, then a clean up routine becomes
+        // more cumbersome. More specific example of that is: your LifecycleObserver listens for
+        // a web connection, in the usual routine in OnStop method you report to a server that a
+        // session has just ended and you close the connection. Now let's assume now that you
+        // lost an internet and as a result you removed this observer. If you get destruction
+        // events in removeObserver, you should have a special case in your onStop method that
+        // checks if your web connection died and you shouldn't try to report anything to a server.
+        observerMap.remove(observer)
+    }
+
+    /**
+     * The number of observers.
+     *
+     * @return The number of observers.
+     */
+    open val observerCount: Int
+        get() {
+            enforceMainThreadIfNeeded("getObserverCount")
+            return observerMap.size()
+        }
+
+    private fun forwardPass(lifecycleOwner: LifecycleOwner) {
+        @Suppress()
+        val ascendingIterator: Iterator<Map.Entry<LifecycleObserver, ObserverWithState>> =
+            observerMap.iteratorWithAdditions()
+        while (ascendingIterator.hasNext() && !newEventOccurred) {
+            val (key, observer) = ascendingIterator.next()
+            while (observer.state < state && !newEventOccurred && observerMap.contains(key)
+            ) {
+                pushParentState(observer.state)
+                val event = Event.upFrom(observer.state)
+                    ?: throw IllegalStateException("no event up from ${observer.state}")
+                observer.dispatchEvent(lifecycleOwner, event)
+                popParentState()
+            }
+        }
+    }
+
+    private fun backwardPass(lifecycleOwner: LifecycleOwner) {
+        val descendingIterator = observerMap.descendingIterator()
+        while (descendingIterator.hasNext() && !newEventOccurred) {
+            val (key, observer) = descendingIterator.next()
+            while (observer.state > state && !newEventOccurred && observerMap.contains(key)
+            ) {
+                val event = Event.downFrom(observer.state)
+                    ?: throw IllegalStateException("no event down from ${observer.state}")
+                pushParentState(event.targetState)
+                observer.dispatchEvent(lifecycleOwner, event)
+                popParentState()
+            }
+        }
+    }
+
+    // happens only on the top of stack (never in reentrance),
+    // so it doesn't have to take in account parents
+    private fun sync() {
+        val lifecycleOwner = lifecycleOwner.get()
+            ?: throw IllegalStateException(
+                "LifecycleOwner of this LifecycleRegistry is already " +
+                    "garbage collected. It is too late to change lifecycle state."
+            )
+        while (!isSynced) {
+            newEventOccurred = false
+            if (state < observerMap.eldest()!!.value.state) {
+                backwardPass(lifecycleOwner)
+            }
+            val newest = observerMap.newest()
+            if (!newEventOccurred && newest != null && state > newest.value.state) {
+                forwardPass(lifecycleOwner)
+            }
+        }
+        newEventOccurred = false
+    }
+
+    @SuppressLint("RestrictedApi")
+    private fun enforceMainThreadIfNeeded(methodName: String) {
+        if (enforceMainThread) {
+            check(ArchTaskExecutor.getInstance().isMainThread) {
+                ("Method $methodName must be called on the main thread")
+            }
+        }
+    }
+
+    internal class ObserverWithState(observer: LifecycleObserver?, initialState: State) {
+        var state: State
+        var lifecycleObserver: LifecycleEventObserver
+
+        init {
+            lifecycleObserver = Lifecycling.lifecycleEventObserver(observer!!)
+            state = initialState
+        }
+
+        fun dispatchEvent(owner: LifecycleOwner?, event: Event) {
+            val newState = event.targetState
+            state = min(state, newState)
+            lifecycleObserver.onStateChanged(owner!!, event)
+            state = newState
+        }
+    }
+
+    companion object {
+        /**
+         * Creates a new LifecycleRegistry for the given provider, that doesn't check
+         * that its methods are called on the threads other than main.
+         *
+         * LifecycleRegistry is not synchronized: if multiple threads access this `LifecycleRegistry`, it must be synchronized externally.
+         *
+         * Another possible use-case for this method is JVM testing, when main thread is not present.
+         */
+        @JvmStatic
+        @VisibleForTesting
+        fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
+            return LifecycleRegistry(owner, false)
+        }
+
+        @JvmStatic
+        internal fun min(state1: State, state2: State?): State {
+            return if ((state2 != null) && (state2 < state1)) state2 else state1
+        }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel/build.gradle b/lifecycle/lifecycle-viewmodel/build.gradle
index 21eb489..c52cb40 100644
--- a/lifecycle/lifecycle-viewmodel/build.gradle
+++ b/lifecycle/lifecycle-viewmodel/build.gradle
@@ -59,11 +59,3 @@
     inceptionYear = "2017"
     description = "Android Lifecycle ViewModel"
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
index ebcb9f9..1136d96 100644
--- a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
@@ -28,6 +28,7 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.android.tools.lint.detector.api.isKotlin
+import com.android.tools.lint.model.LintModelMavenName
 import com.intellij.psi.PsiJvmModifiersOwner
 import org.jetbrains.uast.UClass
 import org.jetbrains.uast.UMethod
@@ -48,8 +49,16 @@
         return InterfaceChecker(context)
     }
 
+    fun LintModelMavenName.asProjectString() = "$groupId.$artifactId"
+
     private inner class InterfaceChecker(val context: JavaContext) : UElementHandler() {
         override fun visitClass(node: UClass) {
+            // Don't run lint on the set of projects that already used `-Xjvm-default=all` before
+            // all projects were switched over.
+            if (alreadyDefaultAll.contains(context.project.mavenCoordinate?.asProjectString())) {
+                return
+            }
+
             if (!isKotlin(node)) return
             if (!node.isInterface) return
             if (node.annotatedWithAnyOf(
@@ -148,5 +157,76 @@
         )
 
         const val JVM_DEFAULT_WITH_COMPATIBILITY = "kotlin.jvm.JvmDefaultWithCompatibility"
+
+        // This set of projects was created by running `grep "Xjvm-default=all" . -r` in the
+        // `frameworks/support` directory and converting the `build.gradle` files in that list to
+        // this format.
+        private val alreadyDefaultAll = setOf(
+            "androidx.room.room-compiler-processing",
+            "androidx.room.room-migration",
+            "androidx.room.room-testing",
+            "androidx.room.room-compiler",
+            "androidx.room.room-ktx",
+            "androidx.room.room-common",
+            "androidx.room.room-runtime",
+            "androidx.compose.ui.ui",
+            "androidx.compose.ui.ui-unit",
+            "androidx.compose.ui.ui-tooling-preview",
+            "androidx.compose.ui.ui-tooling-data",
+            "androidx.compose.ui.ui-util",
+            "androidx.compose.ui.ui-test",
+            "androidx.compose.ui.ui-test-manifest",
+            "androidx.compose.ui.ui-inspection",
+            "androidx.compose.ui.ui-viewbinding",
+            "androidx.compose.ui.ui-geometry",
+            "androidx.compose.ui.ui-graphics",
+            "androidx.compose.ui.ui-text",
+            "androidx.compose.ui.ui-text-google-fonts",
+            "androidx.compose.ui.ui-test-junit4",
+            "androidx.compose.ui.ui-tooling",
+            "androidx.compose.test-utils",
+            "androidx.compose.runtime.runtime",
+            "androidx.compose.runtime.runtime-livedata",
+            "androidx.compose.runtime.runtime-saveable",
+            "androidx.compose.runtime.runtime-rxjava2",
+            "androidx.compose.runtime.runtime-tracing",
+            "androidx.compose.runtime.runtime-rxjava3",
+            "androidx.compose.animation.animation-tooling-internal",
+            "androidx.compose.animation.animation",
+            "androidx.compose.animation.animation-graphics",
+            "androidx.compose.animation.animation-core",
+            "androidx.compose.foundation.foundation",
+            "androidx.compose.foundation.foundation-layout",
+            "androidx.compose.material3.material3-window-size-class",
+            "androidx.compose.material3.material3.integration-tests.material3-catalog",
+            "androidx.compose.material3.material3",
+            "androidx.compose.material.material-ripple",
+            "androidx.lifecycle.lifecycle-viewmodel",
+            "androidx.sqlite.sqlite-ktx",
+            "androidx.sqlite.sqlite-framework",
+            "androidx.sqlite.integration-tests.inspection-sqldelight-testapp",
+            "androidx.sqlite.integration-tests.inspection-room-testapp",
+            "androidx.sqlite.sqlite",
+            "androidx.sqlite.sqlite-inspection",
+            "androidx.tv.tv-foundation",
+            "androidx.tv.tv-material",
+            "androidx.window.window",
+            "androidx.credentials.credentials",
+            "androidx.wear.compose.compose-material",
+            "androidx.wear.watchface.watchface-complications-data-source",
+            "androidx.wear.watchface.watchface",
+            "androidx.wear.watchface.watchface-client",
+            "androidx.lifecycle.lifecycle-common",
+            // These projects didn't already have "Xjvm-default=al", but the only have the error in
+            // integration tests, where the annotation isn't needed.
+            "androidx.annotation.annotation-experimental-lint-integration-tests",
+            "androidx.annotation.annotation-experimental-lint",
+            "androidx.camera.integration-tests.camera-testapp-camera2-pipe",
+            "androidx.compose.integration-tests.docs-snippets",
+            // These projects are excluded due to b/259578592
+            "androidx.camera.camera-camera2-pipe",
+            "androidx.camera.camera-camera2-pipe-integration",
+            "androidx.camera.camera-camera2-pipe-testing",
+        )
     }
 }
\ No newline at end of file
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index c4e41b3..52ae967 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -56,9 +56,11 @@
 import android.view.KeyEvent;
 import android.view.Surface;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.concurrent.futures.AbstractResolvableFuture;
 import androidx.concurrent.futures.ResolvableFuture;
 import androidx.core.util.ObjectsCompat;
@@ -203,8 +205,12 @@
             mBroadcastReceiver = new MediaButtonReceiver();
             IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
             filter.addDataScheme(mSessionUri.getScheme());
-            // TODO(b/197817693): Explicitly indicate whether the receiver should be exported.
-            context.registerReceiver(mBroadcastReceiver, filter);
+            if (Build.VERSION.SDK_INT < 33) {
+                context.registerReceiver(mBroadcastReceiver, filter);
+            } else {
+                Api33.registerReceiver(context, mBroadcastReceiver, filter,
+                        Context.RECEIVER_NOT_EXPORTED);
+            }
         } else {
             // Has MediaSessionService to revive playback after it's dead.
             Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, mSessionUri);
@@ -1690,4 +1696,13 @@
             getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
         }
     };
+
+    @RequiresApi(33)
+    private static class Api33 {
+        @DoNotInline
+        static void registerReceiver(@NonNull Context context, @NonNull BroadcastReceiver receiver,
+                @NonNull IntentFilter filter, int flags) {
+            context.registerReceiver(receiver, filter, flags);
+        }
+    }
 }
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index 8bfb087..742ab63 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -191,7 +191,7 @@
             val nowTime = System.nanoTime()
             markStateForRemoval(key, states, nowTime)
             states.add(
-                StateData(
+                getStateData(
                     nowTime, -1,
                     StateInfo(key, value)
                 )
diff --git a/navigation/navigation-benchmark/lint-baseline.xml b/navigation/navigation-benchmark/lint-baseline.xml
new file mode 100644
index 0000000..a59f98e
--- /dev/null
+++ b/navigation/navigation-benchmark/lint-baseline.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-alpha05" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha05)" variant="all" version="8.0.0-alpha05">
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun navGraphDestinations_withRoutes() = inflateNavGraph_withRoutes(1)"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun navGraphDestinations_withRoutes10() = inflateNavGraph_withRoutes(10)"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun navGraphDestinations_withRoutes50() = inflateNavGraph_withRoutes(50)"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun navGraphDestinations_withRoutes100() = inflateNavGraph_withRoutes(100)"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+    </issue>
+
+</issues>
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 4496d11..9e03597 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -141,6 +141,11 @@
     enum_constant public static final androidx.paging.LoadType REFRESH;
   }
 
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+    method public boolean isLoggable(int level);
+    method public void log(int level, String message, optional Throwable? tr);
+  }
+
   @Deprecated public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
     ctor @Deprecated public PageKeyedDataSource();
     method @Deprecated public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 20d1eb3..e2b9e00 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -144,6 +144,11 @@
     enum_constant public static final androidx.paging.LoadType REFRESH;
   }
 
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+    method public boolean isLoggable(int level);
+    method public void log(int level, String message, optional Throwable? tr);
+  }
+
   @Deprecated public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
     ctor @Deprecated public PageKeyedDataSource();
     method @Deprecated public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 4496d11..9e03597 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -141,6 +141,11 @@
     enum_constant public static final androidx.paging.LoadType REFRESH;
   }
 
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+    method public boolean isLoggable(int level);
+    method public void log(int level, String message, optional Throwable? tr);
+  }
+
   @Deprecated public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
     ctor @Deprecated public PageKeyedDataSource();
     method @Deprecated public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt b/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt
index 38cf905..76de299 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt
@@ -28,6 +28,7 @@
 
 public const val LOG_TAG: String = "Paging"
 
+@JvmDefaultWithCompatibility
 /**
  * @hide
  */
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -2,10 +2,12 @@
 package androidx.paging.testing {
 
   public final class PagerFlowSnapshotKt {
-    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
   }
 
   public final class SnapshotLoader<Value> {
+    method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
   }
 
   public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -2,10 +2,12 @@
 package androidx.paging.testing {
 
   public final class PagerFlowSnapshotKt {
-    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
   }
 
   public final class SnapshotLoader<Value> {
+    method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
   }
 
   public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -2,10 +2,12 @@
 package androidx.paging.testing {
 
   public final class PagerFlowSnapshotKt {
-    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
   }
 
   public final class SnapshotLoader<Value> {
+    method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
   }
 
   public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
index abf0652..43eea72 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -17,6 +17,7 @@
 package androidx.paging.testing
 
 import androidx.paging.DifferCallback
+import androidx.paging.ItemSnapshotList
 import androidx.paging.LoadState
 import androidx.paging.LoadStates
 import androidx.paging.NullPaddedList
@@ -33,7 +34,8 @@
 import kotlinx.coroutines.withContext
 
 /**
- * Runs the [SnapshotLoader] load operations that are passed in and returns a List of loaded data.
+ * Runs the [SnapshotLoader] load operations that are passed in and returns a List of data
+ * that would be presented to the UI after all load operations are complete.
  *
  * @param coroutineScope The [CoroutineScope] to collect from this Flow<PagingData> and contains
  * the [CoroutineScope.coroutineContext] to load data from.
@@ -42,20 +44,20 @@
  */
 public suspend fun <Value : Any> Flow<PagingData<Value>>.asSnapshot(
     coroutineScope: CoroutineScope,
-    loadOperations: suspend SnapshotLoader<Value>.() -> Unit
-): List<Value> {
+    loadOperations: suspend SnapshotLoader<Value>.() -> @JvmSuppressWildcards Unit
+): @JvmSuppressWildcards List<Value> {
 
     lateinit var loader: SnapshotLoader<Value>
 
     val callback = object : DifferCallback {
         override fun onChanged(position: Int, count: Int) {
-            loader.onDataSetChanged(loader.generation.value)
+            loader.onDataSetChanged(loader.generations.value)
         }
         override fun onInserted(position: Int, count: Int) {
-            loader.onDataSetChanged(loader.generation.value)
+            loader.onDataSetChanged(loader.generations.value)
         }
         override fun onRemoved(position: Int, count: Int) {
-            loader.onDataSetChanged(loader.generation.value)
+            loader.onDataSetChanged(loader.generations.value)
         }
     }
 
@@ -68,6 +70,19 @@
             onListPresentable: () -> Unit
         ): Int? {
             onListPresentable()
+            /**
+             * On new generation, SnapshotLoader needs the latest [ItemSnapshotList]
+             * index last updated by the initial refresh so that it can
+             * prepend/append from then on based on that index.
+             *
+             * This last updated index is necessary because initial load
+             * key may not be 0, for example when [Pager].initialKey != 0
+             *
+             * Any subsequent SnapshotLoader loads are based on the index tracked by
+             * [SnapshotLoader] internally.
+             */
+            val lastLoadedIndex = snapshot().placeholdersBefore + snapshot().items.size - 1
+            loader.generations.value.lastAccessedIndex.set(lastLoadedIndex)
             return null
         }
     }
@@ -81,7 +96,7 @@
       */
     val job = coroutineScope.launch {
         this@asSnapshot.collectLatest {
-            // TODO increase generation count
+            incrementGeneration(loader)
             differ.collectFrom(it)
         }
     }
@@ -90,14 +105,16 @@
      * Runs the input [loadOperations].
      *
      * Awaits for initial refresh to complete before invoking [loadOperations]. Automatically
-     * cancels the collection on this [Pager.flow] after [loadOperations] completes.
+     * cancels the collection on this [Pager.flow] after [loadOperations] completes and Paging
+     * is idle.
      *
      * Returns a List of loaded data.
      */
     return withContext(coroutineScope.coroutineContext) {
         differ.awaitNotLoading()
-
         loader.loadOperations()
+        differ.awaitNotLoading()
+
         job.cancelAndJoin()
 
         differ.snapshot().items
@@ -118,4 +135,13 @@
 private fun LoadStates.isIdle(): Boolean {
     return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
         prepend is LoadState.NotLoading
+}
+
+private fun <Value : Any> incrementGeneration(loader: SnapshotLoader<Value>) {
+    val currGen = loader.generations.value
+    if (currGen.id == loader.generations.value.id) {
+        loader.generations.value = Generation(
+            id = currGen.id + 1
+        )
+    }
 }
\ No newline at end of file
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
index 56cce09..86c1b61 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
@@ -17,8 +17,16 @@
 package androidx.paging.testing
 
 import androidx.paging.DifferCallback
+import androidx.paging.PagingData
 import androidx.paging.PagingDataDiffer
+import androidx.paging.PagingSource
+import androidx.paging.LoadType.APPEND
+import androidx.paging.PagingConfig
+import java.util.concurrent.atomic.AtomicInteger
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.mapLatest
 
 /**
  * Contains the public APIs for load operations in tests.
@@ -29,28 +37,122 @@
 public class SnapshotLoader<Value : Any> internal constructor(
     private val differ: PagingDataDiffer<Value>
 ) {
-    internal val generation = MutableStateFlow(Generation(0))
+    internal val generations = MutableStateFlow(Generation())
 
-    // TODO add public loading APIs such as scrollTo(index)
+    /**
+     * Refresh the data that is presented on the UI.
+     *
+     * [refresh] triggers a new generation of [PagingData] / [PagingSource]
+     * to represent an updated snapshot of the backing dataset.
+     *
+     * This fake paging operation mimics UI-driven refresh signals such as swipe-to-refresh.
+     */
+    public suspend fun refresh(): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading()
+        differ.refresh()
+        differ.awaitNotLoading()
+    }
 
-    // the callback to be invoked by DifferCallback on a single generation
-    // increase the callbackCount to notify SnapshotLoader that the dataset has updated
+    /**
+     * Imitates scrolling down paged items, [appending][APPEND] data until the given
+     * predicate returns false.
+     *
+     * Note: This API loads an item before passing it into the predicate. This means the
+     * loaded pages may include the page which contains the item that does not match the
+     * predicate. For example, if pageSize = 2, the predicate
+     * {item: Int -> item < 3 } will return items [[1, 2],[3, 4]] where [3, 4] is the page
+     * containing the boundary item[3] not matching the predicate.
+     *
+     * The loaded pages are also dependent on [PagingConfig] settings such as
+     * [PagingConfig.prefetchDistance]:
+     * - if `prefetchDistance` > 0, the resulting appends will include prefetched items.
+     * For example, if pageSize = 2 and prefetchDistance = 2, the predicate
+     * {item: Int -> item < 3 } will load items [[1, 2], [3, 4], [5, 6]] where [5, 6] is the
+     * prefetched page.
+     *
+     * @param [predicate] the predicate to match (return true) to continue append scrolls
+     */
+    public suspend fun appendScrollWhile(
+        predicate: suspend (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
+    ): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading()
+        appendOrPrepend(LoadType.APPEND, predicate)
+        differ.awaitNotLoading()
+    }
+
+    private suspend fun appendOrPrepend(
+        loadType: LoadType,
+        predicate: suspend (item: Value) -> Boolean
+    ) {
+        do {
+            // Get and update the index to load from. Return if index is invalid.
+            val index = nextLoadIndexOrNull(loadType) ?: return
+            val item = loadItem(index)
+        } while (predicate(item))
+    }
+
+    /**
+     * Get and update the index to load from. Returns null if next index is out of bounds.
+     *
+     * This method is responsible for updating the [Generation.lastAccessedIndex] that is then sent
+     * to the differ to trigger load for that index.
+     */
+    private fun nextLoadIndexOrNull(loadType: LoadType): Int? {
+        val currGen = generations.value
+        return when (loadType) {
+            LoadType.PREPEND -> {
+                if (currGen.lastAccessedIndex.get() <= 0) {
+                    return null
+                }
+                currGen.lastAccessedIndex.decrementAndGet()
+            }
+            LoadType.APPEND -> {
+                if (currGen.lastAccessedIndex.get() >= differ.size - 1) {
+                    return null
+                }
+                currGen.lastAccessedIndex.incrementAndGet()
+            }
+        }
+    }
+
+    // Executes actual loading by accessing the PagingDataDiffer
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private suspend fun loadItem(index: Int): Value {
+        differ[index]
+
+        // awaits for the item to be loaded
+        return generations.mapLatest {
+            differ.peek(index)
+        }.filterNotNull().first()
+    }
+
+    /**
+     * The callback to be invoked by DifferCallback on a single generation.
+     * Increase the callbackCount to notify SnapshotLoader that the dataset has updated
+     */
     internal fun onDataSetChanged(gen: Generation) {
-        val currGen = generation.value
+        val currGen = generations.value
         // we make sure the generation with the dataset change is still valid because we
         // want to disregard callbacks on stale generations
         if (gen.id == currGen.id) {
-            generation.value = gen.copy(
+            generations.value = gen.copy(
                 callbackCount = currGen.callbackCount + 1
             )
         }
     }
+
+    private enum class LoadType {
+        PREPEND,
+        APPEND
+    }
 }
 
 internal data class Generation(
-    // Id of the current Paging generation. Incremented on each new generation (when a new
-    // PagingData is received).
-    val id: Int,
+    /**
+     * Id of the current Paging generation. Incremented on each new generation (when a new
+     * PagingData is received).
+     */
+    val id: Int = -1,
 
     /**
      * A count of the number of times Paging invokes a [DifferCallback] callback within a single
@@ -59,5 +161,10 @@
      * The callbackCount enables [SnapshotLoader] to await for a requested item and continue
      * loading next item only after a callback is invoked.
      */
-    val callbackCount: Int = 0
+    val callbackCount: Int = 0,
+
+    /**
+     * Tracks the last accessed index on the differ for this generation
+      */
+    var lastAccessedIndex: AtomicInteger = AtomicInteger()
 )
\ No newline at end of file
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index 9304ec8..89a711a 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -18,9 +18,13 @@
 
 import androidx.paging.Pager
 import androidx.paging.PagingConfig
+import androidx.paging.cachedIn
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -43,15 +47,29 @@
     }
 
     @Test
-    fun simpleInitialRefresh() {
+    fun initialRefresh() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = factory
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {}
+            // first page + prefetched page
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun initialRefresh_withoutPrefetch() {
         val dataFlow = flowOf(List(30) { it })
         val factory = dataFlow.asPagingSourceFactory(testScope)
         val pager = Pager(
-            config = PagingConfig(
-                pageSize = 3,
-                initialLoadSize = 5,
-                prefetchDistance = 0
-            ),
+            config = CONFIG_NO_PREFETCH,
             pagingSourceFactory = factory
         )
         testScope.runTest {
@@ -64,21 +82,408 @@
     }
 
     @Test
-    fun emptyInitialRefresh() {
-        val dataFlow = emptyFlow<List<Int>>()
+    fun initialRefresh_withInitialKey() {
+        val dataFlow = flowOf(List(30) { it })
         val factory = dataFlow.asPagingSourceFactory(testScope)
         val pager = Pager(
-            config = PagingConfig(
-                pageSize = 3,
-                initialLoadSize = 5,
-                prefetchDistance = 0
-            ),
+            config = CONFIG,
+            initialKey = 10,
             pagingSourceFactory = factory
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {}
 
-            assertThat(snapshot).isEmpty()
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+            )
         }
     }
+
+    @Test
+    fun initialRefresh_withInitialKey_withoutPrefetch() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            initialKey = 10,
+            pagingSourceFactory = factory
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {}
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(10, 11, 12, 13, 14)
+            )
+        }
+    }
+
+    @Test
+    fun emptyInitialRefresh() {
+        val dataFlow = emptyFlow<List<Int>>()
+        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = factory
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {}
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+        }
+    }
+
+    @Test
+    fun manualRefresh() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            pagingSourceFactory = factory
+        ).flow.cachedIn(testScope.backgroundScope)
+
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4),
+            )
+        }
+    }
+
+    @Test
+    fun manualEmptyRefresh() {
+        val dataFlow = emptyFlow<List<Int>>()
+        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            pagingSourceFactory = factory
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+        }
+    }
+
+    @Test
+    fun append() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = factory,
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    item < 7
+                }
+            }
+
+            // includes initial load, 1st page, 2nd page (from prefetch)
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun append_withInitialKey() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG,
+            initialKey = 10,
+            pagingSourceFactory = factory,
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    item < 18
+                }
+            }
+
+            // items[7-23]
+            // extra prepended page from prefetch after initial refresh
+            // extra appended page from prefetch after append
+            assertThat(snapshot).containsExactlyElementsIn(
+                List(17) { it + 7 }
+            )
+        }
+    }
+
+    @Test
+    fun append_withInitialKey_withoutPrefetch() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            initialKey = 10,
+            pagingSourceFactory = factory,
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    item < 18
+                }
+            }
+
+            // items[10-20]
+            // although no prefetch, extra appended page because paging loaded item 18
+            // and its entire page before the predicate returned false
+            assertThat(snapshot).containsExactlyElementsIn(
+                List(11) { it + 10 }
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppend() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = factory,
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    item < 7
+                }
+            }
+
+            val snapshot2 = pager.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    item < 22
+                }
+            }
+
+            // includes initial load, 1st page, 2nd page (from prefetch)
+            assertThat(snapshot1).containsExactlyElementsIn(
+                List(11) { it }
+            )
+
+            // includes extra page from prefetch
+            assertThat(snapshot2).containsExactlyElementsIn(
+                List(26) { it }
+            )
+        }
+    }
+
+    @Test
+    fun append_outOfBounds_returnsCurrentlyLoadedItems() {
+        val dataFlow = flowOf(List(10) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = factory,
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    // condition scrolls till end of data since we only have 10 items
+                    item < 18
+                }
+            }
+
+            // returns the items loaded before index becomes out of bounds
+            assertThat(snapshot).containsExactlyElementsIn(
+                List(10) { it }
+            )
+        }
+    }
+
+    @Test
+    fun refreshAndAppend() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = factory,
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                refresh() // triggers second gen
+                appendScrollWhile { item: Int ->
+                    item < 10
+                }
+            }
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+            )
+        }
+    }
+
+    @Test
+    fun appendAndRefresh() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = factory,
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    item < 10
+                }
+                refresh()
+            }
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                // second gen initial load, anchorPos = 10, refreshKey = 8
+                listOf(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_fromSharedFlow() {
+        val dataFlow = MutableSharedFlow<List<Int>>()
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) { }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                dataFlow.emit(List(30) { it })
+            }
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+
+            val snapshot3 = pager.asSnapshot(this) {
+                dataFlow.emit(List(30) { it + 30 })
+            }
+            assertThat(snapshot3).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_fromFlow() {
+        val dataFlow = flow {
+            // first gen
+            emit(emptyList())
+            delay(500)
+            // second gen
+            emit(List(30) { it })
+            delay(500)
+            // third gen
+            emit(List(30) { it + 30 })
+        }
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) { }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                delay(500)
+            }
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+
+            val snapshot3 = pager.asSnapshot(this) {
+                delay(500)
+            }
+            assertThat(snapshot3).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_withInitialKey_nullRefreshKey() {
+        val dataFlow = flow {
+            // first gen
+            emit(List(20) { it })
+            delay(500)
+            // second gen
+            emit(List(20) { it })
+        }
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            initialKey = 10,
+            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) { }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(10, 11, 12, 13, 14)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                // wait for second gen to complete
+                delay(500)
+            }
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_withInitialKey_nonNullRefreshKey() {
+        val dataFlow = flow {
+            // first gen
+            emit(List(20) { it })
+            delay(500)
+            // second gen
+            emit(List(20) { it })
+        }
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            initialKey = 10,
+            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) {
+                // we scroll to register a non-null anchorPos
+                appendScrollWhile { item: Int ->
+                    item < 15
+                }
+            }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(10, 11, 12, 13, 14, 15, 16, 17)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                delay(500)
+            }
+            // anchorPos = 15, refreshKey = 13
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(13, 14, 15, 16, 17)
+            )
+        }
+    }
+
+    val CONFIG = PagingConfig(
+        pageSize = 3,
+        initialLoadSize = 5,
+    )
+
+    val CONFIG_NO_PREFETCH = PagingConfig(
+        pageSize = 3,
+        initialLoadSize = 5,
+        prefetchDistance = 0
+    )
 }
\ No newline at end of file
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index c032acf..e853b9d 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
 kotlin.code.style=official
 # Disable docs
 androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=9282206
-androidx.playground.metalavaBuildId=9295138
+androidx.playground.snapshotBuildId=9304612
+androidx.playground.metalavaBuildId=9297860
 androidx.playground.dokkaBuildId=7472101
 androidx.studio.type=playground
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
index f684502..4606a745 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
@@ -16,6 +16,7 @@
 
 package androidx.privacysandbox.tools.apicompiler
 
+import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.apicompiler.generator.SandboxApiVersion
 import androidx.privacysandbox.tools.apicompiler.generator.SdkCodeGenerator
 import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
@@ -35,16 +36,19 @@
 ) : SymbolProcessor {
     companion object {
         const val AIDL_COMPILER_PATH_OPTIONS_KEY = "aidl_compiler_path"
-        const val USE_COMPAT_LIBRARY_OPTIONS_KEY = "use_sdk_runtime_compat_library"
+        const val SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY = "skip_sdk_runtime_compat_library"
     }
 
-    var invoked = false
-
     override fun process(resolver: Resolver): List<KSAnnotated> {
-        if (invoked) {
+        // This method is called multiple times during compilation and the resolver will only return
+        // relevant files for each particular processing round. This instance might also be kept
+        // by KSP between rounds or for incremental compilation. This means that at some point
+        // KSP will always invoke this processor with no valid services, so we should just stop
+        // processing.
+        if (resolver.getSymbolsWithAnnotation(
+                PrivacySandboxService::class.qualifiedName!!).none()) {
             return emptyList()
         }
-        invoked = true
 
         val path = options[AIDL_COMPILER_PATH_OPTIONS_KEY]?.let(Paths::get)
         if (path == null) {
@@ -52,9 +56,11 @@
             return emptyList()
         }
 
-        val target = if (options[USE_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase() == "true") {
-            SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
-        } else SandboxApiVersion.API_33
+        val skipCompatLibrary =
+            options[SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase().toBoolean()
+        val target = if (skipCompatLibrary) {
+            SandboxApiVersion.API_33
+        } else SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
 
         val parsedApi = ApiParser(resolver, logger).parseApi()
 
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index bc189a4..78e3098 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -75,7 +75,7 @@
                     // Sources created by the AIDL compiler have to be copied to files created
                     // through the KSP APIs, so that they are included in downstream compilation.
                     val kspGeneratedFile = codeGenerator.createNewFile(
-                        Dependencies(false),
+                        Dependencies.ALL_FILES,
                         source.packageName,
                         source.interfaceName,
                         extensionName = "java"
@@ -113,7 +113,7 @@
 
     private fun generateToolMetadata() {
         codeGenerator.createNewFile(
-            Dependencies(false),
+            Dependencies.ALL_FILES,
             Metadata.filePath.parent.toString(),
             Metadata.filePath.nameWithoutExtension,
             Metadata.filePath.extension,
@@ -138,8 +138,7 @@
     }
 
     private fun write(spec: FileSpec) {
-
-        codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
+        codeGenerator.createNewFile(Dependencies.ALL_FILES, spec.packageName, spec.name)
             .bufferedWriter().use(spec::writeTo)
     }
 
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
index 23fb1b7..5782e08 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -39,8 +39,9 @@
 
 /** Top-level entry point to parse a complete user-defined sandbox SDK API into a [ParsedApi]. */
 class ApiParser(private val resolver: Resolver, private val logger: KSPLogger) {
-    private val interfaceParser = InterfaceParser(logger)
-    private val valueParser = ValueParser(logger)
+    private val typeParser = TypeParser(logger)
+    private val interfaceParser = InterfaceParser(logger, typeParser)
+    private val valueParser = ValueParser(logger, typeParser)
 
     fun parseApi(): ParsedApi {
         val services = parseAllServices()
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
deleted file mode 100644
index 2c0f555..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
+++ /dev/null
@@ -1,29 +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.privacysandbox.tools.apicompiler.parser
-
-import androidx.privacysandbox.tools.core.model.Type
-import com.google.devtools.ksp.symbol.KSDeclaration
-
-internal object Converters {
-    fun typeFromDeclaration(declaration: KSDeclaration): Type {
-        return Type(
-            packageName = declaration.packageName.getFullName(),
-            simpleName = declaration.simpleName.getShortName(),
-        )
-    }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index 79c69c9..cb2b9d0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -19,7 +19,6 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
 import androidx.privacysandbox.tools.core.model.Parameter
-import androidx.privacysandbox.tools.core.model.Type
 import com.google.devtools.ksp.getDeclaredFunctions
 import com.google.devtools.ksp.getDeclaredProperties
 import com.google.devtools.ksp.isPublic
@@ -27,12 +26,10 @@
 import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
-import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSValueParameter
 import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
 
-internal class InterfaceParser(private val logger: KSPLogger) {
+internal class InterfaceParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
     private val validInterfaceModifiers = setOf(Modifier.PUBLIC)
     private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
 
@@ -47,13 +44,15 @@
         }
         if (interfaceDeclaration.getDeclaredProperties().any()) {
             logger.error(
-                "Error in $name: annotated interfaces cannot declare properties.")
+                "Error in $name: annotated interfaces cannot declare properties."
+            )
         }
         if (interfaceDeclaration.declarations.filterIsInstance<KSClassDeclaration>()
                 .any(KSClassDeclaration::isCompanionObject)
         ) {
             logger.error(
-                "Error in $name: annotated interfaces cannot declare companion objects.")
+                "Error in $name: annotated interfaces cannot declare companion objects."
+            )
         }
         val invalidModifiers =
             interfaceDeclaration.modifiers.filterNot(validInterfaceModifiers::contains)
@@ -75,7 +74,7 @@
 
         val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
         return AnnotatedInterface(
-            type = Converters.typeFromDeclaration(interfaceDeclaration),
+            type = typeParser.parseFromDeclaration(interfaceDeclaration),
             methods = methods,
         )
     }
@@ -102,7 +101,7 @@
         if (method.returnType == null) {
             logger.error("Error in $name: failed to resolve return type.")
         }
-        val returnType = parseType(method, method.returnType!!.resolve())
+        val returnType = typeParser.parseFromTypeReference(method.returnType!!, name)
 
         val parameters =
             method.parameters.map { parameter -> parseParameter(method, parameter) }.toList()
@@ -128,15 +127,7 @@
 
         return Parameter(
             name = parameter.name!!.getFullName(),
-            type = parseType(method, parameter.type.resolve()),
+            type = typeParser.parseFromTypeReference(parameter.type, name),
         )
     }
-
-    private fun parseType(method: KSFunctionDeclaration, type: KSType): Type {
-        val name = method.qualifiedName?.getFullName() ?: method.simpleName.getFullName()
-        if (type.nullability == Nullability.NULLABLE) {
-            logger.error("Error in $name: nullable types are not supported.")
-        }
-        return Converters.typeFromDeclaration(type.declaration)
-    }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
new file mode 100644
index 0000000..38a96b6
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.core.model.Type
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Nullability
+import com.google.devtools.ksp.symbol.Variance
+
+internal class TypeParser(private val logger: KSPLogger) {
+    fun parseFromDeclaration(declaration: KSDeclaration): Type {
+        return Type(
+            packageName = declaration.packageName.getFullName(),
+            simpleName = declaration.simpleName.getShortName(),
+        )
+    }
+
+    fun parseFromTypeReference(typeReference: KSTypeReference, debugName: String): Type {
+        val resolvedType = typeReference.resolve()
+        if (resolvedType.isError) {
+            logger.error("Failed to resolve type for $debugName.")
+        }
+        if (resolvedType.nullability == Nullability.NULLABLE) {
+            logger.error("Error in $debugName: nullable types are not supported.")
+        }
+        val typeArguments = typeReference.element?.typeArguments?.mapNotNull {
+            if (it.type == null) {
+                logger.error("Error in $debugName: null type argument.")
+            }
+            if (it.variance != Variance.INVARIANT) {
+                logger.error("Error in $debugName: only invariant type arguments are supported.")
+            }
+            it.type
+        } ?: emptyList()
+        return Type(
+            packageName = resolvedType.declaration.packageName.getFullName(),
+            simpleName = resolvedType.declaration.simpleName.getShortName(),
+            typeParameters = typeArguments.map { parseFromTypeReference(it, debugName) },
+        )
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
index 39d8a26..64f9c6b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
@@ -25,13 +25,13 @@
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSPropertyDeclaration
 import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
 
-internal class ValueParser(private val logger: KSPLogger) {
+internal class ValueParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
     fun parseValue(value: KSAnnotated): AnnotatedValue? {
         if (value !is KSClassDeclaration ||
             value.classKind != ClassKind.CLASS ||
-            !value.modifiers.contains(Modifier.DATA)) {
+            !value.modifiers.contains(Modifier.DATA)
+        ) {
             logger.error(
                 "Only data classes can be annotated with @PrivacySandboxValue."
             )
@@ -61,7 +61,7 @@
         }
 
         return AnnotatedValue(
-            type = Converters.typeFromDeclaration(value),
+            type = typeParser.parseFromDeclaration(value),
             properties = value.getAllProperties().map(::parseProperty).toList()
         )
     }
@@ -71,17 +71,9 @@
         if (property.isMutable) {
             logger.error("Error in $name: properties cannot be mutable.")
         }
-        val type = property.type.resolve()
-        if (type.isError) {
-            logger.error("Failed to resolve type for property $name.")
-        }
-        if (type.nullability == Nullability.NULLABLE) {
-            logger.error("Error in $name: nullable types are not supported.")
-        }
-
         return ValueProperty(
             name = property.simpleName.getShortName(),
-            type = Converters.typeFromDeclaration(type.declaration),
+            type = typeParser.parseFromTypeReference(property.type, name),
         )
     }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
index 78f636c..454d7b8 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
@@ -34,7 +34,11 @@
         val inputSources = loadSourcesFromDirectory(inputTestDataDir)
         val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
 
-        val result = compileWithPrivacySandboxKspCompiler(inputSources)
+        val result = compileWithPrivacySandboxKspCompiler(
+            inputSources,
+            platformStubs = PlatformStubs.API_33,
+            extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
+        )
         assertThat(result).succeeds()
 
         val expectedAidlFilepath = listOf(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
index cda0661..1ef27b0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
@@ -32,11 +32,7 @@
         val inputSources = loadSourcesFromDirectory(inputTestDataDir)
         val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
 
-        val result = compileWithPrivacySandboxKspCompiler(
-            inputSources,
-            platformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
-            extraProcessorOptions = mapOf("use_sdk_runtime_compat_library" to "true")
-        )
+        val result = compileWithPrivacySandboxKspCompiler(inputSources)
         assertThat(result).succeeds()
 
         val expectedAidlFilepath = listOf(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index 5de917b..e6b6e3d 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -28,7 +28,7 @@
  */
 fun compileWithPrivacySandboxKspCompiler(
     sources: List<Source>,
-    platformStubs: PlatformStubs = PlatformStubs.API_33,
+    platformStubs: PlatformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
     extraProcessorOptions: Map<String, String> = mapOf(),
 ): TestCompilationResult {
     val provider = PrivacySandboxKspCompiler.Provider()
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index afda28e..f37b09bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -43,6 +43,7 @@
                     @PrivacySandboxService
                     interface MySdk {
                         suspend fun doStuff(x: Int, y: Int): String
+                        suspend fun processList(list: List<Int>): List<String>
                         fun doMoreStuff()
                     }
                 """,
@@ -66,6 +67,17 @@
                                 ),
                                 returnType = Types.string,
                                 isSuspend = true,
+                            ),
+                            Method(
+                                name = "processList",
+                                parameters = listOf(
+                                    Parameter(
+                                        name = "list",
+                                        type = Types.list(Types.int),
+                                    )
+                                ),
+                                returnType = Types.list(Types.string),
+                                isSuspend = true,
                             ), Method(
                                 name = "doMoreStuff",
                                 parameters = listOf(),
@@ -220,9 +232,18 @@
     fun parameterWithGenerics_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(x: MutableList<Int>)"))
             .containsExactlyErrors(
-                "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
-                    "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
-                    "or @PrivacySandboxInterface are supported as parameter types."
+                "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+                    "with @PrivacySandboxValue and interfaces annotated with " +
+                    "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+                    "parameter types."
+            )
+    }
+
+    @Test
+    fun listParameterWithNonInvariantTypeArgument_fails() {
+        checkSourceFails(serviceMethod("suspend fun foo(x: List<in Int>)"))
+            .containsExactlyErrors(
+                "Error in com.mysdk.MySdk.foo: only invariant type arguments are supported."
             )
     }
 
@@ -230,9 +251,10 @@
     fun parameterLambda_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(x: (Int) -> Int)"))
             .containsExactlyErrors(
-                "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
-                    "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
-                    "or @PrivacySandboxInterface are supported as parameter types."
+                "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+                    "with @PrivacySandboxValue and interfaces annotated with " +
+                    "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+                    "parameter types."
             )
     }
 
@@ -251,7 +273,7 @@
                 """
         )
         checkSourceFails(source).containsExactlyErrors(
-            "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
+            "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated with " +
                 "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
                 "supported as return types."
         )
@@ -361,12 +383,14 @@
                                         name = "request",
                                         type = Type(
                                             packageName = "com.mysdk",
-                                            simpleName = "MyInterface"),
+                                            simpleName = "MyInterface"
+                                        ),
                                     )
                                 ),
                                 returnType = Type(
                                     packageName = "com.mysdk",
-                                    simpleName = "MyInterface"),
+                                    simpleName = "MyInterface"
+                                ),
                                 isSuspend = true,
                             ),
                         )
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index 5dd00bf..132a95a 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -52,7 +52,7 @@
                     data class MySdkResponse(
                         val magicPayload: MagicPayload, val isTrulyMagic: Boolean)
                     @PrivacySandboxValue
-                    data class MagicPayload(val magicNumber: Long)
+                    data class MagicPayload(val magicList: List<Long>)
                 """
         )
         assertThat(parseSources(source)).isEqualTo(
@@ -92,9 +92,7 @@
                     ),
                     AnnotatedValue(
                         type = Type(packageName = "com.mysdk", simpleName = "MagicPayload"),
-                        properties = listOf(
-                            ValueProperty("magicNumber", Types.long),
-                        )
+                        properties = listOf(ValueProperty("magicList", Types.list(Types.long)))
                     ),
                 )
             )
@@ -178,7 +176,7 @@
         )
         checkSourceFails(dataClass)
             .containsExactlyErrors(
-                "Error in com.mysdk.MySdkRequest.foo: only primitives, data classes " +
+                "Error in com.mysdk.MySdkRequest.foo: only primitives, lists, data classes " +
                     "annotated with @PrivacySandboxValue and interfaces annotated with " +
                     "@PrivacySandboxInterface are supported as properties."
             )
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt
index d130eaf..a9fb9fc 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt
@@ -1,7 +1,7 @@
 package com.mysdk
 
 import android.os.IBinder
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.`internal`.GeneratedPublicApi
 
 @GeneratedPublicApi
 public object MySdkFactory {
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
index 8fa3062..63749650 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
@@ -1,7 +1,7 @@
 package com.mysdk
 
 import android.os.IBinder
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.`internal`.GeneratedPublicApi
 
 @GeneratedPublicApi
 public object BackwardsCompatibleSdkFactory {
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index 1db70b6..e588bbd 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -109,10 +109,14 @@
         if (classifier !is KmClassifier.Class) {
             throw PrivacySandboxParsingException("Unsupported type in API description: $type")
         }
-        return parseClassName(classifier.name)
+        val typeArguments = type.arguments.map { parseType(it.type!!) }
+        return parseClassName(classifier.name, typeArguments)
     }
 
-    private fun parseClassName(className: ClassName): Type {
+    private fun parseClassName(
+        className: ClassName,
+        typeArguments: List<Type> = emptyList()
+    ): Type {
         // Package names are separated with slashes and nested classes are separated with dots.
         // (e.g com/example/OuterClass.InnerClass).
         val (packageName, simpleName) = className.split('/').run {
@@ -126,7 +130,7 @@
             )
         }
 
-        return Type(packageName, simpleName)
+        return Type(packageName, simpleName, typeArguments)
     }
 
     private fun validate(api: ParsedApi) {
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index cc006e8..bb3c24f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -49,6 +49,7 @@
                       fun doSomething(magicNumber: Int, awesomeString: String)
                       suspend fun getPayload(request: PayloadRequest): PayloadResponse
                       suspend fun getInterface(): MyInterface
+                      suspend fun processList(list: List<Long>): List<Long>
                     }
                     @PrivacySandboxInterface
                     interface MyInterface {
@@ -64,7 +65,7 @@
                     interface CustomCallback {
                       fun onComplete(status: Int)
                     }
-                """
+                """,
         )
 
         val expectedPayloadType = AnnotatedValue(
@@ -118,7 +119,18 @@
                         ),
                         returnType = expectedPayloadResponse.type,
                         isSuspend = true,
-                    )
+                    ),
+                    Method(
+                        name = "processList",
+                        parameters = listOf(
+                            Parameter(
+                                "list",
+                                Types.list(Types.long)
+                            )
+                        ),
+                        returnType = Types.list(Types.long),
+                        isSuspend = true,
+                    ),
                 )
             )
         val expectedInterface =
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
index c7020f3..6b0bafd 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
@@ -20,7 +20,7 @@
 import androidx.privacysandbox.tools.PrivacySandboxInterface
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.PrivacySandboxValue
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.internal.GeneratedPublicApi
 import java.nio.file.Path
 import kotlin.io.path.readBytes
 import org.objectweb.asm.AnnotationVisitor
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
index bd3cc87..e537412 100644
--- a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -90,7 +90,7 @@
                     |import androidx.privacysandbox.tools.PrivacySandboxCallback
                     |import androidx.privacysandbox.tools.PrivacySandboxService
                     |import androidx.privacysandbox.tools.PrivacySandboxValue
-                    |import androidx.privacysandbox.tools.core.GeneratedPublicApi
+                    |import androidx.privacysandbox.tools.internal.GeneratedPublicApi
                     |
                     |@PrivacySandboxService
                     |interface MySdk
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index f18b9ba..bb18c7b 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -29,6 +29,7 @@
     api(libs.kotlinStdlib)
     api(libs.protobufLite)
     api(libs.kotlinPoet)
+    implementation project(path: ':privacysandbox:tools:tools')
 
     testImplementation(libs.junit)
     testImplementation(libs.truth)
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt
index 8a68c35..c0fe64f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt
@@ -16,7 +16,7 @@
 
 package androidx.privacysandbox.tools.core.generator
 
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.internal.GeneratedPublicApi
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.ClassName
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt
index 6d61221..19ea909 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt
@@ -18,6 +18,7 @@
 
 import androidx.privacysandbox.tools.core.model.Type
 
+@JvmDefaultWithCompatibility
 /** Describes the contents of an AIDL file. */
 internal interface AidlFileSpec {
     val type: Type
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index 467d8ac..0ee111d 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -41,4 +41,9 @@
     val char = Type(packageName = "kotlin", simpleName = "Char")
     val short = Type(packageName = "kotlin", simpleName = "Short")
     val primitiveTypes = setOf(unit, boolean, int, long, float, double, string, char, short)
+    fun list(elementType: Type) = Type(
+        packageName = "kotlin.collections",
+        simpleName = "List",
+        typeParameters = listOf(elementType)
+    )
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
index 71e5cd2..3442e0f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
@@ -19,6 +19,7 @@
 data class Type(
   val packageName: String,
   val simpleName: String,
+  val typeParameters: List<Type> = emptyList(),
 ) {
   val qualifiedName = "$packageName.$simpleName"
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
index 0f6e3f8..3b5f012 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
@@ -19,9 +19,14 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.AnnotatedValue
 import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Type
 import androidx.privacysandbox.tools.core.model.Types
 
 class ModelValidator private constructor(val api: ParsedApi) {
+    private val values = api.values.map(AnnotatedValue::type)
+    private val interfaces = api.interfaces.map(AnnotatedInterface::type)
+    private val callbacks = api.callbacks.map(AnnotatedInterface::type)
+
     private val errors: MutableList<String> = mutableListOf()
 
     companion object {
@@ -61,33 +66,24 @@
     }
 
     private fun validateServiceAndInterfaceMethods() {
-        val allowedParameterTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.callbacks.map(AnnotatedInterface::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-        val allowedReturnValueTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-
         val annotatedInterfaces = api.services + api.interfaces
         for (annotatedInterface in annotatedInterfaces) {
             for (method in annotatedInterface.methods) {
-                if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+                if (method.parameters.any { !(isValidInterfaceParameterType(it.type)) }) {
                     errors.add(
                         "Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
-                            "only primitives, data classes annotated with @PrivacySandboxValue " +
-                            "and interfaces annotated with @PrivacySandboxCallback or " +
-                            "@PrivacySandboxInterface are supported as parameter types."
+                            "only primitives, lists, data classes annotated with " +
+                            "@PrivacySandboxValue and interfaces annotated with " +
+                            "@PrivacySandboxCallback or @PrivacySandboxInterface are supported " +
+                            "as parameter types."
                     )
                 }
-                if (!allowedReturnValueTypes.contains(method.returnType)) {
+                if (!isValidInterfaceReturnType(method.returnType)) {
                     errors.add(
                         "Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
-                            "only primitives, data classes annotated with @PrivacySandboxValue " +
-                            "and interfaces annotated with @PrivacySandboxInterface are " +
-                            "supported as return types."
+                            "only primitives, lists, data classes annotated with " +
+                            "@PrivacySandboxValue and interfaces annotated with " +
+                            "@PrivacySandboxInterface are supported as return types."
                     )
                 }
             }
@@ -95,17 +91,12 @@
     }
 
     private fun validateValuePropertyTypes() {
-        val allowedValuePropertyTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-
         for (value in api.values) {
             for (property in value.properties) {
-                if (!allowedValuePropertyTypes.contains(property.type)) {
+                if (!isValidValuePropertyType(property.type)) {
                     errors.add(
                         "Error in ${value.type.qualifiedName}.${property.name}: " +
-                            "only primitives, data classes annotated with " +
+                            "only primitives, lists, data classes annotated with " +
                             "@PrivacySandboxValue and interfaces annotated with " +
                             "@PrivacySandboxInterface are supported as properties."
                     )
@@ -115,17 +106,12 @@
     }
 
     private fun validateCallbackMethods() {
-        val allowedParameterTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-
         for (callback in api.callbacks) {
             for (method in callback.methods) {
-                if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+                if (method.parameters.any { !isValidCallbackParameterType(it.type) }) {
                     errors.add(
                         "Error in ${callback.type.qualifiedName}.${method.name}: " +
-                            "only primitives, data classes annotated with " +
+                            "only primitives, lists, data classes annotated with " +
                             "@PrivacySandboxValue and interfaces annotated with " +
                             "@PrivacySandboxInterface are supported as callback parameter types."
                     )
@@ -139,6 +125,29 @@
             }
         }
     }
+
+    private fun isValidInterfaceParameterType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type) || isCallback(type)
+    private fun isValidInterfaceReturnType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+    private fun isValidValuePropertyType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+    private fun isValidCallbackParameterType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+
+    private fun isValue(type: Type) = values.contains(type)
+    private fun isInterface(type: Type) = interfaces.contains(type)
+    private fun isCallback(type: Type) = callbacks.contains(type)
+    private fun isPrimitive(type: Type) = Types.primitiveTypes.contains(type)
+    private fun isList(type: Type): Boolean {
+        if (type.qualifiedName == "kotlin.collections.List") {
+            require(type.typeParameters.size == 1) {
+                "List type should have one type parameter, found ${type.typeParameters}."
+            }
+            return type.typeParameters[0].let { isValue(it) || isPrimitive(it) }
+        }
+        return false
+    }
 }
 
 data class ValidationResult(val errors: List<String>) {
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
index c3a654d..d13fd0e 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
@@ -239,12 +239,45 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.MySdk.returnFoo: only primitives, data classes annotated with " +
-                "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
-                "supported as return types.",
-            "Error in com.mysdk.MySdk.receiveFoo: only primitives, data classes annotated with " +
-                "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback or " +
-                "@PrivacySandboxInterface are supported as parameter types."
+            "Error in com.mysdk.MySdk.returnFoo: only primitives, lists, data classes annotated " +
+                "with @PrivacySandboxValue and interfaces annotated with " +
+                "@PrivacySandboxInterface are supported as return types.",
+            "Error in com.mysdk.MySdk.receiveFoo: only primitives, lists, data classes " +
+                "annotated with @PrivacySandboxValue and interfaces annotated with " +
+                "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as parameter " +
+                "types."
+        )
+    }
+
+    @Test
+    fun nestedList_throws() {
+        val api = ParsedApi(
+            services = setOf(
+                AnnotatedInterface(
+                    type = Type(packageName = "com.mysdk", simpleName = "MySdk"),
+                    methods = listOf(
+                        Method(
+                            name = "processNestedList",
+                            parameters = listOf(
+                                Parameter(
+                                    name = "foo",
+                                    type = Types.list(Types.list(Types.int))
+                                )
+                            ),
+                            returnType = Types.unit,
+                            isSuspend = true,
+                        ),
+                    ),
+                ),
+            )
+        )
+        val validationResult = ModelValidator.validate(api)
+        assertThat(validationResult.isFailure).isTrue()
+        assertThat(validationResult.errors).containsExactly(
+            "Error in com.mysdk.MySdk.processNestedList: only primitives, lists, data classes " +
+                "annotated with @PrivacySandboxValue and interfaces annotated with " +
+                "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+                "parameter types."
         )
     }
 
@@ -266,7 +299,7 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.Foo.bar: only primitives, data classes annotated with " +
+            "Error in com.mysdk.Foo.bar: only primitives, lists, data classes annotated with " +
                 "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface " +
                 "are supported as properties."
         )
@@ -333,8 +366,8 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.MySdkCallback.foo: only primitives, data classes annotated " +
-                "with @PrivacySandboxValue and interfaces annotated with " +
+            "Error in com.mysdk.MySdkCallback.foo: only primitives, lists, data classes " +
+                "annotated with @PrivacySandboxValue and interfaces annotated with " +
                 "@PrivacySandboxInterface are supported as callback parameter types."
         )
     }
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/GeneratedPublicApi.kt b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/internal/GeneratedPublicApi.kt
similarity index 88%
rename from privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/GeneratedPublicApi.kt
rename to privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/internal/GeneratedPublicApi.kt
index 6a4fef0..410eacb 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/GeneratedPublicApi.kt
+++ b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/internal/GeneratedPublicApi.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.core
+package androidx.privacysandbox.tools.internal
 
 /**
  * Indicates that a class was generated by the API Compiler and is part of the public facing
@@ -22,7 +22,7 @@
  *
  * The API Packager will include these classes in the API descriptors.
  *
- * This annotation is for internal usage and will only be set by the API Compiler.
+ * THIS ANNOTATION IS FOR INTERNAL USAGE ONLY.
  * The API Compiler will fail if the annotation is present in the source code.
  */
 @Retention(AnnotationRetention.BINARY)
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index f00957f..6bf3a91 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -1296,6 +1296,19 @@
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
     @Test
+    public void onInitializeAccessibilityNodeInfo_classNameAdded()
+            throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(0)), false);
+        final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+        mActivityRule.runOnUiThread(
+                () -> mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo));
+
+        assertEquals(nodeInfo.getClassName(), "android.widget.ListView");
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
     public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
             throws Throwable {
         setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(0)), false);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
index aa53614..7fca165 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
@@ -1427,6 +1427,19 @@
     }
 
     @Test
+    public void rowCountForAccessibility_verticalOrientation()
+            throws Throwable {
+        Config config = new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(100);
+        setupByConfig(config);
+        waitFirstLayout();
+
+        int count = mLayoutManager.getRowCountForAccessibility(mRecyclerView.mRecycler,
+                mRecyclerView.mState);
+
+        assertEquals(-1, count);
+    }
+
+    @Test
     public void rowCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
             throws Throwable {
         final int itemCount = 2;
@@ -1441,6 +1454,19 @@
     }
 
     @Test
+    public void columnCountForAccessibility_horizontalOrientation()
+            throws Throwable {
+        Config config = new Config(HORIZONTAL, false, 3, GAP_HANDLING_NONE).itemCount(100);
+        setupByConfig(config);
+        waitFirstLayout();
+
+        int count = mLayoutManager.getColumnCountForAccessibility(mRecyclerView.mRecycler,
+                mRecyclerView.mState);
+
+        assertEquals(-1, count);
+    }
+
+    @Test
     public void columnCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
             throws Throwable {
         final int itemCount = 2;
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index ac79210..da23dcc 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -30,6 +30,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.ListView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -293,6 +294,10 @@
     public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
             @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
         super.onInitializeAccessibilityNodeInfo(recycler, state, info);
+        // Set the class name so this is treated as a list. This helps accessibility services
+        // distinguish lists from one row or one column grids.
+        info.setClassName(ListView.class.getName());
+
         // TODO(b/251823537)
         if (mRecyclerView.mAdapter != null && mRecyclerView.mAdapter.getItemCount() > 0) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
index df03e317..754d879 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
@@ -1358,7 +1358,7 @@
         if (mOrientation == HORIZONTAL) {
             return Math.min(mSpanCount, state.getItemCount());
         }
-        return super.getRowCountForAccessibility(recycler, state);
+        return -1;
     }
 
     @Override
@@ -1367,7 +1367,7 @@
         if (mOrientation == VERTICAL) {
             return Math.min(mSpanCount, state.getItemCount());
         }
-        return super.getColumnCountForAccessibility(recycler, state);
+        return -1;
     }
 
     /**
diff --git a/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt b/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt
index 197a5a1..7bbefa1 100644
--- a/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt
+++ b/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt
@@ -29,6 +29,7 @@
     val className: ClassName = ClassName.get(type)
 }
 
+@JvmDefaultWithCompatibility
 internal interface Attribute {
     val name: String
     val namespace: String
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index 37e8d81..13949d9 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -51,6 +51,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.runBlocking
 
+@JvmDefaultWithCompatibility
 @Dao
 @TypeConverters(DateConverter::class, AnswerConverter::class)
 interface BooksDao {
@@ -452,8 +453,9 @@
         action: suspend (input: Book) -> Book
     ): Book = action(input)
 
+    // Commented out because of https://youtrack.jetbrains.com/issue/KT-48013
     // This is a private method to validate b/194706278
-    private fun getNullAuthor(): Author? = null
+    // private fun getNullAuthor(): Author? = null
 
     @Query("SELECT * FROM Publisher JOIN Book ON (Publisher.publisherId == Book.bookPublisherId)")
     fun getBooksByPublisher(): Map<Publisher, List<Book>>
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
index 8e5ba0c..b50dbce 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
@@ -21,6 +21,7 @@
 import androidx.room.Transaction
 import androidx.room.integration.kotlintestapp.vo.Author
 
+@JvmDefaultWithCompatibility
 @Dao
 interface DerivedDao : BaseDao<Author> {
 
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 64092d5..6b3ebcc 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -22,12 +22,6 @@
     id("kotlin")
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
-}
-
 dependencies {
     api("androidx.annotation:annotation:1.3.0")
     api(libs.kotlinStdlibJdk8)
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index 251ca6d5..1056652 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -48,7 +48,6 @@
 tasks.withType(KotlinCompile).configureEach {
     kotlinOptions {
         freeCompilerArgs += [
-                "-Xjvm-default=all",
                 "-opt-in=kotlin.contracts.ExperimentalContracts",
                 "-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi",
                 "-opt-in=com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview"
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
index 227ba42..533b018 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
@@ -22,6 +22,8 @@
 import androidx.room.compiler.codegen.kotlin.KotlinTypeSpec
 import com.squareup.javapoet.JavaFile
 import com.squareup.kotlinpoet.FileSpec
+import java.io.OutputStream
+import java.nio.file.Path
 
 /**
  * Code generation interface for XProcessing.
@@ -33,6 +35,20 @@
     fun write(fileSpec: FileSpec, mode: Mode = Mode.Isolating)
 
     /**
+     * Writes a resource file that will be part of the output artifact (e.g. jar).
+     *
+     * Only non-source files should be written via this function, if the file path corresponds to a
+     * source file, `.java` or `.kt` this function will throw an exception.
+     *
+     * @return the output stream to write the resource file.
+     */
+    fun writeResource(
+        filePath: Path,
+        originatingElements: List<XElement>,
+        mode: Mode = Mode.Isolating
+    ): OutputStream
+
+    /**
      * Specifies whether a file represents aggregating or isolating inputs for incremental
      * build purposes. This does not apply in Javac processing because aggregating vs isolating
      * is set on the processor level. For more on KSP's definitions of isolating vs aggregating
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
index d61dc26c..93578c7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
@@ -16,11 +16,16 @@
 
 package androidx.room.compiler.processing.javac
 
+import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XFiler
 import androidx.room.compiler.processing.XProcessingEnv
 import com.squareup.javapoet.JavaFile
 import com.squareup.kotlinpoet.FileSpec
+import java.io.OutputStream
+import java.nio.file.Path
 import javax.annotation.processing.Filer
+import javax.tools.StandardLocation
+import kotlin.io.path.extension
 
 internal class JavacFiler(
     private val processingEnv: XProcessingEnv,
@@ -40,4 +45,24 @@
         }
         fileSpec.writeTo(delegate)
     }
+
+    override fun writeResource(
+        filePath: Path,
+        originatingElements: List<XElement>,
+        mode: XFiler.Mode
+    ): OutputStream {
+        require(filePath.extension != "java" && filePath.extension != "kt") {
+            "Could not create resource file with a source type extension. File must not be " +
+                "neither '.java' nor '.kt', but was: $filePath"
+        }
+        val javaOriginatingElements =
+            originatingElements.filterIsInstance<JavacElement>().map { it.element }.toTypedArray()
+        val fileObject = delegate.createResource(
+            StandardLocation.CLASS_OUTPUT,
+            "",
+            filePath.toString(),
+            *javaOriginatingElements
+        )
+        return fileObject.openOutputStream()
+    }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
index 68af580..1080617 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
@@ -17,24 +17,23 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.room.compiler.processing.InternalXAnnotated
-import androidx.room.compiler.processing.XAnnotationBox
 import androidx.room.compiler.processing.XAnnotation
+import androidx.room.compiler.processing.XAnnotationBox
 import androidx.room.compiler.processing.unwrapRepeatedAnnotationsFromContainer
-import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
 import com.google.devtools.ksp.symbol.KSAnnotated
 import com.google.devtools.ksp.symbol.KSAnnotation
 import com.google.devtools.ksp.symbol.KSTypeAlias
+import java.lang.annotation.ElementType
 import kotlin.reflect.KClass
 
-@OptIn(KspExperimental::class)
 internal sealed class KspAnnotated(
     val env: KspProcessingEnv
 ) : InternalXAnnotated {
     abstract fun annotations(): Sequence<KSAnnotation>
 
     private fun <T : Annotation> findAnnotations(annotation: KClass<T>): Sequence<KSAnnotation> {
-        return annotations().filter { isSameAnnotationClass(it, annotation) }
+        return annotations().filter { it.isSameAnnotationClass(annotation) }
     }
 
     override fun getAllAnnotations(): List<XAnnotation> {
@@ -82,31 +81,19 @@
         containerAnnotation: KClass<out Annotation>?
     ): Boolean {
         return annotations().any {
-            isSameAnnotationClass(it, annotation) ||
-                (containerAnnotation != null && isSameAnnotationClass(it, containerAnnotation))
+            it.isSameAnnotationClass(annotation) ||
+                (containerAnnotation != null && it.isSameAnnotationClass(containerAnnotation))
         }
     }
 
-    private fun isSameAnnotationClass(
-        ksAnnotation: KSAnnotation,
-        annotationClass: KClass<out Annotation>
-    ): Boolean {
-        var declaration = ksAnnotation.annotationType.resolve().declaration
-        while (declaration is KSTypeAlias) {
-            declaration = declaration.type.resolve().declaration
-        }
-        val qualifiedName = declaration.qualifiedName?.asString() ?: return false
-        return qualifiedName == annotationClass.qualifiedName
-    }
-
     private class KSAnnotatedDelegate(
         env: KspProcessingEnv,
         private val delegate: KSAnnotated,
         private val useSiteFilter: UseSiteFilter
     ) : KspAnnotated(env) {
         override fun annotations(): Sequence<KSAnnotation> {
-            return delegate.annotations.asSequence().filter {
-                useSiteFilter.accept(it)
+            return delegate.annotations.filter {
+                useSiteFilter.accept(env, it)
             }
         }
     }
@@ -118,50 +105,96 @@
     }
 
     /**
-     * TODO: The implementation of UseSiteFilter is not 100% correct until
-     * https://github.com/google/ksp/issues/96 is fixed.
-     * https://kotlinlang.org/docs/reference/annotations.html
+     * Annotation use site filter
      *
-     * More specifically, when a use site is not defined in an annotation, we need to find the
-     * declaration of the annotation and decide on the use site based on that.
-     * Unfortunately, due to KSP issue #96, we cannot yet read values from a `@Target` annotation
-     * which prevents implementing it correctly.
-     *
-     * Current implementation just approximates it which should work for Room.
+     * https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets
      */
     interface UseSiteFilter {
-        fun accept(annotation: KSAnnotation): Boolean
+        fun accept(env: KspProcessingEnv, annotation: KSAnnotation): Boolean
 
         private class Impl(
-            val acceptedTarget: AnnotationUseSiteTarget,
+            val acceptedSiteTarget: AnnotationUseSiteTarget,
+            val acceptedTarget: AnnotationTarget,
             private val acceptNoTarget: Boolean = true,
         ) : UseSiteFilter {
-            override fun accept(annotation: KSAnnotation): Boolean {
-                val target = annotation.useSiteTarget
-                return if (target == null) {
-                    acceptNoTarget
+            override fun accept(env: KspProcessingEnv, annotation: KSAnnotation): Boolean {
+                val useSiteTarget = annotation.useSiteTarget
+                val annotationTargets = annotation.getDeclaredTargets(env)
+                return if (useSiteTarget != null) {
+                    acceptedSiteTarget == useSiteTarget
+                } else if (annotationTargets.isNotEmpty()) {
+                    annotationTargets.contains(acceptedTarget)
                 } else {
-                    acceptedTarget == target
+                    acceptNoTarget
                 }
             }
         }
 
         companion object {
             val NO_USE_SITE = object : UseSiteFilter {
-                override fun accept(annotation: KSAnnotation): Boolean {
+                override fun accept(env: KspProcessingEnv, annotation: KSAnnotation): Boolean {
                     return annotation.useSiteTarget == null
                 }
             }
-            val NO_USE_SITE_OR_FIELD: UseSiteFilter = Impl(AnnotationUseSiteTarget.FIELD)
-            val NO_USE_SITE_OR_METHOD_PARAMETER: UseSiteFilter =
-                Impl(AnnotationUseSiteTarget.PARAM)
-            val NO_USE_SITE_OR_GETTER: UseSiteFilter = Impl(AnnotationUseSiteTarget.GET)
-            val NO_USE_SITE_OR_SETTER: UseSiteFilter = Impl(AnnotationUseSiteTarget.SET)
-            val NO_USE_SITE_OR_SET_PARAM: UseSiteFilter = Impl(AnnotationUseSiteTarget.SETPARAM)
+            val NO_USE_SITE_OR_FIELD: UseSiteFilter = Impl(
+                acceptedSiteTarget = AnnotationUseSiteTarget.FIELD,
+                acceptedTarget = AnnotationTarget.FIELD
+            )
+            val NO_USE_SITE_OR_METHOD_PARAMETER: UseSiteFilter = Impl(
+                acceptedSiteTarget = AnnotationUseSiteTarget.PARAM,
+                acceptedTarget = AnnotationTarget.VALUE_PARAMETER
+            )
+            val NO_USE_SITE_OR_GETTER: UseSiteFilter = Impl(
+                acceptedSiteTarget = AnnotationUseSiteTarget.GET,
+                acceptedTarget = AnnotationTarget.PROPERTY_GETTER
+            )
+            val NO_USE_SITE_OR_SETTER: UseSiteFilter = Impl(
+                acceptedSiteTarget = AnnotationUseSiteTarget.SET,
+                acceptedTarget = AnnotationTarget.PROPERTY_SETTER
+            )
+            val NO_USE_SITE_OR_SET_PARAM: UseSiteFilter = Impl(
+                acceptedSiteTarget = AnnotationUseSiteTarget.SETPARAM,
+                acceptedTarget = AnnotationTarget.PROPERTY_SETTER
+            )
             val FILE: UseSiteFilter = Impl(
-                acceptedTarget = AnnotationUseSiteTarget.FILE,
+                acceptedSiteTarget = AnnotationUseSiteTarget.FILE,
+                acceptedTarget = AnnotationTarget.FILE,
                 acceptNoTarget = false
             )
+
+            private fun KSAnnotation.getDeclaredTargets(
+                env: KspProcessingEnv
+            ): Set<AnnotationTarget> {
+                val annotationDeclaration = this.annotationType.resolve().declaration
+                val kotlinTargets = annotationDeclaration.annotations.firstOrNull {
+                    it.isSameAnnotationClass(kotlin.annotation.Target::class)
+                }?.let { targetAnnotation ->
+                    KspAnnotation(env, targetAnnotation)
+                        .asAnnotationBox(kotlin.annotation.Target::class.java)
+                        .value.allowedTargets
+                }?.toSet() ?: emptySet()
+                val javaTargets = annotationDeclaration.annotations.firstOrNull {
+                    it.isSameAnnotationClass(java.lang.annotation.Target::class)
+                }?.let { targetAnnotation ->
+                    KspAnnotation(env, targetAnnotation)
+                        .asAnnotationBox(java.lang.annotation.Target::class.java)
+                        .value.value.toList()
+                }?.mapNotNull { it.toAnnotationTarget() }?.toSet() ?: emptySet()
+                return kotlinTargets + javaTargets
+            }
+
+            private fun ElementType.toAnnotationTarget() = when (this) {
+                ElementType.TYPE -> AnnotationTarget.CLASS
+                ElementType.FIELD -> AnnotationTarget.FIELD
+                ElementType.METHOD -> AnnotationTarget.FUNCTION
+                ElementType.PARAMETER -> AnnotationTarget.VALUE_PARAMETER
+                ElementType.CONSTRUCTOR -> AnnotationTarget.CONSTRUCTOR
+                ElementType.LOCAL_VARIABLE -> AnnotationTarget.LOCAL_VARIABLE
+                ElementType.ANNOTATION_TYPE -> AnnotationTarget.ANNOTATION_CLASS
+                ElementType.TYPE_PARAMETER -> AnnotationTarget.TYPE_PARAMETER
+                ElementType.TYPE_USE -> AnnotationTarget.TYPE
+                else -> null
+            }
         }
     }
 
@@ -175,5 +208,16 @@
                 KSAnnotatedDelegate(env, it, filter)
             } ?: NotAnnotated(env)
         }
+
+        internal fun KSAnnotation.isSameAnnotationClass(
+            annotationClass: KClass<out Annotation>
+        ): Boolean {
+            var declaration = annotationType.resolve().declaration
+            while (declaration is KSTypeAlias) {
+                declaration = declaration.type.resolve().declaration
+            }
+            val qualifiedName = declaration.qualifiedName?.asString() ?: return false
+            return qualifiedName == annotationClass.qualifiedName
+        }
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
index 373d9607..7c92ac3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
@@ -16,8 +16,10 @@
 
 package androidx.room.compiler.processing.ksp
 
+import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XFiler
 import androidx.room.compiler.processing.XMessager
+import androidx.room.compiler.processing.originatingElementForPoet
 import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
 import com.google.devtools.ksp.processing.CodeGenerator
 import com.google.devtools.ksp.processing.Dependencies
@@ -27,8 +29,11 @@
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.OriginatingElementsHolder
 import java.io.OutputStream
+import java.nio.file.Path
 import javax.lang.model.element.Element
 import javax.tools.Diagnostic
+import kotlin.io.path.extension
+import kotlin.io.path.nameWithoutExtension
 
 internal class KspFiler(
     private val delegate: CodeGenerator,
@@ -70,6 +75,27 @@
         }
     }
 
+    override fun writeResource(
+        filePath: Path,
+        originatingElements: List<XElement>,
+        mode: XFiler.Mode
+    ): OutputStream {
+        require(filePath.extension != "java" && filePath.extension != "kt") {
+            "Could not create resource file with a source type extension. File must not be " +
+                "neither '.java' nor '.kt', but was: $filePath"
+        }
+        val kspFilerOriginatingElements = originatingElements
+            .mapNotNull { it.originatingElementForPoet() }
+            .toOriginatingElements()
+        return createNewFile(
+            originatingElements = kspFilerOriginatingElements,
+            packageName = filePath.parent?.toString() ?: "",
+            fileName = filePath.nameWithoutExtension,
+            extensionName = filePath.extension,
+            aggregating = mode == XFiler.Mode.Aggregating
+        )
+    }
+
     private fun createNewFile(
         originatingElements: OriginatingElements,
         packageName: String,
@@ -78,14 +104,16 @@
         aggregating: Boolean
     ): OutputStream {
         val dependencies = if (originatingElements.isEmpty()) {
-            messager.printMessage(
-                Diagnostic.Kind.WARNING,
-                """
-                    No dependencies are reported for $fileName which will prevent
-                    incremental compilation.
-                    Please file a bug at $ISSUE_TRACKER_LINK.
-                """.trimIndent()
-            )
+            val isSourceFile = extensionName == "java" || extensionName == "kt"
+            if (isSourceFile) {
+                val filePath = "$packageName.$fileName.$extensionName"
+                messager.printMessage(
+                    Diagnostic.Kind.WARNING,
+                    "No dependencies reported for generated source $filePath which will" +
+                        "prevent incremental compilation.\n" +
+                        "Please file a bug at $ISSUE_TRACKER_LINK."
+                )
+            }
             Dependencies.ALL_FILES
         } else {
             Dependencies(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
index b9c13a4..0fc88da 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
@@ -60,6 +60,12 @@
                     }
                     else -> error("Unsupported $symbol with annotation $annotationQualifiedName")
                 }
+            }.filter {
+                // Due to the bug in https://github.com/google/ksp/issues/1198, KSP may incorrectly
+                // copy annotations from a constructor KSValueParameter to its KSPropertyDeclaration
+                // which we remove manually, so check here to make sure this is in sync with the
+                // actual annotations on the element.
+                it.getAllAnnotations().any { it.qualifiedName == annotationQualifiedName }
             }.toSet()
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
index 889a878..d96e81d 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
@@ -34,6 +34,7 @@
 import androidx.room.compiler.processing.util.asJTypeName
 import androidx.room.compiler.processing.util.asKTypeName
 import androidx.room.compiler.processing.util.compileFiles
+import androidx.room.compiler.processing.util.getDeclaredField
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethodByJvmName
 import androidx.room.compiler.processing.util.getParameter
@@ -49,6 +50,7 @@
 
 // used in typealias test
 typealias OtherAnnotationTypeAlias = OtherAnnotation
+
 @RunWith(Parameterized::class)
 class XAnnotationTest(
     private val preCompiled: Boolean
@@ -963,6 +965,147 @@
         }
     }
 
+    // This is testing the workaround for https://github.com/google/ksp/issues/1198
+    @Test
+    fun paramTargetInPrimaryCtorProperty() {
+        runTest(
+            sources = listOf(Source.kotlin(
+                "Foo.kt",
+                """
+            package test
+            class Subject(
+                @MyAnnotation field: String,
+                @MyAnnotation val valField: String,
+                @MyAnnotation var varField: String,
+            )
+            @Target(AnnotationTarget.VALUE_PARAMETER)
+            annotation class MyAnnotation
+            """.trimIndent()
+            )),
+        ) { invocation ->
+            // Verifies the KspRoundEnv side of the workaround.
+            if (!preCompiled) {
+                val annotatedElements =
+                    invocation.roundEnv.getElementsAnnotatedWith("test.MyAnnotation")
+                assertThat(annotatedElements.all { it is XExecutableParameterElement }).isTrue()
+                assertThat(annotatedElements.map { it.name })
+                    .containsExactly("field", "valField", "varField")
+            }
+
+            val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+            val myAnnotation = invocation.processingEnv.requireTypeElement("test.MyAnnotation")
+
+            val constructorParameters = subject.getConstructors().single().parameters
+            assertThat(constructorParameters.map { it.name })
+                .containsExactly("field", "valField", "varField")
+            fun getCtorParameterAnnotationElements(paramName: String): List<XTypeElement> {
+                return constructorParameters
+                    .first { it.name == paramName }
+                    .getAllAnnotations()
+                    .map(XAnnotation::typeElement)
+            }
+            assertThat(getCtorParameterAnnotationElements("field")).contains(myAnnotation)
+            assertThat(getCtorParameterAnnotationElements("valField")).contains(myAnnotation)
+            assertThat(getCtorParameterAnnotationElements("varField")).contains(myAnnotation)
+
+            assertThat(subject.getDeclaredFields().map(XFieldElement::name))
+                .containsExactly("valField", "varField")
+            fun getDeclaredFieldAnnotationElements(fieldName: String): List<XTypeElement> {
+                return subject.getDeclaredField(fieldName)
+                    .getAllAnnotations()
+                    .map(XAnnotation::typeElement)
+            }
+            assertThat(getDeclaredFieldAnnotationElements("valField")).doesNotContain(myAnnotation)
+            assertThat(getDeclaredFieldAnnotationElements("varField")).doesNotContain(myAnnotation)
+        }
+    }
+
+    @Test
+    fun fieldTargetInPrimaryCtorProperty() {
+        runTest(
+            sources = listOf(Source.kotlin(
+                "Foo.kt",
+                """
+            package test
+            class Subject(
+                @MyAnnotation val valField: String,
+                @MyAnnotation var varField: String,
+            )
+            @Target(AnnotationTarget.FIELD)
+            annotation class MyAnnotation
+            """.trimIndent()
+            )),
+        ) { invocation ->
+            val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+            val myAnnotation = invocation.processingEnv.requireTypeElement("test.MyAnnotation")
+
+            val constructorParameters = subject.getConstructors().single().parameters
+            assertThat(constructorParameters.map { it.name })
+                .containsExactly("valField", "varField")
+            fun getCtorParameterAnnotationElements(paramName: String): List<XTypeElement> {
+                return constructorParameters
+                    .first { it.name == paramName }
+                    .getAllAnnotations()
+                    .map(XAnnotation::typeElement)
+            }
+            assertThat(getCtorParameterAnnotationElements("valField")).doesNotContain(myAnnotation)
+            assertThat(getCtorParameterAnnotationElements("varField")).doesNotContain(myAnnotation)
+
+            assertThat(subject.getDeclaredFields().map(XFieldElement::name))
+                .containsExactly("valField", "varField")
+            fun getDeclaredFieldAnnotationElements(fieldName: String): List<XTypeElement> {
+                return subject.getDeclaredField(fieldName)
+                    .getAllAnnotations()
+                    .map(XAnnotation::typeElement)
+            }
+            assertThat(getDeclaredFieldAnnotationElements("valField")).contains(myAnnotation)
+            assertThat(getDeclaredFieldAnnotationElements("varField")).contains(myAnnotation)
+        }
+    }
+
+    @Test
+    fun propertyTargetInPrimaryCtorProperty() {
+        runTest(
+            sources = listOf(Source.kotlin(
+                "Foo.kt",
+                """
+            package test
+            class Subject(
+                @MyAnnotation val valField: String,
+                @MyAnnotation var varField: String,
+            )
+            @Target(AnnotationTarget.PROPERTY)
+            annotation class MyAnnotation
+            """.trimIndent()
+            )),
+        ) { invocation ->
+            val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+            val myAnnotation = invocation.processingEnv.requireTypeElement("test.MyAnnotation")
+
+            val constructorParameters = subject.getConstructors().single().parameters
+            assertThat(constructorParameters.map { it.name })
+                .containsExactly("valField", "varField")
+            fun getCtorParameterAnnotationElements(paramName: String): List<XTypeElement> {
+                return constructorParameters
+                    .first { it.name == paramName }
+                    .getAllAnnotations()
+                    .map(XAnnotation::typeElement)
+            }
+            assertThat(getCtorParameterAnnotationElements("valField")).doesNotContain(myAnnotation)
+            assertThat(getCtorParameterAnnotationElements("varField")).doesNotContain(myAnnotation)
+
+            assertThat(subject.getDeclaredFields().map(XFieldElement::name))
+                .containsExactly("valField", "varField")
+            fun getDeclaredFieldAnnotationElements(fieldName: String): List<XTypeElement> {
+                return subject.getDeclaredField(fieldName)
+                    .getAllAnnotations()
+                    .map(XAnnotation::typeElement)
+            }
+            assertThat(getDeclaredFieldAnnotationElements("valField")).doesNotContain(myAnnotation)
+            assertThat(getDeclaredFieldAnnotationElements("varField")).doesNotContain(myAnnotation)
+        }
+    }
+
     // helper function to read what we need
     private fun XAnnotated.getSuppressValues(): List<String>? {
         return this.findAnnotation<TestSuppressWarnings>()
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt
new file mode 100644
index 0000000..f69f171
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing
+
+import androidx.room.compiler.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.io.path.Path
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class XFilerTest {
+
+    @Test
+    fun writeResource_sourceFile_java() {
+        runProcessorTest {
+            try {
+                it.processingEnv.filer.writeResource(Path("Test.java"), emptyList()).close()
+                fail("Expected an exception!")
+            } catch (ex: java.lang.IllegalArgumentException) {
+                assertThat(ex.message).isEqualTo(
+                    "Could not create resource file with a source type extension. " +
+                        "File must not be neither '.java' nor '.kt', but was: Test.java"
+                )
+            }
+        }
+    }
+
+    @Test
+    fun writeResource_sourceFile_kotlin() {
+        runProcessorTest {
+            try {
+                it.processingEnv.filer.writeResource(Path("Test.kt"), emptyList()).close()
+                fail("Expected an exception!")
+            } catch (ex: IllegalArgumentException) {
+                assertThat(ex.message).isEqualTo(
+                    "Could not create resource file with a source type extension. " +
+                        "File must not be neither '.java' nor '.kt', but was: Test.kt"
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 3802cb2..c9ec393 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -41,10 +41,46 @@
 import com.squareup.kotlinpoet.javapoet.KTypeVariableName
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import org.junit.runners.Parameterized
 
-@RunWith(JUnit4::class)
-class XTypeElementTest {
+@RunWith(Parameterized::class)
+class XTypeElementTest(
+    private val isPreCompiled: Boolean,
+) {
+    private fun runTest(
+        sources: List<Source>,
+        handler: (XTestInvocation) -> Unit
+    ) {
+        if (isPreCompiled) {
+            val compiled = compileFiles(sources)
+            val hasKotlinSources = sources.any {
+                it is Source.KotlinSource
+            }
+            val kotlinSources = if (hasKotlinSources) {
+                listOf(
+                    Source.kotlin("placeholder.kt", "class PlaceholderKotlin")
+                )
+            } else {
+                emptyList()
+            }
+            val newSources = kotlinSources + Source.java(
+                "PlaceholderJava",
+                "public class " +
+                    "PlaceholderJava {}"
+            )
+            runProcessorTest(
+                sources = newSources,
+                handler = handler,
+                classpath = compiled
+            )
+        } else {
+            runProcessorTest(
+                sources = sources,
+                handler = handler
+            )
+        }
+    }
+
     @Test
     fun qualifiedNames() {
         val src1 = Source.kotlin(
@@ -70,7 +106,7 @@
             }
             """
         )
-        runProcessorTest(
+        runTest(
             sources = listOf(src1, src2, src3)
         ) { invocation ->
             invocation.processingEnv.requireTypeElement("TopLevel").let {
@@ -140,54 +176,54 @@
             interface AnotherInterface : MyInterface
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
-                assertThat(it.superClass).isEqualTo(
-                    invocation.processingEnv.requireType("foo.bar.AbstractClass")
+                assertThat(it.superClass!!.asTypeName()).isEqualTo(
+                    invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName()
                 )
-                assertThat(it.type.superTypes).containsExactly(
-                    invocation.processingEnv.requireType("foo.bar.AbstractClass"),
-                    invocation.processingEnv.requireType("foo.bar.MyInterface")
+                assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+                    invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName(),
+                    invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
                 )
-                assertThat(it.type).isEqualTo(
-                    invocation.processingEnv.requireType("foo.bar.Baz")
+                assertThat(it.type.asTypeName()).isEqualTo(
+                    invocation.processingEnv.requireType("foo.bar.Baz").asTypeName()
                 )
                 assertThat(it.isInterface()).isFalse()
                 assertThat(it.isKotlinObject()).isFalse()
                 assertThat(it.isAbstract()).isFalse()
             }
             invocation.processingEnv.requireTypeElement("foo.bar.AbstractClass").let {
-                assertThat(it.superClass).isEqualTo(
-                    invocation.processingEnv.requireType(JTypeName.OBJECT)
+                assertThat(it.superClass!!.asTypeName()).isEqualTo(
+                    invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
                 )
-                assertThat(it.type.superTypes).containsExactly(
-                    invocation.processingEnv.requireType(JTypeName.OBJECT)
+                assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+                    invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
                 )
                 assertThat(it.isAbstract()).isTrue()
                 assertThat(it.isInterface()).isFalse()
-                assertThat(it.type).isEqualTo(
-                    invocation.processingEnv.requireType("foo.bar.AbstractClass")
+                assertThat(it.type.asTypeName()).isEqualTo(
+                    invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName()
                 )
             }
             invocation.processingEnv.requireTypeElement("foo.bar.MyInterface").let {
                 assertThat(it.superClass).isNull()
-                assertThat(it.type.superTypes).containsExactly(
-                    invocation.processingEnv.requireType(JTypeName.OBJECT)
+                assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+                    invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
                 )
                 assertThat(it.isInterface()).isTrue()
-                assertThat(it.type).isEqualTo(
-                    invocation.processingEnv.requireType("foo.bar.MyInterface")
+                assertThat(it.type.asTypeName()).isEqualTo(
+                    invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
                 )
             }
             invocation.processingEnv.requireTypeElement("foo.bar.AnotherInterface").let {
                 assertThat(it.superClass).isNull()
-                assertThat(it.type.superTypes).containsExactly(
-                    invocation.processingEnv.requireType("java.lang.Object"),
-                    invocation.processingEnv.requireType("foo.bar.MyInterface")
+                assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+                    invocation.processingEnv.requireType("java.lang.Object").asTypeName(),
+                    invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
                 )
                 assertThat(it.isInterface()).isTrue()
-                assertThat(it.type).isEqualTo(
-                    invocation.processingEnv.requireType("foo.bar.AnotherInterface")
+                assertThat(it.type.asTypeName()).isEqualTo(
+                    invocation.processingEnv.requireType("foo.bar.AnotherInterface").asTypeName()
                 )
             }
         }
@@ -204,7 +240,7 @@
             interface MyInterface<E>
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
                 assertThat(it.superInterfaces).hasSize(1)
                 val superInterface = it.superInterfaces.first { type ->
@@ -232,7 +268,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("foo.bar.Outer").let {
                 assertThat(it.asClassName())
                     .isEqualTo(XClassName.get("foo.bar", "Outer"))
@@ -291,7 +327,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        runTest(
             sources = listOf(kotlinSrc, javaSrc, javaAnnotationSrc)
         ) { invocation ->
             fun getModifiers(element: XTypeElement): Set<String> {
@@ -346,8 +382,15 @@
                 .containsExactly("public", "class")
             assertThat(getModifiers("OuterJavaClass.NestedJavaClass"))
                 .containsExactly("public", "static", "class")
-            assertThat(getModifiers("JavaAnnotation"))
-                .containsExactly("abstract", "public", "annotation")
+            assertThat(getModifiers("JavaAnnotation")).apply {
+                // KSP vs KAPT metadata have a difference in final vs abstract modifiers
+                // for annotation types.
+                if (isPreCompiled && invocation.isKsp) {
+                    containsExactly("final", "public", "annotation")
+                } else {
+                    containsExactly("abstract", "public", "annotation")
+                }
+            }
             assertThat(getModifiers("KotlinAnnotation")).apply {
                 // KSP vs KAPT metadata have a difference in final vs abstract modifiers
                 // for annotation types.
@@ -359,8 +402,13 @@
             }
             assertThat(getModifiers("DataClass"))
                 .containsExactly("public", "final", "class", "data")
-            assertThat(getModifiers("InlineClass"))
-                .containsExactly("public", "final", "class", "value")
+            assertThat(getModifiers("InlineClass")).apply {
+                if (isPreCompiled && invocation.isKsp) {
+                    containsExactly("public", "final", "class")
+                } else {
+                    containsExactly("public", "final", "class", "value")
+                }
+            }
             assertThat(getModifiers("FunInterface"))
                 .containsExactly("public", "abstract", "interface", "fun")
         }
@@ -375,7 +423,7 @@
             interface MyInterface
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("MyClass").let {
                 assertThat(it.kindName()).isEqualTo("class")
             }
@@ -398,7 +446,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
             assertThat(baseClass.getAllFieldNames()).containsExactly("genericProp")
             assertThat(baseClass.getDeclaredFields().map { it.name })
@@ -439,7 +487,7 @@
             ) : BaseClass(value)
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
             assertThat(baseClass.getAllFieldNames()).containsExactly("value")
             val subClass = invocation.processingEnv.requireTypeElement("SubClass")
@@ -465,10 +513,11 @@
 
     @Test
     fun fieldsMethodsWithoutBacking() {
-        fun buildSrc(pkg: String) = Source.kotlin(
-            "Foo.kt",
-            """
-            package $pkg
+        runTest(
+            sources = listOf(Source.kotlin(
+                "Foo.kt",
+                """
+            package test
             class Subject {
                 val realField: String = ""
                     get() = field
@@ -492,42 +541,37 @@
                 }
             }
             """.trimIndent()
-        )
-        val lib = compileFiles(listOf(buildSrc("lib")))
-        runProcessorTest(
-            sources = listOf(buildSrc("main")),
-            classpath = lib
+            )),
         ) { invocation ->
-            listOf("lib", "main").forEach { pkg ->
-                val subject = invocation.processingEnv.requireTypeElement("$pkg.Subject")
-                val declaredFields = subject.getDeclaredFields().map { it.name } -
-                    listOf("Companion") // skip Companion, KAPT generates it
-                val expectedFields = listOf("realField", "staticRealField")
-                assertWithMessage(subject.qualifiedName)
-                    .that(declaredFields)
-                    .containsExactlyElementsIn(expectedFields)
-                val allFields = subject.getAllFieldsIncludingPrivateSupers().map { it.name } -
-                    listOf("Companion") // skip Companion, KAPT generates it
-                assertWithMessage(subject.qualifiedName)
-                    .that(allFields.toList())
-                    .containsExactlyElementsIn(expectedFields)
-                val methodNames = subject.getDeclaredMethods().map { it.jvmName }
-                assertWithMessage(subject.qualifiedName)
-                    .that(methodNames)
-                    .containsAtLeast("getNoBackingVal", "getNoBackingVar", "setNoBackingVar")
-                assertWithMessage(subject.qualifiedName)
-                    .that(methodNames)
-                    .doesNotContain("setNoBackingVal")
-            }
+            val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+            val declaredFields = subject.getDeclaredFields().map { it.name } -
+                listOf("Companion") // skip Companion, KAPT generates it
+            val expectedFields = listOf("realField", "staticRealField")
+            assertWithMessage(subject.qualifiedName)
+                .that(declaredFields)
+                .containsExactlyElementsIn(expectedFields)
+            val allFields = subject.getAllFieldsIncludingPrivateSupers().map { it.name } -
+                listOf("Companion") // skip Companion, KAPT generates it
+            assertWithMessage(subject.qualifiedName)
+                .that(allFields.toList())
+                .containsExactlyElementsIn(expectedFields)
+            val methodNames = subject.getDeclaredMethods().map { it.jvmName }
+            assertWithMessage(subject.qualifiedName)
+                .that(methodNames)
+                .containsAtLeast("getNoBackingVal", "getNoBackingVar", "setNoBackingVar")
+            assertWithMessage(subject.qualifiedName)
+                .that(methodNames)
+                .doesNotContain("setNoBackingVal")
         }
     }
 
     @Test
     fun abstractFields() {
-        fun buildSource(pkg: String) = Source.kotlin(
-            "Foo.kt",
-            """
-            package $pkg
+        runTest(
+            sources = listOf(Source.kotlin(
+                "Foo.kt",
+                """
+            package test
             abstract class Subject {
                 val value: String = ""
                 abstract val abstractValue: String
@@ -538,57 +582,46 @@
                 }
             }
             """.trimIndent()
-        )
-
-        val lib = compileFiles(listOf(buildSource("lib")))
-        runProcessorTest(
-            sources = listOf(buildSource("main")),
-            classpath = lib
+            )),
         ) { invocation ->
-            listOf("lib", "main").forEach { pkg ->
-                val subject = invocation.processingEnv.requireTypeElement("$pkg.Subject")
-                val declaredFields = subject.getDeclaredFields().map { it.name } -
-                    listOf("Companion")
-                val expectedFields = listOf("value", "realCompanion", "jvmStatic")
-                assertWithMessage(subject.qualifiedName)
-                    .that(declaredFields)
-                    .containsExactlyElementsIn(expectedFields)
-            }
+            val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+            val declaredFields = subject.getDeclaredFields().map { it.name } -
+                listOf("Companion")
+            val expectedFields = listOf("value", "realCompanion", "jvmStatic")
+            assertWithMessage(subject.qualifiedName)
+                .that(declaredFields)
+                .containsExactlyElementsIn(expectedFields)
         }
     }
 
     @Test
     fun lateinitFields() {
-        fun buildSource(pkg: String) = Source.kotlin(
-            "Foo.kt",
-            """
-            package $pkg
+        runTest(
+            sources = listOf(Source.kotlin(
+                "Foo.kt",
+                """
+            package test
             class Subject {
                 lateinit var x:String
                 var y:String = "abc"
             }
             """.trimIndent()
-        )
-        runProcessorTest(
-            sources = listOf(buildSource("app")),
-            classpath = compileFiles(listOf(buildSource("lib")))
+            )),
         ) { invocation ->
-            listOf("app", "lib").forEach { pkg ->
-                val subject = invocation.processingEnv.requireTypeElement("$pkg.Subject")
-                assertWithMessage(subject.fallbackLocationText)
-                    .that(subject.getDeclaredFields().map { it.name })
-                    .containsExactly(
-                        "x", "y"
-                    )
-                assertWithMessage(subject.fallbackLocationText)
-                    .that(subject.getDeclaredMethods().map { it.jvmName })
-                    .containsExactly(
-                        "getX", "setX", "getY", "setY"
-                    )
-                subject.getField("x").let { field ->
-                    assertThat(field.isFinal()).isFalse()
-                    assertThat(field.isPrivate()).isFalse()
-                }
+            val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+            assertWithMessage(subject.fallbackLocationText)
+                .that(subject.getDeclaredFields().map { it.name })
+                .containsExactly(
+                    "x", "y"
+                )
+            assertWithMessage(subject.fallbackLocationText)
+                .that(subject.getDeclaredMethods().map { it.jvmName })
+                .containsExactly(
+                    "getX", "setX", "getY", "setY"
+                )
+            subject.getField("x").let { field ->
+                assertThat(field.isFinal()).isFalse()
+                assertThat(field.isPrivate()).isFalse()
             }
         }
     }
@@ -603,7 +636,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val element = invocation.processingEnv.requireTypeElement("MyInterface")
             assertThat(element.getAllFieldsIncludingPrivateSupers().toList()).isEmpty()
             element.getMethodByJvmName("getX").let {
@@ -628,7 +661,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val element = invocation.processingEnv.requireTypeElement("MyAbstractClass")
             assertThat(
                 element.getAllFieldNames()
@@ -659,61 +692,34 @@
 
     @Test
     fun propertyGettersSetters() {
-        val dependencyJavaSource = Source.java(
-            "DependencyJavaSubject.java",
-            """
-            class DependencyJavaSubject {
-                int myField;
-                private int mutable;
-                int immutable;
-                int getMutable() {return 3;}
-                void setMutable(int x) {}
-                int getImmutable() {return 3;}
-            }
-            """.trimIndent()
-        )
-        val dependencyKotlinSource = Source.kotlin(
-            "DependencyKotlinSubject.kt",
-            """
-            class DependencyKotlinSubject {
-                private val myField = 0
-                var mutable: Int = 0
-                val immutable:Int = 0
-            }
-            """.trimIndent()
-        )
-        val dependency = compileFiles(listOf(dependencyJavaSource, dependencyKotlinSource))
-        val javaSource = Source.java(
-            "JavaSubject.java",
-            """
-            class JavaSubject {
-                int myField;
-                private int mutable;
-                int immutable;
-                int getMutable() {return 3;}
-                void setMutable(int x) {}
-                int getImmutable() {return 3;}
-            }
-            """.trimIndent()
-        )
-        val kotlinSource = Source.kotlin(
-            "KotlinSubject.kt",
-            """
-            class KotlinSubject {
-                private val myField = 0
-                var mutable: Int = 0
-                val immutable:Int = 0
-            }
-            """.trimIndent()
-        )
-        runProcessorTest(
-            listOf(javaSource, kotlinSource),
-            classpath = dependency
-        ) { invocation ->
+        runTest(
             listOf(
-                "JavaSubject", "DependencyJavaSubject",
-                "KotlinSubject", "DependencyKotlinSubject"
-            ).map {
+                Source.java(
+                    "JavaSubject.java",
+                    """
+                    class JavaSubject {
+                        int myField;
+                        private int mutable;
+                        int immutable;
+                        int getMutable() {return 3;}
+                        void setMutable(int x) {}
+                        int getImmutable() {return 3;}
+                    }
+                    """.trimIndent()
+                ),
+                Source.kotlin(
+                    "KotlinSubject.kt",
+                    """
+                    class KotlinSubject {
+                        private val myField = 0
+                        var mutable: Int = 0
+                        val immutable:Int = 0
+                    }
+                    """.trimIndent()
+                )
+            ),
+        ) { invocation ->
+            listOf("JavaSubject", "KotlinSubject",).map {
                 invocation.processingEnv.requireTypeElement(it)
             }.forEach { subject ->
                 val methods = subject.getDeclaredMethods()
@@ -770,7 +776,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val base = invocation.processingEnv.requireTypeElement("Base")
             val objectMethodNames = invocation.objectMethodNames()
             assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
@@ -791,10 +797,11 @@
 
     @Test
     fun diamondOverride() {
-        fun buildSrc(pkg: String) = Source.kotlin(
-            "Foo.kt",
-            """
-            package $pkg
+        runTest(
+            sources = listOf(Source.kotlin(
+                "Foo.kt",
+                """
+            package test
             interface Parent<T> {
                 fun parent(t: T)
             }
@@ -813,38 +820,32 @@
                 abstract override fun parent(t: String)
             }
             """.trimIndent()
-        )
-
-        runProcessorTest(
-            sources = listOf(buildSrc("app")),
-            classpath = compileFiles(listOf(buildSrc("lib")))
+            )),
         ) { invocation ->
-            listOf("lib", "app").forEach { pkg ->
-                invocation.processingEnv.requireTypeElement("$pkg.Subject1").let { subject ->
-                    assertWithMessage(subject.qualifiedName).that(
-                        invocation.nonObjectMethodSignatures(subject)
-                    ).containsExactly(
-                        "child1(java.lang.String):void",
-                        "child2(java.lang.String):void",
-                        "parent(java.lang.String):void",
-                    )
-                }
-                invocation.processingEnv.requireTypeElement("$pkg.Subject2").let { subject ->
-                    assertWithMessage(subject.qualifiedName).that(
-                        invocation.nonObjectMethodSignatures(subject)
-                    ).containsExactly(
-                        "child1(java.lang.String):void",
-                        "parent(java.lang.String):void",
-                    )
-                }
-                invocation.processingEnv.requireTypeElement("$pkg.Subject3").let { subject ->
-                    assertWithMessage(subject.qualifiedName).that(
-                        invocation.nonObjectMethodSignatures(subject)
-                    ).containsExactly(
-                        "child1(java.lang.String):void",
-                        "parent(java.lang.String):void",
-                    )
-                }
+            invocation.processingEnv.requireTypeElement("test.Subject1").let { subject ->
+                assertWithMessage(subject.qualifiedName).that(
+                    invocation.nonObjectMethodSignatures(subject)
+                ).containsExactly(
+                    "child1(java.lang.String):void",
+                    "child2(java.lang.String):void",
+                    "parent(java.lang.String):void",
+                )
+            }
+            invocation.processingEnv.requireTypeElement("test.Subject2").let { subject ->
+                assertWithMessage(subject.qualifiedName).that(
+                    invocation.nonObjectMethodSignatures(subject)
+                ).containsExactly(
+                    "child1(java.lang.String):void",
+                    "parent(java.lang.String):void",
+                )
+            }
+            invocation.processingEnv.requireTypeElement("test.Subject3").let { subject ->
+                assertWithMessage(subject.qualifiedName).that(
+                    invocation.nonObjectMethodSignatures(subject)
+                ).containsExactly(
+                    "child1(java.lang.String):void",
+                    "parent(java.lang.String):void",
+                )
             }
         }
     }
@@ -871,7 +872,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val base = invocation.processingEnv.requireTypeElement("DerivedInterface")
             val methodNames = base.getAllMethods().toList().jvmNames()
             assertThat(methodNames).containsExactly(
@@ -899,7 +900,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val base = invocation.processingEnv.requireTypeElement("DerivedClass")
             val methodNamesCount =
                 base.getAllMethods().toList().jvmNames().groupingBy { it }.eachCount()
@@ -930,7 +931,7 @@
             """.trimIndent()
         )
 
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement(
                 "ParentWithExplicitOverride"
             ).let { parent ->
@@ -990,7 +991,7 @@
                 """.trimIndent()
             ),
         )
-        runProcessorTest(sources = srcs) { invocation ->
+        runTest(sources = srcs) { invocation ->
             invocation.processingEnv.requireTypeElement("foo.child.FooChild")
                 .let { fooChild ->
                     assertWithMessage(fooChild.qualifiedName).that(
@@ -1046,7 +1047,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val objectMethodNames = invocation.objectMethodNames()
             val klass = invocation.processingEnv.requireTypeElement("SubClass")
             assertThat(
@@ -1073,11 +1074,12 @@
      */
     @Test
     fun allMethods_withJvmNames() {
-        fun buildSource(pkg: String) = listOf(
-            Source.kotlin(
-                "Foo.kt",
-                """
-                package $pkg
+        runTest(
+            sources = listOf(
+                Source.kotlin(
+                    "Foo.kt",
+                    """
+                package test
                 interface Interface {
                     fun f1()
                     @JvmName("notF2")
@@ -1091,33 +1093,27 @@
                     }
                 }
             """.trimIndent()
-            )
-        )
-
-        runProcessorTest(
-            sources = buildSource("app"),
-            classpath = compileFiles(buildSource("lib"))
+                )
+            ),
         ) { invocation ->
-            listOf("app", "lib").forEach {
-                val appSubject = invocation.processingEnv.requireTypeElement("$it.Subject")
-                val methodNames = appSubject.getAllMethods().map { it.name }.toList()
-                val methodJvmNames = appSubject.getAllMethods().map { it.jvmName }.toList()
-                val objectMethodNames = invocation.objectMethodNames()
-                if (invocation.isKsp) {
-                    assertThat(methodNames - objectMethodNames).containsExactly(
-                        "f1", "f2"
-                    )
-                    assertThat(methodJvmNames - objectMethodNames).containsExactly(
-                        "notF1", "notF2"
-                    )
-                } else {
-                    assertThat(methodNames - objectMethodNames).containsExactly(
-                        "f1", "f1", "f2"
-                    )
-                    assertThat(methodJvmNames - objectMethodNames).containsExactly(
-                        "f1", "notF1", "notF2"
-                    )
-                }
+            val appSubject = invocation.processingEnv.requireTypeElement("test.Subject")
+            val methodNames = appSubject.getAllMethods().map { it.name }.toList()
+            val methodJvmNames = appSubject.getAllMethods().map { it.jvmName }.toList()
+            val objectMethodNames = invocation.objectMethodNames()
+            if (invocation.isKsp) {
+                assertThat(methodNames - objectMethodNames).containsExactly(
+                    "f1", "f2"
+                )
+                assertThat(methodJvmNames - objectMethodNames).containsExactly(
+                    "notF1", "notF2"
+                )
+            } else {
+                assertThat(methodNames - objectMethodNames).containsExactly(
+                    "f1", "f1", "f2"
+                )
+                assertThat(methodJvmNames - objectMethodNames).containsExactly(
+                    "f1", "notF1", "notF2"
+                )
             }
         }
     }
@@ -1137,7 +1133,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val objectMethodNames = invocation.objectMethodNames()
             invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
                 assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
@@ -1195,7 +1191,7 @@
             class SubClass : CompanionSubject()
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val objectMethodNames = invocation.processingEnv.requireTypeElement(
                 Any::class
             ).getAllMethods().jvmNames()
@@ -1270,7 +1266,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
                 assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
                     "getX"
@@ -1319,7 +1315,7 @@
             abstract class AbstractExplicit(x:Int)
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val subjects = listOf(
                 "MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
                 "BaseWithSecondary", "Sub", "SubWith3Constructors",
@@ -1375,7 +1371,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("MyInterface")
             assertThat(subject.getMethodByJvmName("notJvmDefault").isJavaDefault()).isFalse()
             assertThat(subject.getMethodByJvmName("jvmDefault").isJavaDefault()).isTrue()
@@ -1422,7 +1418,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val subjects = listOf(
                 "MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
                 "BaseWithSecondary", "Sub", "SubWith3Constructors",
@@ -1454,48 +1450,41 @@
 
     @Test
     fun enumTypeElement() {
-        fun createSources(packageName: String) = listOf(
-            Source.kotlin(
-                "$packageName/KotlinEnum.kt",
-                """
-                package $packageName
-                enum class KotlinEnum(private val x:Int) {
-                    VAL1(1),
-                    VAL2(2);
+        runTest(
+            sources = listOf(
+                Source.kotlin(
+                    "test/KotlinEnum.kt",
+                    """
+                    package test
+                    enum class KotlinEnum(private val x:Int) {
+                        VAL1(1),
+                        VAL2(2);
 
-                    fun enumMethod(): Unit {}
-                }
-                """.trimIndent()
-            ),
-            Source.java(
-                "$packageName.JavaEnum",
-                """
-                package $packageName;
-                public enum JavaEnum {
-                    VAL1(1),
-                    VAL2(2);
-
-                    private int x;
-
-                    JavaEnum(int x) {
-                        this.x = x;
+                        fun enumMethod(): Unit {}
                     }
-                    void enumMethod() {}
-                }
-                """.trimIndent()
-            )
-        )
+                    """.trimIndent()
+                ),
+                Source.java(
+                    "test.JavaEnum",
+                    """
+                    package test;
+                    public enum JavaEnum {
+                        VAL1(1),
+                        VAL2(2);
 
-        val classpath = compileFiles(
-            createSources("lib")
-        )
-        runProcessorTest(
-            sources = createSources("app"),
-            classpath = classpath
+                        private int x;
+
+                        JavaEnum(int x) {
+                            this.x = x;
+                        }
+                        void enumMethod() {}
+                    }
+                    """.trimIndent()
+                )
+            ),
         ) { invocation ->
             listOf(
-                "lib.KotlinEnum", "lib.JavaEnum",
-                "app.KotlinEnum", "app.JavaEnum"
+                "test.KotlinEnum", "test.JavaEnum",
             ).forEach { qName ->
                 val typeElement = invocation.processingEnv.requireTypeElement(qName)
                 assertWithMessage("$qName is enum")
@@ -1541,7 +1530,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val topLevelClass = invocation.processingEnv.requireTypeElement("TopLevelClass")
             val enclosedTypeElements = topLevelClass.getEnclosedTypeElements()
 
@@ -1571,7 +1560,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(sources = listOf(src)) { invocation ->
+        runTest(sources = listOf(src)) { invocation ->
             val topLevelClass = invocation.processingEnv.requireTypeElement("TopLevelClass")
             val enclosedTypeElements = topLevelClass.getEnclosedTypeElements()
 
@@ -1597,7 +1586,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(listOf(kotlinSrc)) { invocation ->
+        runTest(listOf(kotlinSrc)) { invocation ->
             val kotlinClass = invocation.processingEnv.requireTypeElement(
                 "foo.bar.KotlinClass")
             val companionObjects = kotlinClass.getEnclosedTypeElements().filter {
@@ -1611,7 +1600,7 @@
 
     @Test
     fun inheritedGenericMethod() {
-        runProcessorTest(
+        runTest(
             sources = listOf(
                 Source.kotlin(
                     "test.ConcreteClass.kt",
@@ -1678,7 +1667,7 @@
 
     @Test
     fun overriddenGenericMethod() {
-        runProcessorTest(
+        runTest(
             sources = listOf(
                 Source.kotlin(
                     "test.ConcreteClass.kt",
@@ -1789,7 +1778,7 @@
 
     @Test
     fun overriddenGenericConstructor() {
-        runProcessorTest(
+        runTest(
             sources = listOf(
                 Source.kotlin(
                     "test.ConcreteClass.kt",
@@ -1832,7 +1821,7 @@
 
     @Test
     fun inheritedGenericField() {
-        runProcessorTest(
+        runTest(
             sources = listOf(
                 Source.kotlin(
                     "test.ConcreteClass.kt",
@@ -1904,4 +1893,12 @@
         typeElement.getAllMethods()
             .filterNot { it.jvmName in objectMethodNames() }
             .map { it.signature(typeElement.type) }.toList()
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "isPreCompiled_{0}")
+        fun params(): List<Array<Any>> {
+            return listOf(arrayOf(false), arrayOf(true))
+        }
+    }
 }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
index 4075f36..62c1489 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
@@ -32,10 +32,11 @@
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
-import org.junit.Test
 import java.io.File
 import java.io.OutputStream
 import javax.tools.Diagnostic
+import kotlin.io.path.Path
+import org.junit.Test
 
 class KspFilerTest {
 
@@ -143,6 +144,29 @@
         }
     }
 
+    @Test
+    fun writeResource() {
+        runKspTest(
+            sources = emptyList()
+        ) { invocation ->
+            invocation.processingEnv.filer.writeResource(
+                filePath = Path("test.log"),
+                originatingElements = emptyList()
+            ).bufferedWriter(Charsets.UTF_8).use {
+                it.write("Hello!")
+            }
+            invocation.processingEnv.filer.writeResource(
+                filePath = Path("META-INF/services/com.test.Foo"),
+                originatingElements = emptyList()
+            ).bufferedWriter(Charsets.UTF_8).use {
+                it.write("Not a real service...")
+            }
+            invocation.assertCompilationResult {
+                hasNoWarnings()
+            }
+        }
+    }
+
     private fun Dependencies?.containsExactlySimpleKotlinClass() {
         assertThat(this).isNotNull()
         val originatingFiles = this!!.originatingFiles.map { it.fileName }
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 571dda1..dbf84e5 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -281,7 +281,6 @@
 tasks.withType(KotlinCompile).configureEach {
     kotlinOptions {
         freeCompilerArgs += [
-                "-Xjvm-default=all",
                 "-opt-in=kotlin.contracts.ExperimentalContracts",
                 "-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi",
                 "-opt-in=com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview"
diff --git a/room/room-ktx/build.gradle b/room/room-ktx/build.gradle
index d824294..b4ed97d 100644
--- a/room/room-ktx/build.gradle
+++ b/room/room-ktx/build.gradle
@@ -51,10 +51,3 @@
 android {
     namespace "androidx.room.ktx"
 }
-
-// Allow usage of Kotlin's @OptIn.
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
-}
\ No newline at end of file
diff --git a/room/room-migration/build.gradle b/room/room-migration/build.gradle
index 6fa47eaf..4924037 100644
--- a/room/room-migration/build.gradle
+++ b/room/room-migration/build.gradle
@@ -22,12 +22,6 @@
     id("kotlin")
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
-}
-
 dependencies {
     implementation(project(":room:room-common"))
     implementation(libs.kotlinStdlib)
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index 49b6c29..1d8ce5d 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -30,9 +30,6 @@
         consumerProguardFiles "proguard-rules.pro"
     }
     namespace "androidx.room"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
 
 dependencies {
diff --git a/room/room-testing/build.gradle b/room/room-testing/build.gradle
index 39e53dd..7f0951e 100644
--- a/room/room-testing/build.gradle
+++ b/room/room-testing/build.gradle
@@ -24,10 +24,6 @@
 
 android {
     namespace "androidx.room.testing"
-
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
 
 dependencies {
diff --git a/settings.gradle b/settings.gradle
index 31e3e3c..afcd43e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -376,8 +376,8 @@
 includeProject(":appsearch:appsearch-local-storage", [BuildType.MAIN])
 includeProject(":appsearch:appsearch-platform-storage", [BuildType.MAIN])
 includeProject(":appsearch:appsearch-test-util", [BuildType.MAIN])
-includeProject(":arch:core:core-common", [BuildType.MAIN])
-includeProject(":arch:core:core-runtime", [BuildType.MAIN])
+includeProject(":arch:core:core-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":arch:core:core-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
 includeProject(":arch:core:core-testing", [BuildType.MAIN])
 includeProject(":asynclayoutinflater:asynclayoutinflater", [BuildType.MAIN])
 includeProject(":asynclayoutinflater:asynclayoutinflater-appcompat", [BuildType.MAIN])
@@ -901,8 +901,12 @@
 includeProject(":wear:compose:compose-foundation", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-foundation-samples", "wear/compose/compose-foundation/samples", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material3", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material-benchmark", "wear/compose/compose-material/benchmark", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material3-benchmark", "wear/compose/compose-material3/benchmark", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material-core", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material-samples", "wear/compose/compose-material/samples", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material3-samples", "wear/compose/compose-material3/samples", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-navigation", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-navigation-samples", "wear/compose/compose-navigation/samples", [BuildType.COMPOSE])
 includeProject(":wear:compose:integration-tests:demos", [BuildType.COMPOSE])
diff --git a/sqlite/integration-tests/inspection-room-testapp/build.gradle b/sqlite/integration-tests/inspection-room-testapp/build.gradle
index 13b7d0b..a4048b05 100644
--- a/sqlite/integration-tests/inspection-room-testapp/build.gradle
+++ b/sqlite/integration-tests/inspection-room-testapp/build.gradle
@@ -42,7 +42,4 @@
         minSdkVersion 26
     }
     namespace "androidx.sqlite.inspection.roomtestapp"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
diff --git a/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle b/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
index f0e032f..9856bad 100644
--- a/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
+++ b/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
@@ -44,7 +44,4 @@
         minSdkVersion 26
     }
     namespace "androidx.sqlite.inspection.sqldeligttestapp"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
diff --git a/sqlite/sqlite-framework/build.gradle b/sqlite/sqlite-framework/build.gradle
index 173c1d4..657c4f3 100644
--- a/sqlite/sqlite-framework/build.gradle
+++ b/sqlite/sqlite-framework/build.gradle
@@ -46,7 +46,4 @@
 
 android {
     namespace "androidx.sqlite.db.framework"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
diff --git a/sqlite/sqlite-inspection/build.gradle b/sqlite/sqlite-inspection/build.gradle
index a6c166d..ba9d20e 100644
--- a/sqlite/sqlite-inspection/build.gradle
+++ b/sqlite/sqlite-inspection/build.gradle
@@ -57,10 +57,3 @@
     }
     namespace "androidx.sqlite.inspection"
 }
-
-// Allow usage of Kotlin's @OptIn.
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"]
-    }
-}
\ No newline at end of file
diff --git a/sqlite/sqlite-ktx/build.gradle b/sqlite/sqlite-ktx/build.gradle
index 32c89ad..f0012cf 100644
--- a/sqlite/sqlite-ktx/build.gradle
+++ b/sqlite/sqlite-ktx/build.gradle
@@ -40,7 +40,4 @@
 
 android {
     namespace "androidx.sqlite.db.ktx"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
diff --git a/sqlite/sqlite/build.gradle b/sqlite/sqlite/build.gradle
index 47256e3..36053c0 100644
--- a/sqlite/sqlite/build.gradle
+++ b/sqlite/sqlite/build.gradle
@@ -33,9 +33,6 @@
 
 android {
     namespace "androidx.sqlite.db"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
 
 androidx {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
index 7f8f775..ec91a3f 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
@@ -40,8 +40,19 @@
      * @param height pixel height of the display
      * @return null if node is null, else a Rect containing visible bounds
      */
-    @SuppressWarnings("RectIntersectReturnValueIgnored")
     static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
+        return getVisibleBoundsInScreen(node, new Rect(0, 0, width, height));
+    }
+
+    /**
+     * Returns the node's bounds clipped to the size of the display
+     *
+     * @param node
+     * @param displayRect the display rect
+     * @return null if node is null, else a Rect containing visible bounds
+     */
+    @SuppressWarnings("RectIntersectReturnValueIgnored")
+    static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, Rect displayRect) {
         if (node == null) {
             return null;
         }
@@ -49,12 +60,9 @@
         Rect nodeRect = new Rect();
         node.getBoundsInScreen(nodeRect);
 
-        Rect displayRect = new Rect();
-        displayRect.top = 0;
-        displayRect.left = 0;
-        displayRect.right = width;
-        displayRect.bottom = height;
-
+        if (displayRect == null) {
+            displayRect = new Rect();
+        }
         nodeRect.intersect(displayRect);
 
         // On platforms that give us access to the node's window
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
index 8ee007d..04de5ee 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
@@ -180,8 +180,7 @@
     public @NonNull BySelector descContains(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return desc(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring)),
-                Pattern.DOTALL));
+        return desc(RegexHelper.getPatternContains(substring));
     }
 
     /**
@@ -195,8 +194,7 @@
     public @NonNull BySelector descStartsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return desc(
-                Pattern.compile(String.format("^%s.*$", Pattern.quote(substring)), Pattern.DOTALL));
+        return desc(RegexHelper.getPatternStartsWith(substring));
     }
 
     /**
@@ -210,8 +208,7 @@
     public @NonNull BySelector descEndsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return desc(
-                Pattern.compile(String.format("^.*%s$", Pattern.quote(substring)), Pattern.DOTALL));
+        return desc(RegexHelper.getPatternEndsWith(substring));
     }
 
     /**
@@ -338,8 +335,7 @@
     public @NonNull BySelector textContains(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return text(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring)),
-                Pattern.DOTALL));
+        return text(RegexHelper.getPatternContains(substring));
     }
 
     /**
@@ -353,8 +349,7 @@
     public @NonNull BySelector textStartsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return text(
-                Pattern.compile(String.format("^%s.*$", Pattern.quote(substring)), Pattern.DOTALL));
+        return text(RegexHelper.getPatternStartsWith(substring));
     }
 
     /**
@@ -368,8 +363,7 @@
     public @NonNull BySelector textEndsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return text(
-                Pattern.compile(String.format("^.*%s$", Pattern.quote(substring)), Pattern.DOTALL));
+        return text(RegexHelper.getPatternEndsWith(substring));
     }
 
     /** Sets the text value criteria for matching. A UI element will be considered a match if its
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/RegexHelper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/RegexHelper.java
new file mode 100644
index 0000000..75c2980
--- /dev/null
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/RegexHelper.java
@@ -0,0 +1,52 @@
+/*
+ * 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.test.uiautomator;
+
+import androidx.annotation.NonNull;
+
+import java.util.regex.Pattern;
+
+/**
+ * This class contains static helper methods about regex.
+ */
+class RegexHelper {
+
+    private RegexHelper(){}
+
+    /**
+     * Returns a {@link Pattern} that matches when content starts with given string
+     * (case-sensitive).
+     */
+    static Pattern getPatternStartsWith(@NonNull String text) {
+        return Pattern.compile(String.format("^%s.*$", Pattern.quote(text)), Pattern.DOTALL);
+    }
+
+    /**
+     * Returns a {@link Pattern} that matches when content ends with given string
+     * (case-sensitive).
+     */
+    static Pattern getPatternEndsWith(@NonNull String text) {
+        return Pattern.compile(String.format("^.*%s$", Pattern.quote(text)), Pattern.DOTALL);
+    }
+
+    /**
+     * Returns a {@link Pattern} that matches when content contains given string (case-sensitive).
+     */
+    static Pattern getPatternContains(@NonNull String text) {
+        return Pattern.compile(String.format("^.*%s.*$", Pattern.quote(text)), Pattern.DOTALL);
+    }
+}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index cc5a089..86f9e40 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -257,6 +257,8 @@
         if (bounds.contains(point.x, point.y)) {
             return true;
         }
+        Log.d(TAG, String.format("Clipping out-of-bound (%d, %d) into %s.", point.x, point.y,
+                bounds));
         point.x = Math.max(bounds.left, Math.min(point.x, bounds.right));
         point.y = Math.max(bounds.top, Math.min(point.y, bounds.bottom));
         return false;
@@ -265,16 +267,10 @@
     /** Returns the visible bounds of a {@code node}. */
     @SuppressWarnings("RectIntersectReturnValueIgnored")
     private Rect getVisibleBounds(AccessibilityNodeInfo node) {
-        // Get the object bounds in screen coordinates
-        Rect ret = new Rect();
-        node.getBoundsInScreen(ret);
-
-        // Trim any portion of the bounds that are not on the screen
+        Rect screen = new Rect();
         final int displayId = getDisplayId();
         if (displayId == Display.DEFAULT_DISPLAY) {
-            final Rect screen =
-                    new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
-            ret.intersect(screen);
+            screen = new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
         } else {
             final DisplayManager dm =
                     (DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
@@ -283,21 +279,12 @@
             if (display != null) {
                 final Point size = new Point();
                 display.getRealSize(size);
-                final Rect screen = new Rect(0, 0, size.x, size.y);
-                ret.intersect(screen);
+                screen = new Rect(0, 0, size.x, size.y);
+            } else {
+                Log.d(TAG, String.format("Unable to get the display with id %d.", displayId));
             }
         }
-
-        // On platforms that give us access to the node's window
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            // Trim any portion of the bounds that are outside the window
-            Rect bounds = new Rect();
-            AccessibilityWindowInfo window = Api21Impl.getWindow(node);
-            if (window != null) {
-                Api21Impl.getBoundsInScreen(window, bounds);
-                ret.intersect(bounds);
-            }
-        }
+        Rect ret = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, screen);
 
         // Find the visible bounds of our first scrollable ancestor
         for (AccessibilityNodeInfo ancestor = node.getParent(); ancestor != null;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
index 5a6d9b5..fa34a40 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
@@ -351,7 +351,7 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> descContains(@NonNull String substring) {
-        return descMatches(String.format("^.*%s.*$", Pattern.quote(substring)));
+        return descMatches(RegexHelper.getPatternContains(substring));
     }
 
     /**
@@ -360,7 +360,7 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> descStartsWith(@NonNull String substring) {
-        return descMatches(String.format("^%s.*$", Pattern.quote(substring)));
+        return descMatches(RegexHelper.getPatternStartsWith(substring));
     }
 
     /**
@@ -369,7 +369,7 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> descEndsWith(@NonNull String substring) {
-        return descMatches(String.format("^.*%s$", Pattern.quote(substring)));
+        return descMatches(RegexHelper.getPatternEndsWith(substring));
     }
 
     /**
@@ -435,7 +435,7 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> textContains(@NonNull String substring) {
-        return textMatches(String.format("^.*%s.*$", Pattern.quote(substring)));
+        return textMatches(RegexHelper.getPatternContains(substring));
     }
 
     /**
@@ -444,7 +444,7 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> textStartsWith(@NonNull String substring) {
-        return textMatches(String.format("^%s.*$", Pattern.quote(substring)));
+        return textMatches(RegexHelper.getPatternStartsWith(substring));
     }
 
     /**
@@ -453,7 +453,7 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> textEndsWith(@NonNull String substring) {
-        return textMatches(String.format("^.*%s$", Pattern.quote(substring)));
+        return textMatches(RegexHelper.getPatternEndsWith(substring));
     }
 
 
diff --git a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
index 4255c21..201f12e 100644
--- a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
+++ b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
@@ -19,6 +19,7 @@
 import androidx.testutils.paparazzi.ImageDiffer.DiffResult.Similar
 import java.awt.image.BufferedImage
 
+@JvmDefaultWithCompatibility
 /**
  *  Functional interface to compare two images and returns a [ImageDiffer.DiffResult] ADT containing
  *  comparison statistics and a difference image, if applicable.
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index 201bb55..6c1c56a 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -38,7 +38,7 @@
   public final class LazyGridItemPlacementAnimatorKt {
   }
 
-  public final class LazyGridItemsProviderImplKt {
+  public final class LazyGridItemProviderKt {
   }
 
   public final class LazyGridKt {
@@ -47,7 +47,7 @@
   public final class LazyGridMeasureKt {
   }
 
-  public final class LazyGridScrollingKt {
+  public final class LazyGridScrollPositionKt {
   }
 
   public final class LazyGridSpanKt {
@@ -169,6 +169,16 @@
 
 }
 
+package androidx.tv.foundation.lazy.layout {
+
+  public final class LazyAnimateScrollKt {
+  }
+
+  public final class LazyLayoutSemanticsKt {
+  }
+
+}
+
 package androidx.tv.foundation.lazy.list {
 
   public final class LazyDslKt {
@@ -186,7 +196,7 @@
   public final class LazyListItemPlacementAnimatorKt {
   }
 
-  public final class LazyListItemsProviderImplKt {
+  public final class LazyListItemProviderKt {
   }
 
   public final class LazyListKt {
@@ -195,7 +205,7 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListScrollingKt {
+  public final class LazyListScrollPositionKt {
   }
 
   public final class LazyListStateKt {
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index 70eb94426..033c89a 100644
--- a/tv/tv-foundation/api/public_plus_experimental_current.txt
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -42,7 +42,7 @@
   public final class LazyGridItemPlacementAnimatorKt {
   }
 
-  public final class LazyGridItemsProviderImplKt {
+  public final class LazyGridItemProviderKt {
   }
 
   public final class LazyGridKt {
@@ -51,7 +51,7 @@
   public final class LazyGridMeasureKt {
   }
 
-  public final class LazyGridScrollingKt {
+  public final class LazyGridScrollPositionKt {
   }
 
   public final class LazyGridSpanKt {
@@ -175,6 +175,16 @@
 
 }
 
+package androidx.tv.foundation.lazy.layout {
+
+  public final class LazyAnimateScrollKt {
+  }
+
+  public final class LazyLayoutSemanticsKt {
+  }
+
+}
+
 package androidx.tv.foundation.lazy.list {
 
   public final class LazyDslKt {
@@ -192,7 +202,7 @@
   public final class LazyListItemPlacementAnimatorKt {
   }
 
-  public final class LazyListItemsProviderImplKt {
+  public final class LazyListItemProviderKt {
   }
 
   public final class LazyListKt {
@@ -201,7 +211,7 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListScrollingKt {
+  public final class LazyListScrollPositionKt {
   }
 
   public final class LazyListStateKt {
@@ -253,6 +263,7 @@
   @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
     method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
     method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    method @androidx.tv.foundation.ExperimentalTvFoundationApi public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
   }
 
   @kotlin.DslMarker public @interface TvLazyListScopeMarker {
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index 201bb55..6c1c56a 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -38,7 +38,7 @@
   public final class LazyGridItemPlacementAnimatorKt {
   }
 
-  public final class LazyGridItemsProviderImplKt {
+  public final class LazyGridItemProviderKt {
   }
 
   public final class LazyGridKt {
@@ -47,7 +47,7 @@
   public final class LazyGridMeasureKt {
   }
 
-  public final class LazyGridScrollingKt {
+  public final class LazyGridScrollPositionKt {
   }
 
   public final class LazyGridSpanKt {
@@ -169,6 +169,16 @@
 
 }
 
+package androidx.tv.foundation.lazy.layout {
+
+  public final class LazyAnimateScrollKt {
+  }
+
+  public final class LazyLayoutSemanticsKt {
+  }
+
+}
+
 package androidx.tv.foundation.lazy.list {
 
   public final class LazyDslKt {
@@ -186,7 +196,7 @@
   public final class LazyListItemPlacementAnimatorKt {
   }
 
-  public final class LazyListItemsProviderImplKt {
+  public final class LazyListItemProviderKt {
   }
 
   public final class LazyListKt {
@@ -195,7 +205,7 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListScrollingKt {
+  public final class LazyListScrollPositionKt {
   }
 
   public final class LazyListStateKt {
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index 29362d9..df16827 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -75,14 +75,6 @@
     targetsJavaConsumers = false
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
-
 // Functions and tasks to monitor changes in copied files.
 
 task generateMd5 {
@@ -112,15 +104,15 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/ScrollableWithPivot.kt",
-                "afaf0f2be6b57df076db42d9218f83d9"
+                "src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt",
+                "86acc593cd77d52784532163b5ab8156"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/DataIndex.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt",
                 "2aa3c6d2dd05057478e723b2247517e1"
         )
 )
@@ -128,15 +120,15 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyItemScopeImpl.kt",
-                "31e6796d0d03cb84483396a39fc5b7e7"
+                "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt",
+                "37cb0caf8a170a4161da346806c7a236"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListHeaders.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt",
                 "4d71c69f9cb38f741da9cfc4109567dd"
         )
 )
@@ -144,47 +136,39 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt",
                 "a74bfa05e68e2b6c2e108f022dfbfa26"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
-                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemProviderImpl.kt",
-                "57ff505cbdfa854e15b4fbd9d4a574eb"
-        )
-)
-
-copiedClasses.add(
-        new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemsProvider.kt",
-                "42a2c446c81fba89fd7b8480d063b308"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt",
+                "4c69e8a60a068e1e8191ed3840868881"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyList.kt",
-                "c605794683c01c516674436c9ebc1f44"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt",
+                "22078ee2f09dce3f39cdc23dc1188a82"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasure.kt",
-                "95c14abd0367f0f39218c9bdd175b242"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt",
+                "c58eaf4619972afbee7da7714dc072fc"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt",
                 "d4407572c6550d184133f8b3fd37869f"
         )
 )
@@ -192,63 +176,55 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScopeImpl.kt",
-                "1888e8b115c73b5ea7f33d48d9887845"
-        )
-)
-
-copiedClasses.add(
-        new CopiedClass(
-                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrolling.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrolling.kt",
-                "a32b856a1e8740a6a521df04c9d51ed1"
+                "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt",
+                "fab951ddba90c5c5426e4d0104bc9929"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt",
-                "82df7d370ba5b20309e5191a0af431a0"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt",
+                "08d08515f25eb3032f6efbf9f86be102"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListState.kt",
-                "45821a5bf14d3e6e25fee63e61930f57"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt",
+                "1d16fbb5025b282ffeb8fe3a63a9de3d"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt",
-                "78b09b4d78ec9d761274b9ca8d24f4f7"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt",
+                "c1b403d4fcd43c423b3f1b0433e8bb43"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt",
-                "bec4211cb3d91bb936e9f0872864244b"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt",
+                "0dcde73635efe26203f70351437cb6fa"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazySemantics.kt",
-                "739205f656bf107604ba7167e3cee7e7"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt",
+                "3a1e86a55ea2282c12745717b5a60cfd"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/ItemIndex.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/ItemIndex.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt",
                 "1031b8b91a81c684b3c4584bc93d3fb0"
         )
 )
@@ -256,7 +232,7 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt",
                 "6a0b2db56ef38fb1ac004e4fc9847db8"
         )
 )
@@ -264,7 +240,7 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemInfo.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemInfo.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt",
                 "1f3b13ee45de79bc67ace4133e634600"
         )
 )
@@ -272,15 +248,15 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
-                "0bbc162aab675ca2a34350e3044433e7"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
+                "6f93637153ebd05d9cba7ebaf12311c9"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt",
                 "b3ff4600791c73028b8661c0e2b49110"
         )
 )
@@ -288,39 +264,31 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScope.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt",
                 "1a40313cc5e67b5808586c012bbfb058"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
-                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt",
-                "48fdfb1dfa5d39c88d4aa96732192421"
-        )
-)
-
-copiedClasses.add(
-        new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt",
-                "e5e95e6cad43cec2b0c30bf201e3cae9"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt",
+                "ba8ee64efc5bcd18f28fe9bb9d987166"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGrid.kt",
-                "69dbab5e83deab809219d4d7a9ee7fa8"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt",
+                "3f91a6975c10c6a49ddc21f7828d7298"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridLayoutInfo.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt",
                 "b421c5e74856a78982efe0d8a79d10cb"
         )
 )
@@ -328,15 +296,15 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt",
-                "2b38f5261ad092d9048cfc4f0a841a1a"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt",
+                "c600148ddfab1dde9f3ebe8349e77001"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasureResult.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt",
                 "1277598d36d8507d7bf0305cc629a11c"
         )
 )
@@ -344,39 +312,31 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeImpl.kt",
-                "3296c6edcbd56450ba919df105cb36c0"
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt",
+                "e92ebc01a8b205d304e0b0d3c40636f8"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeMarker.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeMarker.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt",
                 "0b7ff258a80e2413f89d56ab0ef41b46"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
-                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt",
-                "15f2f9bb89c1603aa4b7e7d1f8a2de5a"
-        )
-)
-
-copiedClasses.add(
-        new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt",
-                "9b3d47322ad526fb17a3d9505a80f673"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt",
+                "70bac76aeb2617b8f5c706f1867800fd"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt",
                 "cc63cb4f05cc556e8fcf7504ac0ea57c"
         )
 )
@@ -384,71 +344,71 @@
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
-                "894b9f69a27e247bbe609bdac22bb5ed"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
+                "062f95aa00d36fb1e048aa1ddb8154bc"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridState.kt",
-                "1e37d8a6f159aabe11f488121de59b70"
+                "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt",
+                "c6b402b685824ff216650da77063a131"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt",
-                "09d9b21d33325a94cac738aad58e2422"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt",
+                "b9e6230825d8688bf1164abef07b4e14"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
-                "3acdfddfd06eb17aac5dbdd326482e35"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
+                "ab9a58f65e85b4fe4d621e9ed5b2db68"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt",
-                "1104f01e8b1f6eced2401b207114f4a4"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt",
+                "3b99751e25cebc9945df800ce1aa04f8"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
-                "b7b731e6e8fdc520064aaef989575bda"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
+                "e2bdba6cdbc870ea9607658ec60eb1eb"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazySemantics.kt",
-                "dab277484b4ec57a5275095b505f79d4"
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt",
+                "bb397307f2cc3fd87bcc7585bf403039"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyDsl.kt",
-                "8462c0a61f14639f39dd6f76c6a2aebc"
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt",
+                "9d86fad30c0f3de2231fbef3f63db53e"
         )
 )
 
 copiedClasses.add(
         new CopiedClass(
                 "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinningModifier.kt",
-                "src/commonMain/kotlin/androidx/tv/foundation/lazy/LazyListPinningModifier.kt",
+                "src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt",
                 "e37450505d13ab0fd1833f136ec8aa3c"
         )
 )
@@ -485,36 +445,71 @@
         )
 )
 
-task doCopiesNeedUpdate {
-    ext.genMd5 = { fileNameToHash ->
-        try {
-            MessageDigest digest = MessageDigest.getInstance("MD5")
-            file(fileNameToHash).withInputStream() { is ->
-                byte[] buffer = new byte[8192]
-                int read
-                while ((read = is.read(buffer)) > 0) {
-                    digest.update(buffer, 0, read);
-                }
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt",
+                "src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt",
+                "8b9e4a03c5097b4ef7377f98da95bbcd"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt",
+                "src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt",
+                "72859815545394de5b9f7269f1366d21"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt",
+                "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt",
+                "315f220a2674a50f82633a725dc39c1b"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt",
+                "d0d48557af324db3af7f4c46a6810026"
+        )
+)
+
+String generateMd5(String fileNameToHash) {
+    try {
+        MessageDigest digest = MessageDigest.getInstance("MD5")
+        file(fileNameToHash).withInputStream() { is ->
+            byte[] buffer = new byte[8192]
+            int read
+            while ((read = is.read(buffer)) > 0) {
+                digest.update(buffer, 0, read);
             }
-            byte[] md5sum = digest.digest()
-            BigInteger bigInt = new BigInteger(1, md5sum)
-            bigInt.toString(16).padLeft(32, '0')
-        } catch (Exception e) {
-            throw new GradleException("Failed for file=$fileNameToHash", e)
         }
+        byte[] md5sum = digest.digest()
+        BigInteger bigInt = new BigInteger(1, md5sum)
+        bigInt.toString(16).padLeft(32, '0')
+    } catch (Exception e) {
+        throw new GradleException("Failed for file=$fileNameToHash", e)
     }
+}
 
-
+task doCopiesNeedUpdate {
     doLast {
         List<String> failureFiles = new ArrayList<>()
+        boolean failures = false
         copiedClasses.forEach(copiedClass -> {
+            assert file(copiedClass.originalFilePath).exists()
+            assert file(copiedClass.copyFilePath).exists()
             try {
-                String actualMd5 = genMd5(copiedClass.originalFilePath)
+                String actualMd5 = generateMd5(copiedClass.originalFilePath)
                 if (copiedClass.lastKnownGoodHash != actualMd5) {
-                    failureFiles.add(copiedClass.toString()+ ", actual=" + actualMd5)
+                    failureFiles.add(copiedClass.toString()+ "\nactual= " + actualMd5 + "\n")
                 }
             } catch (Exception e) {
-                throw new GradleException("Failed for file=${copiedClass.originalFilePath}", e)
+                logger.error("Failed for file=${copiedClass.originalFilePath}", e)
+                failures = true
             }
         })
 
@@ -523,7 +518,11 @@
                     "Files that were copied have been updated at the source. " +
                             "Please update the copy and then" +
                             " update the hash in the compose-foundation build.gradle file." +
-                            failureFiles.stream().collect(Collectors.joining("\n", "\n", "")))
+                            failureFiles.stream().collect(Collectors.joining("\n", "\n", "")) + "\ncount=${failureFiles.size()}")
+        }
+
+        if (failures) {
+            throw new GradleException("There were errors. Check the logs.")
         }
     }
 }
@@ -541,8 +540,9 @@
 
     @Override
     String toString() {
-        return "originalFilePath='" + originalFilePath + '\'' +
-                ", copyFilePath='" + copyFilePath + '\'' +
-                ", lastKnownGoodHash='" + lastKnownGoodHash + '\''
+        return "originalFilePath='" + originalFilePath + '\'\n' +
+                "copyFilePath='" + copyFilePath + '\'\n' +
+                "lastKnownGoodHash='" + lastKnownGoodHash + '\'\n' +
+                "diffCmd='" + "kdiff3 " + originalFilePath + " " + copyFilePath + "\'"
     }
 }
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
index 3f108d0..a7bed08 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
@@ -49,6 +49,7 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.IterableSubject
 import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -147,6 +148,7 @@
         )
     }
 
+    @Ignore // b/242180919
     @Test
     fun verifyScrollActionsAtEnd() {
         createScrollableContent_StartAtEnd()
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
index b14edf9..7c3174f 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
@@ -38,6 +38,7 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -250,6 +251,7 @@
         assertThat(state.canScrollBackward).isFalse()
     }
 
+    @Ignore("b/259608530")
     @Test
     fun canScrollBackward() = runBlocking {
         withContext(Dispatchers.Main + AutoTestFrameClock()) {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt
new file mode 100644
index 0000000..fa613db
--- /dev/null
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt
@@ -0,0 +1,538 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.ExperimentalTvFoundationApi
+import androidx.tv.foundation.PivotOffsets
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTvFoundationApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyListHeadersTest {
+
+    private val TvLazyListTag = "TvLazyList"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun tvLazyColumnShowsHeader_withoutBeyondBoundsItemCount() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+
+        rule.setContent {
+            TvLazyColumn(Modifier.height(300.dp), beyondBoundsItemCount = 0) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth()
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it))
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth()
+                            .testTag(secondHeaderTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun tvLazyColumnPlaceSecondHeader_ifBeyondBoundsItemCountIsUsed() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+
+        rule.setContent {
+            TvLazyColumn(Modifier.height(300.dp), beyondBoundsItemCount = 1) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth()
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it))
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth()
+                            .testTag(secondHeaderTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertExists()
+    }
+
+    @Test
+    fun tvLazyColumnShowsHeadersOnScroll() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+        lateinit var state: TvLazyListState
+
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                Modifier.height(300.dp).testTag(TvLazyListTag).border(2.dp, Color.Black),
+                rememberTvLazyListState().also { state = it }
+            ) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth().border(2.dp, Color.Green)
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.height(101.dp).fillParentMaxWidth().border(2.dp, Color.Blue)
+                        .testTag(it))
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth().border(2.dp, Color.Yellow)
+                            .testTag(secondHeaderTag)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsDisplayed()
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.runOnIdle {
+            assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+            assertEquals(0, state.layoutInfo.visibleItemsInfo.first().offset)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun tvLazyColumnHeaderIsReplaced() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+        lateinit var state: TvLazyListState
+
+        rule.setContentWithTestViewConfiguration {
+            state = rememberTvLazyListState()
+            TvLazyColumn(
+                modifier = Modifier.height(300.dp).testTag(TvLazyListTag),
+                state = state
+            ) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth()
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth()
+                            .testTag(secondHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it))
+                }
+            }
+        }
+
+        rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun tvLazyRowShowsHeader_withoutOffscreenItens() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+
+        rule.setContent {
+            TvLazyRow(Modifier.width(300.dp), beyondBoundsItemCount = 0) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(secondHeaderTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun tvLazyRowPlaceSecondHeader_ifBeyondBoundsItemCountIsUsed() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+
+        rule.setContent {
+            TvLazyRow(Modifier.width(300.dp), beyondBoundsItemCount = 1) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(secondHeaderTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertExists()
+    }
+
+    @Test
+    fun tvLazyRowShowsHeadersOnScroll() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+        lateinit var state: TvLazyListState
+
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                Modifier.width(300.dp).testTag(TvLazyListTag),
+                rememberTvLazyListState().also { state = it }
+            ) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(secondHeaderTag)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+        rule.onNodeWithTag(TvLazyListTag)
+            .scrollBy(x = 102.dp, density = rule.density)
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsDisplayed()
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.runOnIdle {
+            assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+            assertEquals(0, state.layoutInfo.visibleItemsInfo.first().offset)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun tvLazyRowHeaderIsReplaced() {
+        val items = (1..2).map { it.toString() }
+        val firstHeaderTag = "firstHeaderTag"
+        val secondHeaderTag = "secondHeaderTag"
+        lateinit var state: TvLazyListState
+
+        rule.setContentWithTestViewConfiguration {
+            state = rememberTvLazyListState()
+            TvLazyRow(modifier = Modifier.width(300.dp).testTag(TvLazyListTag), state = state) {
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(firstHeaderTag)
+                    )
+                }
+
+                stickyHeader {
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag(secondHeaderTag)
+                    )
+                }
+
+                items(items) {
+                    Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+                }
+            }
+        }
+
+        rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+        rule.onNodeWithTag(firstHeaderTag)
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag(secondHeaderTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun headerIsDisplayedWhenItIsFullyInContentPadding() {
+        val headerTag = "header"
+        val itemIndexPx = 100
+        val itemIndexDp = with(rule.density) { itemIndexPx.toDp() }
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            TvLazyColumn(
+                Modifier.requiredSize(itemIndexDp * 4),
+                state = rememberTvLazyListState().also { state = it },
+                contentPadding = PaddingValues(top = itemIndexDp * 2)
+            ) {
+                stickyHeader {
+                    Spacer(Modifier.requiredSize(itemIndexDp).testTag(headerTag))
+                }
+
+                items((0..4).toList()) {
+                    Spacer(Modifier.requiredSize(itemIndexDp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking { state.scrollToItem(1, itemIndexPx / 2) }
+        }
+
+        rule.onNodeWithTag(headerTag)
+            .assertTopPositionInRootIsEqualTo(itemIndexDp / 2)
+
+        rule.runOnIdle {
+            assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+            assertEquals(
+                itemIndexPx / 2 - /* content padding size */ itemIndexPx * 2,
+                state.layoutInfo.visibleItemsInfo.first().offset
+            )
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemIndexDp * 3 / 2)
+    }
+}
+
+@Composable
+private fun TvLazyColumn(
+    modifier: Modifier = Modifier,
+    state: TvLazyListState = rememberTvLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+    userScrollEnabled: Boolean = true,
+    beyondBoundsItemCount: Int,
+    content: TvLazyListScope.() -> Unit
+) {
+    LazyList(
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        horizontalAlignment = horizontalAlignment,
+        verticalArrangement = verticalArrangement,
+        isVertical = true,
+        reverseLayout = reverseLayout,
+        userScrollEnabled = userScrollEnabled,
+        beyondBoundsItemCount = beyondBoundsItemCount,
+        content = content,
+        pivotOffsets = PivotOffsets()
+    )
+}
+
+@Composable
+private fun TvLazyRow(
+    modifier: Modifier = Modifier,
+    state: TvLazyListState = rememberTvLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal =
+        if (!reverseLayout) Arrangement.Start else Arrangement.End,
+    verticalAlignment: Alignment.Vertical = Alignment.Top,
+    userScrollEnabled: Boolean = true,
+    beyondBoundsItemCount: Int,
+    content: TvLazyListScope.() -> Unit
+) {
+    LazyList(
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        verticalAlignment = verticalAlignment,
+        horizontalArrangement = horizontalArrangement,
+        isVertical = false,
+        reverseLayout = reverseLayout,
+        userScrollEnabled = userScrollEnabled,
+        beyondBoundsItemCount = beyondBoundsItemCount,
+        content = content,
+        pivotOffsets = PivotOffsets()
+    )
+}
+
+internal fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
+    performTouchInput {
+        with(density) {
+            val touchSlop = TestTouchSlop.toInt()
+            val xPx = x.roundToPx()
+            val yPx = y.roundToPx()
+            val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
+            val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
+            swipeWithVelocity(
+                start = center,
+                end = Offset(center.x - offsetX, center.y - offsetY),
+                endVelocity = 0f
+            )
+        }
+    }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
index edc6150..9f0c871 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
@@ -40,14 +40,20 @@
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.OnPlacedModifier
 import androidx.compose.ui.layout.OnRemeasuredModifier
 import androidx.compose.ui.modifier.ModifierLocalProvider
 import androidx.compose.ui.modifier.modifierLocalOf
 import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.util.fastForEach
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
@@ -111,6 +117,20 @@
     }
 )
 
+internal interface ScrollConfig {
+    fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset
+}
+
+@Composable
+internal fun platformScrollConfig(): ScrollConfig = AndroidConfig
+
+private object AndroidConfig : ScrollConfig {
+    override fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset {
+        // 64 dp value is taken from ViewConfiguration.java, replace with better solution
+        return event.changes.fastFold(Offset.Zero) { acc, c -> acc + c.scrollDelta } * -64.dp.toPx()
+    }
+}
+
 @Suppress("ComposableModifierFactory")
 @Composable
 private fun Modifier.pointerScrollable(
@@ -325,3 +345,26 @@
     override val key = ModifierLocalScrollableContainer
     override val value = true
 }
+
+/**
+ * Accumulates value starting with [initial] value and applying [operation] from left to right
+ * to current accumulator value and each element.
+ *
+ * Returns the specified [initial] value if the collection is empty.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ *
+ * @param [operation] function that takes current accumulator value and an element, and calculates the next accumulator value.
+ */
+@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T, R> List<T>.fastFold(initial: R, operation: (acc: R, T) -> R): R {
+    contract { callsInPlace(operation) }
+    var accumulator = initial
+    fastForEach { e ->
+        accumulator = operation(accumulator, e)
+    }
+    return accumulator
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
index 82f9608..cecf3d1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.checkScrollableContainerConstraints
 import androidx.compose.foundation.clipScrollableContainer
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.calculateEndPadding
@@ -36,7 +37,6 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
@@ -44,6 +44,7 @@
 import androidx.compose.ui.util.fastForEach
 import androidx.tv.foundation.ExperimentalTvFoundationApi
 import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.layout.lazyLayoutSemantics
 import androidx.tv.foundation.scrollableWithPivot
 
 @Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@@ -74,7 +75,9 @@
     /** The content of the grid */
     content: TvLazyGridScope.() -> Unit
 ) {
-    val itemProvider = rememberItemProvider(state, content)
+    val itemProvider = rememberLazyGridItemProvider(state, content)
+
+    val semanticState = rememberLazyGridSemanticState(state, itemProvider, reverseLayout)
 
     val scope = rememberCoroutineScope()
     val placementAnimator = remember(state, isVertical) {
@@ -103,28 +106,20 @@
         modifier = modifier
             .then(state.remeasurementModifier)
             .then(state.awaitLayoutModifier)
-            .lazyGridSemantics(
+            .lazyLayoutSemantics(
                 itemProvider = itemProvider,
-                state = state,
-                coroutineScope = scope,
-                isVertical = isVertical,
-                reverseScrolling = reverseLayout,
+                state = semanticState,
+                orientation = orientation,
                 userScrollEnabled = userScrollEnabled
             )
             .clipScrollableContainer(orientation)
             .scrollableWithPivot(
                 orientation = orientation,
-                reverseDirection = run {
-                    // A finger moves with the content, not with the viewport. Therefore,
-                    // always reverse once to have "natural" gesture that goes reversed to layout
-                    var reverseDirection = !reverseLayout
-                    // But if rtl and horizontal, things move the other way around
-                    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-                    if (isRtl && !isVertical) {
-                        reverseDirection = !reverseDirection
-                    }
-                    reverseDirection
-                },
+                reverseDirection = ScrollableDefaults.reverseDirection(
+                    LocalLayoutDirection.current,
+                    orientation,
+                    reverseLayout
+                ),
                 state = state,
                 enabled = userScrollEnabled,
                 pivotOffsets = pivotOffsets
@@ -332,7 +327,6 @@
             measuredLineProvider = measuredLineProvider,
             measuredItemProvider = measuredItemProvider,
             mainAxisAvailableSize = mainAxisAvailableSize,
-            slotsPerLine = resolvedSlotSizesSums.size,
             beforeContentPadding = beforeContentPadding,
             afterContentPadding = afterContentPadding,
             spaceBetweenLines = spaceBetweenLines,
@@ -346,6 +340,7 @@
             reverseLayout = reverseLayout,
             density = this,
             placementAnimator = placementAnimator,
+            spanLayoutProvider = itemProvider.spanLayoutProvider,
             layout = { width, height, placement ->
                 layout(
                     containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
new file mode 100644
index 0000000..6584f8f
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
+import kotlin.math.max
+
+internal class LazyGridAnimateScrollScope(
+    private val state: TvLazyGridState
+) : LazyAnimateScrollScope {
+    override val density: Density get() = state.density
+
+    override val firstVisibleItemIndex: Int get() = state.firstVisibleItemIndex
+
+    override val firstVisibleItemScrollOffset: Int get() = state.firstVisibleItemScrollOffset
+
+    override val lastVisibleItemIndex: Int
+        get() = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
+
+    override val itemCount: Int get() = state.layoutInfo.totalItemsCount
+
+    override fun getTargetItemOffset(index: Int): Int? =
+        state.layoutInfo.visibleItemsInfo
+            .fastFirstOrNull {
+                it.index == index
+            }?.let { item ->
+                if (state.isVertical) {
+                    item.offset.y
+                } else {
+                    item.offset.x
+                }
+            }
+
+    override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
+        state.snapToItemIndexInternal(index, scrollOffset)
+    }
+
+    override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
+        val visibleItems = state.layoutInfo.visibleItemsInfo
+        val slotsPerLine = state.slotsPerLine
+        val averageLineMainAxisSize = calculateLineAverageMainAxisSize(
+            visibleItems,
+            state.isVertical
+        )
+        val before = index < firstVisibleItemIndex
+        val linesDiff =
+            (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
+                slotsPerLine
+
+        return (averageLineMainAxisSize * linesDiff).toFloat() +
+            targetScrollOffset - firstVisibleItemScrollOffset
+    }
+
+    override val numOfItemsForTeleport: Int get() = 100 * state.slotsPerLine
+
+    private fun calculateLineAverageMainAxisSize(
+        visibleItems: List<TvLazyGridItemInfo>,
+        isVertical: Boolean
+    ): Int {
+        val lineOf: (Int) -> Int = {
+            if (isVertical) visibleItems[it].row else visibleItems[it].column
+        }
+
+        var totalLinesMainAxisSize = 0
+        var linesCount = 0
+
+        var lineStartIndex = 0
+        while (lineStartIndex < visibleItems.size) {
+            val currentLine = lineOf(lineStartIndex)
+            if (currentLine == -1) {
+                // Filter out exiting items.
+                ++lineStartIndex
+                continue
+            }
+
+            var lineMainAxisSize = 0
+            var lineEndIndex = lineStartIndex
+            while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
+                lineMainAxisSize = max(
+                    lineMainAxisSize,
+                    if (isVertical) {
+                        visibleItems[lineEndIndex].size.height
+                    } else {
+                        visibleItems[lineEndIndex].size.width
+                    }
+                )
+                ++lineEndIndex
+            }
+
+            totalLinesMainAxisSize += lineMainAxisSize
+            ++linesCount
+
+            lineStartIndex = lineEndIndex
+        }
+
+        return totalLinesMainAxisSize / linesCount
+    }
+
+    override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
+        state.scroll(block = block)
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 6db01c4..b050f5e 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
-import kotlin.math.absoluteValue
 import kotlin.math.max
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
@@ -50,8 +49,6 @@
     private val scope: CoroutineScope,
     private val isVertical: Boolean
 ) {
-    private var slotsPerLine = 0
-
     // state containing an animation and all relevant info for each item.
     private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
 
@@ -76,10 +73,10 @@
         consumedScroll: Int,
         layoutWidth: Int,
         layoutHeight: Int,
-        slotsPerLine: Int,
         reverseLayout: Boolean,
         positionedItems: MutableList<TvLazyGridPositionedItem>,
         measuredItemProvider: LazyMeasuredItemProvider,
+        spanLayoutProvider: LazyGridSpanLayoutProvider
     ) {
         if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
             // no animations specified - no work needed
@@ -87,8 +84,6 @@
             return
         }
 
-        this.slotsPerLine = slotsPerLine
-
         val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
 
         // the consumed scroll is considered as a delta we don't need to animate
@@ -172,7 +167,9 @@
                             scrolledBy = notAnimatableDelta,
                             fallback = fallback,
                             reverseLayout = reverseLayout,
-                            mainAxisLayoutSize = mainAxisLayoutSize
+                            mainAxisLayoutSize = mainAxisLayoutSize,
+                            visibleItems = positionedItems,
+                            spanLayoutProvider = spanLayoutProvider
                         )
                     }
                     val targetPlaceableOffset = if (isVertical) {
@@ -274,7 +271,9 @@
                         scrolledBy = notAnimatableDelta,
                         fallback = mainAxisLayoutSize,
                         reverseLayout = reverseLayout,
-                        mainAxisLayoutSize = mainAxisLayoutSize
+                        mainAxisLayoutSize = mainAxisLayoutSize,
+                        visibleItems = positionedItems,
+                        spanLayoutProvider = spanLayoutProvider
                     )
                     val targetOffset = if (reverseLayout) {
                         mainAxisLayoutSize - absoluteTargetOffset - measuredItem.mainAxisSize
@@ -355,27 +354,48 @@
         scrolledBy: IntOffset,
         reverseLayout: Boolean,
         mainAxisLayoutSize: Int,
-        fallback: Int
+        fallback: Int,
+        visibleItems: List<TvLazyGridPositionedItem>,
+        spanLayoutProvider: LazyGridSpanLayoutProvider
     ): Int {
-        require(slotsPerLine != 0)
-        val beforeViewportStart =
-            if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
         val afterViewportEnd =
+            if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
+        val beforeViewportStart =
             if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
         return when {
-            beforeViewportStart -> {
-                val diff = ((index - viewportEndItemIndex).absoluteValue + slotsPerLine - 1) /
-                    slotsPerLine
-                mainAxisLayoutSize + viewportEndItemNotVisiblePartSize +
-                    averageLineMainAxisSize * (diff - 1) +
-                    scrolledBy.mainAxis
-            }
             afterViewportEnd -> {
-                val diff = ((viewportStartItemIndex - index).absoluteValue + slotsPerLine - 1) /
-                    slotsPerLine
-                viewportStartItemNotVisiblePartSize - mainAxisSizeWithSpacings -
-                    averageLineMainAxisSize * (diff - 1) +
-                    scrolledBy.mainAxis
+                val fromIndex = spanLayoutProvider.firstIndexInNextLineAfter(
+                    if (!reverseLayout) viewportEndItemIndex else index
+                )
+                val toIndex = spanLayoutProvider.lastIndexInPreviousLineBefore(
+                    if (!reverseLayout) index else viewportEndItemIndex
+                )
+                mainAxisLayoutSize + viewportEndItemNotVisiblePartSize + scrolledBy.mainAxis +
+                    // add sizes of the lines between the last visible one and this one.
+                    spanLayoutProvider.getLinesMainAxisSizesSum(
+                        fromIndex = fromIndex,
+                        toIndex = toIndex,
+                        averageLineMainAxisSize = averageLineMainAxisSize,
+                        visibleItems = visibleItems
+                    )
+            }
+            beforeViewportStart -> {
+                val fromIndex = spanLayoutProvider.firstIndexInNextLineAfter(
+                    if (!reverseLayout) index else viewportStartItemIndex
+                )
+                val toIndex = spanLayoutProvider.lastIndexInPreviousLineBefore(
+                    if (!reverseLayout) viewportStartItemIndex else index
+                )
+                viewportStartItemNotVisiblePartSize + scrolledBy.mainAxis +
+                    // minus the size of this item as we are looking for the start offset of it.
+                    -mainAxisSizeWithSpacings +
+                    // minus sizes of the lines between the first visible one and this one.
+                    -spanLayoutProvider.getLinesMainAxisSizesSum(
+                        fromIndex = fromIndex,
+                        toIndex = toIndex,
+                        averageLineMainAxisSize = averageLineMainAxisSize,
+                        visibleItems = visibleItems
+                    )
             }
             else -> {
                 fallback
@@ -461,3 +481,81 @@
     stiffness = Spring.StiffnessMediumLow,
     visibilityThreshold = IntOffset.VisibilityThreshold
 )
+
+private fun LazyGridSpanLayoutProvider.getLinesMainAxisSizesSum(
+    fromIndex: Int,
+    toIndex: Int,
+    averageLineMainAxisSize: Int,
+    visibleItems: List<TvLazyGridPositionedItem>
+): Int {
+    var index = fromIndex
+    var sizes = 0
+    while (index <= toIndex) {
+        val lastItemInTheLine = firstIndexInNextLineAfter(index) - 1
+        if (lastItemInTheLine <= toIndex) {
+            sizes += visibleItems.getLineSize(lastItemInTheLine, averageLineMainAxisSize)
+        }
+        index = lastItemInTheLine + 1
+    }
+    return sizes
+}
+
+private fun List<TvLazyGridPositionedItem>.getLineSize(itemIndex: Int, fallback: Int): Int {
+    if (isEmpty() || itemIndex < first().index || itemIndex > last().index) return fallback
+    if ((itemIndex - first().index) < (last().index - itemIndex)) {
+        for (index in indices) {
+            val item = get(index)
+            if (item.index == itemIndex) return item.lineMainAxisSizeWithSpacings
+            if (item.index > itemIndex) break
+        }
+    } else {
+        for (index in lastIndex downTo 0) {
+            val item = get(index)
+            if (item.index == itemIndex) return item.lineMainAxisSizeWithSpacings
+            if (item.index < itemIndex) break
+        }
+    }
+    return fallback
+}
+
+private fun LazyGridSpanLayoutProvider.lastIndexInPreviousLineBefore(index: Int) =
+    firstIndexInLineContaining(index) - 1
+
+private fun LazyGridSpanLayoutProvider.firstIndexInNextLineAfter(index: Int) =
+    if (index >= totalSize) {
+        // after totalSize we just approximate with 1 slot per item
+        firstIndexInLineContaining(index) + slotsPerLine
+    } else {
+        val lineIndex = getLineIndexOfItem(index)
+        val lineConfiguration = getLineConfiguration(lineIndex.value)
+        lineConfiguration.firstItemIndex + lineConfiguration.spans.size
+    }
+
+private fun LazyGridSpanLayoutProvider.firstIndexInLineContaining(index: Int): Int {
+    return if (index >= totalSize) {
+        val firstIndexForLastKnowLine = getFirstIndexInNextLineAfterTheLastKnownOne()
+        // after totalSize we just approximate with 1 slot per item
+        val linesBetween = (index - firstIndexForLastKnowLine) / slotsPerLine
+        firstIndexForLastKnowLine + slotsPerLine * linesBetween
+    } else {
+        val lineIndex = getLineIndexOfItem(index)
+        val lineConfiguration = getLineConfiguration(lineIndex.value)
+        lineConfiguration.firstItemIndex
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyGridSpanLayoutProvider.getFirstIndexInNextLineAfterTheLastKnownOne(): Int {
+    // first we find the line for the `totalSize - 1` item
+    val lineConfiguration = getLineConfiguration(getLineIndexOfItem(totalSize - 1).value)
+    var currentSpan = 0
+    var currentIndex = lineConfiguration.firstItemIndex - 1
+    // then we go through all the known spans
+    lineConfiguration.spans.fastForEach {
+        currentSpan += it.currentLineSpan
+        currentIndex++
+    }
+    // and increment index as if we had more items with slot == 1 until we switch to the next line
+    currentIndex += slotsPerLine - currentSpan + 1
+    return currentIndex
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
index bfab189..c77504d 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -17,10 +17,95 @@
 package androidx.tv.foundation.lazy.grid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
 
 @Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
 @ExperimentalFoundationApi
 internal interface LazyGridItemProvider : LazyLayoutItemProvider {
     val spanLayoutProvider: LazyGridSpanLayoutProvider
+    val hasCustomSpans: Boolean
+
+    fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan
 }
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyGridItemProvider(
+    state: TvLazyGridState,
+    content: TvLazyGridScope.() -> Unit,
+): LazyGridItemProvider {
+    val latestContent = rememberUpdatedState(content)
+    val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+        firstVisibleItemIndex = remember(state) {
+            { state.firstVisibleItemIndex }
+        },
+        slidingWindowSize = { NearestItemsSlidingWindowSize },
+        extraItemCount = { NearestItemsExtraItemCount }
+    )
+
+    return remember(nearestItemsRangeState) {
+        val itemProviderState: State<LazyGridItemProvider> = derivedStateOf {
+            val gridScope = TvLazyGridScopeImpl().apply(latestContent.value)
+            LazyGridItemProviderImpl(
+                gridScope.intervals,
+                gridScope.hasCustomSpans,
+                nearestItemsRangeState.value
+            )
+        }
+
+        object : LazyGridItemProvider,
+            LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+            override val spanLayoutProvider: LazyGridSpanLayoutProvider
+                get() = itemProviderState.value.spanLayoutProvider
+
+            override val hasCustomSpans: Boolean
+                get() = itemProviderState.value.hasCustomSpans
+
+            override fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan =
+                with(itemProviderState.value) {
+                    getSpan(index)
+                }
+        }
+    }
+}
+
+@ExperimentalFoundationApi
+private class LazyGridItemProviderImpl(
+    private val intervals: IntervalList<LazyGridIntervalContent>,
+    override val hasCustomSpans: Boolean,
+    nearestItemsRange: IntRange
+) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
+    intervals = intervals,
+    nearestItemsRange = nearestItemsRange,
+    itemContent = { interval, index ->
+        interval.item.invoke(TvLazyGridItemScopeImpl, index)
+    }
+) {
+    override val spanLayoutProvider: LazyGridSpanLayoutProvider =
+        LazyGridSpanLayoutProvider(this)
+
+    override fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        return interval.value.span.invoke(this, localIntervalIndex)
+    }
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 200
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
deleted file mode 100644
index 9ee4e5e..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.Snapshot
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
-    state: TvLazyGridState,
-    content: TvLazyGridScope.() -> Unit,
-): LazyGridItemProvider {
-    val latestContent = rememberUpdatedState(content)
-    // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
-    // of derivedState in return expr will only happen after the state value has been changed.
-    val nearestItemsRangeState = remember(state) {
-        mutableStateOf(
-            Snapshot.withoutReadObservation {
-                // State read is observed in composition, causing it to recompose 1 additional time.
-                calculateNearestItemsRange(state.firstVisibleItemIndex)
-            }
-        )
-    }
-    LaunchedEffect(nearestItemsRangeState) {
-        snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
-            // MutableState's SnapshotMutationPolicy will make sure the provider is only
-            // recreated when the state is updated with a new range.
-            .collect { nearestItemsRangeState.value = it }
-    }
-    return remember(nearestItemsRangeState) {
-        LazyGridItemProviderImpl(
-            derivedStateOf {
-                val listScope = TvLazyGridScopeImpl().apply(latestContent.value)
-                LazyGridItemsSnapshot(
-                    listScope.intervals,
-                    listScope.hasCustomSpans,
-                    nearestItemsRangeState.value
-                )
-            }
-        )
-    }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyGridItemsSnapshot(
-    private val intervals: IntervalList<LazyGridIntervalContent>,
-    val hasCustomSpans: Boolean,
-    nearestItemsRange: IntRange
-) {
-    val itemsCount get() = intervals.size
-
-    val spanLayoutProvider = LazyGridSpanLayoutProvider(this)
-
-    fun getKey(index: Int): Any {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        val key = interval.value.key?.invoke(localIntervalIndex)
-        return key ?: getDefaultLazyLayoutKey(index)
-    }
-
-    fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        return interval.value.span.invoke(this, localIntervalIndex)
-    }
-
-    @Composable
-    fun Item(index: Int) {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        interval.value.item.invoke(TvLazyGridItemScopeImpl, localIntervalIndex)
-    }
-
-    val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
-    fun getContentType(index: Int): Any? {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        return interval.value.type.invoke(localIntervalIndex)
-    }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyGridItemProviderImpl(
-    private val itemsSnapshot: State<LazyGridItemsSnapshot>
-) : LazyGridItemProvider {
-
-    override val itemCount get() = itemsSnapshot.value.itemsCount
-
-    override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
-    @Composable
-    override fun Item(index: Int) {
-        itemsSnapshot.value.Item(index)
-    }
-
-    override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
-    override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-
-    override val spanLayoutProvider: LazyGridSpanLayoutProvider
-        get() = itemsSnapshot.value.spanLayoutProvider
-}
-
-/**
- * Traverses the interval [list] in order to create a mapping from the key to the index for all
- * the indexes in the passed [range].
- * The returned map will not contain the values for intervals with no key mapping provided.
- */
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
-    range: IntRange,
-    list: IntervalList<LazyGridIntervalContent>
-): Map<Any, Int> {
-    val first = range.first
-    check(first >= 0)
-    val last = minOf(range.last, list.size - 1)
-    return if (last < first) {
-        emptyMap()
-    } else {
-        hashMapOf<Any, Int>().also { map ->
-            list.forEach(
-                fromIndex = first,
-                toIndex = last,
-            ) {
-                if (it.value.key != null) {
-                    val keyFactory = requireNotNull(it.value.key)
-                    val start = maxOf(first, it.startIndex)
-                    val end = minOf(last, it.startIndex + it.size - 1)
-                    for (i in start..end) {
-                        map[keyFactory(i - it.startIndex)] = i
-                    }
-                }
-            }
-        }
-    }
-}
-
-/**
- * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
- * the first visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
-    val slidingWindowStart = VisibleItemsSlidingWindowSize *
-        (firstVisibleItem / VisibleItemsSlidingWindowSize)
-
-    val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
-    val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
-    return start until end
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private val VisibleItemsSlidingWindowSize = 90
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private val ExtraItemsNearTheSlidingWindow = 200
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
index 03003be..d599225 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -41,7 +41,6 @@
     measuredLineProvider: LazyMeasuredLineProvider,
     measuredItemProvider: LazyMeasuredItemProvider,
     mainAxisAvailableSize: Int,
-    slotsPerLine: Int,
     beforeContentPadding: Int,
     afterContentPadding: Int,
     spaceBetweenLines: Int,
@@ -55,6 +54,7 @@
     reverseLayout: Boolean,
     density: Density,
     placementAnimator: LazyGridItemPlacementAnimator,
+    spanLayoutProvider: LazyGridSpanLayoutProvider,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): TvLazyGridMeasureResult {
     require(beforeContentPadding >= 0)
@@ -245,10 +245,10 @@
             consumedScroll = consumedScroll.toInt(),
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
-            slotsPerLine = slotsPerLine,
             reverseLayout = reverseLayout,
             positionedItems = positionedItems,
-            measuredItemProvider = measuredItemProvider
+            measuredItemProvider = measuredItemProvider,
+            spanLayoutProvider = spanLayoutProvider
         )
 
         return TvLazyGridMeasureResult(
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
index c7a6165..1dbbbf0 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -92,7 +92,10 @@
      */
     fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
         Snapshot.withoutReadObservation {
-            update(findLazyGridIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+            update(
+                ItemIndex(itemProvider.findIndexByKey(lastKnownFirstItemKey, index.value)),
+                scrollOffset
+            )
         }
     }
 
@@ -105,33 +108,31 @@
             this.scrollOffset = scrollOffset
         }
     }
+}
 
-    private companion object {
-        /**
-         * Finds a position of the item with the given key in the grid. This logic allows us to
-         * detect when there were items added or removed before our current first item.
-         */
-        private fun findLazyGridIndexByKey(
-            key: Any?,
-            lastKnownIndex: ItemIndex,
-            itemProvider: LazyGridItemProvider
-        ): ItemIndex {
-            if (key == null) {
-                // there were no real item during the previous measure
-                return lastKnownIndex
-            }
-            if (lastKnownIndex.value < itemProvider.itemCount &&
-                key == itemProvider.getKey(lastKnownIndex.value)
-            ) {
-                // this item is still at the same index
-                return lastKnownIndex
-            }
-            val newIndex = itemProvider.keyToIndexMap[key]
-            if (newIndex != null) {
-                return ItemIndex(newIndex)
-            }
-            // fallback to the previous index if we don't know the new index of the item
-            return lastKnownIndex
-        }
+/**
+ * Finds a position of the item with the given key in the lists. This logic allows us to
+ * detect when there were items added or removed before our current first item.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal fun LazyGridItemProvider.findIndexByKey(
+    key: Any?,
+    lastKnownIndex: Int,
+): Int {
+    if (key == null) {
+        // there were no real item during the previous measure
+        return lastKnownIndex
     }
+    if (lastKnownIndex < itemCount &&
+        key == getKey(lastKnownIndex)
+    ) {
+        // this item is still at the same index
+        return lastKnownIndex
+    }
+    val newIndex = keyToIndexMap[key]
+    if (newIndex != null) {
+        return newIndex
+    }
+    // fallback to the previous index if we don't know the new index of the item
+    return lastKnownIndex
 }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
deleted file mode 100644
index 20bbd68..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.copy
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFirstOrNull
-import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.abs
-import kotlin.math.max
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@OptIn(ExperimentalFoundationApi::class)
-private class ItemFoundInScroll(
-    val item: TvLazyGridItemInfo,
-    val previousAnimation: AnimationState<Float, AnimationVector1D>
-) : CancellationException()
-
-private val TargetDistance = 2500.dp
-private val BoundDistance = 1500.dp
-
-private const val DEBUG = false
-private inline fun debugLog(generateMsg: () -> String) {
-    if (DEBUG) {
-        println("LazyGridScrolling: ${generateMsg()}")
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal suspend fun TvLazyGridState.doSmoothScrollToItem(
-    index: Int,
-    scrollOffset: Int,
-    slotsPerLine: Int
-) {
-    require(index >= 0f) { "Index should be non-negative ($index)" }
-    fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
-        it.index == index
-    }
-    scroll {
-        try {
-            val targetDistancePx = with(density) { TargetDistance.toPx() }
-            val boundDistancePx = with(density) { BoundDistance.toPx() }
-            var loop = true
-            var anim = AnimationState(0f)
-            val targetItemInitialInfo = getTargetItem()
-            if (targetItemInitialInfo != null) {
-                // It's already visible, just animate directly
-                throw ItemFoundInScroll(
-                    targetItemInitialInfo,
-                    anim
-                )
-            }
-            val forward = index > firstVisibleItemIndex
-
-            fun isOvershot(): Boolean {
-                // Did we scroll past the item?
-                @Suppress("RedundantIf") // It's way easier to understand the logic this way
-                return if (forward) {
-                    if (firstVisibleItemIndex > index) {
-                        true
-                    } else if (
-                        firstVisibleItemIndex == index &&
-                        firstVisibleItemScrollOffset > scrollOffset
-                    ) {
-                        true
-                    } else {
-                        false
-                    }
-                } else { // backward
-                    if (firstVisibleItemIndex < index) {
-                        true
-                    } else if (
-                        firstVisibleItemIndex == index &&
-                        firstVisibleItemScrollOffset < scrollOffset
-                    ) {
-                        true
-                    } else {
-                        false
-                    }
-                }
-            }
-
-            var loops = 1
-            while (loop && layoutInfo.totalItemsCount > 0) {
-                val visibleItems = layoutInfo.visibleItemsInfo
-                val averageLineMainAxisSize = calculateLineAverageMainAxisSize(
-                    visibleItems,
-                    true // TODO(b/191238807)
-                )
-                val before = index < firstVisibleItemIndex
-                val linesDiff =
-                    (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
-                        slotsPerLine
-
-                val expectedDistance = (averageLineMainAxisSize * linesDiff).toFloat() +
-                    scrollOffset - firstVisibleItemScrollOffset
-                val target = if (abs(expectedDistance) < targetDistancePx) {
-                    expectedDistance
-                } else {
-                    if (forward) targetDistancePx else -targetDistancePx
-                }
-
-                debugLog {
-                    "Scrolling to index=$index offset=$scrollOffset from " +
-                        "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
-                        "averageSize=$averageLineMainAxisSize and calculated target=$target"
-                }
-
-                anim = anim.copy(value = 0f)
-                var prevValue = 0f
-                anim.animateTo(
-                    target,
-                    sequentialAnimation = (anim.velocity != 0f)
-                ) {
-                    // If we haven't found the item yet, check if it's visible.
-                    var targetItem = getTargetItem()
-
-                    if (targetItem == null) {
-                        // Springs can overshoot their target, clamp to the desired range
-                        val coercedValue = if (target > 0) {
-                            value.coerceAtMost(target)
-                        } else {
-                            value.coerceAtLeast(target)
-                        }
-                        val delta = coercedValue - prevValue
-                        debugLog {
-                            "Scrolling by $delta (target: $target, coercedValue: $coercedValue)"
-                        }
-
-                        val consumed = scrollBy(delta)
-                        targetItem = getTargetItem()
-                        if (targetItem != null) {
-                            debugLog { "Found the item after performing scrollBy()" }
-                        } else if (!isOvershot()) {
-                            if (delta != consumed) {
-                                debugLog { "Hit end without finding the item" }
-                                cancelAnimation()
-                                loop = false
-                                return@animateTo
-                            }
-                            prevValue += delta
-                            if (forward) {
-                                if (value > boundDistancePx) {
-                                    debugLog { "Struck bound going forward" }
-                                    cancelAnimation()
-                                }
-                            } else {
-                                if (value < -boundDistancePx) {
-                                    debugLog { "Struck bound going backward" }
-                                    cancelAnimation()
-                                }
-                            }
-
-                            // Magic constants for teleportation chosen arbitrarily by experiment
-                            if (forward) {
-                                if (
-                                    loops >= 2 &&
-                                    index - layoutInfo.visibleItemsInfo.last().index > 200
-                                ) {
-                                    // Teleport
-                                    debugLog { "Teleport forward" }
-                                    snapToItemIndexInternal(index = index - 200, scrollOffset = 0)
-                                }
-                            } else {
-                                if (
-                                    loops >= 2 &&
-                                    layoutInfo.visibleItemsInfo.first().index - index > 100
-                                ) {
-                                    // Teleport
-                                    debugLog { "Teleport backward" }
-                                    snapToItemIndexInternal(index = index + 200, scrollOffset = 0)
-                                }
-                            }
-                        }
-                    }
-
-                    // We don't throw ItemFoundInScroll when we snap, because once we've snapped to
-                    // the final position, there's no need to animate to it.
-                    if (isOvershot()) {
-                        debugLog { "Overshot" }
-                        snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
-                        loop = false
-                        cancelAnimation()
-                        return@animateTo
-                    } else if (targetItem != null) {
-                        debugLog { "Found item" }
-                        throw ItemFoundInScroll(
-                            targetItem,
-                            anim
-                        )
-                    }
-                }
-
-                loops++
-            }
-        } catch (itemFound: ItemFoundInScroll) {
-            // We found it, animate to it
-            // Bring to the requested position - will be automatically stopped if not possible
-            val anim = itemFound.previousAnimation.copy(value = 0f)
-            // TODO(b/191238807)
-            val target = (itemFound.item.offset.y + scrollOffset).toFloat()
-            var prevValue = 0f
-            debugLog {
-                "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
-            }
-            anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
-                // Springs can overshoot their target, clamp to the desired range
-                val coercedValue = when {
-                    target > 0 -> {
-                        value.coerceAtMost(target)
-                    }
-                    target < 0 -> {
-                        value.coerceAtLeast(target)
-                    }
-                    else -> {
-                        debugLog { "WARNING: somehow ended up seeking 0px, this shouldn't happen" }
-                        0f
-                    }
-                }
-                val delta = coercedValue - prevValue
-                debugLog { "Seeking by $delta (coercedValue = $coercedValue)" }
-                val consumed = scrollBy(delta)
-                if (delta != consumed /* hit the end, stop */ ||
-                    coercedValue != value /* would have overshot, stop */
-                ) {
-                    cancelAnimation()
-                }
-                prevValue += delta
-            }
-            // Once we're finished the animation, snap to the exact position to account for
-            // rounding error (otherwise we tend to end up with the previous item scrolled the
-            // tiniest bit onscreen)
-            // TODO: prevent temporarily scrolling *past* the item
-            snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
-        }
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun calculateLineAverageMainAxisSize(
-    visibleItems: List<TvLazyGridItemInfo>,
-    isVertical: Boolean
-): Int {
-    val lineOf: (Int) -> Int = {
-        if (isVertical) visibleItems[it].row else visibleItems[it].column
-    }
-
-    var totalLinesMainAxisSize = 0
-    var linesCount = 0
-
-    var lineStartIndex = 0
-    while (lineStartIndex < visibleItems.size) {
-        val currentLine = lineOf(lineStartIndex)
-        if (currentLine == -1) {
-            // Filter out exiting items.
-            ++lineStartIndex
-            continue
-        }
-
-        var lineMainAxisSize = 0
-        var lineEndIndex = lineStartIndex
-        while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
-            lineMainAxisSize = max(
-                lineMainAxisSize,
-                if (isVertical) {
-                    visibleItems[lineEndIndex].size.height
-                } else {
-                    visibleItems[lineEndIndex].size.width
-                }
-            )
-            ++lineEndIndex
-        }
-
-        totalLinesMainAxisSize += lineMainAxisSize
-        ++linesCount
-
-        lineStartIndex = lineEndIndex
-    }
-
-    return totalLinesMainAxisSize / linesCount
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index e72ee1e..3a900332 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -22,7 +22,7 @@
 
 @Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
 @OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridSpanLayoutProvider(private val itemsSnapshot: LazyGridItemsSnapshot) {
+internal class LazyGridSpanLayoutProvider(private val itemProvider: LazyGridItemProvider) {
     class LineConfiguration(val firstItemIndex: Int, val spans: List<TvGridItemSpan>)
 
     /** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
@@ -61,7 +61,7 @@
             List(currentSlotsPerLine) { TvGridItemSpan(1) }.also { previousDefaultSpans = it }
         }
 
-    val totalSize get() = itemsSnapshot.itemsCount
+    val totalSize get() = itemProvider.itemCount
 
     /** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
     var slotsPerLine = 0
@@ -73,7 +73,7 @@
         }
 
     fun getLineConfiguration(lineIndex: Int): LineConfiguration {
-        if (!itemsSnapshot.hasCustomSpans) {
+        if (!itemProvider.hasCustomSpans) {
             // Quick return when all spans are 1x1 - in this case we can easily calculate positions.
             val firstItemIndex = lineIndex * slotsPerLine
             return LineConfiguration(
@@ -173,7 +173,7 @@
             return LineIndex(0)
         }
         require(itemIndex < totalSize)
-        if (!itemsSnapshot.hasCustomSpans) {
+        if (!itemProvider.hasCustomSpans) {
             return LineIndex(itemIndex / slotsPerLine)
         }
 
@@ -211,7 +211,7 @@
         return LineIndex(currentLine)
     }
 
-    private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsSnapshot) {
+    private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
         with(TvLazyGridItemSpanScopeImpl) {
             maxCurrentLineSpan = maxSpan
             maxLineSpan = slotsPerLine
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
index 5cf56f4..867f0c1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -25,8 +25,8 @@
 @OptIn(ExperimentalFoundationApi::class)
 internal class LazyMeasuredLineProvider(
     private val isVertical: Boolean,
-    slotSizesSums: List<Int>,
-    crossAxisSpacing: Int,
+    private val slotSizesSums: List<Int>,
+    private val crossAxisSpacing: Int,
     private val gridItemsCount: Int,
     private val spaceBetweenLines: Int,
     private val measuredItemProvider: LazyMeasuredItemProvider,
@@ -34,12 +34,12 @@
     private val measuredLineFactory: MeasuredLineFactory
 ) {
     // The constraints for cross axis size. The main axis is not restricted.
-    internal val childConstraints: (startSlot: Int, span: Int) -> Constraints = { startSlot, span ->
+    internal fun childConstraints(startSlot: Int, span: Int): Constraints {
         val lastSlotSum = slotSizesSums[startSlot + span - 1]
         val prevSlotSum = if (startSlot == 0) 0 else slotSizesSums[startSlot - 1]
         val slotsSize = lastSlotSum - prevSlotSum
-        val crossAxisSize = slotsSize + crossAxisSpacing * (span - 1)
-        if (isVertical) {
+        val crossAxisSize = (slotsSize + crossAxisSpacing * (span - 1)).coerceAtLeast(0)
+        return if (isVertical) {
             Constraints.fixedWidth(crossAxisSize)
         } else {
             Constraints.fixedHeight(crossAxisSize)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
index ea19b91..27e5ca3 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
@@ -31,10 +32,57 @@
 import androidx.compose.ui.semantics.scrollToIndex
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.tv.foundation.lazy.layout.LazyLayoutSemanticState
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun rememberLazyGridSemanticState(
+    state: TvLazyGridState,
+    itemProvider: LazyLayoutItemProvider,
+    reverseScrolling: Boolean
+): LazyLayoutSemanticState =
+    remember(state, itemProvider, reverseScrolling) {
+        object : LazyLayoutSemanticState {
+            override fun scrollAxisRange(): ScrollAxisRange =
+                ScrollAxisRange(
+                    value = {
+                        // This is a simple way of representing the current position without
+                        // needing any lazy items to be measured. It's good enough so far, because
+                        // screen-readers care mostly about whether scroll position changed or not
+                        // rather than the actual offset in pixels.
+                        state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+                    },
+                    maxValue = {
+                        if (state.canScrollForward) {
+                            // If we can scroll further, we don't know the end yet,
+                            // but it's upper bounded by #items + 1
+                            itemProvider.itemCount + 1f
+                        } else {
+                            // If we can't scroll further, the current value is the max
+                            state.firstVisibleItemIndex +
+                                state.firstVisibleItemScrollOffset / 100_000f
+                        }
+                    },
+                    reverseScrolling = reverseScrolling
+                )
+
+            override suspend fun animateScrollBy(delta: Float) {
+                state.animateScrollBy(delta)
+            }
+
+            override suspend fun scrollToItem(index: Int) {
+                state.scrollToItem(index)
+            }
+
+            // TODO(popam): check if this is correct - it would be nice to provide correct columns
+            override fun collectionInfo(): CollectionInfo =
+                CollectionInfo(rowCount = -1, columnCount = -1)
+        }
+    }
+
+@OptIn(ExperimentalFoundationApi::class)
 @Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
 @Composable
 internal fun Modifier.lazyGridSemantics(
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
index bfd2510..2af17a5 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
@@ -17,6 +17,7 @@
 package androidx.tv.foundation.lazy.grid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
 import androidx.compose.foundation.lazy.layout.MutableIntervalList
 import androidx.compose.runtime.Composable
 
@@ -66,9 +67,10 @@
     }
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 internal class LazyGridIntervalContent(
-    val key: ((index: Int) -> Any)?,
+    override val key: ((index: Int) -> Any)?,
     val span: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan,
-    val type: ((index: Int) -> Any?),
+    override val type: ((index: Int) -> Any?),
     val item: @Composable TvLazyGridItemScope.(Int) -> Unit
-)
+) : LazyLayoutIntervalContent
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
index 15f05c7..52e13bf 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.lazy.layout.animateScrollToItem
 import androidx.tv.foundation.lazy.list.AwaitFirstLayoutModifier
 import kotlin.math.abs
 
@@ -172,7 +173,7 @@
     /**
      * The list of handles associated with the items from the [lineToPrefetch] line.
      */
-    private var currentLinePrefetchHandles =
+    private val currentLinePrefetchHandles =
         mutableVectorOf<LazyLayoutPrefetchState.PrefetchHandle>()
 
     /**
@@ -210,6 +211,8 @@
 
     internal var placementAnimator by mutableStateOf<LazyGridItemPlacementAnimator?>(null)
 
+    private val animateScrollScope = LazyGridAnimateScrollScope(this)
+
     /**
      * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
      * pixels.
@@ -307,7 +310,6 @@
         }
         val info = layoutInfo
         if (info.visibleItemsInfo.isNotEmpty()) {
-            // check(isActive)
             val scrollingForward = delta < 0
             val lineToPrefetch: Int
             val closestNextItemToPrefetch: Int
@@ -378,7 +380,7 @@
         index: Int,
         scrollOffset: Int = 0
     ) {
-        doSmoothScrollToItem(index, scrollOffset, slotsPerLine)
+        animateScrollScope.animateScrollToItem(index, scrollOffset)
     }
 
     /**
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
similarity index 74%
rename from tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
rename to tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
index b78decf..06ecb8d 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
@@ -14,20 +14,20 @@
  * limitations under the License.
  */
 
-package androidx.tv.foundation.lazy.list
+package androidx.tv.foundation.lazy.layout
 
 import androidx.compose.animation.core.AnimationState
 import androidx.compose.animation.core.AnimationVector1D
 import androidx.compose.animation.core.animateTo
 import androidx.compose.animation.core.copy
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastSumBy
+import java.lang.Math.abs
 import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.abs
 
 private class ItemFoundInScroll(
-    val item: TvLazyListItemInfo,
+    val itemOffset: Int,
     val previousAnimation: AnimationState<Float, AnimationVector1D>
 ) : CancellationException()
 
@@ -37,28 +37,52 @@
 private const val DEBUG = false
 private inline fun debugLog(generateMsg: () -> String) {
     if (DEBUG) {
-        println("LazyListScrolling: ${generateMsg()}")
+        println("LazyScrolling: ${generateMsg()}")
     }
 }
 
-internal suspend fun TvLazyListState.doSmoothScrollToItem(
+/**
+ * Abstraction over animated scroll for using [animateScrollToItem] in different layouts.
+ * todo(b/243786897): revisit this API and make it public
+ **/
+internal interface LazyAnimateScrollScope {
+    val density: Density
+
+    val firstVisibleItemIndex: Int
+
+    val firstVisibleItemScrollOffset: Int
+
+    val lastVisibleItemIndex: Int
+
+    val itemCount: Int
+
+    fun getTargetItemOffset(index: Int): Int?
+
+    fun ScrollScope.snapToItem(index: Int, scrollOffset: Int)
+
+    fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float
+
+    /** defines min number of items that forces scroll to snap if animation did not reach it */
+    val numOfItemsForTeleport: Int
+
+    suspend fun scroll(block: suspend ScrollScope.() -> Unit)
+}
+
+internal suspend fun LazyAnimateScrollScope.animateScrollToItem(
     index: Int,
-    scrollOffset: Int
+    scrollOffset: Int,
 ) {
-    require(index >= 0f) { "Index should be non-negative ($index)" }
-    fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
-        it.index == index
-    }
     scroll {
+        require(index >= 0f) { "Index should be non-negative ($index)" }
         try {
             val targetDistancePx = with(density) { TargetDistance.toPx() }
             val boundDistancePx = with(density) { BoundDistance.toPx() }
             var loop = true
             var anim = AnimationState(0f)
-            val targetItemInitialInfo = getTargetItem()
-            if (targetItemInitialInfo != null) {
+            val targetItemInitialOffset = getTargetItemOffset(index)
+            if (targetItemInitialOffset != null) {
                 // It's already visible, just animate directly
-                throw ItemFoundInScroll(targetItemInitialInfo, anim)
+                throw ItemFoundInScroll(targetItemInitialOffset, anim)
             }
             val forward = index > firstVisibleItemIndex
 
@@ -91,12 +115,8 @@
             }
 
             var loops = 1
-            while (loop && layoutInfo.totalItemsCount > 0) {
-                val visibleItems = layoutInfo.visibleItemsInfo
-                val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
-                val indexesDiff = index - firstVisibleItemIndex
-                val expectedDistance = (averageSize * indexesDiff).toFloat() +
-                    scrollOffset - firstVisibleItemScrollOffset
+            while (loop && itemCount > 0) {
+                val expectedDistance = expectedDistanceTo(index, scrollOffset)
                 val target = if (abs(expectedDistance) < targetDistancePx) {
                     expectedDistance
                 } else {
@@ -106,7 +126,7 @@
                 debugLog {
                     "Scrolling to index=$index offset=$scrollOffset from " +
                         "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
-                        "averageSize=$averageSize and calculated target=$target"
+                        " calculated target=$target"
                 }
 
                 anim = anim.copy(value = 0f)
@@ -116,9 +136,9 @@
                     sequentialAnimation = (anim.velocity != 0f)
                 ) {
                     // If we haven't found the item yet, check if it's visible.
-                    var targetItem = getTargetItem()
+                    var targetItemOffset = getTargetItemOffset(index)
 
-                    if (targetItem == null) {
+                    if (targetItemOffset == null) {
                         // Springs can overshoot their target, clamp to the desired range
                         val coercedValue = if (target > 0) {
                             value.coerceAtMost(target)
@@ -131,8 +151,8 @@
                         }
 
                         val consumed = scrollBy(delta)
-                        targetItem = getTargetItem()
-                        if (targetItem != null) {
+                        targetItemOffset = getTargetItemOffset(index)
+                        if (targetItemOffset != null) {
                             debugLog { "Found the item after performing scrollBy()" }
                         } else if (!isOvershot()) {
                             if (delta != consumed) {
@@ -154,24 +174,29 @@
                                 }
                             }
 
-                            // Magic constants for teleportation chosen arbitrarily by experiment
                             if (forward) {
                                 if (
                                     loops >= 2 &&
-                                    index - layoutInfo.visibleItemsInfo.last().index > 100
+                                    index - lastVisibleItemIndex > numOfItemsForTeleport
                                 ) {
                                     // Teleport
                                     debugLog { "Teleport forward" }
-                                    snapToItemIndexInternal(index = index - 100, scrollOffset = 0)
+                                    snapToItem(
+                                        index = index - numOfItemsForTeleport,
+                                        scrollOffset = 0
+                                    )
                                 }
                             } else {
                                 if (
                                     loops >= 2 &&
-                                    layoutInfo.visibleItemsInfo.first().index - index > 100
+                                    firstVisibleItemIndex - index > numOfItemsForTeleport
                                 ) {
                                     // Teleport
                                     debugLog { "Teleport backward" }
-                                    snapToItemIndexInternal(index = index + 100, scrollOffset = 0)
+                                    snapToItem(
+                                        index = index + numOfItemsForTeleport,
+                                        scrollOffset = 0
+                                    )
                                 }
                             }
                         }
@@ -181,13 +206,13 @@
                     // the final position, there's no need to animate to it.
                     if (isOvershot()) {
                         debugLog { "Overshot" }
-                        snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+                        snapToItem(index = index, scrollOffset = scrollOffset)
                         loop = false
                         cancelAnimation()
                         return@animateTo
-                    } else if (targetItem != null) {
+                    } else if (targetItemOffset != null) {
                         debugLog { "Found item" }
-                        throw ItemFoundInScroll(targetItem, anim)
+                        throw ItemFoundInScroll(targetItemOffset, anim)
                     }
                 }
 
@@ -197,7 +222,7 @@
             // We found it, animate to it
             // Bring to the requested position - will be automatically stopped if not possible
             val anim = itemFound.previousAnimation.copy(value = 0f)
-            val target = (itemFound.item.offset + scrollOffset).toFloat()
+            val target = (itemFound.itemOffset + scrollOffset).toFloat()
             var prevValue = 0f
             debugLog {
                 "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
@@ -230,7 +255,7 @@
             // rounding error (otherwise we tend to end up with the previous item scrolled the
             // tiniest bit onscreen)
             // TODO: prevent temporarily scrolling *past* the item
-            snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+            snapToItem(index = index, scrollOffset = scrollOffset)
         }
     }
 }
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
new file mode 100644
index 0000000..ddf0cfa
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.tv.foundation.lazy.layout
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.indexForKey
+import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollToIndex
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Composable
+internal fun Modifier.lazyLayoutSemantics(
+    itemProvider: LazyLayoutItemProvider,
+    state: LazyLayoutSemanticState,
+    orientation: Orientation,
+    userScrollEnabled: Boolean
+): Modifier {
+    val coroutineScope = rememberCoroutineScope()
+    return this.then(
+        remember(
+            itemProvider,
+            state,
+            orientation,
+            userScrollEnabled
+        ) {
+            val isVertical = orientation == Orientation.Vertical
+            val indexForKeyMapping: (Any) -> Int = { needle ->
+                var result = -1
+                for (index in 0 until itemProvider.itemCount) {
+                    if (itemProvider.getKey(index) == needle) {
+                        result = index
+                        break
+                    }
+                }
+                result
+            }
+
+            val accessibilityScrollState = state.scrollAxisRange()
+
+            val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+                { x, y ->
+                    val delta = if (isVertical) {
+                        y
+                    } else {
+                        x
+                    }
+                    coroutineScope.launch {
+                        state.animateScrollBy(delta)
+                    }
+                    // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+                    true
+                }
+            } else {
+                null
+            }
+
+            val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+                { index ->
+                    require(index >= 0 && index < itemProvider.itemCount) {
+                        "Can't scroll to index $index, it is out of " +
+                            "bounds [0, ${itemProvider.itemCount})"
+                    }
+                    coroutineScope.launch {
+                        state.scrollToItem(index)
+                    }
+                    true
+                }
+            } else {
+                null
+            }
+
+            val collectionInfo = state.collectionInfo()
+
+            Modifier.semantics {
+                indexForKey(indexForKeyMapping)
+
+                if (isVertical) {
+                    verticalScrollAxisRange = accessibilityScrollState
+                } else {
+                    horizontalScrollAxisRange = accessibilityScrollState
+                }
+
+                if (scrollByAction != null) {
+                    scrollBy(action = scrollByAction)
+                }
+
+                if (scrollToIndexAction != null) {
+                    scrollToIndex(action = scrollToIndexAction)
+                }
+
+                this.collectionInfo = collectionInfo
+            }
+        }
+    )
+}
+
+internal interface LazyLayoutSemanticState {
+    fun scrollAxisRange(): ScrollAxisRange
+    fun collectionInfo(): CollectionInfo
+    suspend fun animateScrollBy(delta: Float)
+    suspend fun scrollToItem(index: Int)
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
index 64c697a..6510890 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
@@ -22,6 +22,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.ExperimentalTvFoundationApi
 import androidx.tv.foundation.PivotOffsets
 
 /* Copied from
@@ -74,6 +75,30 @@
         contentType: (index: Int) -> Any? = { null },
         itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
     )
+
+    /**
+     * Adds a sticky header item, which will remain pinned even when scrolling after it.
+     * The header will remain pinned until the next header will take its place.
+     *
+     * @sample androidx.compose.foundation.samples.StickyHeaderSample
+     *
+     * @param key a stable and unique key representing the item. Using the same key
+     * for multiple items in the list is not allowed. Type of the key should be saveable
+     * via Bundle on Android. If null is passed the position in the list will represent the key.
+     * When you specify the key the scroll position will be maintained based on the key, which
+     * means if you add/remove items before the current visible item the item with the given key
+     * will be kept as the first visible one.
+     * @param contentType the type of the content of this item. The item compositions of the same
+     * type could be reused more efficiently. Note that null is a valid type and items of such
+     * type will be considered compatible.
+     * @param content the content of the header
+     */
+    @ExperimentalTvFoundationApi
+    fun stickyHeader(
+        key: Any? = null,
+        contentType: Any? = null,
+        content: @Composable TvLazyListItemScope.() -> Unit
+    )
 }
 
 /**
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
index 69b09b8..cfb8ba4 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.checkScrollableContainerConstraints
 import androidx.compose.foundation.clipScrollableContainer
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.calculateEndPadding
@@ -36,13 +37,13 @@
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.offset
 import androidx.tv.foundation.ExperimentalTvFoundationApi
 import androidx.tv.foundation.PivotOffsets
 import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.tv.foundation.lazy.layout.lazyLayoutSemantics
 import androidx.tv.foundation.lazy.lazyListBeyondBoundsModifier
 import androidx.tv.foundation.lazy.lazyListPinningModifier
 import androidx.tv.foundation.scrollableWithPivot
@@ -63,6 +64,8 @@
     isVertical: Boolean,
     /** Whether scrolling via the user gestures is allowed. */
     userScrollEnabled: Boolean,
+    /** Number of items to layout before and after the visible items */
+    beyondBoundsItemCount: Int = 0,
     /** offsets of child element within the parent and starting edge of the child from the pivot
      * defined by the parentOffset. */
     pivotOffsets: PivotOffsets,
@@ -77,7 +80,9 @@
     /** The content of the list */
     content: TvLazyListScope.() -> Unit
 ) {
-    val itemProvider = rememberItemProvider(state, content)
+    val itemProvider = rememberLazyListItemProvider(state, content)
+    val semanticState =
+        rememberLazyListSemanticState(state, itemProvider, reverseLayout, isVertical)
     val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
     val scope = rememberCoroutineScope()
     val placementAnimator = remember(state, isVertical) {
@@ -92,6 +97,7 @@
         contentPadding,
         reverseLayout,
         isVertical,
+        beyondBoundsItemCount,
         horizontalAlignment,
         verticalAlignment,
         horizontalArrangement,
@@ -107,12 +113,10 @@
         modifier = modifier
             .then(state.remeasurementModifier)
             .then(state.awaitLayoutModifier)
-            .lazyListSemantics(
+            .lazyLayoutSemantics(
                 itemProvider = itemProvider,
-                state = state,
-                coroutineScope = scope,
-                isVertical = isVertical,
-                reverseScrolling = reverseLayout,
+                state = semanticState,
+                orientation = orientation,
                 userScrollEnabled = userScrollEnabled
             )
             .clipScrollableContainer(orientation)
@@ -120,17 +124,11 @@
             .lazyListPinningModifier(state, beyondBoundsInfo)
             .scrollableWithPivot(
                 orientation = orientation,
-                reverseDirection = run {
-                    // A finger moves with the content, not with the viewport. Therefore,
-                    // always reverse once to have "natural" gesture that goes reversed to layout
-                    var reverseDirection = !reverseLayout
-                    // But if rtl and horizontal, things move the other way around
-                    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-                    if (isRtl && !isVertical) {
-                        reverseDirection = !reverseDirection
-                    }
-                    reverseDirection
-                },
+                reverseDirection = ScrollableDefaults.reverseDirection(
+                    LocalLayoutDirection.current,
+                    orientation,
+                    reverseLayout
+                ),
                 state = state,
                 enabled = userScrollEnabled,
                 pivotOffsets = pivotOffsets
@@ -170,6 +168,8 @@
     reverseLayout: Boolean,
     /** The layout orientation of the list */
     isVertical: Boolean,
+    /** Number of items to layout before and after the visible items */
+    beyondBoundsItemCount: Int,
     /** The alignment to align items horizontally. Required when isVertical is true */
     horizontalAlignment: Alignment.Horizontal? = null,
     /** The alignment to align items vertically. Required when isVertical is false */
@@ -317,6 +317,7 @@
             verticalArrangement = verticalArrangement,
             horizontalArrangement = horizontalArrangement,
             reverseLayout = reverseLayout,
+            beyondBoundsItemCount = beyondBoundsItemCount,
             density = this,
             placementAnimator = placementAnimator,
             beyondBoundsInfo = beyondBoundsInfo,
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
new file mode 100644
index 0000000..510480d
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastSumBy
+import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
+
+internal class LazyListAnimateScrollScope(
+    private val state: TvLazyListState
+) : LazyAnimateScrollScope {
+    override val density: Density get() = state.density
+
+    override val firstVisibleItemIndex: Int get() = state.firstVisibleItemIndex
+
+    override val firstVisibleItemScrollOffset: Int get() = state.firstVisibleItemScrollOffset
+
+    override val lastVisibleItemIndex: Int
+        get() = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
+
+    override val itemCount: Int
+        get() = state.layoutInfo.totalItemsCount
+
+    override val numOfItemsForTeleport: Int = 100
+
+    override fun getTargetItemOffset(index: Int): Int? =
+        state.layoutInfo.visibleItemsInfo.fastFirstOrNull {
+            it.index == index
+        }?.offset
+
+    override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
+        state.snapToItemIndexInternal(index, scrollOffset)
+    }
+
+    override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
+        val visibleItems = state.layoutInfo.visibleItemsInfo
+        val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
+        val indexesDiff = index - firstVisibleItemIndex
+        return (averageSize * indexesDiff).toFloat() +
+            targetScrollOffset - firstVisibleItemScrollOffset
+    }
+
+    override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
+        state.scroll(block = block)
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
index 7d4137f..1cde8e1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
@@ -17,13 +17,79 @@
 package androidx.tv.foundation.lazy.list
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
 
 @Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
 @ExperimentalFoundationApi
-internal sealed interface LazyListItemProvider : LazyLayoutItemProvider {
+internal interface LazyListItemProvider : LazyLayoutItemProvider {
     /** The list of indexes of the sticky header items */
     val headerIndexes: List<Int>
     /** The scope used by the item content lambdas */
     val itemScope: TvLazyListItemScopeImpl
 }
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyListItemProvider(
+    state: TvLazyListState,
+    content: TvLazyListScope.() -> Unit
+): LazyListItemProvider {
+    val latestContent = rememberUpdatedState(content)
+    val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+        firstVisibleItemIndex = { state.firstVisibleItemIndex },
+        slidingWindowSize = { NearestItemsSlidingWindowSize },
+        extraItemCount = { NearestItemsExtraItemCount }
+    )
+
+    return remember(nearestItemsRangeState) {
+        val itemScope = TvLazyListItemScopeImpl()
+        val itemProviderState = derivedStateOf {
+            val listScope = TvLazyListScopeImpl().apply(latestContent.value)
+            LazyListItemProviderImpl(
+                listScope.intervals,
+                nearestItemsRangeState.value,
+                listScope.headerIndexes,
+                itemScope
+            )
+        }
+        object : LazyListItemProvider,
+            LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+            override val headerIndexes: List<Int> get() = itemProviderState.value.headerIndexes
+            override val itemScope: TvLazyListItemScopeImpl get() =
+                itemProviderState.value.itemScope
+        }
+    }
+}
+
+@ExperimentalFoundationApi
+private class LazyListItemProviderImpl(
+    intervals: IntervalList<LazyListIntervalContent>,
+    nearestItemsRange: IntRange,
+    override val headerIndexes: List<Int>,
+    override val itemScope: TvLazyListItemScopeImpl
+) : LazyListItemProvider,
+    LazyLayoutItemProvider by LazyLayoutItemProvider(
+        intervals = intervals,
+        nearestItemsRange = nearestItemsRange,
+        itemContent = { interval: LazyListIntervalContent, index: Int ->
+            interval.item.invoke(itemScope, index)
+        }
+    )
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 100
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
deleted file mode 100644
index 1e9966d..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.Snapshot
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
-    state: TvLazyListState,
-    content: TvLazyListScope.() -> Unit
-): LazyListItemProvider {
-    val latestContent = rememberUpdatedState(content)
-    // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
-    // of derivedState in return expr will only happen after the state value has been changed.
-    val nearestItemsRangeState = remember(state) {
-        mutableStateOf(
-            Snapshot.withoutReadObservation {
-                // State read is observed in composition, causing it to recompose 1 additional time.
-                calculateNearestItemsRange(state.firstVisibleItemIndex)
-            }
-        )
-    }
-
-    LaunchedEffect(nearestItemsRangeState) {
-        snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
-            // MutableState's SnapshotMutationPolicy will make sure the provider is only
-            // recreated when the state is updated with a new range.
-            .collect { nearestItemsRangeState.value = it }
-    }
-    return remember(nearestItemsRangeState) {
-        LazyListItemProviderImpl(
-            derivedStateOf {
-                val listScope = TvLazyListScopeImpl().apply(latestContent.value)
-                LazyListItemsSnapshot(
-                    listScope.intervals,
-                    listScope.headerIndexes,
-                    nearestItemsRangeState.value
-                )
-            }
-        )
-    }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyListItemsSnapshot(
-    private val intervals: IntervalList<LazyListIntervalContent>,
-    val headerIndexes: List<Int>,
-    nearestItemsRange: IntRange
-) {
-    val itemsCount get() = intervals.size
-
-    fun getKey(index: Int): Any {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        val key = interval.value.key?.invoke(localIntervalIndex)
-        return key ?: getDefaultLazyLayoutKey(index)
-    }
-
-    @Composable
-    fun Item(scope: TvLazyListItemScope, index: Int) {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        interval.value.item.invoke(scope, localIntervalIndex)
-    }
-
-    val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
-    fun getContentType(index: Int): Any? {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        return interval.value.type.invoke(localIntervalIndex)
-    }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyListItemProviderImpl(
-    private val itemsSnapshot: State<LazyListItemsSnapshot>
-) : LazyListItemProvider {
-
-    override val itemScope = TvLazyListItemScopeImpl()
-
-    override val headerIndexes: List<Int> get() = itemsSnapshot.value.headerIndexes
-
-    override val itemCount get() = itemsSnapshot.value.itemsCount
-
-    override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
-    @Composable
-    override fun Item(index: Int) {
-        itemsSnapshot.value.Item(itemScope, index)
-    }
-
-    override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
-    override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-}
-
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
-    range: IntRange,
-    list: IntervalList<LazyListIntervalContent>
-): Map<Any, Int> {
-    val first = range.first
-    check(first >= 0)
-    val last = minOf(range.last, list.size - 1)
-    return if (last < first) {
-        emptyMap()
-    } else {
-        hashMapOf<Any, Int>().also { map ->
-            list.forEach(
-                fromIndex = first,
-                toIndex = last,
-            ) {
-                if (it.value.key != null) {
-                    val keyFactory = requireNotNull(it.value.key)
-                    val start = maxOf(first, it.startIndex)
-                    val end = minOf(last, it.startIndex + it.size - 1)
-                    for (i in start..end) {
-                        map[keyFactory(i - it.startIndex)] = i
-                    }
-                }
-            }
-        }
-    }
-}
-
-/**
- * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
- * the first visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
-    val slidingWindowStart = VisibleItemsSlidingWindowSize *
-        (firstVisibleItem / VisibleItemsSlidingWindowSize)
-
-    val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
-    val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
-    return start until end
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private val VisibleItemsSlidingWindowSize = 30
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private val ExtraItemsNearTheSlidingWindow = 100
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
index 856d77b..dd1161b 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
@@ -57,6 +57,7 @@
     density: Density,
     placementAnimator: LazyListItemPlacementAnimator,
     beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    beyondBoundsItemCount: Int,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyListMeasureResult {
     require(beforeContentPadding >= 0)
@@ -227,31 +228,33 @@
             }
         }
 
-        // Compose extra items before or after the visible items.
-        fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
-        fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
-        val extraItemsBefore =
-            if (beyondBoundsInfo.hasIntervals() &&
-                visibleItems.first().index > beyondBoundsInfo.startIndex()) {
-                mutableListOf<LazyMeasuredItem>().apply {
-                    for (i in visibleItems.first().index - 1 downTo beyondBoundsInfo.startIndex()) {
-                        add(itemProvider.getAndMeasure(DataIndex(i)))
-                    }
-                }
-            } else {
-                emptyList()
-            }
-        val extraItemsAfter =
-            if (beyondBoundsInfo.hasIntervals() &&
-                visibleItems.last().index < beyondBoundsInfo.endIndex()) {
-                mutableListOf<LazyMeasuredItem>().apply {
-                    for (i in visibleItems.last().index until beyondBoundsInfo.endIndex()) {
-                        add(itemProvider.getAndMeasure(DataIndex(i + 1)))
-                    }
-                }
-            } else {
-                emptyList()
-            }
+        // Compose extra items before
+        val extraItemsBefore = createItemsBeforeList(
+            beyondBoundsInfo = beyondBoundsInfo,
+            currentFirstItemIndex = currentFirstItemIndex,
+            itemProvider = itemProvider,
+            itemsCount = itemsCount,
+            beyondBoundsItemCount = beyondBoundsItemCount
+        )
+
+        // Update maxCrossAxis with extra items
+        extraItemsBefore.fastForEach {
+            maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+        }
+
+        // Compose items after last item
+        val extraItemsAfter = createItemsAfterList(
+            beyondBoundsInfo = beyondBoundsInfo,
+            visibleItems = visibleItems,
+            itemProvider = itemProvider,
+            itemsCount = itemsCount,
+            beyondBoundsItemCount = beyondBoundsItemCount
+        )
+
+        // Update maxCrossAxis with extra items
+        extraItemsAfter.fastForEach {
+            maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+        }
 
         val noExtraItems = firstItem == visibleItems.first() &&
             extraItemsBefore.isEmpty() &&
@@ -278,6 +281,15 @@
             density = density,
         )
 
+        placementAnimator.onMeasured(
+            consumedScroll = consumedScroll.toInt(),
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
+            reverseLayout = reverseLayout,
+            positionedItems = positionedItems,
+            itemProvider = itemProvider
+        )
+
         val headerItem = if (headerIndexes.isNotEmpty()) {
             findOrComposeLazyListHeader(
                 composedVisibleItems = positionedItems,
@@ -291,15 +303,6 @@
             null
         }
 
-        placementAnimator.onMeasured(
-            consumedScroll = consumedScroll.toInt(),
-            layoutWidth = layoutWidth,
-            layoutHeight = layoutHeight,
-            reverseLayout = reverseLayout,
-            positionedItems = positionedItems,
-            itemProvider = itemProvider
-        )
-
         return LazyListMeasureResult(
             firstVisibleItem = firstItem,
             firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
@@ -328,6 +331,109 @@
     }
 }
 
+private fun createItemsAfterList(
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    visibleItems: MutableList<LazyMeasuredItem>,
+    itemProvider: LazyMeasuredItemProvider,
+    itemsCount: Int,
+    beyondBoundsItemCount: Int
+): List<LazyMeasuredItem> {
+
+    fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
+
+    fun addItemsAfter(startIndex: Int, endIndex: Int): List<LazyMeasuredItem> {
+        return mutableListOf<LazyMeasuredItem>().apply {
+            for (i in startIndex until endIndex) {
+                val item = itemProvider.getAndMeasure(DataIndex(i + 1))
+                add(item)
+            }
+        }
+    }
+
+    val (startNonVisibleItems, endNonVisibleItems) = if (beyondBoundsItemCount != 0 &&
+        visibleItems.last().index + beyondBoundsItemCount <= itemsCount - 1
+    ) {
+        visibleItems.last().index to visibleItems.last().index + beyondBoundsItemCount
+    } else {
+        EmptyRange
+    }
+
+    val (startBeyondBoundItems, endBeyondBoundItems) = if (beyondBoundsInfo.hasIntervals() &&
+        visibleItems.last().index < beyondBoundsInfo.endIndex()
+    ) {
+        val start = (visibleItems.last().index + beyondBoundsItemCount).coerceAtMost(itemsCount - 1)
+        val end =
+            (beyondBoundsInfo.endIndex() + beyondBoundsItemCount).coerceAtMost(itemsCount - 1)
+        start to end
+    } else {
+        EmptyRange
+    }
+
+    return if (startNonVisibleItems.notInEmptyRange && startBeyondBoundItems.notInEmptyRange) {
+        addItemsAfter(
+            startNonVisibleItems,
+            endBeyondBoundItems
+        )
+    } else if (startNonVisibleItems.notInEmptyRange) {
+        addItemsAfter(startNonVisibleItems, endNonVisibleItems)
+    } else if (startBeyondBoundItems.notInEmptyRange) {
+        addItemsAfter(startBeyondBoundItems, endBeyondBoundItems)
+    } else {
+        emptyList()
+    }
+}
+
+private fun createItemsBeforeList(
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    currentFirstItemIndex: DataIndex,
+    itemProvider: LazyMeasuredItemProvider,
+    itemsCount: Int,
+    beyondBoundsItemCount: Int
+): List<LazyMeasuredItem> {
+
+    fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
+
+    fun addItemsBefore(startIndex: Int, endIndex: Int): List<LazyMeasuredItem> {
+        return mutableListOf<LazyMeasuredItem>().apply {
+            for (i in startIndex downTo endIndex) {
+                val item = itemProvider.getAndMeasure(DataIndex(i))
+                add(item)
+            }
+        }
+    }
+
+    val (startNonVisibleItems, endNonVisibleItems) =
+        if (beyondBoundsItemCount != 0 && currentFirstItemIndex.value - beyondBoundsItemCount > 0) {
+            currentFirstItemIndex.value - 1 to currentFirstItemIndex.value - beyondBoundsItemCount
+        } else {
+            EmptyRange
+        }
+
+    val (startBeyondBoundItems, endBeyondBoundItems) = if (beyondBoundsInfo.hasIntervals() &&
+        currentFirstItemIndex.value > beyondBoundsInfo.startIndex()
+    ) {
+        val start =
+            (currentFirstItemIndex.value - beyondBoundsItemCount - 1).coerceAtLeast(0)
+        val end = (beyondBoundsInfo.startIndex() - beyondBoundsItemCount).coerceAtLeast(0)
+        start to end
+    } else {
+        EmptyRange
+    }
+
+    return if (startNonVisibleItems.notInEmptyRange && startBeyondBoundItems.notInEmptyRange) {
+        addItemsBefore(
+            startNonVisibleItems,
+            endBeyondBoundItems
+        )
+    } else if (startNonVisibleItems.notInEmptyRange) {
+        addItemsBefore(startNonVisibleItems, endNonVisibleItems)
+    } else if (startBeyondBoundItems.notInEmptyRange) {
+        addItemsBefore(startBeyondBoundItems, endBeyondBoundItems)
+    } else {
+        emptyList()
+    }
+}
+
 /**
  * Calculates [LazyMeasuredItem]s offsets.
  */
@@ -428,3 +534,7 @@
     }
     return target
 }
+
+private val EmptyRange = Int.MIN_VALUE to Int.MIN_VALUE
+private val Int.notInEmptyRange
+    get() = this != Int.MIN_VALUE
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
index 8242e4a..24230f3 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
@@ -17,6 +17,7 @@
 package androidx.tv.foundation.lazy.list
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -90,7 +91,10 @@
     @ExperimentalFoundationApi
     fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
         Snapshot.withoutReadObservation {
-            update(findLazyListIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+            update(
+                DataIndex(itemProvider.findIndexByKey(lastKnownFirstItemKey, index.value)),
+                scrollOffset
+            )
         }
     }
 
@@ -103,34 +107,31 @@
             this.scrollOffset = scrollOffset
         }
     }
+}
 
-    private companion object {
-        /**
-         * Finds a position of the item with the given key in the lists. This logic allows us to
-         * detect when there were items added or removed before our current first item.
-         */
-        @ExperimentalFoundationApi
-        private fun findLazyListIndexByKey(
-            key: Any?,
-            lastKnownIndex: DataIndex,
-            itemProvider: LazyListItemProvider
-        ): DataIndex {
-            if (key == null) {
-                // there were no real item during the previous measure
-                return lastKnownIndex
-            }
-            if (lastKnownIndex.value < itemProvider.itemCount &&
-                key == itemProvider.getKey(lastKnownIndex.value)
-            ) {
-                // this item is still at the same index
-                return lastKnownIndex
-            }
-            val newIndex = itemProvider.keyToIndexMap[key]
-            if (newIndex != null) {
-                return DataIndex(newIndex)
-            }
-            // fallback to the previous index if we don't know the new index of the item
-            return lastKnownIndex
-        }
+/**
+ * Finds a position of the item with the given key in the lists. This logic allows us to
+ * detect when there were items added or removed before our current first item.
+ */
+@ExperimentalFoundationApi
+internal fun LazyLayoutItemProvider.findIndexByKey(
+    key: Any?,
+    lastKnownIndex: Int,
+): Int {
+    if (key == null) {
+        // there were no real item during the previous measure
+        return lastKnownIndex
     }
+    if (lastKnownIndex < itemCount &&
+        key == getKey(lastKnownIndex)
+    ) {
+        // this item is still at the same index
+        return lastKnownIndex
+    }
+    val newIndex = keyToIndexMap[key]
+    if (newIndex != null) {
+        return newIndex
+    }
+    // fallback to the previous index if we don't know the new index of the item
+    return lastKnownIndex
 }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
index 2f0cc8c..2de3658 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
+import androidx.tv.foundation.lazy.layout.animateScrollToItem
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
@@ -88,6 +89,8 @@
     private val scrollPosition =
         LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
 
+    private val animateScrollScope = LazyListAnimateScrollScope(this)
+
     /**
      * The index of the first item that is visible.
      *
@@ -309,7 +312,6 @@
         }
         val info = layoutInfo
         if (info.visibleItemsInfo.isNotEmpty()) {
-            // check(isActive)
             val scrollingForward = delta < 0
             val indexToPrefetch = if (scrollingForward) {
                 info.visibleItemsInfo.last().index + 1
@@ -335,6 +337,21 @@
         }
     }
 
+    private fun cancelPrefetchIfVisibleItemsChanged(info: TvLazyListLayoutInfo) {
+        if (indexToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
+            val expectedPrefetchIndex = if (wasScrollingForward) {
+                info.visibleItemsInfo.last().index + 1
+            } else {
+                info.visibleItemsInfo.first().index - 1
+            }
+            if (indexToPrefetch != expectedPrefetchIndex) {
+                indexToPrefetch = -1
+                currentPrefetchHandle?.cancel()
+                currentPrefetchHandle = null
+            }
+        }
+    }
+
     internal val prefetchState = LazyLayoutPrefetchState()
 
     /**
@@ -350,7 +367,7 @@
         index: Int,
         scrollOffset: Int = 0
     ) {
-        doSmoothScrollToItem(index, scrollOffset)
+        animateScrollScope.animateScrollToItem(index, scrollOffset)
     }
 
     /**
@@ -370,21 +387,6 @@
         cancelPrefetchIfVisibleItemsChanged(result)
     }
 
-    private fun cancelPrefetchIfVisibleItemsChanged(info: TvLazyListLayoutInfo) {
-        if (indexToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
-            val expectedPrefetchIndex = if (wasScrollingForward) {
-                info.visibleItemsInfo.last().index + 1
-            } else {
-                info.visibleItemsInfo.first().index - 1
-            }
-            if (indexToPrefetch != expectedPrefetchIndex) {
-                indexToPrefetch = -1
-                currentPrefetchHandle?.cancel()
-                currentPrefetchHandle = null
-            }
-        }
-    }
-
     /**
      * When the user provided custom keys for the items we can try to detect when there were
      * items added or removed before our current first visible item and keep this item
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
index 1e3bb60..560f0f1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
@@ -17,129 +17,62 @@
 package androidx.tv.foundation.lazy.list
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.CollectionInfo
 import androidx.compose.ui.semantics.ScrollAxisRange
-import androidx.compose.ui.semantics.collectionInfo
-import androidx.compose.ui.semantics.horizontalScrollAxisRange
-import androidx.compose.ui.semantics.indexForKey
-import androidx.compose.ui.semantics.scrollBy
-import androidx.compose.ui.semantics.scrollToIndex
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.verticalScrollAxisRange
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
+import androidx.tv.foundation.lazy.layout.LazyLayoutSemanticState
 
 // TODO (b/233188423): Address IllegalExperimentalApiUsage before moving to beta
 @Suppress("ComposableModifierFactory", "ModifierInspectorInfo", "IllegalExperimentalApiUsage")
 @ExperimentalFoundationApi
 @Composable
-internal fun Modifier.lazyListSemantics(
-    itemProvider: LazyListItemProvider,
+internal fun rememberLazyListSemanticState(
     state: TvLazyListState,
-    coroutineScope: CoroutineScope,
-    isVertical: Boolean,
+    itemProvider: LazyLayoutItemProvider,
     reverseScrolling: Boolean,
-    userScrollEnabled: Boolean
-) = this.then(
-    remember(
-        itemProvider,
-        state,
-        isVertical,
-        reverseScrolling,
-        userScrollEnabled
-    ) {
-        val indexForKeyMapping: (Any) -> Int = { needle ->
-            val key = itemProvider::getKey
-            var result = -1
-            for (index in 0 until itemProvider.itemCount) {
-                if (key(index) == needle) {
-                    result = index
-                    break
-                }
-            }
-            result
-        }
+    isVertical: Boolean
+): LazyLayoutSemanticState =
+    remember(state, itemProvider, reverseScrolling, isVertical) {
+        object : LazyLayoutSemanticState {
+            override fun scrollAxisRange(): ScrollAxisRange =
+                ScrollAxisRange(
+                    value = {
+                        // This is a simple way of representing the current position without
+                        // needing any lazy items to be measured. It's good enough so far, because
+                        // screen-readers care mostly about whether scroll position changed or not
+                        // rather than the actual offset in pixels.
+                        state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+                    },
+                    maxValue = {
+                        if (state.canScrollForward) {
+                            // If we can scroll further, we don't know the end yet,
+                            // but it's upper bounded by #items + 1
+                            itemProvider.itemCount + 1f
+                        } else {
+                            // If we can't scroll further, the current value is the max
+                            state.firstVisibleItemIndex +
+                                state.firstVisibleItemScrollOffset / 100_000f
+                        }
+                    },
+                    reverseScrolling = reverseScrolling
+                )
 
-        val accessibilityScrollState = ScrollAxisRange(
-            value = {
-                // This is a simple way of representing the current position without
-                // needing any lazy items to be measured. It's good enough so far, because
-                // screen-readers care mostly about whether scroll position changed or not
-                // rather than the actual offset in pixels.
-                state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
-            },
-            maxValue = {
-                if (state.canScrollForward) {
-                    // If we can scroll further, we don't know the end yet,
-                    // but it's upper bounded by #items + 1
-                    itemProvider.itemCount + 1f
+            override suspend fun animateScrollBy(delta: Float) {
+                state.animateScrollBy(delta)
+            }
+
+            override suspend fun scrollToItem(index: Int) {
+                state.scrollToItem(index)
+            }
+
+            override fun collectionInfo(): CollectionInfo =
+                if (isVertical) {
+                    CollectionInfo(rowCount = -1, columnCount = 1)
                 } else {
-                    // If we can't scroll further, the current value is the max
-                    state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+                    CollectionInfo(rowCount = 1, columnCount = -1)
                 }
-            },
-            reverseScrolling = reverseScrolling
-        )
-        val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
-            { x, y ->
-                val delta = if (isVertical) {
-                    y
-                } else {
-                    x
-                }
-                coroutineScope.launch {
-                    (state as ScrollableState).animateScrollBy(delta)
-                }
-                // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
-                true
-            }
-        } else {
-            null
         }
-
-        val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
-            { index ->
-                require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
-                    "Can't scroll to index $index, it is out of " +
-                        "bounds [0, ${state.layoutInfo.totalItemsCount})"
-                }
-                coroutineScope.launch {
-                    state.scrollToItem(index)
-                }
-                true
-            }
-        } else {
-            null
-        }
-
-        val collectionInfo = CollectionInfo(
-            rowCount = if (isVertical) -1 else 1,
-            columnCount = if (isVertical) 1 else -1
-        )
-
-        Modifier.semantics {
-            indexForKey(indexForKeyMapping)
-
-            if (isVertical) {
-                verticalScrollAxisRange = accessibilityScrollState
-            } else {
-                horizontalScrollAxisRange = accessibilityScrollState
-            }
-
-            if (scrollByAction != null) {
-                scrollBy(action = scrollByAction)
-            }
-
-            if (scrollToIndexAction != null) {
-                scrollToIndex(action = scrollToIndexAction)
-            }
-
-            this.collectionInfo = collectionInfo
-        }
-    }
-)
+    }
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
index cc31459..89559d8 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
@@ -57,27 +57,32 @@
      * The size of the viewport in pixels. It is the lazy list layout size including all the
      * content paddings.
      */
+    // DO NOT ADD DEFAULT get() HERE
     val viewportSize: IntSize
 
     /**
      * The orientation of the lazy list.
      */
+    // DO NOT ADD DEFAULT get() HERE
     val orientation: Orientation
 
     /**
      * True if the direction of scrolling and layout is reversed.
      */
+    // DO NOT ADD DEFAULT get() HERE
     val reverseLayout: Boolean
 
     /**
      * The content padding in pixels applied before the first item in the direction of scrolling.
      * For example it is a top content padding for LazyColumn with reverseLayout set to false.
      */
+    // DO NOT ADD DEFAULT get() HERE
     val beforeContentPadding: Int
 
     /**
      * The content padding in pixels applied after the last item in the direction of scrolling.
      * For example it is a bottom content padding for LazyColumn with reverseLayout set to false.
      */
+    // DO NOT ADD DEFAULT get() HERE
     val afterContentPadding: Int
 }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
index e91c249..23e84e4 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
@@ -18,8 +18,10 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
 import androidx.compose.foundation.lazy.layout.MutableIntervalList
 import androidx.compose.runtime.Composable
+import androidx.tv.foundation.ExperimentalTvFoundationApi
 
 @Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
 @OptIn(ExperimentalFoundationApi::class)
@@ -61,10 +63,25 @@
             )
         )
     }
+
+    @ExperimentalTvFoundationApi
+    override fun stickyHeader(
+        key: Any?,
+        contentType: Any?,
+        content: @Composable TvLazyListItemScope.() -> Unit
+    ) {
+        val headersIndexes = _headerIndexes ?: mutableListOf<Int>().also {
+            _headerIndexes = it
+        }
+        headersIndexes.add(_intervals.size)
+
+        item(key, contentType, content)
+    }
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 internal class LazyListIntervalContent(
-    val key: ((index: Int) -> Any)?,
-    val type: ((index: Int) -> Any?),
+    override val key: ((index: Int) -> Any)?,
+    override val type: ((index: Int) -> Any?),
     val item: @Composable TvLazyListItemScope.(index: Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 50fc402..892bde3 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -68,11 +68,3 @@
     description = "build TV applications using controls that adhere to Material Design Language."
     targetsJavaConsumers = false
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
index 9cda913..6ed68d5 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
@@ -34,6 +34,7 @@
 import androidx.tv.material.TabDefaults
 import androidx.tv.material.TabRow
 import androidx.tv.material.TabRowDefaults
+import kotlinx.coroutines.delay
 
 enum class Navigation(val displayName: String, val action: @Composable () -> Unit) {
   LazyRowsAndColumns("Lazy Rows and Columns", { LazyRowsAndColumns() }),
@@ -55,7 +56,12 @@
     updateSelectedTab = { selectedTabIndex = it }
   )
 
-  LaunchedEffect(selectedTabIndex) { updateSelectedTab(Navigation.values()[selectedTabIndex]) }
+  LaunchedEffect(selectedTabIndex) {
+    // Only update the tab after 250 milliseconds to avoid loading intermediate tabs while
+    // fast scrolling in the TabRow
+    delay(250)
+    updateSelectedTab(Navigation.values()[selectedTabIndex])
+  }
 }
 
 /**
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 4d7713c..c4d2004 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -106,7 +106,7 @@
     method @androidx.compose.runtime.Composable public static void CurvedLayout(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional androidx.wear.compose.foundation.CurvedAlignment.Radial? radialAlignment, optional int angularDirection, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> contentBuilder);
   }
 
-  @androidx.compose.runtime.Stable public sealed interface CurvedModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public sealed interface CurvedModifier {
     method public default infix androidx.wear.compose.foundation.CurvedModifier then(androidx.wear.compose.foundation.CurvedModifier other);
     field public static final androidx.wear.compose.foundation.CurvedModifier.Companion Companion;
   }
diff --git a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
index 4d7713c..c4d2004 100644
--- a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
@@ -106,7 +106,7 @@
     method @androidx.compose.runtime.Composable public static void CurvedLayout(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional androidx.wear.compose.foundation.CurvedAlignment.Radial? radialAlignment, optional int angularDirection, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> contentBuilder);
   }
 
-  @androidx.compose.runtime.Stable public sealed interface CurvedModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public sealed interface CurvedModifier {
     method public default infix androidx.wear.compose.foundation.CurvedModifier then(androidx.wear.compose.foundation.CurvedModifier other);
     field public static final androidx.wear.compose.foundation.CurvedModifier.Companion Companion;
   }
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 4d7713c..c4d2004 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -106,7 +106,7 @@
     method @androidx.compose.runtime.Composable public static void CurvedLayout(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional androidx.wear.compose.foundation.CurvedAlignment.Radial? radialAlignment, optional int angularDirection, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> contentBuilder);
   }
 
-  @androidx.compose.runtime.Stable public sealed interface CurvedModifier {
+  @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public sealed interface CurvedModifier {
     method public default infix androidx.wear.compose.foundation.CurvedModifier then(androidx.wear.compose.foundation.CurvedModifier other);
     field public static final androidx.wear.compose.foundation.CurvedModifier.Companion Companion;
   }
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 716a32a..cca24b1 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -102,6 +102,10 @@
     testOptions.unitTests.includeAndroidResources = true
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/wear/compose/foundation"
+
+    buildTypes.all {
+        consumerProguardFiles("proguard-rules.pro")
+    }
     namespace "androidx.wear.compose.foundation"
 }
 
diff --git a/wear/compose/compose-foundation/proguard-rules.pro b/wear/compose/compose-foundation/proguard-rules.pro
new file mode 100644
index 0000000..9c18e80
--- /dev/null
+++ b/wear/compose/compose-foundation/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+-keepnames class androidx.wear.compose.foundation.BasicCurvedTextKt
+
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
index c48a7389..fd7d85d 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.Placeable
 
+@JvmDefaultWithCompatibility
 /**
  * An ordered, immutable, collection of modifier elements that work with curved components, in a
  * polar coordinate space.
diff --git a/wear/compose/compose-material-core/api/current.txt b/wear/compose/compose-material-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material-core/api/public_plus_experimental_current.txt b/wear/compose/compose-material-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material-core/api/res-current.txt b/wear/compose/compose-material-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/compose/compose-material-core/api/res-current.txt
diff --git a/wear/compose/compose-material-core/api/restricted_current.txt b/wear/compose/compose-material-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material-core/build.gradle b/wear/compose/compose-material-core/build.gradle
new file mode 100644
index 0000000..30aac6a
--- /dev/null
+++ b/wear/compose/compose-material-core/build.gradle
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.AndroidXComposePlugin
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+}
+
+// Disable multi-platform.
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+        api(project(":compose:foundation:foundation"))
+        api(project(":compose:ui:ui"))
+        api(project(":compose:ui:ui-text"))
+        api(project(":compose:runtime:runtime"))
+
+        implementation(libs.kotlinStdlib)
+        implementation(project(":compose:animation:animation"))
+        implementation(project(":compose:material:material-icons-core"))
+        implementation(project(":compose:material:material-ripple"))
+        implementation(project(":compose:ui:ui-util"))
+        implementation(project(":wear:compose:compose-foundation"))
+        implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+
+        androidTestImplementation(project(":compose:ui:ui-test"))
+        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+        androidTestImplementation(project(":compose:test-utils"))
+
+        androidTestImplementation(project(":test:screenshot:screenshot"))
+        androidTestImplementation(libs.testRunner)
+        androidTestImplementation(libs.truth)
+
+        testImplementation(libs.testRules)
+        testImplementation(libs.testRunner)
+        testImplementation(libs.junit)
+    }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+    androidXComposeMultiplatform {
+        android()
+        desktop()
+    }
+
+    kotlin {
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block above
+         */
+        sourceSets {
+            commonMain.dependencies {
+                implementation(libs.kotlinStdlibCommon)
+
+                api(project(":compose:foundation:foundation"))
+                api(project(":compose:ui:ui"))
+                api(project(":compose:ui:ui-text"))
+                api(project(":compose:runtime:runtime"))
+                api("androidx.annotation:annotation:1.1.0")
+
+                implementation(project(":compose:animation:animation"))
+                implementation(project(":compose:material:material-icons-core"))
+                implementation(project(":compose:material:material-ripple"))
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":wear:compose:compose-foundation"))
+            }
+            jvmMain.dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+
+            commonTest.dependencies {
+                implementation(kotlin("test-junit"))
+            }
+            androidAndroidTest.dependencies {
+                implementation(libs.testExtJunit)
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.truth)
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":compose:ui:ui-test"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
+                implementation(project(":test:screenshot:screenshot"))
+            }
+        }
+    }
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+    sourceSets.androidTest.assets.srcDirs +=
+            project.rootDir.absolutePath + "/../../golden/wear/compose/materialcore"
+    buildTypes.all {
+        consumerProguardFiles("proguard-rules.pro")
+    }
+    namespace "androidx.wear.compose.material.core"
+    lint {
+        baseline = file("lint-baseline.xml")
+    }
+}
+
+androidx {
+    name = "Android Wear Compose Material Core"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_COMPOSE
+    inceptionYear = "2022"
+    description = "WearOS Compose Material Core Library. This library contains themeless " +
+            "components that are shared between different WearOS Compose Material libraries. It " +
+            "builds upon the Jetpack Compose libraries."
+    targetsJavaConsumers = false
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=all",
+        ]
+    }
+}
diff --git a/wear/compose/compose-material-core/proguard-rules.pro b/wear/compose/compose-material-core/proguard-rules.pro
new file mode 100644
index 0000000..5987eb2
--- /dev/null
+++ b/wear/compose/compose-material-core/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+# -keepnames class androidx.wear.compose.material.MaterialTheme
diff --git a/wear/compose/compose-material-core/src/androidMain/AndroidManifest.xml b/wear/compose/compose-material-core/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..e0788d6
--- /dev/null
+++ b/wear/compose/compose-material-core/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest />
diff --git a/wear/compose/compose-material/api/current.ignore b/wear/compose/compose-material/api/current.ignore
new file mode 100644
index 0000000..05e58f9
--- /dev/null
+++ b/wear/compose/compose-material/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
+    Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index c87b5b0..26422ef 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -634,7 +634,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
     field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
-    field public static final String TimeFormat12Hours = "h:mm a";
+    field public static final String TimeFormat12Hours = "h:mm";
     field public static final String TimeFormat24Hours = "HH:mm";
   }
 
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index da6559b..a231a61 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -735,7 +735,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
     field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
-    field public static final String TimeFormat12Hours = "h:mm a";
+    field public static final String TimeFormat12Hours = "h:mm";
     field public static final String TimeFormat24Hours = "HH:mm";
   }
 
diff --git a/wear/compose/compose-material/api/restricted_current.ignore b/wear/compose/compose-material/api/restricted_current.ignore
new file mode 100644
index 0000000..05e58f9
--- /dev/null
+++ b/wear/compose/compose-material/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
+    Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index c87b5b0..26422ef 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -634,7 +634,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
     field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
-    field public static final String TimeFormat12Hours = "h:mm a";
+    field public static final String TimeFormat12Hours = "h:mm";
     field public static final String TimeFormat24Hours = "HH:mm";
   }
 
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index d19e7c5..52f456a 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -36,10 +36,11 @@
 
         implementation(libs.kotlinStdlib)
         implementation(project(":compose:animation:animation"))
-        implementation(project(":compose:material:material"))
+        implementation(project(":compose:material:material-icons-core"))
         implementation(project(":compose:material:material-ripple"))
         implementation(project(":compose:ui:ui-util"))
         implementation(project(":wear:compose:compose-foundation"))
+        implementation(project(":wear:compose:compose-material-core"))
         implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
         androidTestImplementation(project(":compose:ui:ui-test"))
@@ -80,10 +81,11 @@
                 api("androidx.annotation:annotation:1.1.0")
 
                 implementation(project(":compose:animation:animation"))
-                implementation(project(":compose:material:material"))
+                implementation(project(":compose:material:material-icons-core"))
                 implementation(project(":compose:material:material-ripple"))
                 implementation(project(":compose:ui:ui-util"))
                 implementation(project(":wear:compose:compose-foundation"))
+                implementation(project(":wear:compose:compose-material-core"))
             }
             jvmMain.dependencies {
                 implementation(libs.kotlinStdlib)
@@ -118,6 +120,9 @@
     testOptions.unitTests.includeAndroidResources = true
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/wear/compose/material"
+    buildTypes.all {
+        consumerProguardFiles("proguard-rules.pro")
+    }
     namespace "androidx.wear.compose.material"
     lint {
         baseline = file("lint-baseline.xml")
@@ -135,11 +140,3 @@
             " libraries."
     targetsJavaConsumers = false
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += [
-                "-Xjvm-default=all",
-        ]
-    }
-}
diff --git a/wear/compose/compose-material/proguard-rules.pro b/wear/compose/compose-material/proguard-rules.pro
new file mode 100644
index 0000000..a38fa51
--- /dev/null
+++ b/wear/compose/compose-material/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+-keepnames class androidx.wear.compose.material.MaterialTheme
+-keepnames class androidx.wear.compose.material.ScalingLazyColumnKt
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
index d83eac1..a71d872 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
@@ -19,9 +19,9 @@
 import android.util.Log
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material.Surface
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Add
 import androidx.compose.runtime.Composable
@@ -67,11 +67,11 @@
 
 fun ComposeContentTestRule.setContentWithTheme(
     modifier: Modifier = Modifier,
-    composable: @Composable () -> Unit
+    composable: @Composable BoxScope.() -> Unit
 ) {
     setContent {
         MaterialTheme {
-            Surface(modifier = modifier, content = composable)
+            Box(modifier = modifier, content = composable)
         }
     }
 }
@@ -84,16 +84,14 @@
 ): SemanticsNodeInteraction {
     setContent {
         MaterialTheme {
-            Surface {
-                Box {
-                    Box(
-                        Modifier.sizeIn(
-                            maxWidth = parentMaxWidth,
-                            maxHeight = parentMaxHeight
-                        ).testTag("containerForSizeAssertion")
-                    ) {
-                        content()
-                    }
+            Box {
+                Box(
+                    Modifier.sizeIn(
+                        maxWidth = parentMaxWidth,
+                        maxHeight = parentMaxHeight
+                    ).testTag("containerForSizeAssertion")
+                ) {
+                    content()
                 }
             }
         }
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
index a20b5bc..936958e 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
@@ -123,9 +123,9 @@
             )
         }
         rule.waitForIdle()
-        // by default fully filled progress approximately takes 23-26% of the control
+        // by default fully filled progress approximately takes 23-27% of the control
         rule.onNodeWithTag(TEST_TAG).captureToImage()
-            .assertColorInPercentageRange(Color.Yellow, 23f..26f)
+            .assertColorInPercentageRange(Color.Yellow, 23f..27f)
         rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Red)
     }
 
@@ -142,9 +142,9 @@
         }
         rule.waitForIdle()
         rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Yellow)
-        // by default progress track approximately takes 23-26% of the control
+        // by default progress track approximately takes 23-27% of the control
         rule.onNodeWithTag(TEST_TAG).captureToImage()
-            .assertColorInPercentageRange(Color.Red, 23f..26f)
+            .assertColorInPercentageRange(Color.Red, 23f..27f)
     }
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -241,7 +241,7 @@
         rule.waitForIdle()
         // Because of the stroke cap, progress color takes a little bit more space than track color
         rule.onNodeWithTag(TEST_TAG).captureToImage()
-            .assertColorInPercentageRange(Color.Yellow, 24f..27f)
+            .assertColorInPercentageRange(Color.Yellow, 24f..28f)
         rule.onNodeWithTag(TEST_TAG).captureToImage()
             .assertColorInPercentageRange(Color.Red, 18f..23f)
     }
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
index 382300c..e9644a6 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.wear.compose.foundation.curvedComposable
 import java.util.Calendar
+import java.util.TimeZone
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -541,6 +542,22 @@
         }
         assertEquals(convertedTime, actualTime)
     }
+
+    @Test
+    fun formats_current_time_12H() {
+        val currentTimeInMillis = 1631544258000L // 2021-09-13 14:44:18
+        val expectedTime = "2:44"
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
+
+        var actualTime: String? = null
+        rule.setContentWithTheme {
+            actualTime = currentTime(
+                { currentTimeInMillis },
+                TimeTextDefaults.TimeFormat12Hours
+            ).value
+        }
+        assertEquals(expectedTime, actualTime)
+    }
 }
 
 private const val LINEAR_ITEM_TAG = "LINEAR_ITEM_TAG"
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
index 494693d..9e694ec 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
@@ -18,6 +18,7 @@
 
 import android.os.Build
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.runtime.Composable
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
@@ -124,7 +125,7 @@
 
     private fun verifyScreenshot(
         threshold: Double = 0.98,
-        content: @Composable () -> Unit
+        content: @Composable BoxScope.() -> Unit
     ) {
         rule.setContentWithTheme(composable = content)
         rule.onNodeWithTag(TEST_TAG)
diff --git a/wear/compose/compose-material/src/androidMain/baseline-prof.txt b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
index 2eb04f1..f2a2297 100644
--- a/wear/compose/compose-material/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
@@ -1,5 +1,5 @@
 HPLandroidx/wear/compose/material/AbstractPlaceholderModifier;->**(**)**
-PLandroidx/wear/compose/material/AnimationKt**->**(**)**
+SPLandroidx/wear/compose/material/AnimationKt**->**(**)**
 PLandroidx/wear/compose/material/AutoCenteringParams;->**(**)**
 Landroidx/wear/compose/material/ButtonBorder;
 Landroidx/wear/compose/material/ButtonColors;
@@ -8,7 +8,7 @@
 HSPLandroidx/wear/compose/material/CardDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/CardKt**->**(**)**
 Landroidx/wear/compose/material/CheckboxColors;
-HPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
 Landroidx/wear/compose/material/ChipBorder;
 Landroidx/wear/compose/material/ChipColors;
 HSPLandroidx/wear/compose/material/ChipDefaults;->**(**)**
@@ -22,16 +22,16 @@
 HSPLandroidx/wear/compose/material/CurvedTextKt**->**(**)**
 HSPLandroidx/wear/compose/material/DefaultButtonBorder;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultButtonColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultChipColors;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultInlineSliderColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultScalingLazyListItemInfo;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultScalingLazyListLayoutInfo;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultScalingParams;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultSplitToggleChipColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
 SPLandroidx/wear/compose/material/DefaultTimeSource;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultTimeSourceKt**->**(**)**
 SPLandroidx/wear/compose/material/DefaultToggleButtonColors;->**(**)**
@@ -53,17 +53,17 @@
 HSPLandroidx/wear/compose/material/MaterialTheme;->**(**)**
 SPLandroidx/wear/compose/material/MaterialThemeKt**->**(**)**
 HSPLandroidx/wear/compose/material/Modifiers;->**(**)**
-PLandroidx/wear/compose/material/PainterWithBrushOverlay;->**(**)**
 HSPLandroidx/wear/compose/material/PickerDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/PickerKt**->**(**)**
 SPLandroidx/wear/compose/material/PickerScopeImpl;->**(**)**
 HSPLandroidx/wear/compose/material/PickerState;->**(**)**
+PLandroidx/wear/compose/material/PlaceholderBackgroundPainter;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderDefaults;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderKt**->**(**)**
 PLandroidx/wear/compose/material/PlaceholderModifier;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderShimmerModifier;->**(**)**
 PLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
-PLandroidx/wear/compose/material/PlaceholderState;->**(**)**
+HPLandroidx/wear/compose/material/PlaceholderState;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorAlignment;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
 Landroidx/wear/compose/material/PositionIndicatorState;
@@ -71,8 +71,9 @@
 PLandroidx/wear/compose/material/ProgressIndicatorDefaults;->**(**)**
 HPLandroidx/wear/compose/material/ProgressIndicatorKt**->**(**)**
 Landroidx/wear/compose/material/R;
-HPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
-HSPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
+Landroidx/wear/compose/material/RadioButtonColors;
+HSPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
+SPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
 SPLandroidx/wear/compose/material/RangeDefaultsKt**->**(**)**
 SPLandroidx/wear/compose/material/RangeIcons;->**(**)**
 HSPLandroidx/wear/compose/material/ResistanceConfig;->**(**)**
@@ -100,7 +101,6 @@
 HSPLandroidx/wear/compose/material/ShapesKt**->**(**)**
 HSPLandroidx/wear/compose/material/SliderKt**->**(**)**
 Landroidx/wear/compose/material/SplitToggleChipColors;
-SPLandroidx/wear/compose/material/SqueezeMotion;->**(**)**
 SPLandroidx/wear/compose/material/StepperDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/StepperKt**->**(**)**
 Landroidx/wear/compose/material/SwipeProgress;
@@ -113,7 +113,7 @@
 HSPLandroidx/wear/compose/material/SwipeableKt**->**(**)**
 HSPLandroidx/wear/compose/material/SwipeableState;->**(**)**
 Landroidx/wear/compose/material/SwitchColors;
-HPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/TextKt**->**(**)**
 Landroidx/wear/compose/material/ThresholdConfig;
 SPLandroidx/wear/compose/material/TimeBroadcastReceiver;->**(**)**
@@ -126,8 +126,8 @@
 Landroidx/wear/compose/material/ToggleChipColors;
 HSPLandroidx/wear/compose/material/ToggleChipDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/ToggleChipKt**->**(**)**
-HPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
-PLandroidx/wear/compose/material/ToggleStage;->**(**)**
+HSPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
+SPLandroidx/wear/compose/material/ToggleStage;->**(**)**
 HSPLandroidx/wear/compose/material/Typography;->**(**)**
 HSPLandroidx/wear/compose/material/TypographyKt**->**(**)**
 HSPLandroidx/wear/compose/material/VignetteKt**->**(**)**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
index 2ff5ef9..cb8a7d4 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
@@ -157,7 +157,7 @@
     /**
      * Default format for 12h clock.
      */
-    const val TimeFormat12Hours = "h:mm a"
+    const val TimeFormat12Hours = "h:mm"
 
     /**
      * The default content padding used by [TimeText]
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material3/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material3/api/public_plus_experimental_current.txt b/wear/compose/compose-material3/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material3/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material3/api/res-current.txt b/wear/compose/compose-material3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/compose/compose-material3/api/res-current.txt
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material3/benchmark/benchmark-proguard-rules.pro b/wear/compose/compose-material3/benchmark/benchmark-proguard-rules.pro
new file mode 100644
index 0000000..e4061d2
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/benchmark-proguard-rules.pro
@@ -0,0 +1,37 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-dontobfuscate
+
+-ignorewarnings
+
+-keepattributes *Annotation*
+
+-dontnote junit.framework.**
+-dontnote junit.runner.**
+
+-dontwarn androidx.test.**
+-dontwarn org.junit.**
+-dontwarn org.hamcrest.**
+-dontwarn com.squareup.javawriter.JavaWriter
+
+-keepclasseswithmembers @org.junit.runner.RunWith public class *
\ No newline at end of file
diff --git a/wear/compose/compose-material3/benchmark/build.gradle b/wear/compose/compose-material3/benchmark/build.gradle
new file mode 100644
index 0000000..9bd32e84
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+    id("org.jetbrains.kotlin.android")
+    id("androidx.benchmark")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    buildTypes.all {
+        consumerProguardFiles "benchmark-proguard-rules.pro"
+    }
+    namespace "androidx.wear.compose.material3.benchmark"
+}
+
+dependencies {
+
+    androidTestImplementation project(":benchmark:benchmark-junit4")
+    androidTestImplementation project(":compose:runtime:runtime")
+    androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
+    androidTestImplementation project(":compose:foundation:foundation")
+    androidTestImplementation project(":compose:runtime:runtime")
+    androidTestImplementation project(":compose:benchmark-utils")
+    androidTestImplementation project(":wear:compose:compose-foundation")
+    androidTestImplementation project(":wear:compose:compose-material3")
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.kotlinReflect)
+    androidTestImplementation(libs.kotlinTestCommon)
+    androidTestImplementation(libs.truth)
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/build.gradle b/wear/compose/compose-material3/build.gradle
new file mode 100644
index 0000000..297ea36
--- /dev/null
+++ b/wear/compose/compose-material3/build.gradle
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.AndroidXComposePlugin
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+}
+
+// Disable multi-platform.
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+        api(project(":compose:foundation:foundation"))
+        api(project(":compose:ui:ui"))
+        api(project(":compose:ui:ui-text"))
+        api(project(":compose:runtime:runtime"))
+
+        implementation(libs.kotlinStdlib)
+        implementation(project(":compose:animation:animation"))
+        implementation(project(":compose:material:material-icons-core"))
+        implementation(project(":compose:material:material-ripple"))
+        implementation(project(":compose:ui:ui-util"))
+        implementation(project(":wear:compose:compose-foundation"))
+        implementation(project(":wear:compose:compose-material-core"))
+        implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+
+        androidTestImplementation(project(":compose:ui:ui-test"))
+        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+        androidTestImplementation(project(":compose:test-utils"))
+
+        androidTestImplementation(project(":test:screenshot:screenshot"))
+        androidTestImplementation(libs.testRunner)
+        androidTestImplementation(libs.truth)
+
+        testImplementation(libs.testRules)
+        testImplementation(libs.testRunner)
+        testImplementation(libs.junit)
+
+        samples(project(":wear:compose:compose-material3-samples"))
+    }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+    androidXComposeMultiplatform {
+        android()
+        desktop()
+    }
+
+    kotlin {
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block above
+         */
+        sourceSets {
+            commonMain.dependencies {
+                implementation(libs.kotlinStdlibCommon)
+
+                api(project(":compose:foundation:foundation"))
+                api(project(":compose:ui:ui"))
+                api(project(":compose:ui:ui-text"))
+                api(project(":compose:runtime:runtime"))
+                api("androidx.annotation:annotation:1.1.0")
+
+                implementation(project(":compose:animation:animation"))
+                implementation(project(":compose:material:material-icons-core"))
+                implementation(project(":compose:material:material-ripple"))
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":wear:compose:compose-foundation"))
+                implementation(project(":wear:compose:compose-material-core"))
+            }
+            jvmMain.dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+
+            commonTest.dependencies {
+                implementation(kotlin("test-junit"))
+            }
+            androidAndroidTest.dependencies {
+                implementation(libs.testExtJunit)
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.truth)
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":compose:ui:ui-test"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
+                implementation(project(":test:screenshot:screenshot"))
+            }
+        }
+    }
+    dependencies {
+        samples(project(":wear:compose:compose-material3-samples"))
+    }
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+    sourceSets.androidTest.assets.srcDirs +=
+            project.rootDir.absolutePath + "/../../golden/wear/compose/material3"
+    buildTypes.all {
+        consumerProguardFiles("proguard-rules.pro")
+    }
+    namespace "androidx.wear.compose.material3"
+    lint {
+        baseline = file("lint-baseline.xml")
+    }
+}
+
+androidx {
+    name = "Android Wear Compose Material 3"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenVersion = LibraryVersions.WEAR_COMPOSE_MATERIAL3
+    mavenGroup = LibraryGroups.WEAR_COMPOSE
+    inceptionYear = "2022"
+    description = "WearOS Compose Material 3 Library. This library makes it easier for " +
+            "developers to write Jetpack Compose applications for Wearable devices that " +
+            "implement Wear Material 3 Design UX guidelines and specifications. It builds upon " +
+            "the Jetpack Compose libraries."
+    targetsJavaConsumers = false
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=all",
+        ]
+    }
+}
diff --git a/wear/compose/compose-material3/proguard-rules.pro b/wear/compose/compose-material3/proguard-rules.pro
new file mode 100644
index 0000000..ae637fe
--- /dev/null
+++ b/wear/compose/compose-material3/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+# -keepnames class androidx.wear.compose.material3.MaterialTheme
diff --git a/wear/compose/compose-material3/samples/build.gradle b/wear/compose/compose-material3/samples/build.gradle
new file mode 100644
index 0000000..03b746a
--- /dev/null
+++ b/wear/compose/compose-material3/samples/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+
+    implementation(libs.kotlinStdlib)
+
+    compileOnly(project(":annotation:annotation-sampled"))
+    implementation(project(":compose:animation:animation-graphics"))
+    implementation(project(":compose:foundation:foundation"))
+    implementation(project(":compose:foundation:foundation-layout"))
+    implementation(project(":compose:runtime:runtime"))
+    implementation(project(":compose:ui:ui"))
+    implementation(project(":compose:ui:ui-text"))
+    implementation(project(":compose:material:material-icons-core"))
+    implementation(project(":wear:compose:compose-material3"))
+    implementation(project(":wear:compose:compose-foundation"))
+
+    androidTestImplementation(project(":compose:ui:ui-test"))
+    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(project(":compose:test-utils"))
+
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.truth)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    namespace "androidx.wear.compose.material3.samples"
+}
+
+androidx {
+    name = "Android Wear Compose Material 3 Samples"
+    type = LibraryType.SAMPLES
+    mavenGroup = LibraryGroups.WEAR_COMPOSE
+    inceptionYear = "2022"
+    description = "Contains the sample code for the Android Wear Compose Material 3 Classes"
+}
diff --git a/wear/compose/compose-material3/src/androidMain/AndroidManifest.xml b/wear/compose/compose-material3/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..e0788d6
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest />
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 2f3d8cc..4f3ab02 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -51,6 +51,9 @@
     }
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
+    buildTypes.all {
+        consumerProguardFiles("proguard-rules.pro")
+    }
     namespace "androidx.wear.compose.navigation"
 }
 
diff --git a/wear/compose/compose-navigation/proguard-rules.pro b/wear/compose/compose-navigation/proguard-rules.pro
new file mode 100644
index 0000000..03b1b42
--- /dev/null
+++ b/wear/compose/compose-navigation/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+-keepnames class androidx.wear.compose.navigation.NavGraphBuilderKt
\ No newline at end of file
diff --git a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
index e343b99..5068468 100644
--- a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
@@ -1,4 +1,3 @@
-Landroidx/wear/compose/navigation/ComposableSingletons;
 HSPLandroidx/wear/compose/navigation/NavGraphBuilderKt**->**(**)**
 SPLandroidx/wear/compose/navigation/SwipeDismissableNavHostControllerKt**->**(**)**
 HSPLandroidx/wear/compose/navigation/SwipeDismissableNavHostKt**->**(**)**
diff --git a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
index b14e334..1945520 100644
--- a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
+++ b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.test.performScrollToNode
 import androidx.test.espresso.Espresso
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.wear.compose.integration.demos.Demo
 import androidx.wear.compose.integration.demos.DemoActivity
@@ -49,6 +50,7 @@
     @get:Rule
     val rule = createAndroidComposeRule<DemoActivity>()
 
+    @FlakyTest(bugId = 259724403)
     @Test
     fun navigateThroughAllDemos() {
         // Compose integration-tests are split into batches due to size,
diff --git a/wear/watchface/watchface-client/build.gradle b/wear/watchface/watchface-client/build.gradle
index 48e14cb..dcad002 100644
--- a/wear/watchface/watchface-client/build.gradle
+++ b/wear/watchface/watchface-client/build.gradle
@@ -62,9 +62,6 @@
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
     namespace "androidx.wear.watchface.client"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
 
 androidx {
@@ -73,4 +70,4 @@
     mavenGroup = LibraryGroups.WEAR_WATCHFACE
     inceptionYear = "2020"
     description = "Client library for controlling androidx watchfaces"
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index dc02bd9..85af687 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -39,7 +39,6 @@
 import androidx.test.screenshot.AndroidXScreenshotTestRule
 import androidx.test.screenshot.assertAgainstGolden
 import androidx.wear.watchface.BoundingArc
-import androidx.wear.watchface.ComplicationSlot
 import androidx.wear.watchface.ComplicationSlotBoundsType
 import androidx.wear.watchface.ContentDescriptionLabel
 import androidx.wear.watchface.DrawMode
@@ -242,30 +241,6 @@
         return value!!
     }
 
-    /**
-     * Updates the complications for [interactiveInstance] and waits until they have been applied.
-     */
-    protected fun updateComplicationsBlocking(
-        interactiveInstance: InteractiveWatchFaceClient,
-        slotIdToComplicationData: Map<Int, ComplicationData>
-    ) {
-        val slotIdToWatchForUpdates = slotIdToComplicationData.keys.first()
-        var slot: ComplicationSlot
-
-        runBlocking {
-            slot = engine.deferredWatchFaceImpl.await()
-                .complicationSlotsManager.complicationSlots[slotIdToWatchForUpdates]!!
-        }
-
-        val updateCountDownLatch = CountDownLatch(1)
-        handlerCoroutineScope.launch {
-            slot.complicationData.collect { updateCountDownLatch.countDown() }
-        }
-
-        interactiveInstance.updateComplicationData(slotIdToComplicationData)
-        assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
-    }
-
     protected fun tapOnComplication(
         interactiveInstance: InteractiveWatchFaceClient,
         slotId: Int
@@ -471,8 +446,7 @@
     fun updateComplicationData() {
         val interactiveInstance = getOrCreateTestSubject()
 
-        updateComplicationsBlocking(
-            interactiveInstance,
+        interactiveInstance.updateComplicationData(
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
                     rangedValueComplicationBuilder().build(),
@@ -1132,8 +1106,7 @@
             surfaceHolder
         )
         val interactiveInstance = getOrCreateTestSubject(wallpaperService)
-        updateComplicationsBlocking(
-            interactiveInstance,
+        interactiveInstance.updateComplicationData(
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
                     rangedValueComplicationBuilder()
@@ -1194,8 +1167,7 @@
             )
         )
 
-        updateComplicationsBlocking(
-            interactiveInstance,
+        interactiveInstance.updateComplicationData(
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
                     timelineComplication.toApiComplicationData()
diff --git a/wear/watchface/watchface-complications-data-source/build.gradle b/wear/watchface/watchface-complications-data-source/build.gradle
index a60100a..32940e3 100644
--- a/wear/watchface/watchface-complications-data-source/build.gradle
+++ b/wear/watchface/watchface-complications-data-source/build.gradle
@@ -49,9 +49,6 @@
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
     namespace "androidx.wear.watchface.complications.datasource"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
 
 androidx {
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index ff7ac43..c473a58 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -41,7 +41,7 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationPersistencePolicies INSTANCE;
   }
 
-  public interface ComplicationText {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ComplicationText {
     method public java.time.Instant getNextChangeTime(java.time.Instant afterInstant);
     method public CharSequence getTextAt(android.content.res.Resources resources, java.time.Instant instant);
     method public boolean isAlwaysEmpty();
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index c92a5a5..fdbd262 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -44,7 +44,7 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationPersistencePolicies INSTANCE;
   }
 
-  public interface ComplicationText {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ComplicationText {
     method public java.time.Instant getNextChangeTime(java.time.Instant afterInstant);
     method public CharSequence getTextAt(android.content.res.Resources resources, java.time.Instant instant);
     method public boolean isAlwaysEmpty();
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index db4540b..83c88a5 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -41,7 +41,7 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationPersistencePolicies INSTANCE;
   }
 
-  public interface ComplicationText {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ComplicationText {
     method public java.time.Instant getNextChangeTime(java.time.Instant afterInstant);
     method public CharSequence getTextAt(android.content.res.Resources resources, java.time.Instant instant);
     method public boolean isAlwaysEmpty();
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index 4b0bb1f..f335e6a 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -43,6 +43,7 @@
 
 private typealias WireTimeDependentText = android.support.wearable.complications.TimeDependentText
 
+@JvmDefaultWithCompatibility
 /**
  * The text within a complication.
  *
diff --git a/wear/watchface/watchface-editor/api/current.txt b/wear/watchface/watchface-editor/api/current.txt
index 817f024..a3e0615 100644
--- a/wear/watchface/watchface-editor/api/current.txt
+++ b/wear/watchface/watchface-editor/api/current.txt
@@ -34,7 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public androidx.wear.watchface.editor.EditorRequest createFromIntent(android.content.Intent intent) throws kotlinx.coroutines.TimeoutCancellationException;
   }
 
-  public interface EditorSession extends java.lang.AutoCloseable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface EditorSession extends java.lang.AutoCloseable {
     method @RequiresApi(27) @UiThread public default static androidx.wear.watchface.editor.EditorSession createHeadlessEditorSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
     method @UiThread @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public default static suspend Object? createOnWatchEditorSession(androidx.activity.ComponentActivity activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession>) throws kotlinx.coroutines.TimeoutCancellationException;
     method public Integer? getBackgroundComplicationSlotId();
diff --git a/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt b/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt
index 817f024..a3e0615 100644
--- a/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt
@@ -34,7 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public androidx.wear.watchface.editor.EditorRequest createFromIntent(android.content.Intent intent) throws kotlinx.coroutines.TimeoutCancellationException;
   }
 
-  public interface EditorSession extends java.lang.AutoCloseable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface EditorSession extends java.lang.AutoCloseable {
     method @RequiresApi(27) @UiThread public default static androidx.wear.watchface.editor.EditorSession createHeadlessEditorSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
     method @UiThread @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public default static suspend Object? createOnWatchEditorSession(androidx.activity.ComponentActivity activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession>) throws kotlinx.coroutines.TimeoutCancellationException;
     method public Integer? getBackgroundComplicationSlotId();
diff --git a/wear/watchface/watchface-editor/api/restricted_current.txt b/wear/watchface/watchface-editor/api/restricted_current.txt
index 817f024..a3e0615 100644
--- a/wear/watchface/watchface-editor/api/restricted_current.txt
+++ b/wear/watchface/watchface-editor/api/restricted_current.txt
@@ -34,7 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public androidx.wear.watchface.editor.EditorRequest createFromIntent(android.content.Intent intent) throws kotlinx.coroutines.TimeoutCancellationException;
   }
 
-  public interface EditorSession extends java.lang.AutoCloseable {
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface EditorSession extends java.lang.AutoCloseable {
     method @RequiresApi(27) @UiThread public default static androidx.wear.watchface.editor.EditorSession createHeadlessEditorSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
     method @UiThread @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public default static suspend Object? createOnWatchEditorSession(androidx.activity.ComponentActivity activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession>) throws kotlinx.coroutines.TimeoutCancellationException;
     method public Integer? getBackgroundComplicationSlotId();
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 699d7c2..bd12e2f 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -87,6 +87,7 @@
 
 private const val TAG = "EditorSession"
 
+@JvmDefaultWithCompatibility
 /**
  * Interface for manipulating watch face state during a watch face editing session. The editor
  * should adjust [userStyle] and call [openComplicationDataSourceChooser] to configure the watch
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 1d98429..d5c8fff 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -684,15 +684,20 @@
     }
 
     /**
-     * At most one [UserStyleSetting.ComplicationSlotsUserStyleSetting] can be active at a time
-     * based on the hierarchy of styles for any given [UserStyle]. This function finds the current
-     * active [UserStyleSetting.ComplicationSlotsUserStyleSetting] based upon the [userStyle] and,
-     * if there is one, it returns the corresponding selected [ComplicationSlotsOption]. Otherwise
-     * it returns `null`.
+     * When a UserStyleSchema contains hierarchical styles, only part of it is deemed to be active
+     * based on the user’s options in [userStyle]. Conversely if the UserStyleSchema doesn’t contain
+     * any hierarchical styles then all of it is considered to be active all the time.
+     *
+     * From the active portion of the UserStyleSchema we only allow there to be at most one
+     * [UserStyleSetting.ComplicationSlotsUserStyleSetting]. This function searches the active
+     * portion of the UserStyleSchema for the [UserStyleSetting.ComplicationSlotsUserStyleSetting],
+     * if one is found then it returns the selected [ComplicationSlotsOption] from that, based on
+     * the [userStyle]. If a [UserStyleSetting.ComplicationSlotsUserStyleSetting] is not found in
+     * the active portion of the UserStyleSchema it returns `null`.
      *
      * @param userStyle The [UserStyle] for which the function will search for the selected
      * [ComplicationSlotsOption], if any.
-     * @return The selected [ComplicationSlotsOption] for the [userStyle] if any, or `null`
+     * @return The selected [ComplicationSlotsOption] based on the [userStyle] if any, or `null`
      * otherwise.
      */
     public fun findComplicationSlotsOptionForUserStyle(
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index d59f829..ddfc94c 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -70,9 +70,6 @@
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
     namespace "androidx.wear.watchface"
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
 }
 
 androidx {
@@ -81,4 +78,4 @@
     mavenGroup = LibraryGroups.WEAR_WATCHFACE
     inceptionYear = "2020"
     description = "Android Wear Watchface"
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 5e8f737..2eb58be 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -47,6 +47,7 @@
 import android.view.SurfaceHolder
 import android.view.WindowInsets
 import android.view.accessibility.AccessibilityManager
+import androidx.annotation.AnyThread
 import androidx.annotation.Px
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
@@ -102,6 +103,8 @@
 import kotlinx.coroutines.Runnable
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
@@ -347,10 +350,20 @@
         public val XML_WATCH_FACE_METADATA =
             "androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
 
-        internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
-            engine: WatchFaceService.EngineWrapper?,
+        internal enum class ExecutionThread {
+            UI,
+            CURRENT
+        }
+        /**
+         * Waits for deferredValue using runBlocking, then executes the task on the thread
+         * specified by executionThread param.
+         */
+        private fun <R, V> awaitDeferredThenRunTaskOnThread(
+            engine: EngineWrapper?,
             traceName: String,
-            task: (watchFaceImpl: WatchFaceImpl) -> R
+            executionThread: ExecutionThread,
+            task: (deferredValue: V) -> R,
+            waitDeferred: suspend (engine: EngineWrapper) -> V
         ): R? = TraceEvent(traceName).use {
             if (engine == null) {
                 Log.w(TAG, "Task $traceName posted after close(), ignoring.")
@@ -359,9 +372,16 @@
             runBlocking {
                 try {
                     withTimeout(AWAIT_DEFERRED_TIMEOUT) {
-                        val watchFaceImpl = engine.deferredWatchFaceImpl.await()
-                        withContext(engine.uiThreadCoroutineScope.coroutineContext) {
-                            task(watchFaceImpl)
+                        val deferredValue = waitDeferred(engine)
+                        when (executionThread) {
+                            ExecutionThread.UI -> {
+                                withContext(engine.uiThreadCoroutineScope.coroutineContext) {
+                                    task(deferredValue)
+                                }
+                            }
+                            ExecutionThread.CURRENT -> {
+                                task(deferredValue)
+                            }
                         }
                     }
                 } catch (e: Exception) {
@@ -371,29 +391,24 @@
             }
         }
 
+        internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
+            engine: EngineWrapper?,
+            traceName: String,
+            task: (watchFaceImpl: WatchFaceImpl) -> R
+        ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+            it.deferredWatchFaceImpl.await()
+        }
+
         /**
          * During startup tasks will run before those posted by
          * [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking].
          */
-        internal fun <R> awaitDeferredWatchFaceThenRunOnBinderThread(
-            engine: WatchFaceService.EngineWrapper?,
+        internal fun <R> awaitDeferredWatchFaceThenRunOnUiThread(
+            engine: EngineWrapper?,
             traceName: String,
             task: (watchFace: WatchFace) -> R
-        ): R? = TraceEvent(traceName).use {
-            if (engine == null) {
-                Log.w(TAG, "Task $traceName posted after close(), ignoring.")
-                return null
-            }
-            runBlocking {
-                try {
-                    withTimeout(AWAIT_DEFERRED_TIMEOUT) {
-                        task(engine.deferredWatchFace.await())
-                    }
-                } catch (e: Exception) {
-                    Log.e(TAG, "Operation $traceName failed", e)
-                    throw e
-                }
-            }
+        ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+            it.deferredWatchFace.await()
         }
 
         /**
@@ -401,25 +416,13 @@
          * [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking] and
          * [awaitDeferredWatchFaceThenRunOnBinderThread].
          */
-        internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
-            engine: WatchFaceService.EngineWrapper?,
+        internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnThread(
+            engine: EngineWrapper?,
             traceName: String,
+            executionThread: ExecutionThread,
             task: (earlyInitDetails: EarlyInitDetails) -> R
-        ): R? = TraceEvent(traceName).use {
-            if (engine == null) {
-                Log.w(TAG, "Task $traceName ignored due to null engine.")
-                return null
-            }
-            runBlocking {
-                try {
-                    withTimeout(AWAIT_DEFERRED_TIMEOUT) {
-                        task(engine.deferredEarlyInitDetails.await())
-                    }
-                } catch (e: Exception) {
-                    Log.e(TAG, "Operation $traceName failed", e)
-                    throw e
-                }
-            }
+        ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, executionThread, task) {
+            it.deferredEarlyInitDetails.await()
         }
     }
 
@@ -816,30 +819,31 @@
     ) = TraceEvent(
         "WatchFaceService.writeComplicationCache"
     ).use {
-        // File IO can be slow so perform the write from a background thread.
-        getBackgroundThreadHandler().post {
-            try {
-                val stream = ByteArrayOutputStream()
-                val objectOutputStream = ObjectOutputStream(stream)
-                objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
-                for (slot in complicationSlotsManager.complicationSlots) {
-                    objectOutputStream.writeInt(slot.key)
-                    objectOutputStream.writeObject(
-                        if ((slot.value.complicationData.value.persistencePolicy and
-                              ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
-                        ) {
-                            NoDataComplicationData().asWireComplicationData()
-                        } else {
-                            slot.value.complicationData.value.asWireComplicationData()
-                        }
-                    )
-                }
-                objectOutputStream.close()
-                val byteArray = stream.toByteArray()
-                writeComplicationDataCacheByteArray(context, fileName, byteArray)
-            } catch (e: Exception) {
-                Log.w(TAG, "Failed to write to complication cache due to exception", e)
+        try {
+            val stream = ByteArrayOutputStream()
+            val objectOutputStream = ObjectOutputStream(stream)
+            objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
+            for (slot in complicationSlotsManager.complicationSlots) {
+                objectOutputStream.writeInt(slot.key)
+                objectOutputStream.writeObject(
+                    if ((slot.value.complicationData.value.persistencePolicy and
+                            ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
+                    ) {
+                        NoDataComplicationData().asWireComplicationData()
+                    } else {
+                        slot.value.complicationData.value.asWireComplicationData()
+                    }
+                )
             }
+            objectOutputStream.close()
+            val byteArray = stream.toByteArray()
+
+            // File IO can be slow so perform the write from a background thread.
+            getBackgroundThreadHandler().post {
+                writeComplicationDataCacheByteArray(context, fileName, byteArray)
+            }
+        } catch (e: Exception) {
+            Log.w(TAG, "Failed to write to complication cache due to exception", e)
         }
     }
 
@@ -1134,8 +1138,7 @@
     internal class EarlyInitDetails(
         val complicationSlotsManager: ComplicationSlotsManager,
         val userStyleRepository: CurrentUserStyleRepository,
-        val userStyleFlavors: UserStyleFlavors,
-        val surfaceHolder: SurfaceHolder
+        val userStyleFlavors: UserStyleFlavors
     )
 
     /** @hide */
@@ -1179,7 +1182,7 @@
          * [deferredSurfaceHolder] will complete after [onSurfaceChanged], before then it's not
          * safe to create a UiThread OpenGL context.
          */
-        internal var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
+        private var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
 
         internal val mutableWatchState = getMutableWatchState().apply {
             isVisible.value = this@EngineWrapper.isVisible || forceIsVisibleForTesting()
@@ -1264,9 +1267,9 @@
 
         private var asyncWatchFaceConstructionPending = false
 
-        // Stores the initial ComplicationSlots which could get updated before they're applied.
-        internal var pendingInitialComplications: List<IdAndComplicationDataWireFormat>? = null
-            private set
+        @VisibleForTesting
+        internal val complicationsFlow =
+            MutableStateFlow<List<IdAndComplicationDataWireFormat>>(emptyList())
 
         private var initialUserStyle: UserStyleWireFormat? = null
         internal lateinit var interactiveInstanceId: String
@@ -1524,61 +1527,46 @@
             }
         }
 
-        @UiThread
+        @AnyThread
         internal fun setComplicationDataList(
             complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
         ): Unit = TraceEvent("EngineWrapper.setComplicationDataList").use {
-            val earlyInitDetails = getEarlyInitDetailsOrNull()
-            if (earlyInitDetails != null) {
-                applyComplications(
-                    earlyInitDetails.complicationSlotsManager,
-                    complicationDataWireFormats
-                )
-            } else {
-                setPendingInitialComplications(complicationDataWireFormats)
-            }
-        }
-
-        @UiThread
-        internal fun applyComplications(
-            complicationSlotsManager: ComplicationSlotsManager,
-            complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
-        ) {
-            val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
-            for (idAndComplicationData in complicationDataWireFormats) {
-                complicationSlotsManager.onComplicationDataUpdate(
-                    idAndComplicationData.id,
-                    idAndComplicationData.complicationData.toApiComplicationData(),
-                    now
-                )
-            }
-            complicationSlotsManager.onComplicationsUpdated()
-            invalidate()
-            scheduleWriteComplicationDataCache()
-        }
-
-        @UiThread
-        internal fun setPendingInitialComplications(
-            complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
-        ) {
-            // If the watchface hasn't been created yet, update pendingInitialComplications so
-            // it can be applied later.
-            if (pendingInitialComplications == null) {
-                pendingInitialComplications = complicationDataWireFormats
-            } else {
+            complicationsFlow.update { base ->
                 // We need to merge the updates.
-                val complicationUpdateMap = pendingInitialComplications!!.associate {
+                val complicationUpdateMap = base.associate {
                     Pair(it.id, it.complicationData)
                 }.toMutableMap()
                 for (data in complicationDataWireFormats) {
                     complicationUpdateMap[data.id] = data.complicationData
                 }
-                pendingInitialComplications = complicationUpdateMap.map {
+                complicationUpdateMap.map {
                     IdAndComplicationDataWireFormat(it.key, it.value)
                 }
             }
         }
 
+        @WorkerThread
+        private fun listenForComplicationChanges(
+            complicationSlotsManager: ComplicationSlotsManager
+        ) {
+            // Add a listener so we can track changes and automatically apply them on the UIThread
+            uiThreadCoroutineScope.launch {
+                complicationsFlow.collect { complicationDataWireFormats ->
+                    val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
+                    for (idAndComplicationData in complicationDataWireFormats) {
+                        complicationSlotsManager.onComplicationDataUpdate(
+                            idAndComplicationData.id,
+                            idAndComplicationData.complicationData.toApiComplicationData(),
+                            now
+                        )
+                    }
+                    complicationSlotsManager.onComplicationsUpdated()
+                    invalidate()
+                    scheduleWriteComplicationDataCache()
+                }
+            }
+        }
+
         @UiThread
         internal suspend fun updateInstance(newInstanceId: String) {
             val watchFaceImpl = deferredWatchFaceImpl.await()
@@ -1999,10 +1987,12 @@
 
             // Store the initial complications, this could be modified by new data before being
             // applied.
-            pendingInitialComplications = params.idAndComplicationDataWireFormats
-
-            if (pendingInitialComplications == null || pendingInitialComplications!!.isEmpty()) {
-                pendingInitialComplications = readComplicationDataCache(_context, params.instanceId)
+            var initialComplications = params.idAndComplicationDataWireFormats
+            if (initialComplications.isNullOrEmpty()) {
+                initialComplications = readComplicationDataCache(_context, params.instanceId)
+            }
+            if (!initialComplications.isNullOrEmpty()) {
+                setComplicationDataList(initialComplications)
             }
 
             createWatchFaceInternal(
@@ -2038,6 +2028,15 @@
             asyncWatchFaceConstructionPending = true
             createdBy = _createdBy
 
+            // In case of overrideSurfaceHolder provided (tests) return its size instead of real
+            // metrics.
+            screenBounds = if (overrideSurfaceHolder != null) {
+                overrideSurfaceHolder.surfaceFrame
+            } else {
+                val displayMetrics = resources.displayMetrics
+                Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
+            }
+
             backgroundThreadCoroutineScope.launch {
                 val timeBefore = System.currentTimeMillis()
                 val currentUserStyleRepository =
@@ -2052,12 +2051,21 @@
                     }
                 complicationSlotsManager.watchState = watchState
                 complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
+                listenForComplicationChanges(complicationSlotsManager)
 
                 val userStyleFlavors =
                     TraceEvent("WatchFaceService.createUserStyleFlavors").use {
                         createUserStyleFlavors(currentUserStyleRepository, complicationSlotsManager)
                     }
 
+                deferredEarlyInitDetails.complete(
+                    EarlyInitDetails(
+                        complicationSlotsManager,
+                        currentUserStyleRepository,
+                        userStyleFlavors
+                    )
+                )
+
                 val deferredWatchFace = CompletableDeferred<WatchFace>()
                 val initComplicationsDone = CompletableDeferred<Unit>()
 
@@ -2076,24 +2084,6 @@
 
                 try {
                     val surfaceHolder = overrideSurfaceHolder ?: deferredSurfaceHolder.await()
-                    deferredEarlyInitDetails.complete(
-                        EarlyInitDetails(
-                            complicationSlotsManager,
-                            currentUserStyleRepository,
-                            userStyleFlavors,
-                            surfaceHolder
-                        )
-                    )
-
-                    withContext(uiThreadCoroutineScope.coroutineContext) {
-                        // Apply any pendingInitialComplications, this must be done after
-                        // deferredEarlyInitDetails has completed or there's a window in which complication
-                        // updates get lost.
-                        pendingInitialComplications?.let {
-                            applyComplications(complicationSlotsManager, it)
-                        }
-                        pendingInitialComplications = null
-                    }
 
                     val watchFace = TraceEvent("WatchFaceService.createWatchFace").use {
                         // Note by awaiting deferredSurfaceHolder we ensure onSurfaceChanged has
@@ -2678,6 +2668,9 @@
             }
         }
 
+        internal lateinit var screenBounds: Rect
+            private set
+
         @UiThread
         internal fun dump(writer: IndentingPrintWriter) {
             require(uiThreadHandler.looper.isCurrentThread) {
@@ -2722,7 +2715,7 @@
             writer.println("destroyed=$destroyed")
             writer.println("surfaceDestroyed=$surfaceDestroyed")
             writer.println(
-                "pendingInitialComplications=" + pendingInitialComplications?.joinToString()
+                "lastComplications=" + complicationsFlow.value.joinToString()
             )
 
             synchronized(lock) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index badd1a8..a5f3897 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -85,11 +85,14 @@
             "HeadlessWatchFaceImpl.getPreviewReferenceTimeMillis"
         ) { it.previewReferenceInstant.toEpochMilli() } ?: 0
 
-    override fun getComplicationState() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
-            engine,
-            "HeadlessWatchFaceImpl.getComplicationState"
-        ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+    override fun getComplicationState() = run {
+        val engineCopy = engine
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+            engineCopy,
+            "HeadlessWatchFaceImpl.getComplicationState",
+            WatchFaceService.Companion.ExecutionThread.UI
+        ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
+    }
 
     override fun renderComplicationToBitmap(params: ComplicationRenderParams) =
         WatchFaceService.awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
@@ -98,21 +101,24 @@
         ) { it.renderComplicationToBitmap(params) }
 
     override fun getUserStyleSchema() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "HeadlessWatchFaceImpl.getUserStyleSchema"
+            "HeadlessWatchFaceImpl.getUserStyleSchema",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleRepository.schema.toWireFormat() }
 
     override fun computeUserStyleSchemaDigestHash() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash"
+            "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleRepository.schema.getDigestHash() }
 
     override fun getUserStyleFlavors() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "HeadlessWatchFaceImpl.getUserStyleFlavors"
+            "HeadlessWatchFaceImpl.getUserStyleFlavors",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleFlavors.toWireFormat() }
 
     override fun release() {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 1eae0b4..ba6fce5 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -87,7 +87,7 @@
     }
 
     override fun getWatchFaceOverlayStyle(): WatchFaceOverlayStyleWireFormat? =
-        WatchFaceService.awaitDeferredWatchFaceThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredWatchFaceThenRunOnUiThread(
             engine,
             "InteractiveWatchFaceImpl.getWatchFaceOverlayStyle"
         ) { WatchFaceOverlayStyleWireFormat(
@@ -160,9 +160,7 @@
 
     override fun updateComplicationData(
         complicationDatumWireFormats: MutableList<IdAndComplicationDataWireFormat>
-    ): Unit = uiThreadCoroutineScope.runBlockingWithTracing(
-        "InteractiveWatchFaceImpl.updateComplicationData"
-    ) {
+    ): Unit = TraceEvent("InteractiveWatchFaceImpl.updateComplicationData").use {
         if ("user" != Build.TYPE) {
             Log.d(TAG, "updateComplicationData " + complicationDatumWireFormats.joinToString())
         }
@@ -191,16 +189,19 @@
     }
 
     override fun getComplicationDetails(): List<IdAndComplicationStateWireFormat>? {
-        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
-            engine,
-            "InteractiveWatchFaceImpl.getComplicationDetails"
-        ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+        val engineCopy = engine
+        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+            engineCopy,
+            "InteractiveWatchFaceImpl.getComplicationDetails",
+            WatchFaceService.Companion.ExecutionThread.UI
+        ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
     }
 
     override fun getUserStyleSchema(): UserStyleSchemaWireFormat? {
-        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "InteractiveWatchFaceImpl.getUserStyleSchema"
+            "InteractiveWatchFaceImpl.getUserStyleSchema",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleRepository.schema.toWireFormat() }
     }
 
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 97ca016..520e72f 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -5801,7 +5801,7 @@
     }
 
     @Test
-    public fun setPendingInitialComplications() {
+    public fun setComplicationDataListMergesCorrectly() {
         initEngine(
             WatchFaceType.ANALOG,
             listOf(leftComplication, rightComplication),
@@ -5829,15 +5829,17 @@
                 .build()
         )
 
-        engineWrapper.setPendingInitialComplications(listOf(left1))
-        assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1)
+        engineWrapper.setComplicationDataList(listOf(left1))
+        // In initEngine we fill initial complication data using
+        // setComplicationViaWallpaperCommand, that's why lastComplications initially is not empty
+        assertThat(engineWrapper.complicationsFlow.value).contains(left1)
 
         // Check merges are working as expected.
-        engineWrapper.setPendingInitialComplications(listOf(right))
-        assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1, right)
+        engineWrapper.setComplicationDataList(listOf(right))
+        assertThat(engineWrapper.complicationsFlow.value).containsExactly(left1, right)
 
-        engineWrapper.setPendingInitialComplications(listOf(left2))
-        assertThat(engineWrapper.pendingInitialComplications).containsExactly(left2, right)
+        engineWrapper.setComplicationDataList(listOf(left2))
+        assertThat(engineWrapper.complicationsFlow.value).containsExactly(left2, right)
     }
 
     @Test
diff --git a/window/window-samples/src/main/AndroidManifest.xml b/window/window-samples/src/main/AndroidManifest.xml
index 9c79acd..17431f8 100644
--- a/window/window-samples/src/main/AndroidManifest.xml
+++ b/window/window-samples/src/main/AndroidManifest.xml
@@ -191,7 +191,7 @@
             android:configChanges="orientation|screenSize|screenLayout|screenSize"
             android:label="@string/ime"/>
 
-        <!-- ActivityEmbedding Initializer -->
+        <!-- Activity embedding initializer -->
 
         <provider android:name="androidx.startup.InitializationProvider"
             android:authorities="${applicationId}.androidx-startup"
@@ -202,7 +202,7 @@
                 android:value="androidx.startup" />
         </provider>
 
-        <!-- The app supports ActivityEmbedding itself, so it doesn't need system override. -->
+        <!-- The app itself supports activity embedding, so a system override is not needed. -->
         <property
             android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
             android:value="false" />
diff --git a/window/window/build.gradle b/window/window/build.gradle
index f045acf..581da26 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -80,12 +80,6 @@
     samples(project(":window:window-samples"))
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += ["-Xjvm-default=all"]
-    }
-}
-
 androidx {
     name = "Jetpack WindowManager Library"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/window/window/src/androidTest/AndroidManifest.xml b/window/window/src/androidTest/AndroidManifest.xml
index af030c1..d543136 100644
--- a/window/window/src/androidTest/AndroidManifest.xml
+++ b/window/window/src/androidTest/AndroidManifest.xml
@@ -21,7 +21,7 @@
         <activity android:name="androidx.window.TestConfigChangeHandlingActivity"
             android:configChanges="orientation|screenLayout|screenSize"/>
 
-        <!-- ActivityEmbedding Property -->
+        <!-- Activity embedding property -->
         <property
             android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
             android:value="true" />
diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
index f661df6..226430b 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -17,25 +17,41 @@
 package androidx.window
 
 /**
- * Window related [android.content.pm.PackageManager.Property] tags for developers to define in app
- * AndroidManifest.
+ * Window-related
+ * [PackageManager.Property][android.content.pm.PackageManager.Property] tags
+ * that can be defined in the app manifest file, `AndroidManifest.xml`.
  */
 object WindowProperties {
     /**
-     * Application level [android.content.pm.PackageManager.Property] tag for developers to
-     * provide consent for their app to allow OEMs to manually provide ActivityEmbedding split
-     * rule configuration on behalf of the app.
-     * <p>If `true`, the system can override the windowing behaviors for the app, such as
-     * showing some activities side-by-side. In this case, it will report that ActivityEmbedding
-     * APIs are disabled for the app to avoid conflict.
-     * <p>If `false`, the system can't override the window behavior for the app. It should
-     * be used if the app wants to provide their own ActivityEmbedding split rules, or if the
-     * app wants to opt-out of system overrides for any other reason.
-     * <p>Default is `false`.
-     * <p>The system enforcement is added in Android 14, but some devices may start following the
-     * requirement before that. The best practice for apps is to always explicitly set this
-     * property in AndroidManifest instead of relying on the default value.
-     * <p>Example usage:
+     * Application-level
+     * [PackageManager.Property][android.content.pm.PackageManager.Property] tag
+     * that specifies whether OEMs are permitted to provide activity embedding
+     * split-rule configurations on behalf of the app.
+     *
+     * 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.
+     *
+     * 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
+     * own activity embedding split rules, or if you want to prevent the system
+     * override for any other reason.
+     *
+     * The default value is `false`.
+     *
+     * <p class="note"><b>Note:</b> Refusal to permit the system override is not
+     * enforceable. OEMs can override the app's activity embedding
+     * implementation whether or not this property is specified and set to
+     * <code>false</code>. The property is, in effect, a hint to OEMs.</p>
+     *
+     * OEMs can implement activity embedding on any API level. The best practice
+     * for apps is to always explicitly set this property in the app manifest
+     * file regardless of targeted API level rather than rely on the default
+     * value.
+     *
+     * **Syntax:**
      * <pre>
      * &lt;application&gt;
      *   &lt;property
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 d24c68a..d2e9c4c4 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -24,16 +24,26 @@
 import kotlin.concurrent.withLock
 
 /**
- * Controller class that will be used to get information about the currently active activity splits,
- * as well as provide interaction points to customize them and form new splits. A split is a pair of
- * containers that host activities in the same or different processes, combined under the same
- * parent window of the hosting task.
- * <p>A pair of activities can be put into split by providing a static or runtime split rule and
- * launching activity in the same task using [android.app.Activity.startActivity].
- * <p>This class is recommended to be configured in [androidx.startup.Initializer] or
- * [android.app.Application.onCreate], so that the rules are applied early in the application
- * startup before any activities complete initialization. The rule updates only apply to future
- * [android.app.Activity] launches and do not apply to already running activities.
+ * Controller class that gets information about the currently active activity
+ * splits and provides interaction points to customize the splits and form new
+ * splits.
+ *
+ * A split is a pair of containers that host activities in the same or different
+ * processes, combined under the same parent window of the hosting task.
+ *
+ * A pair of activities can be put into a split by providing a static or runtime
+ * split rule and then launching the activities in the same task using
+ * [Activity.startActivity()][android.app.Activity.startActivity].
+ *
+ * Configure this class with the Jetpack library
+ * [Initializer][androidx.startup.Initializer] or in
+ * [Application.onCreate()][android.app.Application.onCreate] so the rules are
+ * applied early in the application startup, before any activities complete
+ * initialization.
+ *
+ * Rule updates apply only to future activity launches, not to already running
+ * activities.
+ *
  * @see initialize
  */
 class SplitController private constructor(applicationContext: Context) {
@@ -114,14 +124,14 @@
     }
 
     /**
-     * Indicates whether the split functionality is supported on the device. Note
-     * that the device might enable splits in all conditions, but it should be
-     * available in some states that the device supports. An example can be a
-     * foldable device with multiple screens can choose to collapse all splits for
-     * apps running on a small display, but enable when running on a larger
-     * one - on such devices this method will always return "true".
-     * If the split is not supported, activities will be launched on top, following
-     * the regular model.
+     * Indicates whether split functionality is supported on the device. Note
+     * that devices might not enable splits in all states or conditions. For
+     * example, a foldable device with multiple screens can choose to collapse
+     * splits when apps run on the device's small display, but enable splits
+     * when apps run on the device's large display. In cases like this,
+     * `isSplitSupported` always returns `true`, and if the split is collapsed,
+     * activities are launched on top, following the non-activity embedding
+     * model.
      */
     fun isSplitSupported(): Boolean {
         return embeddingBackend.isSplitSupported()
diff --git a/work/work-runtime/api/current.ignore b/work/work-runtime/api/current.ignore
new file mode 100644
index 0000000..33b7b2f
--- /dev/null
+++ b/work/work-runtime/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedType: androidx.work.WorkInfo#getTags():
+    Method androidx.work.WorkInfo.getTags has changed return type from java.util.Set<java.lang.String!> to java.util.Set<java.lang.String>
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index 446ee00..370e634 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -320,13 +320,23 @@
     method public java.util.UUID getId();
     method public androidx.work.Data getOutputData();
     method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
+    method @IntRange(from=0L) public int getRunAttemptCount();
     method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
+    method public java.util.Set<java.lang.String> getTags();
+    property public final int generation;
+    property public final java.util.UUID id;
+    property public final androidx.work.Data outputData;
+    property public final androidx.work.Data progress;
+    property @IntRange(from=0L) public final int runAttemptCount;
+    property public final androidx.work.WorkInfo.State state;
+    property public final java.util.Set<java.lang.String> tags;
   }
 
   public enum WorkInfo.State {
-    method public boolean isFinished();
+    method public final boolean isFinished();
+    method public static androidx.work.WorkInfo.State valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.WorkInfo.State[] values();
+    property public final boolean isFinished;
     enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
     enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
     enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
diff --git a/work/work-runtime/api/public_plus_experimental_current.txt b/work/work-runtime/api/public_plus_experimental_current.txt
index 446ee00..370e634 100644
--- a/work/work-runtime/api/public_plus_experimental_current.txt
+++ b/work/work-runtime/api/public_plus_experimental_current.txt
@@ -320,13 +320,23 @@
     method public java.util.UUID getId();
     method public androidx.work.Data getOutputData();
     method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
+    method @IntRange(from=0L) public int getRunAttemptCount();
     method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
+    method public java.util.Set<java.lang.String> getTags();
+    property public final int generation;
+    property public final java.util.UUID id;
+    property public final androidx.work.Data outputData;
+    property public final androidx.work.Data progress;
+    property @IntRange(from=0L) public final int runAttemptCount;
+    property public final androidx.work.WorkInfo.State state;
+    property public final java.util.Set<java.lang.String> tags;
   }
 
   public enum WorkInfo.State {
-    method public boolean isFinished();
+    method public final boolean isFinished();
+    method public static androidx.work.WorkInfo.State valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.WorkInfo.State[] values();
+    property public final boolean isFinished;
     enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
     enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
     enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
diff --git a/work/work-runtime/api/restricted_current.ignore b/work/work-runtime/api/restricted_current.ignore
new file mode 100644
index 0000000..33b7b2f
--- /dev/null
+++ b/work/work-runtime/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedType: androidx.work.WorkInfo#getTags():
+    Method androidx.work.WorkInfo.getTags has changed return type from java.util.Set<java.lang.String!> to java.util.Set<java.lang.String>
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index 446ee00..370e634 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -320,13 +320,23 @@
     method public java.util.UUID getId();
     method public androidx.work.Data getOutputData();
     method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
+    method @IntRange(from=0L) public int getRunAttemptCount();
     method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
+    method public java.util.Set<java.lang.String> getTags();
+    property public final int generation;
+    property public final java.util.UUID id;
+    property public final androidx.work.Data outputData;
+    property public final androidx.work.Data progress;
+    property @IntRange(from=0L) public final int runAttemptCount;
+    property public final androidx.work.WorkInfo.State state;
+    property public final java.util.Set<java.lang.String> tags;
   }
 
   public enum WorkInfo.State {
-    method public boolean isFinished();
+    method public final boolean isFinished();
+    method public static androidx.work.WorkInfo.State valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.WorkInfo.State[] values();
+    property public final boolean isFinished;
     enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
     enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
     enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkInfo.java b/work/work-runtime/src/main/java/androidx/work/WorkInfo.java
deleted file mode 100644
index 7560af3..0000000
--- a/work/work-runtime/src/main/java/androidx/work/WorkInfo.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
-
-/**
- * Information about a particular {@link WorkRequest} containing the id of the WorkRequest, its
- * current {@link State}, output, tags, and run attempt count.  Note that output is only available
- * for the terminal states ({@link State#SUCCEEDED} and {@link State#FAILED}).
- */
-
-public final class WorkInfo {
-
-    private @NonNull UUID mId;
-    private @NonNull State mState;
-    private @NonNull Data mOutputData;
-    private @NonNull Set<String> mTags;
-    private @NonNull Data mProgress;
-    private int mRunAttemptCount;
-
-    private final int mGeneration;
-    /**
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public WorkInfo(
-            @NonNull UUID id,
-            @NonNull State state,
-            @NonNull Data outputData,
-            @NonNull List<String> tags,
-            @NonNull Data progress,
-            int runAttemptCount,
-            int generation) {
-        mId = id;
-        mState = state;
-        mOutputData = outputData;
-        mTags = new HashSet<>(tags);
-        mProgress = progress;
-        mRunAttemptCount = runAttemptCount;
-        mGeneration = generation;
-    }
-
-    /**
-     * Gets the identifier of the {@link WorkRequest}.
-     *
-     * @return The identifier of a {@link WorkRequest}
-     */
-    public @NonNull UUID getId() {
-        return mId;
-    }
-
-    /**
-     * Gets the current {@link State} of the {@link WorkRequest}.
-     *
-     * @return The current {@link State} of the {@link WorkRequest}
-     */
-    public @NonNull State getState() {
-        return mState;
-    }
-
-    /**
-     * Gets the output {@link Data} for the {@link WorkRequest}.  If the WorkRequest is unfinished,
-     * this is always {@link Data#EMPTY}.
-     *
-     * @return The output {@link Data} of the {@link WorkRequest}
-     */
-    public @NonNull Data getOutputData() {
-        return mOutputData;
-    }
-
-    /**
-     * Gets the {@link Set} of tags associated with the {@link WorkRequest}.
-     *
-     * @return The {@link Set} of tags associated with the {@link WorkRequest}
-     */
-    public @NonNull Set<String> getTags() {
-        return mTags;
-    }
-
-    /**
-     * Gets the progress {@link Data} associated with the {@link WorkRequest}.
-     *
-     * @return The progress {@link Data} associated with the {@link WorkRequest}
-     */
-    public @NonNull Data getProgress() {
-        return mProgress;
-    }
-
-    /**
-     * Gets the run attempt count of the {@link WorkRequest}.  Note that for
-     * {@link PeriodicWorkRequest}s, the run attempt count gets reset between successful runs.
-     *
-     * @return The run attempt count of the {@link WorkRequest}.
-     */
-    @IntRange(from = 0)
-    public int getRunAttemptCount() {
-        return mRunAttemptCount;
-    }
-
-    /**
-     * Gets the latest generation of this Worker.
-     * <p>
-     * A work has multiple generations, if it was updated via
-     * {@link WorkManager#updateWork(WorkRequest)} or
-     * {@link WorkManager#enqueueUniquePeriodicWork(String,
-     * ExistingPeriodicWorkPolicy, PeriodicWorkRequest)} using
-     * {@link ExistingPeriodicWorkPolicy#UPDATE}.
-     * <p>
-     * If this worker is currently running, it can possibly be of an older generation rather than
-     * returned by this function if an update has happened during an execution of this worker.
-     *
-     * @return a generation of this work.
-     */
-    public int getGeneration() {
-        return mGeneration;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        WorkInfo workInfo = (WorkInfo) o;
-
-        if (mRunAttemptCount != workInfo.mRunAttemptCount) return false;
-        if (mGeneration != workInfo.mGeneration) return false;
-        if (!mId.equals(workInfo.mId)) return false;
-        if (mState != workInfo.mState) return false;
-        if (!mOutputData.equals(workInfo.mOutputData)) return false;
-        if (!mTags.equals(workInfo.mTags)) return false;
-        return mProgress.equals(workInfo.mProgress);
-    }
-
-    @Override
-    public int hashCode() {
-        int result = mId.hashCode();
-        result = 31 * result + mState.hashCode();
-        result = 31 * result + mOutputData.hashCode();
-        result = 31 * result + mTags.hashCode();
-        result = 31 * result + mProgress.hashCode();
-        result = 31 * result + mRunAttemptCount;
-        result = 31 * result + mGeneration;
-        return result;
-    }
-
-    @Override
-    public String toString() {
-        return "WorkInfo{"
-                +   "mId='" + mId + '\''
-                +   ", mState=" + mState
-                +   ", mOutputData=" + mOutputData
-                +   ", mTags=" + mTags
-                +   ", mProgress=" + mProgress
-                + '}';
-    }
-
-    /**
-     * The current lifecycle state of a {@link WorkRequest}.
-     */
-    public enum State {
-
-        /**
-         * Used to indicate that the {@link WorkRequest} is enqueued and eligible to run when its
-         * {@link Constraints} are met and resources are available.
-         */
-        ENQUEUED,
-
-        /**
-         * Used to indicate that the {@link WorkRequest} is currently being executed.
-         */
-        RUNNING,
-
-        /**
-         * Used to indicate that the {@link WorkRequest} has completed in a successful state.  Note
-         * that {@link PeriodicWorkRequest}s will never enter this state (they will simply go back
-         * to {@link #ENQUEUED} and be eligible to run again).
-         */
-        SUCCEEDED,
-
-        /**
-         * Used to indicate that the {@link WorkRequest} has completed in a failure state.  All
-         * dependent work will also be marked as {@code #FAILED} and will never run.
-         */
-        FAILED,
-
-        /**
-         * Used to indicate that the {@link WorkRequest} is currently blocked because its
-         * prerequisites haven't finished successfully.
-         */
-        BLOCKED,
-
-        /**
-         * Used to indicate that the {@link WorkRequest} has been cancelled and will not execute.
-         * All dependent work will also be marked as {@code #CANCELLED} and will not run.
-         */
-        CANCELLED;
-
-        /**
-         * Returns {@code true} if this State is considered finished.
-         *
-         * @return {@code true} for {@link #SUCCEEDED}, {@link #FAILED}, and * {@link #CANCELLED}
-         *         states
-         */
-        public boolean isFinished() {
-            return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
-        }
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt b/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt
new file mode 100644
index 0000000..5d4ab92
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work
+
+import androidx.annotation.IntRange
+import androidx.annotation.RestrictTo
+import androidx.work.WorkInfo.State
+import java.util.UUID
+
+/**
+ * Information about a particular [WorkRequest] containing the id of the WorkRequest, its
+ * current [State], output, tags, and run attempt count.  Note that output is only available
+ * for the terminal states ([State.SUCCEEDED] and [State.FAILED]).
+ */
+class WorkInfo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
+    /**
+     * The identifier of the [WorkRequest].
+     */
+    val id: UUID,
+    /**
+     * The current [State] of the [WorkRequest].
+     */
+    val state: State,
+    /**
+     * The output [Data] for the [WorkRequest]. If the WorkRequest is unfinished,
+     * this is always [Data.EMPTY].
+     */
+    val outputData: Data,
+
+    tags: List<String>,
+    /**
+     * The progress [Data] associated with the [WorkRequest].
+     */
+    val progress: Data,
+
+    /**
+     * The run attempt count of the [WorkRequest].  Note that for
+     * [PeriodicWorkRequest]s, the run attempt count gets reset between successful runs.
+     */
+    @get:IntRange(from = 0)
+    val runAttemptCount: Int,
+
+    /**
+     * The latest generation of this Worker.
+     *
+     *
+     * A work has multiple generations, if it was updated via
+     * [WorkManager.updateWork] or
+     * [WorkManager.enqueueUniquePeriodicWork] using
+     * [ExistingPeriodicWorkPolicy.UPDATE].
+     *
+     *
+     * If this worker is currently running, it can possibly be of an older generation rather than
+     * returned by this function if an update has happened during an execution of this worker.
+     */
+    val generation: Int
+) {
+    /**
+     * The [Set] of tags associated with the [WorkRequest].
+     */
+    val tags: Set<String> = HashSet(tags)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || javaClass != other.javaClass) return false
+        val workInfo = other as WorkInfo
+        if (runAttemptCount != workInfo.runAttemptCount) return false
+        if (generation != workInfo.generation) return false
+        if (id != workInfo.id) return false
+        if (state != workInfo.state) return false
+        if (outputData != workInfo.outputData) return false
+        return if (tags != workInfo.tags) false else progress == workInfo.progress
+    }
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + state.hashCode()
+        result = 31 * result + outputData.hashCode()
+        result = 31 * result + tags.hashCode()
+        result = 31 * result + progress.hashCode()
+        result = 31 * result + runAttemptCount
+        result = 31 * result + generation
+        return result
+    }
+
+    override fun toString(): String {
+        return ("WorkInfo{mId='$id', mState=$state, " +
+            "mOutputData=$outputData, mTags=$tags, mProgress=$progress}")
+    }
+
+    /**
+     * The current lifecycle state of a [WorkRequest].
+     */
+    enum class State {
+        /**
+         * Used to indicate that the [WorkRequest] is enqueued and eligible to run when its
+         * [Constraints] are met and resources are available.
+         */
+        ENQUEUED,
+
+        /**
+         * Used to indicate that the [WorkRequest] is currently being executed.
+         */
+        RUNNING,
+
+        /**
+         * Used to indicate that the [WorkRequest] has completed in a successful state.  Note
+         * that [PeriodicWorkRequest]s will never enter this state (they will simply go back
+         * to [.ENQUEUED] and be eligible to run again).
+         */
+        SUCCEEDED,
+
+        /**
+         * Used to indicate that the [WorkRequest] has completed in a failure state.  All
+         * dependent work will also be marked as `#FAILED` and will never run.
+         */
+        FAILED,
+
+        /**
+         * Used to indicate that the [WorkRequest] is currently blocked because its
+         * prerequisites haven't finished successfully.
+         */
+        BLOCKED,
+
+        /**
+         * Used to indicate that the [WorkRequest] has been cancelled and will not execute.
+         * All dependent work will also be marked as `#CANCELLED` and will not run.
+         */
+        CANCELLED;
+
+        /**
+         * Returns `true` if this State is considered finished:
+         * [.SUCCEEDED], [.FAILED], and * [.CANCELLED]
+         */
+        val isFinished: Boolean
+            get() = this == SUCCEEDED || this == FAILED || this == CANCELLED
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt
index 47b9e55..b07aeca 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt
@@ -20,6 +20,7 @@
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
 
+@JvmDefaultWithCompatibility
 /**
  * A Data Access Object for [SystemIdInfo].
  */
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt
index db0be48..2828c61f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt
@@ -20,6 +20,7 @@
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
 
+@JvmDefaultWithCompatibility
 /**
  * The Data Access Object for [WorkTag]s.
  */