Merge "Allow preceding if-with-return to satisfy BanUncheckedReflection" into androidx-main
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index 92c1f32..c54e415 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -46,6 +46,9 @@
     /**
      * Set to true to enable androidx.tracing.perfetto tracepoints (such as composition tracing)
      *
+     * Note this only affects Macrobenchmarks currently, and only when StartupMode.COLD is not used,
+     * since enabling the tracepoints wakes the target process
+     *
      * Currently internal/experimental
      */
     val fullTracingEnable: Boolean
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index 4e88608..166b3e9 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -20,7 +20,6 @@
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
-import androidx.benchmark.Arguments
 import androidx.benchmark.Outputs
 import androidx.benchmark.Outputs.dateToFileName
 import androidx.benchmark.PropOverride
@@ -41,20 +40,22 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    private fun start(packages: List<String>): Boolean {
+    private fun start(
+        appTagPackages: List<String>,
+        userspaceTracingPackage: String?
+    ): Boolean {
         capture?.apply {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                 Log.d(PerfettoHelper.LOG_TAG, "Recording perfetto trace")
-                if (Arguments.fullTracingEnable &&
-                    packages.isNotEmpty() &&
+                if (userspaceTracingPackage != null &&
                     Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
                 ) {
                     enableAndroidxTracingPerfetto(
-                        targetPackage = packages.first(),
+                        targetPackage = userspaceTracingPackage,
                         provideBinariesIfMissing = true
                     )
                 }
-                start(packages)
+                start(appTagPackages)
             }
         }
 
@@ -80,7 +81,8 @@
 
     fun record(
         benchmarkName: String,
-        packages: List<String>,
+        appTagPackages: List<String>,
+        userspaceTracingPackage: String?,
         iteration: Int? = null,
         block: () -> Unit
     ): String? {
@@ -97,7 +99,7 @@
         } else null
         try {
             propOverride?.forceValue()
-            start(packages)
+            start(appTagPackages, userspaceTracingPackage)
             val path: String
             try {
                 block()
diff --git a/benchmark/benchmark-junit4/api/restricted_current.ignore b/benchmark/benchmark-junit4/api/restricted_current.ignore
new file mode 100644
index 0000000..7b0440e
--- /dev/null
+++ b/benchmark/benchmark-junit4/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.benchmark.junit4.PerfettoRule#PerfettoRule():
+    Removed constructor androidx.benchmark.junit4.PerfettoRule()
diff --git a/benchmark/benchmark-junit4/api/restricted_current.txt b/benchmark/benchmark-junit4/api/restricted_current.txt
index d3bf38c..e6624c1 100644
--- a/benchmark/benchmark-junit4/api/restricted_current.txt
+++ b/benchmark/benchmark-junit4/api/restricted_current.txt
@@ -21,8 +21,12 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PerfettoRule implements org.junit.rules.TestRule {
-    ctor public PerfettoRule();
+    ctor public PerfettoRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public boolean getEnableAppTagTracing();
+    method public boolean getEnableUserspaceTracing();
+    property public final boolean enableAppTagTracing;
+    property public final boolean enableUserspaceTracing;
   }
 
 }
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index cd65676..9a33fe9 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -213,7 +213,8 @@
 
             val tracePath = PerfettoCaptureWrapper().record(
                 benchmarkName = uniqueName,
-                packages = packages,
+                appTagPackages = packages,
+                userspaceTracingPackage = null
             ) {
                 UserspaceTracing.commitToTrace() // clear buffer
 
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt
index 0b8c6a6..97fd9d6 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt
@@ -46,15 +46,30 @@
  * ```
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class PerfettoRule : TestRule {
+public class PerfettoRule(
+    /**
+     * Pass false to disable android.os.Trace API tracing in this process
+     *
+     * Defaults to true.
+     */
+    val enableAppTagTracing: Boolean = true,
+    /**
+     * Pass true to enable userspace tracing (androidx.tracing.tracing-perfetto APIs)
+     *
+     * Defaults to false.
+     */
+    val enableUserspaceTracing: Boolean = false
+) : TestRule {
     override fun apply(
         base: Statement,
         description: Description
     ): Statement = object : Statement() {
         override fun evaluate() {
+            val thisPackage = InstrumentationRegistry.getInstrumentation().context.packageName
             PerfettoCaptureWrapper().record(
                 benchmarkName = "${description.className}_${description.methodName}",
-                packages = listOf(InstrumentationRegistry.getInstrumentation().context.packageName)
+                appTagPackages = if (enableAppTagTracing) listOf(thisPackage) else emptyList(),
+                userspaceTracingPackage = if (enableUserspaceTracing) thisPackage else null
             ) {
                 base.evaluate()
             }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index f206893..c3fbdc2 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -278,11 +278,12 @@
         benchmarkName = packageName,
         // note - packageName may be this package, so we convert to set then list to make unique
         // and on API 23 and below, we use reflection to trace instead within this process
-        packages = if (Build.VERSION.SDK_INT >= 24 && packageName != Packages.TEST) {
+        appTagPackages = if (Build.VERSION.SDK_INT >= 24 && packageName != Packages.TEST) {
             listOf(packageName, Packages.TEST)
         } else {
             listOf(packageName)
         },
+        userspaceTracingPackage = packageName,
         block = measureBlock
     )!!
 
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 16a6787..e63fb5f 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -111,6 +111,7 @@
     iterations: Int,
     launchWithClearTask: Boolean,
     startupModeMetricHint: StartupMode?,
+    userspaceTracingPackage: String?,
     setupBlock: MacrobenchmarkScope.() -> Unit,
     measureBlock: MacrobenchmarkScope.() -> Unit
 ) {
@@ -180,11 +181,12 @@
                      *
                      * @see androidx.benchmark.macro.perfetto.ForceTracing
                      */
-                    packages = if (Build.VERSION.SDK_INT >= 24) {
+                    appTagPackages = if (Build.VERSION.SDK_INT >= 24) {
                         listOf(packageName, macrobenchPackageName)
                     } else {
                         listOf(packageName)
-                    }
+                    },
+                    userspaceTracingPackage = userspaceTracingPackage
                 ) {
                     try {
                         trace("start metrics") {
@@ -312,6 +314,13 @@
     setupBlock: MacrobenchmarkScope.() -> Unit,
     measureBlock: MacrobenchmarkScope.() -> Unit
 ) {
+    val userspaceTracingPackage = if (Arguments.fullTracingEnable &&
+        startupMode != StartupMode.COLD // can't use with COLD, since the broadcast wakes up target
+    ) {
+        packageName
+    } else {
+        null
+    }
     macrobenchmark(
         uniqueName = uniqueName,
         className = className,
@@ -321,6 +330,7 @@
         compilationMode = compilationMode,
         iterations = iterations,
         startupModeMetricHint = startupMode,
+        userspaceTracingPackage = userspaceTracingPackage,
         setupBlock = {
             if (startupMode == StartupMode.COLD) {
                 killProcess()
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index 981353e..bc7d25c 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -67,6 +67,7 @@
     testImplementation(project(":internal-testutils-ktx"))
     testImplementation(project(":internal-testutils-truth"))
     testImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+    testImplementation(project(":camera:camera-video"))
 
     androidTestImplementation(libs.multidex)
     androidTestImplementation(libs.testExtJunit)
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
index 38dc5a0..c0cb37f 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
@@ -106,14 +106,14 @@
     }
 
     @After
-    fun tearDown() {
-        if (this::testSessionParameters.isInitialized) {
+    fun tearDown() = runBlocking {
+        if (::testSessionParameters.isInitialized) {
             testSessionParameters.cleanup()
         }
-        if (this::testUseCaseCamera.isInitialized) {
-            testUseCaseCamera.close()
+        if (::testUseCaseCamera.isInitialized) {
+            testUseCaseCamera.close().join()
         }
-        if (this::cameraHolder.isInitialized) {
+        if (::cameraHolder.isInitialized) {
             CameraUtil.releaseCameraDevice(cameraHolder)
             cameraHolder.closedFuture.get(3, TimeUnit.SECONDS)
         }
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index 1e81e83..8f93913 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -45,6 +45,8 @@
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.Config
 import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
 
 /**
  * Open a [CameraGraph] for the desired [cameraId] and [useCases]
@@ -122,8 +124,10 @@
         throw NotImplementedError("Not implemented")
     }
 
-    override fun close() {
-        useCaseSurfaceManager.stopAsync()
-        useCaseCameraGraphConfig.graph.close()
+    override fun close(): Job {
+        return threads.scope.launch {
+            useCaseCameraGraphConfig.graph.close()
+            useCaseSurfaceManager.stopAsync().await()
+        }
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
index d6e3c92..0064af2 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
@@ -29,6 +29,7 @@
     /**
      * Creates a [CameraXConfig] containing a default CameraPipe implementation for CameraX.
      */
+    @JvmStatic
     fun defaultConfig(): CameraXConfig {
         return CameraXConfig.Builder()
             .setCameraFactoryProvider(::CameraFactoryAdapter)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index 5a44bc0..1286de1 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -21,20 +21,20 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.core.Log.debug
-import androidx.camera.camera2.pipe.core.Log.warn
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
+import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.CameraControlInternal
 import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.CameraInternal
 import androidx.camera.core.impl.LiveDataObservable
 import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.utils.futures.Futures
 import com.google.common.util.concurrent.ListenableFuture
 import javax.inject.Inject
 import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.launch
 
 internal val cameraAdapterIds = atomic(0)
 
@@ -46,7 +46,8 @@
     config: CameraConfig,
     private val useCaseManager: UseCaseManager,
     private val cameraInfo: CameraInfoInternal,
-    private val cameraController: CameraControlInternal
+    private val cameraController: CameraControlInternal,
+    private val threads: UseCaseThreads,
 ) : CameraInternal {
     private val cameraId = config.cameraId
     private val debugId = cameraAdapterIds.incrementAndGet()
@@ -69,9 +70,8 @@
     }
 
     override fun release(): ListenableFuture<Void> {
-        warn { "$this#release is not yet implemented." }
-        // TODO: Determine what the correct way to invoke release is.
-        return Futures.immediateFuture(null)
+        // TODO(b/185207100): Implement when CameraState is ready.
+        return threads.scope.launch { useCaseManager.clear() }.asListenableFuture()
     }
 
     override fun getCameraInfoInternal(): CameraInfoInternal = cameraInfo
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index ee07f3c..9c7b54f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -49,7 +49,7 @@
     init {
         debug { "AvailableCameraIds = $availableCameraIds" }
         debug { "Created StreamConfigurationMap from $context" }
-        initSupportedSurfaceCombinationMap(context, component.getCameraPipe(), availableCameraIds)
+        initSupportedSurfaceCombinationMap(context, availableCameraIds)
     }
 
     /**
@@ -57,7 +57,6 @@
      */
     private fun initSupportedSurfaceCombinationMap(
         context: Context,
-        cameraPipe: CameraPipe,
         availableCameraIds: Set<String>
     ) {
         Preconditions.checkNotNull(context)
@@ -65,7 +64,7 @@
             supportedSurfaceCombinationMap[cameraId] =
                 SupportedSurfaceCombination(
                     context,
-                    runBlocking { cameraPipe.cameras().awaitMetadata(CameraId(cameraId)) },
+                    runBlocking { component.getCameraDevices().awaitMetadata(CameraId(cameraId)) },
                     cameraId,
                     CamcorderProfileProviderAdapter(cameraId)
                 )
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index 89e5b2c..fc355fe 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -82,7 +82,7 @@
     private val outputSizesCache: MutableMap<Int, Array<Size>> = HashMap()
     private var isRawSupported = false
     private var isBurstCaptureSupported = false
-    private lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
+    internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
     private val displayManager: DisplayManager =
         (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
 
@@ -459,7 +459,7 @@
      * @param imageFormat the image format info
      * @return the max supported output size for the image format
      */
-    private fun getMaxOutputSizeByFormat(imageFormat: Int): Size {
+    internal fun getMaxOutputSizeByFormat(imageFormat: Int): Size {
         val outputSizes = getAllOutputSizesByFormat(imageFormat)
         return Collections.max(listOf(*outputSizes), CompareSizesByArea())
     }
@@ -690,7 +690,7 @@
     /**
      * Obtains the supported sizes for a given user case.
      */
-    private fun getSupportedOutputSizes(config: UseCaseConfig<*>): List<Size> {
+    internal fun getSupportedOutputSizes(config: UseCaseConfig<*>): List<Size> {
         val imageFormat = config.inputFormat
         val imageOutputConfig = config as ImageOutputConfig
         var outputSizes: Array<Size>? =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
index 5c014ea..9ce1084 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -20,6 +20,7 @@
 
 import android.content.Context
 import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraDevices
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CameraThreadConfig
@@ -39,6 +40,11 @@
         fun provideCameraPipe(context: Context): CameraPipe {
             return CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
         }
+
+        @Provides
+        fun provideCameraDevices(cameraPipe: CameraPipe): CameraDevices {
+            return cameraPipe.cameras()
+        }
     }
 }
 
@@ -66,6 +72,7 @@
 interface CameraAppComponent {
     fun cameraBuilder(): CameraComponent.Builder
     fun getCameraPipe(): CameraPipe
+    fun getCameraDevices(): CameraDevices
 
     @Component.Builder
     interface Builder {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index dace968..561805c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -36,6 +36,8 @@
 import javax.inject.Inject
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
 
 internal val useCaseCameraIds = atomic(0)
 internal val defaultOptionPriority = Config.OptionPriority.OPTIONAL
@@ -68,7 +70,7 @@
     ): Deferred<Result3A>
 
     // Lifecycle
-    fun close()
+    fun close(): Job
 }
 
 /**
@@ -79,6 +81,7 @@
     private val useCaseGraphConfig: UseCaseGraphConfig,
     private val useCases: java.util.ArrayList<UseCase>,
     private val useCaseSurfaceManager: UseCaseSurfaceManager,
+    private val threads: UseCaseThreads,
     override val requestControl: UseCaseCameraRequestControl,
 ) : UseCaseCamera {
     private val debugId = useCaseCameraIds.incrementAndGet()
@@ -104,10 +107,12 @@
         debug { "Configured $this for $useCases" }
     }
 
-    override fun close() {
-        debug { "Closing $this" }
-        useCaseSurfaceManager.stopAsync()
-        useCaseGraphConfig.graph.close()
+    override fun close(): Job {
+        return threads.scope.launch {
+            debug { "Closing $this" }
+            useCaseGraphConfig.graph.close()
+            useCaseSurfaceManager.stopAsync().await()
+        }
     }
 
     override suspend fun startFocusAndMeteringAsync(
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 956ff2f..9314817 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
@@ -25,6 +25,8 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.UseCase
 import javax.inject.Inject
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.joinAll
 
 /**
  * This class keeps track of the currently attached and active [UseCase]'s for a specific camera.
@@ -71,6 +73,8 @@
     val camera: UseCaseCamera?
         get() = _activeComponent?.getUseCaseCamera()
 
+    private val closingCameraJob = mutableListOf<Job>()
+
     /**
      * This attaches the specified [useCases] to the current set of attached use cases. When any
      * changes are identified (i.e., a new use case is added), the subsequent actions would trigger
@@ -162,6 +166,11 @@
         }
     }
 
+    suspend fun clear() {
+        detach(attachedUseCases.toList())
+        closingCameraJob.toList().joinAll()
+    }
+
     override fun toString(): String = "UseCaseManager<${cameraConfig.cameraId}>"
 
     private fun refreshRunningUseCases() {
@@ -179,7 +188,12 @@
         // Close prior camera graph
         camera.let {
             _activeComponent = null
-            it?.close()
+            it?.close()?.let { closingJob ->
+                closingCameraJob.add(closingJob)
+                closingJob.invokeOnCompletion {
+                    closingCameraJob.remove(closingJob)
+                }
+            }
         }
 
         // Update list of active useCases
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
new file mode 100644
index 0000000..b27be3a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -0,0 +1,2713 @@
+/*
+ * 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.camera2.pipe.integration.adapter
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.media.CamcorderProfile
+import android.media.MediaRecorder
+import android.os.Build
+import android.util.Pair
+import android.util.Rational
+import android.util.Size
+import android.view.Surface
+import android.view.WindowManager
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig.defaultConfig
+import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getFullSupportedCombinationList
+import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getLegacySupportedCombinationList
+import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getLevel3SupportedCombinationList
+import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getLimitedSupportedCombinationList
+import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getRAWSupportedCombinationList
+import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.camera2.pipe.integration.testing.FakeCameraDevicesWithCameraMetaData
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraSelector.LensFacing
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CamcorderProfileProxy
+import androidx.camera.core.impl.CameraThreadConfig
+import androidx.camera.core.impl.MutableStateObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.SurfaceCombination
+import androidx.camera.core.impl.SurfaceConfig
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.CamcorderProfileUtil
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraXUtil
+import androidx.camera.testing.Configs
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
+import androidx.camera.testing.fakes.FakeCamcorderProfileProvider
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import androidx.camera.video.FallbackStrategy
+import androidx.camera.video.MediaSpec
+import androidx.camera.video.Quality
+import androidx.camera.video.QualitySelector
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoOutput
+import androidx.camera.video.VideoOutput.SourceState
+import androidx.camera.video.VideoSpec
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth
+import java.util.Arrays
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowCameraManager
+
+@Suppress("DEPRECATION")
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class SupportedSurfaceCombinationTest {
+    private val cameraId = "0"
+    private val cameraIdExternal = "0-external"
+    private val sensorOrientation0 = 0
+    private val sensorOrientation90 = 90
+    private val aspectRatio43 = Rational(4, 3)
+    private val aspectRatio169 = Rational(16, 9)
+    private val landscapePixelArraySize = Size(4032, 3024)
+    private val portraitPixelArraySize = Size(3024, 4032)
+    private val displaySize = Size(720, 1280)
+    private val vgaSize = Size(640, 480)
+    private val previewSize = Size(1280, 720)
+    private val recordSize = Size(3840, 2160)
+    private val maximumSize = Size(4032, 3024)
+    private val legacyVideoMaximumVideoSize = Size(1920, 1080)
+    private val mod16Size = Size(960, 544)
+    private val profileUhd = CamcorderProfileUtil.createCamcorderProfileProxy(
+        CamcorderProfile.QUALITY_2160P, recordSize.width, recordSize
+            .height
+    )
+    private val profileFhd = CamcorderProfileUtil.createCamcorderProfileProxy(
+        CamcorderProfile.QUALITY_1080P, 1920, 1080
+    )
+    private val profileHd = CamcorderProfileUtil.createCamcorderProfileProxy(
+        CamcorderProfile.QUALITY_720P, previewSize.width, previewSize
+            .height
+    )
+    private val profileSd = CamcorderProfileUtil.createCamcorderProfileProxy(
+        CamcorderProfile.QUALITY_480P, vgaSize.width,
+        vgaSize.height
+    )
+    private val supportedSizes = arrayOf(
+        Size(4032, 3024), // 4:3
+        Size(3840, 2160), // 16:9
+        Size(1920, 1440), // 4:3
+        Size(1920, 1080), // 16:9
+        Size(1280, 960), // 4:3
+        Size(1280, 720), // 16:9
+        Size(1280, 720), // duplicate the size since Nexus 5X emulator has the
+        Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
+        Size(800, 450), // 16:9
+        Size(640, 480), // 4:3
+        Size(320, 240), // 4:3
+        Size(320, 180), // 16:9
+        Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
+    )
+    private val context = InstrumentationRegistry.getInstrumentation().context
+    private var cameraFactory: FakeCameraFactory? = null
+    private var useCaseConfigFactory = Mockito.mock(
+        UseCaseConfigFactory::class.java
+    )
+    private val mockCameraMetadata = Mockito.mock(
+        CameraMetadata::class.java
+    )
+    private val mockCameraAppComponent = Mockito.mock(
+        CameraAppComponent::class.java
+    )
+    private val mockCamcorderProfileAdapter = Mockito.mock(
+        CamcorderProfileProviderAdapter::class.java
+    )
+    private val mockCamcorderProxy = Mockito.mock(
+        CamcorderProfileProxy::class.java
+    )
+
+    @Before
+    fun setUp() {
+        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(displaySize.width)
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(
+            displaySize
+                .height
+        )
+        Mockito.`when`(mockCamcorderProfileAdapter.hasProfile(ArgumentMatchers.anyInt()))
+            .thenReturn(true)
+        Mockito.`when`(mockCamcorderProxy.videoFrameWidth).thenReturn(3840)
+        Mockito.`when`(mockCamcorderProxy.videoFrameHeight).thenReturn(2160)
+        Mockito.`when`(mockCamcorderProfileAdapter[ArgumentMatchers.anyInt()])
+            .thenReturn(mockCamcorderProxy)
+    }
+
+    @After
+    fun tearDown() {
+        CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
+    }
+
+    @Test
+    fun checkLegacySurfaceCombinationSupportedInLegacyDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLegacySupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkLegacySurfaceCombinationSubListSupportedInLegacyDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLegacySupportedCombinationList()
+        val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
+        Truth.assertThat(isSupported).isTrue()
+    }
+
+    @Test
+    fun checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLimitedSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isFalse()
+        }
+    }
+
+    @Test
+    fun checkFullSurfaceCombinationNotSupportedInLegacyDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getFullSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isFalse()
+        }
+    }
+
+    @Test
+    fun checkLevel3SurfaceCombinationNotSupportedInLegacyDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLevel3SupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isFalse()
+        }
+    }
+
+    @Test
+    fun checkLimitedSurfaceCombinationSupportedInLimitedDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLimitedSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkLimitedSurfaceCombinationSubListSupportedInLimited3Device() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLimitedSupportedCombinationList()
+        val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
+        Truth.assertThat(isSupported).isTrue()
+    }
+
+    @Test
+    fun checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getFullSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isFalse()
+        }
+    }
+
+    @Test
+    fun checkLevel3SurfaceCombinationNotSupportedInLimitedDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLevel3SupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isFalse()
+        }
+    }
+
+    @Test
+    fun checkFullSurfaceCombinationSupportedInFullDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getFullSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkFullSurfaceCombinationSubListSupportedInFullDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getFullSupportedCombinationList()
+        val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
+        Truth.assertThat(isSupported).isTrue()
+    }
+
+    @Test
+    fun checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLevel3SupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isFalse()
+        }
+    }
+
+    @Test
+    fun checkLimitedSurfaceCombinationSupportedInRawDevice() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLimitedSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkLegacySurfaceCombinationSupportedInRawDevice() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLegacySupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkFullSurfaceCombinationSupportedInRawDevice() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getFullSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkRawSurfaceCombinationSupportedInRawDevice() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getRAWSupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkLevel3SurfaceCombinationSupportedInLevel3Device() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLevel3SupportedCombinationList()
+        for (combination in combinationList) {
+            val isSupported =
+                supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    @Test
+    fun checkLevel3SurfaceCombinationSubListSupportedInLevel3Device() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val combinationList = getLevel3SupportedCombinationList()
+        val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
+        Truth.assertThat(isSupported).isTrue()
+    }
+
+    @Test
+    fun checkTargetAspectRatio() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val fakeUseCase = FakeUseCaseConfig.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .build()
+        val useCases: MutableList<UseCase> = ArrayList()
+        useCases.add(fakeUseCase)
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            useCases,
+            useCaseConfigFactory
+        )
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            ArrayList(useCaseToConfigMap.values)
+        )
+        val selectedSize = suggestedResolutionMap[useCaseToConfigMap[fakeUseCase]]!!
+        val resultAspectRatio = Rational(
+            selectedSize.width,
+            selectedSize.height
+        )
+        Truth.assertThat(resultAspectRatio).isEqualTo(aspectRatio169)
+    }
+
+    @Test
+    fun checkResolutionForMixedUseCase_AfterBindToLifecycle() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+
+        // The test case make sure the selected result is expected after the regular flow.
+        val targetAspectRatio = aspectRatio169
+        val preview = Preview.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .build()
+        preview.setSurfaceProvider(
+            CameraXExecutors.directExecutor(),
+            SurfaceTextureProvider.createSurfaceTextureProvider(
+                Mockito.mock(
+                    SurfaceTextureCallback::class.java
+                )
+            )
+        )
+        val imageCapture = ImageCapture.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .build()
+        val imageAnalysis = ImageAnalysis.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .build()
+        val cameraUseCaseAdapter = CameraUtil
+            .createCameraUseCaseAdapter(
+                context,
+                CameraSelector.DEFAULT_BACK_CAMERA
+            )
+        cameraUseCaseAdapter.addUseCases(listOf(preview, imageCapture, imageAnalysis))
+        val previewResolution = preview.attachedSurfaceResolution!!
+        val previewRatio = Rational(
+            previewResolution.width,
+            previewResolution.height
+        )
+        val imageCaptureResolution = preview.attachedSurfaceResolution
+        val imageCaptureRatio = Rational(
+            imageCaptureResolution!!.width,
+            imageCaptureResolution.height
+        )
+        val imageAnalysisResolution = preview.attachedSurfaceResolution
+        val imageAnalysisRatio = Rational(
+            imageAnalysisResolution!!.width,
+            imageAnalysisResolution.height
+        )
+
+        // Checks no correction is needed.
+        Truth.assertThat(previewRatio).isEqualTo(targetAspectRatio)
+        Truth.assertThat(imageCaptureRatio).isEqualTo(targetAspectRatio)
+        Truth.assertThat(imageAnalysisRatio).isEqualTo(targetAspectRatio)
+    }
+
+    @Test
+    fun checkDefaultAspectRatioAndResolutionForMixedUseCase() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val preview = Preview.Builder().build()
+        preview.setSurfaceProvider(
+            CameraXExecutors.directExecutor(),
+            SurfaceTextureProvider.createSurfaceTextureProvider(
+                Mockito.mock(
+                    SurfaceTextureCallback::class.java
+                )
+            )
+        )
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+
+        // Preview/ImageCapture/ImageAnalysis' default config settings that will be applied after
+        // bound to lifecycle. Calling bindToLifecycle here to make sure sizes matching to
+        // default aspect ratio will be selected.
+        val cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(
+            context,
+            CameraSelector.DEFAULT_BACK_CAMERA
+        )
+        cameraUseCaseAdapter.addUseCases(
+            listOf(
+                preview,
+                imageCapture, imageAnalysis
+            )
+        )
+        val useCases: MutableList<UseCase> = ArrayList()
+        useCases.add(preview)
+        useCases.add(imageCapture)
+        useCases.add(imageAnalysis)
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            useCases,
+            useCaseConfigFactory
+        )
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            ArrayList(useCaseToConfigMap.values)
+        )
+        val previewSize = suggestedResolutionMap[useCaseToConfigMap[preview]]
+        val imageCaptureSize = suggestedResolutionMap[useCaseToConfigMap[imageCapture]]
+        val imageAnalysisSize = suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]
+        assert(previewSize != null)
+        val previewAspectRatio = Rational(
+            previewSize!!.width,
+            previewSize.height
+        )
+        assert(imageCaptureSize != null)
+        val imageCaptureAspectRatio = Rational(
+            imageCaptureSize!!.width,
+            imageCaptureSize.height
+        )
+        assert(imageAnalysisSize != null)
+        val imageAnalysisAspectRatio = Rational(
+            imageAnalysisSize!!.width,
+            imageAnalysisSize.height
+        )
+
+        // Checks the default aspect ratio.
+        Truth.assertThat(previewAspectRatio).isEqualTo(aspectRatio43)
+        Truth.assertThat(imageCaptureAspectRatio).isEqualTo(aspectRatio43)
+        Truth.assertThat(imageAnalysisAspectRatio).isEqualTo(aspectRatio43)
+
+        // Checks the default resolution.
+        Truth.assertThat(imageAnalysisSize).isEqualTo(vgaSize)
+    }
+
+    @Test
+    fun checkSmallSizesAreFilteredOutByDefaultSize480p() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+
+        /* This test case is for b/139018208 that get small resolution 144x256 with below
+        conditions:
+        1. The target aspect ratio is set to the screen size 1080 x 2220 (9:18.5).
+        2. The camera doesn't provide any 9:18.5 resolution and the size 144x256(9:16)
+         is considered the 9:18.5 mod16 version.
+        3. There is no other bigger resolution matched the target aspect ratio.
+        */
+        val displayWidth = 1080
+        val displayHeight = 2220
+        val preview = Preview.Builder()
+            .setTargetResolution(Size(displayHeight, displayWidth))
+            .build()
+        val useCases: MutableList<UseCase> = ArrayList()
+        useCases.add(preview)
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            useCases,
+            useCaseConfigFactory
+        )
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            ArrayList(useCaseToConfigMap.values)
+        )
+
+        // Checks the preconditions.
+        val preconditionSize = Size(256, 144)
+        val targetRatio = Rational(displayHeight, displayWidth)
+        val sizeList = ArrayList(listOf(*supportedSizes))
+        Truth.assertThat(sizeList).contains(preconditionSize)
+        for (s in supportedSizes) {
+            val supportedRational = Rational(s.width, s.height)
+            Truth.assertThat(supportedRational).isNotEqualTo(targetRatio)
+        }
+
+        // Checks the mechanism has filtered out the sizes which are smaller than default size
+        // 480p.
+        val previewSize = suggestedResolutionMap[useCaseToConfigMap[preview]]
+        Truth.assertThat(previewSize).isNotEqualTo(preconditionSize)
+    }
+
+    @Test
+    fun checkAspectRatioMatchedSizeCanBeSelected() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+
+        // Sets each of mSupportedSizes as target resolution and also sets target rotation as
+        // Surface.ROTATION to make it aligns the sensor direction and then exactly the same size
+        // will be selected as the result. This test can also verify that size smaller than
+        // 640x480 can be selected after set as target resolution.
+        for (targetResolution in supportedSizes) {
+            val imageCapture = ImageCapture.Builder().setTargetResolution(
+                targetResolution
+            ).setTargetRotation(Surface.ROTATION_90).build()
+            val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+                emptyList(),
+                listOf(imageCapture.currentConfig)
+            )
+            Truth.assertThat(targetResolution).isEqualTo(
+                suggestedResolutionMap[imageCapture.currentConfig]
+            )
+        }
+    }
+
+    @Test
+    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+
+        // Sets target resolution as 1200x720, all supported resolutions will be put into aspect
+        // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
+        // checks whether 1280x720 is selected or not.
+        val targetResolution = Size(1200, 720)
+        val imageCapture = ImageCapture.Builder().setTargetResolution(
+            targetResolution
+        ).setTargetRotation(Surface.ROTATION_90).build()
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            listOf(imageCapture.currentConfig)
+        )
+        Truth.assertThat(Size(1280, 720)).isEqualTo(
+            suggestedResolutionMap[imageCapture.currentConfig]
+        )
+    }
+
+    @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(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val imageCapture = ImageCapture.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .build()
+        val videoCapture = createVideoCapture()
+        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 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(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+
+        // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
+        val quality = Quality.UHD
+        val previewResolutionsPairs = listOf(
+            Pair.create(ImageFormat.PRIVATE, arrayOf(previewSize))
+        )
+        val videoCapture = createVideoCapture(quality)
+        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 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() {
+        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 = createVideoCapture(Quality.HIGHEST)
+        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],
+            recordSize
+        )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[preview],
+            previewSize
+        )
+    }
+
+    @Test
+    fun suggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val imageCapture = ImageCapture.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .build()
+        val videoCapture = createVideoCapture(
+            QualitySelector.from(
+                Quality.UHD,
+                FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
+            )
+        )
+        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)
+            )
+
+        // There are two possible combinations in Full level device
+        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD) => should be applied
+        // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[imageCapture],
+            recordSize
+        )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[videoCapture],
+            recordSize
+        )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[preview],
+            previewSize
+        )
+    }
+
+    @Test
+    fun suggestedResInFullDevice_videoRecordSizeLowPriority_imageCanGetMaxSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val imageCapture = ImageCapture.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_4_3) // mMaximumSize(4032x3024) is 4:3
+            .build()
+        val videoCapture = createVideoCapture(
+            QualitySelector.fromOrderedList(
+                listOf(Quality.HD, Quality.FHD, Quality.UHD)
+            )
+        )
+        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)
+            )
+
+        // There are two possible combinations in Full level device
+        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+        // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM) => should be applied
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[imageCapture],
+            maximumSize
+        )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[videoCapture],
+            previewSize
+        ) // Quality.HD
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[preview],
+            previewSize
+        )
+    }
+
+    @Test
+    fun suggestedResolutionsWithSameSupportedListForDifferentUseCases() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+
+        /* This test case is for b/132603284 that divide by zero issue crash happened in below
+    conditions:
+    1. There are duplicated two 1280x720 supported sizes for ImageCapture and Preview.
+    2. supportedOutputSizes for ImageCapture and Preview in
+    SupportedSurfaceCombination#getAllPossibleSizeArrangements are the same.
+    */
+        val imageCapture = ImageCapture.Builder()
+            .setTargetResolution(displaySize)
+            .build()
+        val preview = Preview.Builder()
+            .setTargetResolution(displaySize)
+            .build()
+        val imageAnalysis = ImageAnalysis.Builder()
+            .setTargetResolution(displaySize)
+            .build()
+        val useCases: MutableList<UseCase> = ArrayList()
+        useCases.add(imageCapture)
+        useCases.add(preview)
+        useCases.add(imageAnalysis)
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            useCases,
+            useCaseConfigFactory
+        )
+        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
+            supportedSurfaceCombination.getSuggestedResolutions(
+                emptyList(),
+                ArrayList(useCaseToConfigMap.values)
+            )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[imageCapture],
+            previewSize
+        )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[preview],
+            previewSize
+        )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[imageAnalysis],
+            previewSize
+        )
+    }
+
+    @Test
+    fun throwsWhenSetBothTargetResolutionAndAspectRatioForDifferentUseCases() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        var previewExceptionHappened = false
+        val previewBuilder = Preview.Builder()
+            .setTargetResolution(displaySize)
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+        try {
+            previewBuilder.build()
+        } catch (e: IllegalArgumentException) {
+            previewExceptionHappened = true
+        }
+        Truth.assertThat(previewExceptionHappened).isTrue()
+        var imageCaptureExceptionHappened = false
+        val imageCaptureConfigBuilder = ImageCapture.Builder()
+            .setTargetResolution(displaySize)
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+        try {
+            imageCaptureConfigBuilder.build()
+        } catch (e: IllegalArgumentException) {
+            imageCaptureExceptionHappened = true
+        }
+        Truth.assertThat(imageCaptureExceptionHappened).isTrue()
+        var imageAnalysisExceptionHappened = false
+        val imageAnalysisConfigBuilder = ImageAnalysis.Builder()
+            .setTargetResolution(displaySize)
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+        try {
+            imageAnalysisConfigBuilder.build()
+        } catch (e: IllegalArgumentException) {
+            imageAnalysisExceptionHappened = true
+        }
+        Truth.assertThat(imageAnalysisExceptionHappened).isTrue()
+    }
+
+    @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.
+        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 = createVideoCapture(Quality.SD)
+        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 transformSurfaceConfigWithYUVAnalysisSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.YUV_420_888, vgaSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.VGA)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithYUVPreviewSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.YUV_420_888, previewSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.PREVIEW)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithYUVRecordSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.YUV_420_888, recordSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.RECORD)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithYUVMaximumSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.YUV_420_888, maximumSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.MAXIMUM)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithJPEGAnalysisSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.JPEG, vgaSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.VGA)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithJPEGPreviewSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.JPEG, previewSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.PREVIEW)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithJPEGRecordSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.JPEG, recordSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.RECORD)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun transformSurfaceConfigWithJPEGMaximumSize() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
+            ImageFormat.JPEG, maximumSize
+        )
+        val expectedSurfaceConfig =
+            SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.MAXIMUM)
+        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+    }
+
+    @Test
+    fun maximumSizeForImageFormat() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val maximumYUVSize =
+            supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.YUV_420_888)
+        Truth.assertThat(maximumYUVSize).isEqualTo(maximumSize)
+        val maximumJPEGSize =
+            supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
+        Truth.assertThat(maximumJPEGSize).isEqualTo(maximumSize)
+    }
+
+    @Test
+    fun isAspectRatioMatchWithSupportedMod16Resolution() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val preview = Preview.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .setDefaultResolution(mod16Size)
+            .build()
+        val imageCapture = ImageCapture.Builder()
+            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+            .setDefaultResolution(mod16Size)
+            .build()
+        val useCases: MutableList<UseCase> = ArrayList()
+        useCases.add(preview)
+        useCases.add(imageCapture)
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            useCases,
+            useCaseConfigFactory
+        )
+        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
+            supportedSurfaceCombination.getSuggestedResolutions(
+                emptyList(),
+                ArrayList(useCaseToConfigMap.values)
+            )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[preview],
+            mod16Size
+        )
+        Truth.assertThat(suggestedResolutionMap).containsEntry(
+            useCaseToConfigMap[imageCapture],
+            mod16Size
+        )
+    }
+
+    @Test
+    fun sortByCompareSizesByArea_canSortSizesCorrectly() {
+        val sizes = arrayOfNulls<Size>(supportedSizes.size)
+
+        // Generates a unsorted array from mSupportedSizes.
+        val centerIndex = supportedSizes.size / 2
+        // Puts 2nd half sizes in the front
+        if (supportedSizes.size - centerIndex >= 0) {
+            System.arraycopy(
+                supportedSizes,
+                centerIndex, sizes, 0,
+                supportedSizes.size - centerIndex
+            )
+        }
+        // Puts 1st half sizes inversely in the tail
+        for (j in centerIndex - 1 downTo 0) {
+            sizes[supportedSizes.size - j - 1] = supportedSizes[j]
+        }
+
+        // The testing sizes array will be equal to mSupportedSizes after sorting.
+        Arrays.sort(sizes, CompareSizesByArea(true))
+        Truth.assertThat(listOf(*sizes)).isEqualTo(listOf(*supportedSizes))
+    }
+
+    @Test
+    fun supportedOutputSizes_noConfigSettings() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. No any aspect ratio related setting. The returned sizes list will be sorted in
+        // descending order.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(4032, 3024),
+            Size(3840, 2160),
+            Size(1920, 1440),
+            Size(1920, 1080),
+            Size(1280, 960),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_aspectRatio4x3() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
+            .build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. Sizes of aspect ratio 4/3 will be in front of the returned sizes list and the
+        // list is sorted in descending order. Other items will be put in the following that are
+        // sorted by aspect ratio delta and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(
+                640,
+                480
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_aspectRatio16x9() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
+            AspectRatio.RATIO_16_9
+        ).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
+        // list is sorted in descending order. Other items will be put in the following that are
+        // sorted by aspect ratio delta and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(
+                800,
+                450
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_targetResolution1080x1920InRotation0() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
+            Size(1080, 1920)
+        ).build()
+
+        // Unnecessary big enough sizes will be removed from the result list. There is default
+        // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
+        // target resolution will be calibrated by default target rotation 0 degree. The
+        // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
+        // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
+        // 16/9 will be in front of the returned sizes list and the list is sorted in descending
+        // order. Other items will be put in the following that are sorted by aspect ratio delta
+        // and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(
+                800,
+                450
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_targetResolutionLargerThan640x480() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
+            Surface.ROTATION_90
+        ).setTargetResolution(Size(1280, 960)).build()
+
+        // Unnecessary big enough sizes will be removed from the result list. There is default
+        // minimum size 640x480 setting. Target resolution larger than 640x480 won't overwrite
+        // minimum size setting. Sizes smaller than 640x480 will be removed. The auto-resolution
+        // mechanism will try to select the sizes which aspect ratio is nearest to the aspect
+        // ratio of target resolution in priority. Therefore, sizes of aspect ratio 4/3 will be
+        // in front of the returned sizes list and the list is sorted in descending order. Other
+        // items will be put in the following that are sorted by aspect ratio delta and then area
+        // size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(
+                640,
+                480
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_targetResolutionSmallerThan640x480() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
+            Surface.ROTATION_90
+        ).setTargetResolution(Size(320, 240)).build()
+
+        // Unnecessary big enough sizes will be removed from the result list. Minimum size will
+        // be overwritten as 320x240. Sizes smaller than 320x240 will also be removed. The
+        // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
+        // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
+        // 4/3 will be in front of the returned sizes list and the list is sorted in descending
+        // order. Other items will be put in the following that are sorted by aspect ratio delta
+        // and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(
+                320,
+                240
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(800, 450)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_targetResolution1800x1440NearTo4x3() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
+            Surface.ROTATION_90
+        ).setTargetResolution(Size(1800, 1440)).build()
+
+        // Unnecessary big enough sizes will be removed from the result list. There is default
+        // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
+        // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
+        // to the aspect ratio of target resolution in priority. Size 1800x1440 is near to 4/3
+        // therefore, sizes of aspect ratio 4/3 will be in front of the returned sizes list and
+        // the list is sorted in descending order.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Sizes of 4/3 are near to aspect ratio of 1800/1440
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480), // Sizes of 16/9 are far to aspect ratio of 1800/1440
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_targetResolution1280x600NearTo16x9() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
+            Size(1280, 600)
+        ).setTargetRotation(Surface.ROTATION_90).build()
+
+        // Unnecessary big enough sizes will be removed from the result list. There is default
+        // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
+        // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
+        // to the aspect ratio of target resolution in priority. Size 1280x600 is near to 16/9,
+        // therefore, sizes of aspect ratio 16/9 will be in front of the returned sizes list and
+        // the list is sorted in descending order.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Sizes of 16/9 are near to aspect ratio of 1280/600
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450), // Sizes of 4/3 are far to aspect ratio of 1280/600
+            Size(1280, 960),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_maxResolution1280x720() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(Size(1280, 720)).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 or
+        // larger than 1280x720 will be removed. The returned sizes list will be sorted in
+        // descending order.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_defaultResolution1280x720_noTargetResolution() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setDefaultResolution(
+            Size(
+                1280,
+                720
+            )
+        ).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. If there is no target resolution setting, it will be overwritten by default
+        // resolution as 1280x720. Unnecessary big enough sizes will also be removed. The
+        // returned sizes list will be sorted in descending order.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_defaultResolution1280x720_targetResolution1920x1080() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setDefaultResolution(
+            Size(1280, 720)
+        ).setTargetRotation(Surface.ROTATION_90).setTargetResolution(
+            Size(1920, 1080)
+        ).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. There is target resolution 1920x1080, it won't be overwritten by default
+        // resolution 1280x720. Unnecessary big enough sizes will also be removed. Sizes of
+        // aspect ratio 16/9 will be in front of the returned sizes list and the list is sorted
+        // in descending order.  Other items will be put in the following that are sorted by
+        // aspect ratio delta and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(
+                800,
+                450
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_fallbackToGuaranteedResolution_whenNotFulfillConditions() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
+            Size(1920, 1080)
+        ).setTargetRotation(Surface.ROTATION_90).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. There is target resolution 1920x1080 (16:9). Even 640x480 does not match 16:9
+        // requirement, it will still be returned to use.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(Size(640, 480))
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenMaxSizeSmallerThanDefaultMiniSize() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
+            Size(320, 240)
+        ).build()
+
+        // There is default minimum size 640x480 setting. Originally, sizes smaller than 640x480
+        // will be removed. Due to maximal size bound is smaller than the default minimum size
+        // bound and it is also smaller than 640x480, the default minimum size bound will be
+        // ignored. Then, sizes equal to or smaller than 320x240 will be kept in the result list.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(320, 240),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenMaxSizeSmallerThanSmallTargetResolution() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
+            Size(320, 180)
+        ).setTargetResolution(Size(320, 240)).setTargetRotation(
+            Surface.ROTATION_90
+        ).build()
+
+        // The default minimum size 640x480 will be overwritten by the target resolution 320x240.
+        // Originally, sizes smaller than 320x240 will be removed. Due to maximal size bound is
+        // smaller than the minimum size bound and it is also smaller than 640x480, the minimum
+        // size bound will be ignored. Then, sizes equal to or smaller than 320x180 will be kept
+        // in the result list.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(320, 180),
+            Size(256, 144)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenBothMaxAndTargetResolutionsSmallerThan640x480() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
+            Size(320, 240)
+        ).setTargetResolution(Size(320, 180)).setTargetRotation(
+            Surface.ROTATION_90
+        ).build()
+
+        // The default minimum size 640x480 will be overwritten by the target resolution 320x180.
+        // Originally, sizes smaller than 320x180 will be removed. Due to maximal size bound is
+        // smaller than the minimum size bound and it is also smaller than 640x480, the minimum
+        // size bound will be ignored. Then, all sizes equal to or smaller than 320x320 will be
+        // kept in the result list.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(320, 180),
+            Size(256, 144),
+            Size(320, 240)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenMaxSizeSmallerThanBigTargetResolution() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
+            Size(1920, 1080)
+        ).setTargetResolution(Size(3840, 2160)).setTargetRotation(
+            Surface.ROTATION_90
+        ).build()
+
+        // Because the target size 3840x2160 is larger than 640x480, it won't overwrite the
+        // default minimum size 640x480. Sizes smaller than 640x480 will be removed. The
+        // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
+        // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
+        // 16/9 will be in front of the returned sizes list and the list is sorted in descending
+        // order. Other items will be put in the following that are sorted by aspect ratio delta
+        // and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(
+                800,
+                450
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1280, 960),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenNoSizeBetweenMaxSizeAndTargetResolution() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
+            Size(320, 200)
+        ).setTargetResolution(Size(320, 190)).setTargetRotation(
+            Surface.ROTATION_90
+        ).build()
+
+        // The default minimum size 640x480 will be overwritten by the target resolution 320x190.
+        // Originally, sizes smaller than 320x190 will be removed. Due to there is no available
+        // size between the maximal size and the minimum size bound and the maximal size is
+        // smaller than 640x480, the default minimum size bound will be ignored. Then, sizes
+        // equal to or smaller than 320x200 will be kept in the result list.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(320, 180),
+            Size(256, 144)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenTargetResolutionSmallerThanAnySize() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
+            Size(192, 144)
+        ).setTargetRotation(Surface.ROTATION_90).build()
+
+        // The default minimum size 640x480 will be overwritten by the target resolution 192x144.
+        // Because 192x144 is smaller than any size in the supported list, no one will be
+        // filtered out by it. The result list will only keep one big enough size of aspect ratio
+        // 4:3 and 16:9.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(320, 240),
+            Size(256, 144)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenMaxResolutionSmallerThanAnySize() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
+            Size(192, 144)
+        ).build()
+
+        // All sizes will be filtered out by the max resolution 192x144 setting and an
+        // IllegalArgumentException will be thrown.
+        assertThrows(IllegalArgumentException::class.java) {
+            supportedSurfaceCombination.getSupportedOutputSizes(useCase.currentConfig)
+        }
+    }
+
+    @Test
+    fun supportedOutputSizes_whenMod16IsIgnoredForSmallSizes() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(296, 144),
+                Size(256, 144)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
+            Size(185, 90)
+        ).setTargetRotation(Surface.ROTATION_90).build()
+
+        // The default minimum size 640x480 will be overwritten by the target resolution 185x90
+        // (18.5:9). If mod 16 calculation is not ignored for the sizes smaller than 640x480, the
+        // size 256x144 will be considered to match 18.5:9 and then become the first item in the
+        // result list. After ignoring mod 16 calculation for small sizes, 256x144 will still be
+        // kept as a 16:9 resolution as the result.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(296, 144),
+            Size(256, 144),
+            Size(320, 240)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizes_whenOneMod16SizeClosestToTargetResolution() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, arrayOf(
+                Size(1920, 1080),
+                Size(1440, 1080),
+                Size(1280, 960),
+                Size(1280, 720),
+                Size(864, 480), // This is a 16:9 mod16 size that is closest to 2016x1080
+                Size(768, 432),
+                Size(640, 480),
+                Size(640, 360),
+                Size(480, 360),
+                Size(384, 288)
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
+            Size(1080, 2016)
+        ).build()
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf(
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(864, 480),
+            Size(768, 432),
+            Size(1440, 1080),
+            Size(1280, 960),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
+        val supportedSizes = arrayOf(
+            Size(1080, 1920),
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(720, 1280),
+            Size(1280, 720),
+            Size(480, 640),
+            Size(640, 480),
+            Size(360, 480)
+        )
+
+        // Sets the sensor orientation as 0 and pixel array size as a portrait size to simulate a
+        // phone device which majorly supports portrait output sizes.
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation0, portraitPixelArraySize, supportedSizes, null
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
+            AspectRatio.RATIO_16_9
+        ).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in
+        // front of the returned sizes list and the list is sorted in descending order. Other
+        // items will be put in the following that are sorted by aspect ratio delta and then area
+        // size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(1080, 1920),
+            Size(
+                720,
+                1280
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(480, 640),
+            Size(640, 480),
+            Size(1280, 720)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizesOnTabletWithPortraitPixelArraySize_aspectRatio16x9() {
+        val supportedSizes = arrayOf(
+            Size(1080, 1920),
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(720, 1280),
+            Size(1280, 720),
+            Size(480, 640),
+            Size(640, 480),
+            Size(360, 480)
+        )
+
+        // Sets the sensor orientation as 90 and pixel array size as a portrait size to simulate a
+        // tablet device which majorly supports portrait output sizes.
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation90, portraitPixelArraySize, supportedSizes, null
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
+            AspectRatio.RATIO_16_9
+        ).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in
+        // front of the returned sizes list and the list is sorted in descending order. Other
+        // items will be put in the following that are sorted by aspect ratio delta and then area
+        // size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(1080, 1920),
+            Size(
+                720,
+                1280
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(480, 640),
+            Size(640, 480),
+            Size(1280, 720)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizesOnTablet_aspectRatio16x9() {
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation0, landscapePixelArraySize, supportedSizes, null
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
+            AspectRatio.RATIO_16_9
+        ).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
+        // list is sorted in descending order. Other items will be put in the following that are
+        // sorted by aspect ratio delta and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(
+                800,
+                450
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun supportedOutputSizesOnTabletWithPortraitSizes_aspectRatio16x9() {
+        val supportedSizes = arrayOf(
+            Size(1920, 1080),
+            Size(1440, 1080),
+            Size(1280, 960),
+            Size(1280, 720),
+            Size(720, 1280),
+            Size(640, 480),
+            Size(480, 640),
+            Size(480, 360)
+        )
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation0, landscapePixelArraySize, supportedSizes, null
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
+            AspectRatio.RATIO_16_9
+        ).build()
+
+        // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
+        // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
+        // list is sorted in descending order. Other items will be put in the following that are
+        // sorted by aspect ratio delta and then area size.
+        val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
+            useCase.currentConfig
+        )
+        val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(
+                1280,
+                720
+            ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1440, 1080),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(480, 640),
+            Size(720, 1280)
+        )
+        Truth.assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun determineRecordSizeFromStreamConfigurationMap() {
+        // Setup camera with non-integer camera Id
+        setupCamera(
+            cameraIdExternal, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+            sensorOrientation90, landscapePixelArraySize, supportedSizes, null
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraIdExternal,
+            mockCamcorderProfileAdapter
+        )
+
+        // Checks the determined RECORD size
+        Truth.assertThat(
+            supportedSurfaceCombination.surfaceSizeDefinition.recordSize
+        ).isEqualTo(
+            legacyVideoMaximumVideoSize
+        )
+    }
+
+    @Test
+    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists() {
+        val supportedSizes = arrayOf(
+            Size(4000, 3000),
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1024, 738), // This will create a 512/269 aspect ratio group that
+            // 640x480 will be considered to match in mod16 condition.
+            Size(800, 600),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation90, landscapePixelArraySize, supportedSizes, null
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+
+        // Sets the target resolution as 640x480 with target rotation as ROTATION_90 because the
+        // sensor orientation is 90.
+        val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
+            vgaSize
+        ).setTargetRotation(Surface.ROTATION_90).build()
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            listOf(useCase.currentConfig)
+        )
+
+        // Checks 640x480 is final selected for the use case.
+        Truth.assertThat(suggestedResolutionMap[useCase.currentConfig]).isEqualTo(vgaSize)
+    }
+
+    @Test
+    fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet() {
+        val supportedSizes = arrayOf(
+            Size(480, 480)
+        )
+        setupCamera(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation90, landscapePixelArraySize, supportedSizes, null
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+
+        // Sets the max resolution as 720x1280
+        val useCase = FakeUseCaseConfig.Builder().setMaxResolution(displaySize).build()
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            listOf(useCase.currentConfig)
+        )
+
+        // Checks 480x480 is final selected for the use case.
+        Truth.assertThat(suggestedResolutionMap[useCase.currentConfig]).isEqualTo(
+            Size(480, 480)
+        )
+    }
+
+    @Test
+    fun previewSizeIsSelectedForImageAnalysis_imageCaptureHasNoSetSizeInLimitedDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val preview = Preview.Builder().build()
+        preview.setSurfaceProvider(
+            CameraXExecutors.directExecutor(),
+            SurfaceTextureProvider.createSurfaceTextureProvider(
+                Mockito.mock(
+                    SurfaceTextureCallback::class.java
+                )
+            )
+        )
+
+        // ImageCapture has no explicit target resolution setting
+        val imageCapture = ImageCapture.Builder().build()
+
+        // A LEGACY-level above device supports the following configuration.
+        //     PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
+        //
+        // A LIMITED-level above device supports the following configuration.
+        //     PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
+        //
+        // Even there is a RECORD size target resolution setting for ImageAnalysis, ImageCapture
+        // will still have higher priority to have a MAXIMUM size resolution if the app doesn't
+        // explicitly specify a RECORD size target resolution to ImageCapture.
+        val imageAnalysis = ImageAnalysis.Builder()
+            .setTargetRotation(Surface.ROTATION_90)
+            .setTargetResolution(recordSize)
+            .build()
+        val useCases: MutableList<UseCase> = ArrayList()
+        useCases.add(preview)
+        useCases.add(imageCapture)
+        useCases.add(imageAnalysis)
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            useCases,
+            useCaseConfigFactory
+        )
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            ArrayList(useCaseToConfigMap.values)
+        )
+        Truth.assertThat(suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]).isEqualTo(
+            previewSize
+        )
+    }
+
+    @Test
+    fun recordSizeIsSelectedForImageAnalysis_imageCaptureHasExplicitSizeInLimitedDevice() {
+        setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, mockCameraMetadata, cameraId,
+            mockCamcorderProfileAdapter
+        )
+        val preview = Preview.Builder().build()
+        preview.setSurfaceProvider(
+            CameraXExecutors.directExecutor(),
+            SurfaceTextureProvider.createSurfaceTextureProvider(
+                Mockito.mock(
+                    SurfaceTextureCallback::class.java
+                )
+            )
+        )
+
+        // ImageCapture has no explicit RECORD size target resolution setting
+        val imageCapture = ImageCapture.Builder()
+            .setTargetRotation(Surface.ROTATION_90)
+            .setTargetResolution(recordSize)
+            .build()
+
+        // A LEGACY-level above device supports the following configuration.
+        //     PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
+        //
+        // A LIMITED-level above device supports the following configuration.
+        //     PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
+        //
+        // A RECORD can be selected for ImageAnalysis if the ImageCapture has a explicit RECORD
+        // size target resolution setting. It means that the application know the trade-off and
+        // the ImageAnalysis has higher priority to get a larger resolution than ImageCapture.
+        val imageAnalysis = ImageAnalysis.Builder()
+            .setTargetRotation(Surface.ROTATION_90)
+            .setTargetResolution(recordSize)
+            .build()
+        val useCases: MutableList<UseCase> = ArrayList()
+        useCases.add(preview)
+        useCases.add(imageCapture)
+        useCases.add(imageAnalysis)
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            useCases,
+            useCaseConfigFactory
+        )
+        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+            emptyList(),
+            ArrayList(useCaseToConfigMap.values)
+        )
+        Truth.assertThat(suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]).isEqualTo(
+            recordSize
+        )
+    }
+
+    private fun setupCamera(hardwareLevel: Int, capabilities: IntArray) {
+        setupCamera(
+            hardwareLevel, sensorOrientation90, landscapePixelArraySize,
+            supportedSizes, capabilities
+        )
+    }
+
+    private fun setupCamera(hardwareLevel: Int, supportedSizes: Array<Size>) {
+        setupCamera(
+            hardwareLevel, sensorOrientation90, landscapePixelArraySize,
+            supportedSizes, null
+        )
+    }
+
+    private fun setupCamera(
+        hardwareLevel: Int,
+        sensorOrientation: Int = sensorOrientation90,
+        pixelArraySize: Size = landscapePixelArraySize,
+        supportedSizes: Array<Size> =
+            this.supportedSizes,
+        capabilities: IntArray? = null
+    ) {
+        setupCamera(
+            cameraId,
+            hardwareLevel,
+            sensorOrientation,
+            pixelArraySize,
+            supportedSizes,
+            capabilities
+        )
+    }
+
+    private fun setupCamera(
+        cameraId: String,
+        hardwareLevel: Int,
+        sensorOrientation: Int,
+        pixelArraySize: Size,
+        supportedSizes: Array<Size>,
+        capabilities: IntArray?
+    ) {
+        cameraFactory = FakeCameraFactory()
+        val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+        val shadowCharacteristics = Shadow.extract<ShadowCameraCharacteristics>(characteristics)
+        shadowCharacteristics.set(
+            CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK
+        )
+        shadowCharacteristics.set(
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel
+        )
+        shadowCharacteristics.set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
+        shadowCharacteristics.set(
+            CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE,
+            pixelArraySize
+        )
+        if (capabilities != null) {
+            shadowCharacteristics.set(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities
+            )
+        }
+        val cameraManager = ApplicationProvider.getApplicationContext<Context>()
+            .getSystemService(Context.CAMERA_SERVICE) as CameraManager
+        (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
+            .addCamera(cameraId, characteristics)
+        val mockMap = Mockito.mock(
+            StreamConfigurationMap::class.java
+        )
+        Mockito.`when`(mockMap.getOutputSizes(ArgumentMatchers.anyInt())).thenReturn(supportedSizes)
+        // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
+        // output sizes need to be retrieved via SurfaceTexture.class.
+        Mockito.`when`(
+            mockMap.getOutputSizes(
+                SurfaceTexture::class.java
+            )
+        ).thenReturn(supportedSizes)
+        // This is setup for the test to determine RECORD size from StreamConfigurationMap
+        Mockito.`when`(
+            mockMap.getOutputSizes(
+                MediaRecorder::class.java
+            )
+        ).thenReturn(supportedSizes)
+        shadowCharacteristics.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
+        @LensFacing val lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
+            CameraCharacteristics.LENS_FACING_BACK
+        )
+        val cameraInfo = FakeCameraInfoInternal(cameraId)
+        cameraInfo.camcorderProfileProvider = FakeCamcorderProfileProvider.Builder()
+            .addProfile(
+                CamcorderProfileUtil.asHighQuality(profileUhd),
+                profileUhd,
+                profileFhd,
+                profileHd,
+                profileSd,
+                CamcorderProfileUtil.asLowQuality(profileSd)
+            ).build()
+        cameraFactory!!.insertCamera(
+            lensFacingEnum, cameraId
+        ) { FakeCamera(cameraId, null, cameraInfo) }
+
+        // set up CameraMetaData
+        Mockito.`when`(
+            mockCameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
+        ).thenReturn(hardwareLevel)
+        Mockito.`when`(mockCameraMetadata[CameraCharacteristics.SENSOR_ORIENTATION])
+            .thenReturn(
+                sensorOrientation
+            )
+        Mockito.`when`(
+            mockCameraMetadata[CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE]
+        ).thenReturn(pixelArraySize)
+        Mockito.`when`(mockCameraMetadata[CameraCharacteristics.LENS_FACING]).thenReturn(
+            CameraCharacteristics.LENS_FACING_BACK
+        )
+        Mockito.`when`(
+            mockCameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
+        ).thenReturn(capabilities)
+        Mockito.`when`(
+            mockCameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
+        ).thenReturn(mockMap)
+        initCameraX(cameraId)
+    }
+
+    private fun initCameraX(cameraId: String) {
+        val cameraMetaDataMap = mutableMapOf<CameraId, CameraMetadata>()
+        cameraMetaDataMap[CameraId(cameraId)] = mockCameraMetadata
+        val cameraDevicesWithCameraMetaData =
+            FakeCameraDevicesWithCameraMetaData(cameraMetaDataMap, mockCameraMetadata)
+        Mockito.`when`(mockCameraAppComponent.getCameraDevices())
+            .thenReturn(cameraDevicesWithCameraMetaData)
+        cameraFactory!!.cameraManager = mockCameraAppComponent
+        val cameraXConfig = CameraXConfig.Builder.fromConfig(
+            defaultConfig()
+        )
+            .setDeviceSurfaceManagerProvider { context: Context?, _: Any?, _: Set<String?>? ->
+                CameraSurfaceAdapter(
+                    context!!,
+                    mockCameraAppComponent, setOf(cameraId)
+                )
+            }
+            .setCameraFactoryProvider { _: Context?,
+                _: CameraThreadConfig?,
+                _: CameraSelector?
+                ->
+                cameraFactory!!
+            }
+            .build()
+        val cameraX: CameraX = try {
+            CameraXUtil.getOrCreateInstance(context) { cameraXConfig }.get()
+        } catch (e: ExecutionException) {
+            throw IllegalStateException("Unable to initialize CameraX for test.")
+        } catch (e: InterruptedException) {
+            throw IllegalStateException("Unable to initialize CameraX for test.")
+        }
+        useCaseConfigFactory = cameraX.defaultConfigFactory
+    }
+
+    private fun isAllSubConfigListSupported(
+        supportedSurfaceCombination: SupportedSurfaceCombination,
+        combinationList: List<SurfaceCombination>
+    ): Boolean {
+        for (combination in combinationList) {
+            val configList = combination.surfaceConfigList
+            val length = configList.size
+            if (length <= 1) {
+                continue
+            }
+            for (index in 0 until length) {
+                val subConfigurationList: MutableList<SurfaceConfig> = ArrayList(configList)
+                subConfigurationList.removeAt(index)
+                val isSupported = supportedSurfaceCombination.checkSupported(subConfigurationList)
+                if (!isSupported) {
+                    return false
+                }
+            }
+        }
+        return true
+    }
+
+    /** Creates a VideoCapture with a specific Quality  */
+    private fun createVideoCapture(quality: Quality): VideoCapture<TestVideoOutput> {
+        return createVideoCapture(QualitySelector.from(quality))
+    }
+    /** Creates a VideoCapture with a customized QualitySelector  */
+    /** Creates a VideoCapture with a default QualitySelector  */
+    @JvmOverloads
+    fun createVideoCapture(
+        qualitySelector: QualitySelector = VideoSpec.QUALITY_SELECTOR_AUTO
+    ): VideoCapture<TestVideoOutput> {
+        val mediaSpecBuilder = MediaSpec.builder()
+        mediaSpecBuilder.configureVideo { builder: VideoSpec.Builder ->
+            builder.setQualitySelector(
+                qualitySelector
+            )
+        }
+        val videoOutput = TestVideoOutput()
+        videoOutput.mediaSpecObservable.setState(mediaSpecBuilder.build())
+        return VideoCapture.withOutput(videoOutput)
+    }
+
+    /** A fake implementation of VideoOutput  */
+    class TestVideoOutput : VideoOutput {
+        var mediaSpecObservable =
+            MutableStateObservable.withInitialState(MediaSpec.builder().build())
+        private var surfaceRequest: SurfaceRequest? = null
+        private var sourceState: SourceState? = null
+        override fun onSurfaceRequested(request: SurfaceRequest) {
+            surfaceRequest = request
+        }
+
+        override fun getMediaSpec(): Observable<MediaSpec> {
+            return mediaSpecObservable
+        }
+
+        override fun onSourceStateChanged(sourceState: SourceState) {
+            this.sourceState = sourceState
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
index 76187df..6126329 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
@@ -105,6 +105,7 @@
                 useCaseThreads,
                 CameraPipe(CameraPipe.Config(ApplicationProvider.getApplicationContext()))
             ),
+            threads = useCaseThreads,
             requestControl = requestControl
         ).also {
             it.runningUseCases = setOf(fakeUseCase)
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraDevicesWithCameraMetaData.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraDevicesWithCameraMetaData.kt
new file mode 100644
index 0000000..7e64564
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraDevicesWithCameraMetaData.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.camera.camera2.pipe.integration.testing
+
+import androidx.camera.camera2.pipe.CameraDevices
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import kotlinx.coroutines.runBlocking
+
+class FakeCameraDevicesWithCameraMetaData(
+    private val cameraMetadataMap: Map<CameraId, CameraMetadata>,
+    private val defaultCameraMetadata: CameraMetadata
+) : CameraDevices {
+    @Deprecated(
+        message = "findAll may block the calling thread and is deprecated.",
+        replaceWith = ReplaceWith("ids"),
+        level = DeprecationLevel.WARNING
+    )
+    override fun findAll(): List<CameraId> = runBlocking { ids() }
+    override suspend fun ids(): List<CameraId> = cameraMetadataMap.keys.toList()
+    override suspend fun getMetadata(camera: CameraId): CameraMetadata = awaitMetadata(camera)
+    override fun awaitMetadata(camera: CameraId) =
+        cameraMetadataMap[camera] ?: defaultCameraMetadata
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index ecfd0e3..c4c65e5 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -32,6 +32,7 @@
 import androidx.camera.core.impl.SessionConfig
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
 
 class FakeUseCaseCameraComponentBuilder : UseCaseCameraComponent.Builder {
     private var config: UseCaseCameraConfig = UseCaseCameraConfig(emptyList())
@@ -134,6 +135,7 @@
         return CompletableDeferred(Result3A(status = Result3A.Status.OK))
     }
 
-    override fun close() {
+    override fun close(): Job {
+        return CompletableDeferred(Unit)
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/camera/camera-camera2-pipe-integration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000..1f0955d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/camera/camera-camera2/api/public_plus_experimental_1.2.0-beta02.txt b/camera/camera-camera2/api/public_plus_experimental_1.2.0-beta02.txt
index ebebd4c..ea5cacc 100644
--- a/camera/camera-camera2/api/public_plus_experimental_1.2.0-beta02.txt
+++ b/camera/camera-camera2/api/public_plus_experimental_1.2.0-beta02.txt
@@ -33,6 +33,7 @@
     method @RequiresApi(28) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setPhysicalCameraId(String);
     method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionCaptureCallback(android.hardware.camera2.CameraCaptureSession.CaptureCallback);
     method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionStateCallback(android.hardware.camera2.CameraCaptureSession.StateCallback);
+    method @RequiresApi(33) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setStreamUseCase(long);
   }
 
   @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public class CaptureRequestOptions {
diff --git a/camera/camera-camera2/api/public_plus_experimental_current.txt b/camera/camera-camera2/api/public_plus_experimental_current.txt
index ebebd4c..ea5cacc 100644
--- a/camera/camera-camera2/api/public_plus_experimental_current.txt
+++ b/camera/camera-camera2/api/public_plus_experimental_current.txt
@@ -33,6 +33,7 @@
     method @RequiresApi(28) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setPhysicalCameraId(String);
     method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionCaptureCallback(android.hardware.camera2.CameraCaptureSession.CaptureCallback);
     method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionStateCallback(android.hardware.camera2.CameraCaptureSession.StateCallback);
+    method @RequiresApi(33) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setStreamUseCase(long);
   }
 
   @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public class CaptureRequestOptions {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java
index 4854150..b82574b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java
@@ -29,18 +29,22 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * <p>QuirkSummary
- *     Bug Id: b/157448499, b/192129158
+ *     Bug Id: b/157448499, b/192129158, b/245495234
  *     Description: Quirk required to exclude certain supported surface sizes that are
  *                  problematic. These sizes are dependent on the device, camera and image format.
  *                  An example is the resolution size 4000x3000 which is supported on OnePlus 6,
  *                  but causes a WYSIWYG issue between preview and image capture. Another example
  *                  is on Huawei P20 Lite, the Preview screen will become too bright when 400x400
  *                  or 720x720 Preview resolutions are used together with a large zoom in value.
- *                  The same symptom happens on ImageAnalysis.
- *     Device(s): OnePlus 6, OnePlus 6T, Huawei P20
+ *                  The same symptom happens on ImageAnalysis. On Samsung J7 Prime (SM-G610M) or
+ *                  J7 (SM-J710MN) API 27 devices, the Preview images will be stretched if
+ *                  1920x1080 resolution is used.
+ *     Device(s): OnePlus 6, OnePlus 6T, Huawei P20, Samsung J7 Prime (SM-G610M) API 27, Samsung
+ *     J7 (SM-J710MN) API 27
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ExcludedSupportedSizesQuirk implements Quirk {
@@ -48,7 +52,8 @@
     private static final String TAG = "ExcludedSupportedSizesQuirk";
 
     static boolean load() {
-        return isOnePlus6() || isOnePlus6T() || isHuaweiP20Lite();
+        return isOnePlus6() || isOnePlus6T() || isHuaweiP20Lite() || isSamsungJ7PrimeApi27Above()
+                || isSamsungJ7Api27Above();
     }
 
     private static boolean isOnePlus6() {
@@ -64,6 +69,18 @@
         return "HUAWEI".equalsIgnoreCase(Build.BRAND) && "HWANE".equalsIgnoreCase(Build.DEVICE);
     }
 
+    private static boolean isSamsungJ7PrimeApi27Above() {
+        return "SAMSUNG".equalsIgnoreCase(Build.BRAND.toUpperCase(Locale.US))
+                && "ON7XELTE".equalsIgnoreCase(Build.DEVICE.toUpperCase(Locale.US))
+                && Build.VERSION.SDK_INT >= 27;
+    }
+
+    private static boolean isSamsungJ7Api27Above() {
+        return "SAMSUNG".equalsIgnoreCase(Build.BRAND.toUpperCase(Locale.US))
+                && "J7XELTE".equalsIgnoreCase(Build.DEVICE.toUpperCase(Locale.US))
+                && Build.VERSION.SDK_INT >= 27;
+    }
+
     /**
      * Retrieves problematic supported surface sizes that have to be excluded on the current
      * device, for the given camera id and image format.
@@ -79,6 +96,9 @@
         if (isHuaweiP20Lite()) {
             return getHuaweiP20LiteExcludedSizes(cameraId, imageFormat);
         }
+        if (isSamsungJ7PrimeApi27Above() || isSamsungJ7Api27Above()) {
+            return getSamsungJ7PrimeApi27AboveExcludedSizes(imageFormat);
+        }
         Logger.w(TAG, "Cannot retrieve list of supported sizes to exclude on this device.");
         return Collections.emptyList();
     }
@@ -114,4 +134,13 @@
         }
         return sizes;
     }
+
+    @NonNull
+    private List<Size> getSamsungJ7PrimeApi27AboveExcludedSizes(int imageFormat) {
+        final List<Size> sizes = new ArrayList<>();
+        if (imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+            sizes.add(new Size(1920, 1080));
+        }
+        return sizes;
+    }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExtraCroppingQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExtraCroppingQuirk.java
index 3d4d8e6..b46ea90 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExtraCroppingQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExtraCroppingQuirk.java
@@ -17,6 +17,7 @@
 package androidx.camera.camera2.internal.compat.quirk;
 
 import android.os.Build;
+import android.util.Range;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -25,9 +26,9 @@
 import androidx.camera.core.impl.Quirk;
 import androidx.camera.core.impl.SurfaceConfig;
 
-import java.util.Arrays;
-import java.util.List;
+import java.util.HashMap;
 import java.util.Locale;
+import java.util.Map;
 
 /**
  * Quirk that requires specific resolutions as the workaround.
@@ -50,13 +51,17 @@
 @RequiresApi(21)
 public class ExtraCroppingQuirk implements Quirk {
 
-    private static final List<String> SAMSUNG_DISTORTION_MODELS = Arrays.asList(
-            "SM-T580", // Samsung Galaxy Tab A (2016)
-            "SM-J710MN", // Samsung Galaxy J7 (2016)
-            "SM-A320FL", // Samsung Galaxy A3 (2017)
-            "SM-G570M", // Samsung Galaxy J5 Prime
-            "SM-G610F", // Samsung Galaxy J7 Prime
-            "SM-G610M"); // Samsung Galaxy J7 Prime
+    private static final Map<String, Range<Integer>> SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP =
+            new HashMap<>();
+
+    static {
+        SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.put("SM-T580", null);
+        SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.put("SM-J710MN", new Range<>(21, 26));
+        SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.put("SM-A320FL", null);
+        SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.put("SM-G570M", null);
+        SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.put("SM-G610F", null);
+        SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.put("SM-G610M", new Range<>(21, 26));
+    }
 
     static boolean load() {
         return isSamsungDistortion();
@@ -93,8 +98,18 @@
      * Checks for device model with Samsung output distortion bug (b/190203334).
      */
     private static boolean isSamsungDistortion() {
-        return "samsung".equalsIgnoreCase(Build.BRAND)
-                && SAMSUNG_DISTORTION_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+        boolean isDeviceModelContained = "samsung".equalsIgnoreCase(Build.BRAND)
+                && SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.containsKey(
+                Build.MODEL.toUpperCase(Locale.US));
+
+        if (!isDeviceModelContained) {
+            return false;
+        }
+
+        Range<Integer> apiLevelRange =
+                SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.get(Build.MODEL.toUpperCase(Locale.US));
+
+        return apiLevelRange == null ? true : apiLevelRange.contains(Build.VERSION.SDK_INT);
     }
 
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2Interop.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2Interop.java
index 33cfa4f..7b5d065 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2Interop.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2Interop.java
@@ -103,18 +103,25 @@
         }
 
         /**
-         * Sets a CameraDevice template on the given configuration. Requires API 33 or above.
+         * Sets a stream use case flag on the given extendable builder.
          *
-         * <p>See {@link android.hardware.camera2.CameraMetadata} for valid stream use cases.
-         * See {@link android.hardware.camera2.params.OutputConfiguration} to see how
-         * Camera2 framework uses this.
+         * <p>Requires API 33 or above.
+         *
+         * <p>Calling this method will set the stream use case for all CameraX outputs for the
+         * same stream session. Valid use cases available on devices can be found in
+         * {@link android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES}
+         *
+         * <p>No app should call this without double-checking the supported list first, or at least
+         * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE}
+         * capability which guarantees quite a few use cases.
+         *
+         * <p>See {@link android.hardware.camera2.params.OutputConfiguration#setStreamUseCase}
+         * to see how Camera2 framework uses this.
          *
          * @param streamUseCase The stream use case to set.
          * @return The current Extender.
-         * @hide
          */
         @RequiresApi(33)
-        @RestrictTo(Scope.LIBRARY)
         @NonNull
         public Extender<T> setStreamUseCase(long streamUseCase) {
             mBaseBuilder.getMutableConfig().insertOption(STREAM_USE_CASE_OPTION, streamUseCase);
@@ -227,5 +234,6 @@
     }
 
     // Ensure this class isn't instantiated
-    private Camera2Interop() {}
+    private Camera2Interop() {
+    }
 }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
index 82399e1..07ffbcb 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
@@ -2567,7 +2567,7 @@
                     try {
                         return new Camera2DeviceSurfaceManager(context,
                                 mMockCamcorderProfileHelper,
-                                (CameraManagerCompat) cameraManager, availableCameraIds);
+                                CameraManagerCompat.from(mContext), availableCameraIds);
                     } catch (CameraUnavailableException e) {
                         throw new InitializationException(e);
                     }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionCorrectorQuirkTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionCorrectorQuirkTest.java
index 6faf911..b60fa0f 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionCorrectorQuirkTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionCorrectorQuirkTest.java
@@ -46,13 +46,13 @@
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-T580");
         assertThat(ExtraCroppingQuirk.load()).isTrue();
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-J710MN");
-        assertThat(ExtraCroppingQuirk.load()).isTrue();
+        assertThat(ExtraCroppingQuirk.load()).isEqualTo(Build.VERSION.SDK_INT <= 26);
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-A320FL");
         assertThat(ExtraCroppingQuirk.load()).isTrue();
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G570M");
         assertThat(ExtraCroppingQuirk.load()).isTrue();
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G610M");
-        assertThat(ExtraCroppingQuirk.load()).isTrue();
+        assertThat(ExtraCroppingQuirk.load()).isEqualTo(Build.VERSION.SDK_INT <= 26);
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G610F");
         assertThat(ExtraCroppingQuirk.load()).isTrue();
     }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java
index be4979d..6f01115 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java
@@ -49,30 +49,53 @@
 
     private static final Size SIZE_4000_3000 = new Size(4000, 3000);
     private static final Size SIZE_4160_3120 = new Size(4160, 3120);
+    private static final Size SIZE_1920_1080 = new Size(1920, 1080);
     private static final Size SIZE_720_720 = new Size(720, 720);
     private static final Size SIZE_400_400 = new Size(400, 400);
 
     @ParameterizedRobolectricTestRunner.Parameters
     public static Collection<Object[]> data() {
         final List<Object[]> data = new ArrayList<>();
-        data.add(new Object[]{
-                new Config("OnePlus", "OnePlus6", "0", JPEG, SIZE_4000_3000, SIZE_4160_3120)});
-        data.add(new Object[]{new Config("OnePlus", "OnePlus6", "1", JPEG)});
-        data.add(new Object[]{new Config("OnePlus", "OnePlus6", "0", PRIVATE)});
-        data.add(new Object[]{
-                new Config("OnePlus", "OnePlus6T", "0", JPEG, SIZE_4000_3000, SIZE_4160_3120)});
-        data.add(new Object[]{new Config("OnePlus", "OnePlus6T", "1", JPEG)});
-        data.add(new Object[]{new Config("OnePlus", "OnePlus6T", "0", PRIVATE)});
-        data.add(new Object[]{new Config("OnePlus", "OnePlus3", "0", JPEG)});
-        data.add(new Object[]{new Config(null, null, "0", JPEG)});
+        data.add(new Object[]{new Config("OnePlus", "OnePlus6", "0", JPEG, null, SIZE_4000_3000,
+                SIZE_4160_3120)});
+        data.add(new Object[]{new Config("OnePlus", "OnePlus6", "1", JPEG, null)});
+        data.add(new Object[]{new Config("OnePlus", "OnePlus6", "0", PRIVATE, null)});
+        data.add(new Object[]{new Config("OnePlus", "OnePlus6T", "0", JPEG, null, SIZE_4000_3000,
+                SIZE_4160_3120)});
+        data.add(new Object[]{new Config("OnePlus", "OnePlus6T", "1", JPEG, null)});
+        data.add(new Object[]{new Config("OnePlus", "OnePlus6T", "0", PRIVATE, null)});
+        data.add(new Object[]{new Config("OnePlus", "OnePlus3", "0", JPEG, null)});
+        data.add(new Object[]{new Config(null, null, "0", JPEG, null)});
         // Huawei P20 Lite
         data.add(new Object[]{
-                new Config("HUAWEI", "HWANE", "0", PRIVATE, SIZE_720_720, SIZE_400_400)});
-        data.add(new Object[]{new Config("HUAWEI", "HWANE", "1", PRIVATE)});
+                new Config("HUAWEI", "HWANE", "0", PRIVATE, null, SIZE_720_720, SIZE_400_400)});
+        data.add(new Object[]{new Config("HUAWEI", "HWANE", "1", PRIVATE, null)});
         data.add(new Object[]{
-                new Config("HUAWEI", "HWANE", "0", YUV_420_888, SIZE_720_720, SIZE_400_400)});
-        data.add(new Object[]{new Config("HUAWEI", "HWANE", "1", YUV_420_888)});
-        data.add(new Object[]{new Config("HUAWEI", "HWANE", "0", JPEG)});
+                new Config("HUAWEI", "HWANE", "0", YUV_420_888, null, SIZE_720_720, SIZE_400_400)});
+        data.add(new Object[]{new Config("HUAWEI", "HWANE", "1", YUV_420_888, null)});
+        data.add(new Object[]{new Config("HUAWEI", "HWANE", "0", JPEG, null)});
+        // Samsung J7 Prime (SM-G610M)
+        data.add(new Object[]{
+                new Config("SAMSUNG", "ON7XELTE", "0", PRIVATE, () -> Build.VERSION.SDK_INT >= 27,
+                        SIZE_1920_1080)});
+        data.add(new Object[]{
+                new Config("SAMSUNG", "ON7XELTE", "0", JPEG, () -> Build.VERSION.SDK_INT >= 27)});
+        data.add(new Object[]{
+                new Config("SAMSUNG", "ON7XELTE", "1", PRIVATE, () -> Build.VERSION.SDK_INT >= 27,
+                        SIZE_1920_1080)});
+        data.add(new Object[]{new Config("SAMSUNG", "ON7XELTE", "1", YUV_420_888,
+                () -> Build.VERSION.SDK_INT >= 27)});
+        // Samsung J7 (SM-J710MN)
+        data.add(new Object[]{
+                new Config("SAMSUNG", "J7XELTE", "0", PRIVATE, () -> Build.VERSION.SDK_INT >= 27,
+                        SIZE_1920_1080)});
+        data.add(new Object[]{
+                new Config("SAMSUNG", "J7XELTE", "0", JPEG, () -> Build.VERSION.SDK_INT >= 27)});
+        data.add(new Object[]{
+                new Config("SAMSUNG", "J7XELTE", "1", PRIVATE, () -> Build.VERSION.SDK_INT >= 27,
+                        SIZE_1920_1080)});
+        data.add(new Object[]{new Config("SAMSUNG", "J7XELTE", "1", YUV_420_888,
+                () -> Build.VERSION.SDK_INT >= 27)});
         return data;
     }
 
@@ -98,7 +121,11 @@
         // Get sizes to exclude
         final List<Size> excludedSizes = excludedSupportedSizesContainer.get(mConfig.mImageFormat);
 
-        assertThat(excludedSizes).containsExactly((Object[]) mConfig.mExcludedSizes);
+        if (mConfig.mApiLevelChecker == null || mConfig.mApiLevelChecker.isApiLevelContained()) {
+            assertThat(excludedSizes).containsExactly((Object[]) mConfig.mExcludedSizes);
+        } else {
+            assertThat(excludedSizes).isEmpty();
+        }
     }
 
     static class Config {
@@ -109,16 +136,24 @@
         @NonNull
         final String mCameraId;
         final int mImageFormat;
+        @Nullable
+        final ApiLevelChecker mApiLevelChecker;
         @NonNull
         final Size[] mExcludedSizes;
 
-        Config(@Nullable String brand, @Nullable String device,
-                @NonNull String cameraId, int imageFormat, @NonNull Size... excludedSizes) {
+        Config(@Nullable String brand, @Nullable String device, @NonNull String cameraId,
+                int imageFormat, @Nullable ApiLevelChecker apiLevelChecker,
+                @NonNull Size... excludedSizes) {
             mBrand = brand;
             mDevice = device;
             mCameraId = cameraId;
             mImageFormat = imageFormat;
+            mApiLevelChecker = apiLevelChecker;
             mExcludedSizes = excludedSizes;
         }
     }
+
+    private interface ApiLevelChecker {
+        boolean isApiLevelContained();
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceEffect.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceEffect.java
index 7087c9d..7d127aa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceEffect.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceEffect.java
@@ -64,6 +64,11 @@
     // Only access this on GL thread.
     private int mInputSurfaceCount = 0;
 
+    /** Constructs DefaultSurfaceEffect */
+    public DefaultSurfaceEffect() {
+        this(ShaderProvider.DEFAULT);
+    }
+
     /**
      * Constructs DefaultSurfaceEffect
      *
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
index 7937410..08a26e7 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
@@ -55,6 +55,9 @@
     @Nullable
     private final CameraSelector mAvailableCamerasSelector;
 
+    @Nullable
+    private Object mCameraManager = null;
+
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final Map<String, Pair<Integer, Callable<CameraInternal>>> mCameraMap = new HashMap<>();
 
@@ -167,9 +170,13 @@
         return filteredCameraIds;
     }
 
+    public void setCameraManager(@Nullable Object cameraManager) {
+        mCameraManager = cameraManager;
+    }
+
     @Nullable
     @Override
     public Object getCameraManager() {
-        return null;
+        return mCameraManager;
     }
 }
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/AudioVideoSyncTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/AudioVideoSyncTest.kt
new file mode 100644
index 0000000..5c5334f
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/AudioVideoSyncTest.kt
@@ -0,0 +1,219 @@
+/*
+ * 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.video
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.media.MediaRecorder
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceRequest
+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.CameraXUtil
+import androidx.camera.testing.LabTestRule
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks
+import androidx.core.util.Consumer
+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 kotlin.math.abs
+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
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class AudioVideoSyncTest {
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+
+    @get:Rule
+    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
+        android.Manifest.permission.RECORD_AUDIO
+    )
+
+    @get:Rule
+    val labTest: LabTestRule = LabTestRule()
+
+    private val cameraConfig = Camera2Config.defaultConfig()
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+    @Suppress("UNCHECKED_CAST")
+    private val videoRecordEventListener =
+        Mockito.mock(Consumer::class.java) as Consumer<VideoRecordEvent>
+
+    private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
+    private lateinit var recorder: Recorder
+    private lateinit var preview: Preview
+    private lateinit var surfaceTexturePreview: Preview
+
+    @Before
+    fun setUp() {
+        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        // Skip for b/168175357, b/233661493
+        Assume.assumeFalse(
+            "Skip tests for Cuttlefish MediaCodec issues",
+            Build.MODEL.contains("Cuttlefish") &&
+                (Build.VERSION.SDK_INT == 29 || Build.VERSION.SDK_INT == 33)
+        )
+        Assume.assumeTrue(AudioUtil.canStartAudioRecord(MediaRecorder.AudioSource.CAMCORDER))
+
+        CameraXUtil.initialize(
+            context,
+            cameraConfig
+        ).get()
+        cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
+
+        recorder = Recorder.Builder().build()
+
+        // Using Preview so that the surface provider could be set to control when to issue the
+        // surface request.
+        preview = Preview.Builder().build()
+
+        // Add another Preview to provide an additional surface for b/168187087.
+        surfaceTexturePreview = Preview.Builder().build()
+        instrumentation.runOnMainSync {
+            surfaceTexturePreview.setSurfaceProvider(
+                SurfaceTextureProvider.createSurfaceTextureProvider(
+                    object : SurfaceTextureProvider.SurfaceTextureCallback {
+                        override fun onSurfaceTextureReady(
+                            surfaceTexture: SurfaceTexture,
+                            resolution: Size
+                        ) {
+                            // No-op
+                        }
+
+                        override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
+                            surfaceTexture.release()
+                        }
+                    }
+                )
+            )
+        }
+
+        Assume.assumeTrue(
+            "This combination (preview, surfaceTexturePreview) is not supported.",
+            cameraUseCaseAdapter.isUseCasesCombinationSupported(
+                preview,
+                surfaceTexturePreview
+            )
+        )
+
+        cameraUseCaseAdapter = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            cameraSelector,
+            // Must put surfaceTexturePreview before preview while addUseCases, otherwise
+            // an issue on Samsung device will occur. See b/196755459.
+            surfaceTexturePreview,
+            preview
+        )
+        recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
+    }
+
+    @After
+    fun tearDown() {
+        if (this::cameraUseCaseAdapter.isInitialized) {
+            instrumentation.runOnMainSync {
+                cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
+            }
+            recorder.onSourceStateChanged(VideoOutput.SourceState.INACTIVE)
+        }
+
+        CameraXUtil.shutdown().get(10, TimeUnit.SECONDS)
+    }
+
+    @LabTestRule.LabTestOnly
+    @Test
+    fun canRecord_withAvSyncInStart() {
+        val diffThresholdUs = 50000L // 50,000 is about 0.05 second
+
+        Mockito.clearInvocations(videoRecordEventListener)
+        invokeSurfaceRequest(recorder)
+        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        val recording = recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
+            .withAudioEnabled()
+            .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
+
+        val inOrder = Mockito.inOrder(videoRecordEventListener)
+        inOrder.verify(videoRecordEventListener, Mockito.timeout(5000L))
+            .accept(ArgumentMatchers.any(VideoRecordEvent.Start::class.java))
+        inOrder.verify(videoRecordEventListener, Mockito.timeout(15000L)
+            .atLeast(5))
+            .accept(ArgumentMatchers.any(VideoRecordEvent.Status::class.java))
+
+        // check if the time difference between the first video and audio data is within a threshold
+        val firstAudioTime = recorder.mFirstRecordingAudioDataTimeUs
+        val firstVideoTime = recorder.mFirstRecordingVideoDataTimeUs
+        val timeDiff = abs(firstAudioTime - firstVideoTime)
+        assertThat(timeDiff).isLessThan(diffThresholdUs)
+
+        recording.stopSafely()
+        file.delete()
+    }
+
+    private fun invokeSurfaceRequest(recorder: Recorder) {
+        instrumentation.runOnMainSync {
+            preview.setSurfaceProvider { request: SurfaceRequest ->
+                recorder.onSurfaceRequested(request)
+            }
+            recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_STREAMING)
+        }
+    }
+
+    // It fails on devices with certain chipset if the codec is stopped when the camera is still
+    // producing frames to the provided surface. This method first stop the camera from
+    // producing frames then stops the recording safely on the problematic devices.
+    private fun Recording.stopSafely() {
+        val deactivateSurfaceBeforeStop =
+            DeviceQuirks.get(DeactivateEncoderSurfaceBeforeStopEncoderQuirk::class.java) != null
+        if (deactivateSurfaceBeforeStop) {
+            instrumentation.runOnMainSync {
+                preview.setSurfaceProvider(null)
+            }
+        }
+        stop()
+        if (deactivateSurfaceBeforeStop && Build.VERSION.SDK_INT >= 23) {
+            invokeSurfaceRequest(recorder)
+        }
+    }
+}
\ No newline at end of file
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 1e722eb..d4c903f 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
@@ -74,7 +74,6 @@
 import java.util.concurrent.Executor
 import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
-import kotlin.math.abs
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Dispatchers
@@ -481,35 +480,6 @@
         file.delete()
     }
 
-    @LabTestRule.LabTestOnly
-    @Test
-    fun canRecordWithAvSyncInStart() {
-        val diffThresholdUs = 50000L // 50,000 is about 0.05 second
-
-        clearInvocations(videoRecordEventListener)
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val recording = recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
-
-        val inOrder = inOrder(videoRecordEventListener)
-        inOrder.verify(videoRecordEventListener, timeout(5000L))
-            .accept(any(VideoRecordEvent.Start::class.java))
-        inOrder.verify(videoRecordEventListener, timeout(15000L)
-            .atLeast(5))
-            .accept(any(VideoRecordEvent.Status::class.java))
-
-        // check if the time difference between the first video and audio data is within a threshold
-        val firstAudioTime = recorder.mFirstRecordingAudioDataTimeUs
-        val firstVideoTime = recorder.mFirstRecordingVideoDataTimeUs
-        val timeDiff = abs(firstAudioTime - firstVideoTime)
-        assertThat(timeDiff).isLessThan(diffThresholdUs)
-
-        recording.stopSafely()
-        file.delete()
-    }
-
     @Test
     fun canReceiveRecordingStats() {
         clearInvocations(videoRecordEventListener)
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
index 46b34eb..02f2e3b 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
@@ -211,7 +211,7 @@
         // Arrange.
         val qualityList = QualitySelector.getSupportedQualities(cameraInfo)
         qualityList.forEach loop@{ quality ->
-            val targetResolution = QualitySelector.getResolution(cameraInfo, quality)
+            val targetResolution = QualitySelector.getResolution(cameraInfo, quality)!!
             val videoOutput = TestVideoOutput(
                 mediaSpec = MediaSpec.builder().configureVideo {
                     it.setQualitySelector(QualitySelector.from(quality))
@@ -228,47 +228,8 @@
             }
 
             // Assert.
-            val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
             assertWithMessage("Set quality value by $quality")
-                .that(surfaceRequest.resolution).isEqualTo(targetResolution)
-
-            // Cleanup.
-            withContext(Dispatchers.Main) {
-                cameraUseCaseAdapter.apply {
-                    removeUseCases(listOf(videoCapture))
-                }
-            }
-        }
-    }
-
-    @Test
-    fun addUseCases_setQualityWithRotation_getCorrectResolution() = runBlocking {
-        assumeTrue(QualitySelector.getSupportedQualities(cameraInfo).isNotEmpty())
-        // Cuttlefish API 29 has inconsistent resolution issue. See b/184015059.
-        assumeFalse(Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29)
-
-        val targetResolution = QualitySelector.getResolution(cameraInfo, Quality.LOWEST)
-
-        arrayOf(
-            Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270
-        ).forEach { rotation ->
-            // Arrange.
-            val videoOutput = TestVideoOutput(
-                mediaSpec = MediaSpec.builder().configureVideo {
-                    it.setQualitySelector(QualitySelector.from(Quality.LOWEST))
-                }.build()
-            )
-            val videoCapture = VideoCapture.withOutput(videoOutput)
-
-            // Act.
-            withContext(Dispatchers.Main) {
-                cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
-            }
-
-            // Assert.
-            val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
-            assertWithMessage("Set rotation value by $rotation")
-                .that(surfaceRequest.resolution).isEqualTo(targetResolution)
+                .that(videoCapture.attachedSurfaceResolution).isEqualTo(targetResolution)
 
             // Cleanup.
             withContext(Dispatchers.Main) {
@@ -429,8 +390,6 @@
         }
 
         fun setStreamInfo(streamInfo: StreamInfo) = streamInfoObservable.setState(streamInfo)
-
-        fun setMediaSpec(mediaSpec: MediaSpec) = mediaSpecObservable.setState(mediaSpec)
     }
 
     private suspend fun SurfaceRequest.provideUpdatingSurface(): StateFlow<Int> {
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 94a180b..89933d5 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -29,14 +29,13 @@
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraInfo
-import androidx.camera.core.CameraXConfig
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
-import androidx.camera.core.ImageProxy
 import androidx.camera.core.Preview
-import androidx.camera.core.impl.utils.CameraOrientationUtil
+import androidx.camera.core.impl.utils.TransformUtils.rotateSize
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraPipeConfigTestRule
@@ -229,13 +228,14 @@
         completeVideoRecording(videoCapture, file)
 
         // Verify.
-        verifyMetadataRotation(targetRotation, file)
+        val expectedRotation = if (videoCapture.node != null) 0
+        else cameraInfo.getSensorRotationDegrees(targetRotation)
+        verifyMetadataRotation(expectedRotation, file)
+
         // Cleanup.
         file.delete()
     }
 
-    // TODO: Add other metadata info check, e.g. location, after Recorder add more metadata.
-
     @Test
     fun getCorrectResolution_when_setSupportedQuality() {
         // Pre-arrange.
@@ -282,7 +282,14 @@
             completeVideoRecording(videoCapture, file)
 
             // Verify.
-            verifyVideoResolution(targetResolution, file)
+            val expectResolution = if (videoCapture.node != null) {
+                val relativeRotation =
+                    cameraInfo.getSensorRotationDegrees(videoCapture.targetRotation)
+                rotateSize(targetResolution, relativeRotation)
+            } else {
+                targetResolution
+            }
+            verifyVideoResolution(expectResolution, file)
 
             // Cleanup.
             file.delete()
@@ -390,7 +397,7 @@
         latchForVideoSaved = CountDownLatch(1)
         latchForVideoRecording = CountDownLatch(5)
         val latchForImageAnalysis = CountDownLatch(5)
-        analysis.setAnalyzer(CameraXExecutors.directExecutor()) { it: ImageProxy ->
+        analysis.setAnalyzer(CameraXExecutors.directExecutor()) {
             latchForImageAnalysis.countDown()
             it.close()
         }
@@ -773,64 +780,50 @@
         savedCallback.verifyCaptureResult()
     }
 
-    private fun verifyMetadataRotation(targetRotation: Int, file: File) {
-        // Whether the camera lens and display are facing opposite directions.
-        val isOpposite = cameraSelector.lensFacing == CameraSelector.LENS_FACING_BACK
-        val relativeRotation = CameraOrientationUtil.getRelativeImageRotation(
-            CameraOrientationUtil.surfaceRotationToDegrees(targetRotation),
-            CameraUtil.getSensorOrientation(cameraSelector.lensFacing!!)!!,
-            isOpposite
-        )
-        val videoRotation = getRotationInMetadata(Uri.fromFile(file))
+    private fun verifyMetadataRotation(expectedRotation: Int, file: File) {
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, Uri.fromFile(file))
+            val videoRotation =
+                it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)!!.toInt()
 
-        // Checks the rotation from video file's metadata is matched with the relative rotation.
-        assertWithMessage(
-            TAG + ", $targetRotation rotation test failure:" +
-                ", videoRotation: $videoRotation" +
-                ", relativeRotation: $relativeRotation"
-        ).that(videoRotation).isEqualTo(relativeRotation)
+            // Checks the rotation from video file's metadata is matched with the relative rotation.
+            assertWithMessage(
+                TAG + ", rotation test failure: " +
+                    "videoRotation: $videoRotation" +
+                    ", expectedRotation: $expectedRotation"
+            ).that(videoRotation).isEqualTo(expectedRotation)
+        }
     }
 
-    private fun verifyVideoResolution(targetResolution: Size, file: File) {
-        val mediaRetriever = MediaMetadataRetriever()
-        lateinit var resolution: Size
-        mediaRetriever.apply {
-            setDataSource(context, Uri.fromFile(file))
-            val height = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!
+    private fun verifyVideoResolution(expectedResolution: Size, file: File) {
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, Uri.fromFile(file))
+            val height = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!
                 .toInt()
-            val width = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!
+            val width = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!
                 .toInt()
-            resolution = Size(width, height)
-        }
+            val resolution = Size(width, height)
 
-        // Compare with the resolution of video and the targetResolution in QualitySelector
-        assertWithMessage(
-            TAG + ", verifyVideoResolution failure:" +
-                ", videoResolution: $resolution" +
-                ", targetResolution: $targetResolution"
-        ).that(resolution).isEqualTo(targetResolution)
+            // Compare with the resolution of video and the targetResolution in QualitySelector
+            assertWithMessage(
+                TAG + ", verifyVideoResolution failure:" +
+                    ", videoResolution: $resolution" +
+                    ", expectedResolution: $expectedResolution"
+            ).that(resolution).isEqualTo(expectedResolution)
+        }
     }
 
     private fun verifyRecordingResult(file: File, hasAudio: Boolean = false) {
-        val mediaRetriever = MediaMetadataRetriever()
-        mediaRetriever.apply {
-            setDataSource(context, Uri.fromFile(file))
-            val video = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
-            val audio = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, Uri.fromFile(file))
+            val video = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+            val audio = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
 
             assertThat(video).isEqualTo("yes")
             assertThat(audio).isEqualTo(if (hasAudio) "yes" else null)
         }
     }
 
-    private fun getRotationInMetadata(uri: Uri): Int {
-        val mediaRetriever = MediaMetadataRetriever()
-        return mediaRetriever.let {
-            it.setDataSource(context, uri)
-            it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt()!!
-        }
-    }
-
     private fun getSurfaceProvider(): Preview.SurfaceProvider {
         return SurfaceTextureProvider.createSurfaceTextureProvider(
             object : SurfaceTextureProvider.SurfaceTextureCallback {
@@ -870,4 +863,12 @@
             assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue()
         }
     }
-}
\ No newline at end of file
+}
+
+private fun MediaMetadataRetriever.useAndRelease(block: (MediaMetadataRetriever) -> Unit) {
+    try {
+        block(this)
+    } finally {
+        release()
+    }
+}
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 bbca48f..9f9d5b3 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
@@ -94,6 +94,7 @@
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.core.processing.DefaultSurfaceEffect;
 import androidx.camera.core.processing.SettableSurface;
 import androidx.camera.core.processing.SurfaceEdge;
 import androidx.camera.core.processing.SurfaceEffectInternal;
@@ -101,6 +102,7 @@
 import androidx.camera.video.StreamInfo.StreamState;
 import androidx.camera.video.impl.VideoCaptureConfig;
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.video.internal.compat.quirk.PreviewDelayWhenVideoCaptureIsBoundQuirk;
 import androidx.camera.video.internal.compat.quirk.PreviewStretchWhenVideoCaptureIsBoundQuirk;
 import androidx.camera.video.internal.config.MimeInfo;
 import androidx.camera.video.internal.encoder.InvalidConfigException;
@@ -150,6 +152,8 @@
     private static final Defaults DEFAULT_CONFIG = new Defaults();
     private static final boolean HAS_PREVIEW_STRETCH_QUIRK =
             DeviceQuirks.get(PreviewStretchWhenVideoCaptureIsBoundQuirk.class) != null;
+    private static final boolean HAS_PREVIEW_DELAY_QUIRK =
+            DeviceQuirks.get(PreviewDelayWhenVideoCaptureIsBoundQuirk.class) != null;
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     DeferrableSurface mDeferrableSurface;
@@ -298,6 +302,7 @@
 
         mStreamInfo = fetchObservableValue(getOutput().getStreamInfo(),
                 StreamInfo.STREAM_INFO_ANY_INACTIVE);
+        mNode = createNodeIfNeeded();
         mSessionConfigBuilder = createPipeline(cameraId, config, finalSelectedResolution);
         applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo);
         updateSessionConfig(mSessionConfigBuilder.build());
@@ -323,6 +328,11 @@
     /**
      * Sets a {@link SurfaceEffectInternal}.
      *
+     * <p>The effect is used to setup post-processing pipeline.
+     *
+     * <p>Note: the value will only be used when VideoCapture is bound. Calling this method after
+     * VideoCapture is bound takes no effect until VideoCapture is rebound.
+     *
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -340,6 +350,11 @@
     public void onDetached() {
         clearPipeline();
 
+        if (mNode != null) {
+            mNode.release();
+            mNode = null;
+        }
+
         mVideoEncoderInfo = null;
     }
 
@@ -475,7 +490,7 @@
         Range<Integer> targetFpsRange = requireNonNull(
                 config.getTargetFramerate(Defaults.DEFAULT_FPS_RANGE));
         Timebase timebase;
-        if (mSurfaceEffect != null) {
+        if (mNode != null) {
             MediaSpec mediaSpec = requireNonNull(getMediaSpec());
             Rect cropRect = requireNonNull(getCropRect(resolution));
             timebase = camera.getCameraInfoInternal().getTimebase();
@@ -483,7 +498,6 @@
                     () -> getVideoEncoderInfo(config.getVideoEncoderInfoFinder(),
                             VideoCapabilities.from(camera.getCameraInfo()), timebase, mediaSpec,
                             resolution, targetFpsRange));
-            mNode = new SurfaceEffectNode(camera, APPLY_CROP_ROTATE_AND_MIRRORING, mSurfaceEffect);
             SettableSurface cameraSurface = new SettableSurface(
                     SurfaceEffect.VIDEO_CAPTURE,
                     resolution,
@@ -518,7 +532,7 @@
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
         sessionConfigBuilder.addErrorListener(
                 (sessionConfig, error) -> resetPipeline(cameraId, config, resolution));
-        if (HAS_PREVIEW_STRETCH_QUIRK) {
+        if (HAS_PREVIEW_STRETCH_QUIRK || HAS_PREVIEW_DELAY_QUIRK) {
             sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
         }
 
@@ -536,10 +550,6 @@
             mDeferrableSurface.close();
             mDeferrableSurface = null;
         }
-        if (mNode != null) {
-            mNode.release();
-            mNode = null;
-        }
 
         mSurfaceRequest = null;
         mStreamInfo = StreamInfo.STREAM_INFO_ANY_INACTIVE;
@@ -683,6 +693,23 @@
         setupSurfaceUpdateNotifier(sessionConfigBuilder, isStreamActive);
     }
 
+    @Nullable
+    private SurfaceEffectNode createNodeIfNeeded() {
+        if (mSurfaceEffect != null || HAS_PREVIEW_DELAY_QUIRK) {
+            Logger.d(TAG, "SurfaceEffect is enabled.");
+            return new SurfaceEffectNode(requireNonNull(getCamera()),
+                    APPLY_CROP_ROTATE_AND_MIRRORING,
+                    mSurfaceEffect != null ? mSurfaceEffect : new DefaultSurfaceEffect());
+        }
+        return null;
+    }
+
+    @VisibleForTesting
+    @Nullable
+    SurfaceEffectNode getNode() {
+        return mNode;
+    }
+
     @MainThread
     @NonNull
     private Rect adjustCropRectIfNeeded(@NonNull Rect cropRect, @NonNull Size resolution,
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
index c5f6992..6e7f5e6 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
@@ -16,6 +16,12 @@
 
 package androidx.camera.video.internal;
 
+import static android.media.AudioFormat.ENCODING_PCM_16BIT;
+import static android.media.AudioFormat.ENCODING_PCM_24BIT_PACKED;
+import static android.media.AudioFormat.ENCODING_PCM_32BIT;
+import static android.media.AudioFormat.ENCODING_PCM_8BIT;
+import static android.media.AudioFormat.ENCODING_PCM_FLOAT;
+
 import static androidx.camera.video.internal.AudioSource.InternalState.CONFIGURED;
 import static androidx.camera.video.internal.AudioSource.InternalState.RELEASED;
 import static androidx.camera.video.internal.AudioSource.InternalState.STARTED;
@@ -45,6 +51,8 @@
 import androidx.camera.video.internal.compat.Api24Impl;
 import androidx.camera.video.internal.compat.Api29Impl;
 import androidx.camera.video.internal.compat.Api31Impl;
+import androidx.camera.video.internal.compat.quirk.AudioTimestampFramePositionIncorrectQuirk;
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.video.internal.encoder.InputBuffer;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.core.util.Preconditions;
@@ -107,6 +115,12 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final int mBufferSize;
 
+    final int mSampleRate;
+
+    final int mBytesPerFrame;
+
+    long mTotalFramesRead = 0;
+
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     InternalState mState = CONFIGURED;
 
@@ -174,7 +188,10 @@
 
         mExecutor = CameraXExecutors.newSequentialExecutor(executor);
         mBufferSize = minBufferSize * 2;
+        mSampleRate = settings.getSampleRate();
         try {
+            mBytesPerFrame = getBytesPerFrame(settings.getAudioFormat(),
+                    settings.getChannelCount());
             if (Build.VERSION.SDK_INT >= 23) {
                 AudioFormat audioFormatObj = new AudioFormat.Builder()
                         .setSampleRate(settings.getSampleRate())
@@ -417,6 +434,7 @@
                         byteBuffer.limit(length);
                         inputBuffer.setPresentationTimeUs(generatePresentationTimeUs());
                         inputBuffer.submit();
+                        mTotalFramesRead += length / mBytesPerFrame;
                     } else {
                         Logger.w(TAG, "Unable to read data from AudioRecord.");
                         inputBuffer.cancel();
@@ -474,6 +492,7 @@
             notifyError(new AudioSourceAccessException("Unable to start the audio record.", e));
             return;
         }
+        mTotalFramesRead = 0;
         mIsSendingAudio = true;
         sendNextAudio();
     }
@@ -514,11 +533,12 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     long generatePresentationTimeUs() {
         long presentationTimeUs = -1;
-        if (Build.VERSION.SDK_INT >= 24) {
+        if (Build.VERSION.SDK_INT >= 24 && !hasAudioTimestampQuirk()) {
             AudioTimestamp audioTimestamp = new AudioTimestamp();
             if (Api24Impl.getTimestamp(mAudioRecord, audioTimestamp,
                     AudioTimestamp.TIMEBASE_MONOTONIC) == AudioRecord.SUCCESS) {
-                presentationTimeUs = TimeUnit.NANOSECONDS.toMicros(audioTimestamp.nanoTime);
+                presentationTimeUs = computeInterpolatedTimeUs(mSampleRate, mTotalFramesRead,
+                        audioTimestamp);
             } else {
                 Logger.w(TAG, "Unable to get audio timestamp");
             }
@@ -529,6 +549,19 @@
         return presentationTimeUs;
     }
 
+    private static boolean hasAudioTimestampQuirk() {
+        return DeviceQuirks.get(AudioTimestampFramePositionIncorrectQuirk.class) != null;
+    }
+
+    private static long computeInterpolatedTimeUs(int sampleRate, long framePosition,
+            @NonNull AudioTimestamp timestamp) {
+        long frameDiff = framePosition - timestamp.framePosition;
+        long compensateTimeInNanoSec = TimeUnit.SECONDS.toNanos(1) * frameDiff / sampleRate;
+        long resultInNanoSec = timestamp.nanoTime + compensateTimeInNanoSec;
+
+        return resultInNanoSec < 0 ? 0 : TimeUnit.NANOSECONDS.toMicros(resultInNanoSec);
+    }
+
     /** Check if the combination of sample rate, channel count and audio format is supported. */
     public static boolean isSettingsSupported(int sampleRate, int channelCount, int audioFormat) {
         if (sampleRate <= 0 || channelCount <= 0) {
@@ -553,6 +586,24 @@
                 audioFormat);
     }
 
+    private static int getBytesPerFrame(int audioFormat, int channelCount) {
+        Preconditions.checkState(channelCount > 0);
+
+        switch (audioFormat) {
+            case ENCODING_PCM_8BIT:
+                return channelCount;
+            case ENCODING_PCM_16BIT:
+                return channelCount * 2;
+            case ENCODING_PCM_24BIT_PACKED:
+                return channelCount * 3;
+            case ENCODING_PCM_32BIT:
+            case ENCODING_PCM_FLOAT:
+                return channelCount * 4;
+            default:
+                throw new IllegalArgumentException("Invalid audio format: " + audioFormat);
+        }
+    }
+
     /**
      * Settings required to configure the audio source.
      */
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/AudioTimestampFramePositionIncorrectQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/AudioTimestampFramePositionIncorrectQuirk.java
new file mode 100644
index 0000000..e8128dd
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/AudioTimestampFramePositionIncorrectQuirk.java
@@ -0,0 +1,45 @@
+/*
+ * 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.video.internal.compat.quirk;
+
+import android.media.AudioTimestamp;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: 245518008
+ *     Description: Quirk which denotes {@link android.media.AudioTimestamp#framePosition} queried
+ *                  by {@link android.media.AudioRecord#getTimestamp(AudioTimestamp, int)} returns
+ *                  incorrect info. On Redmi 6A, frame position becomes negative after recording
+ *                  multiple times.
+ *
+ *     Device(s): Redmi 6A
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class AudioTimestampFramePositionIncorrectQuirk implements Quirk {
+
+    static boolean load() {
+        return isRedmi6A();
+    }
+
+    private static boolean isRedmi6A() {
+        return "Xiaomi".equalsIgnoreCase(Build.BRAND) && "Redmi 6A".equalsIgnoreCase(Build.MODEL);
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/CameraUseInconsistentTimebaseQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/CameraUseInconsistentTimebaseQuirk.java
index f56a676..63e0121 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/CameraUseInconsistentTimebaseQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/CameraUseInconsistentTimebaseQuirk.java
@@ -21,7 +21,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.impl.Quirk;
-import androidx.camera.video.internal.workaround.CorrectVideoTimeByTimebase;
+import androidx.camera.video.internal.workaround.VideoTimebaseConverter;
 
 import java.util.Arrays;
 import java.util.HashSet;
@@ -33,7 +33,7 @@
  *     Description: Quirk that denotes some Samsung devices use inconsistent timebase for camera
  *                  frame.
  *     Device(s): Some Samsung devices
- *     @see CorrectVideoTimeByTimebase
+ *     @see VideoTimebaseConverter
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class CameraUseInconsistentTimebaseQuirk implements Quirk {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index 151ba52..7670a3f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -80,6 +80,12 @@
         if (PreviewStretchWhenVideoCaptureIsBoundQuirk.load()) {
             quirks.add(new PreviewStretchWhenVideoCaptureIsBoundQuirk());
         }
+        if (PreviewDelayWhenVideoCaptureIsBoundQuirk.load()) {
+            quirks.add(new PreviewDelayWhenVideoCaptureIsBoundQuirk());
+        }
+        if (AudioTimestampFramePositionIncorrectQuirk.load()) {
+            quirks.add(new AudioTimestampFramePositionIncorrectQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java
index 4cb6541..8e7d374 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java
@@ -24,27 +24,41 @@
 
 /**
  * <p>QuirkSummary
- *     Bug Id: 202792648
+ *     Bug Id: 202792648, 245495234
  *     Description: The captured video is stretched while selecting the quality is greater or
  *                  equality to FHD resolution
- *     Device(s): Samsung J4 (sm-j400g)
+ *     Device(s): Samsung J4 (sm-j400g), Samsung J7 Prime (sm-g610m) API level 27 or above,
+ *     Samsung J7 (sm-J710mn) API level 27 or above
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ExcludeStretchedVideoQualityQuirk implements VideoQualityQuirk {
     static boolean load() {
-        return isSamsungJ4();
+        return isSamsungJ4() || isSamsungJ7PrimeApi27Above() || isSamsungJ7Api27Above();
     }
 
     private static boolean isSamsungJ4() {
         return "Samsung".equalsIgnoreCase(Build.BRAND) && "SM-J400G".equalsIgnoreCase(Build.MODEL);
     }
 
+    private static boolean isSamsungJ7PrimeApi27Above() {
+        return "Samsung".equalsIgnoreCase(Build.BRAND) && "SM-G610M".equalsIgnoreCase(Build.MODEL)
+                && Build.VERSION.SDK_INT >= 27;
+    }
+
+    private static boolean isSamsungJ7Api27Above() {
+        return "Samsung".equalsIgnoreCase(Build.BRAND) && "SM-J710MN".equalsIgnoreCase(Build.MODEL)
+                && Build.VERSION.SDK_INT >= 27;
+    }
+
     /** Checks if the given Quality type is a problematic quality. */
     @Override
     public boolean isProblematicVideoQuality(@NonNull Quality quality) {
         if (isSamsungJ4()) {
             return quality == Quality.FHD || quality == Quality.UHD;
         }
+        if (isSamsungJ7PrimeApi27Above() || isSamsungJ7Api27Above()) {
+            return quality == Quality.FHD;
+        }
         return false;
     }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewDelayWhenVideoCaptureIsBoundQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewDelayWhenVideoCaptureIsBoundQuirk.java
new file mode 100644
index 0000000..6847b67
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewDelayWhenVideoCaptureIsBoundQuirk.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.camera.video.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: b/223643510
+ *     Description: Quirk indicates Preview is delayed on some Huawei devices when the Preview uses
+ *                  certain resolutions and VideoCapture is bound.
+ *     Device(s): Some Huawei devices.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class PreviewDelayWhenVideoCaptureIsBoundQuirk implements Quirk {
+
+    private static final Set<String> HUAWEI_DEVICE_LIST = new HashSet<>(Arrays.asList(
+            "HWELE",  // P30
+            "HWVOG",  // P30 Pro
+            "HWYAL",  // Nova 5T
+            "HWLYA",  // Mate 20 Pro
+            "HWCOL",  // Honor 10
+            "HWPAR"   // Nova 3
+    ));
+
+    static boolean load() {
+        return "Huawei".equalsIgnoreCase(Build.MANUFACTURER)
+                && HUAWEI_DEVICE_LIST.contains(Build.DEVICE.toUpperCase(Locale.US));
+    }
+}
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 7a966e4..bf72036 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
@@ -40,6 +40,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.annotation.ExecutedBy;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
@@ -50,8 +51,8 @@
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
 import androidx.camera.video.internal.compat.quirk.VideoEncoderSuspendDoesNotIncludeSuspendTimeQuirk;
-import androidx.camera.video.internal.workaround.CorrectVideoTimeByTimebase;
 import androidx.camera.video.internal.workaround.EncoderFinder;
+import androidx.camera.video.internal.workaround.VideoTimebaseConverter;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
 import androidx.core.util.Preconditions;
@@ -174,6 +175,8 @@
      */
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final Deque<Range<Long>> mActivePauseResumeTimeRanges = new ArrayDeque<>();
+    final Timebase mInputTimebase;
+    final TimeProvider mTimeProvider = new SystemTimeProvider();
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @GuardedBy("mLock")
@@ -227,6 +230,8 @@
             throw new InvalidConfigException("Unknown encoder config type");
         }
 
+        mInputTimebase = encoderConfig.getInputTimebase();
+        Logger.d(mTag, "mInputTimebase = " + mInputTimebase);
         mMediaFormat = encoderConfig.toMediaFormat();
         Logger.d(mTag, "mMediaFormat = " + mMediaFormat);
         mMediaCodec = mEncoderFinder.findEncoder(mMediaFormat);
@@ -967,8 +972,8 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    static long generatePresentationTimeUs() {
-        return TimeUnit.NANOSECONDS.toMicros(System.nanoTime());
+    long generatePresentationTimeUs() {
+        return mTimeProvider.uptimeUs();
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -985,7 +990,7 @@
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     class MediaCodecCallback extends MediaCodec.Callback {
         @Nullable
-        private final CorrectVideoTimeByTimebase mCorrectVideoTimestamp;
+        private final VideoTimebaseConverter mVideoTimestampConverter;
 
         private boolean mHasSendStartCallback = false;
         private boolean mHasFirstData = false;
@@ -1001,11 +1006,16 @@
         private boolean mIsKeyFrameRequired = false;
 
         MediaCodecCallback() {
-            if (mIsVideoEncoder
-                    && DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) {
-                mCorrectVideoTimestamp = new CorrectVideoTimeByTimebase();
+            if (mIsVideoEncoder) {
+                Timebase inputTimebase;
+                if (DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) {
+                    inputTimebase = null;
+                } else {
+                    inputTimebase = mInputTimebase;
+                }
+                mVideoTimestampConverter = new VideoTimebaseConverter(mTimeProvider, inputTimebase);
             } else {
-                mCorrectVideoTimestamp = null;
+                mVideoTimestampConverter = null;
             }
         }
 
@@ -1055,10 +1065,6 @@
                             Logger.d(mTag, DebugUtils.readableBufferInfo(bufferInfo));
                         }
 
-                        if (mCorrectVideoTimestamp != null) {
-                            mCorrectVideoTimestamp.correctTimestamp(bufferInfo);
-                        }
-
                         // Handle start of stream
                         if (!mHasSendStartCallback) {
                             mHasSendStartCallback = true;
@@ -1069,7 +1075,7 @@
                             }
                         }
 
-                        if (!checkBufferInfo(bufferInfo)) {
+                        if (checkBufferInfo(bufferInfo)) {
                             if (!mHasFirstData) {
                                 mHasFirstData = true;
                             }
@@ -1164,31 +1170,36 @@
         /**
          * Checks the {@link android.media.MediaCodec.BufferInfo} and updates related states.
          *
-         * @return {@code true} if the buffer should be dropped, otherwise {@code false}.
+         * @return {@code true} if the buffer is valid, otherwise {@code false}.
          */
         @ExecutedBy("mEncoderExecutor")
         private boolean checkBufferInfo(@NonNull MediaCodec.BufferInfo bufferInfo) {
             if (mHasEndData) {
                 Logger.d(mTag, "Drop buffer by already reach end of stream.");
-                return true;
+                return false;
             }
 
             if (bufferInfo.size <= 0) {
                 Logger.d(mTag, "Drop buffer by invalid buffer size.");
-                return true;
+                return false;
             }
 
             // Sometimes the codec config data was notified by output callback, they should have
             // been sent out by onOutputFormatChanged(), so ignore it.
             if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                 Logger.d(mTag, "Drop buffer by codec config.");
-                return true;
+                return false;
+            }
+
+            if (mVideoTimestampConverter != null) {
+                bufferInfo.presentationTimeUs =
+                        mVideoTimestampConverter.convertToUptimeUs(bufferInfo.presentationTimeUs);
             }
 
             // MediaCodec may send out of order buffer
             if (bufferInfo.presentationTimeUs <= mLastPresentationTimeUs) {
                 Logger.d(mTag, "Drop buffer by out of order buffer from MediaCodec.");
-                return true;
+                return false;
             }
             mLastPresentationTimeUs = bufferInfo.presentationTimeUs;
 
@@ -1208,12 +1219,12 @@
                     signalCodecStop();
                     mPendingCodecStop = false;
                 }
-                return true;
+                return false;
             }
 
             if (updatePauseRangeStateAndCheckIfBufferPaused(bufferInfo)) {
                 Logger.d(mTag, "Drop buffer by pause.");
-                return true;
+                return false;
             }
 
             // We should check if the adjusted time is valid. see b/189114207.
@@ -1222,7 +1233,7 @@
                 if (mIsVideoEncoder && isKeyFrame(bufferInfo)) {
                     mIsKeyFrameRequired = true;
                 }
-                return true;
+                return false;
             }
 
             if (!mHasFirstData && !mIsKeyFrameRequired && mIsVideoEncoder) {
@@ -1233,12 +1244,12 @@
                 if (!isKeyFrame(bufferInfo)) {
                     Logger.d(mTag, "Drop buffer by not a key frame.");
                     requestKeyFrameToMediaCodec();
-                    return true;
+                    return false;
                 }
                 mIsKeyFrameRequired = false;
             }
 
-            return false;
+            return true;
         }
 
         @ExecutedBy("mEncoderExecutor")
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/SystemTimeProvider.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/SystemTimeProvider.java
new file mode 100644
index 0000000..599bbca
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/SystemTimeProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.encoder;
+
+import android.os.SystemClock;
+
+import androidx.annotation.RequiresApi;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A TimeProvider implementation based on System time.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class SystemTimeProvider implements TimeProvider {
+
+    @Override
+    public long uptimeUs() {
+        return TimeUnit.NANOSECONDS.toMicros(System.nanoTime());
+    }
+
+    @Override
+    public long realtimeUs() {
+        return TimeUnit.NANOSECONDS.toMicros(SystemClock.elapsedRealtimeNanos());
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/TimeProvider.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/TimeProvider.java
new file mode 100644
index 0000000..634767c
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/TimeProvider.java
@@ -0,0 +1,37 @@
+/*
+ * 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.video.internal.encoder;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Timebase;
+
+/**
+ * The time provider used to provider timestamps.
+ *
+ * <p>There are two sets of methods based on {@link Timebase#UPTIME} and {@link Timebase#REALTIME}.
+ *
+ * @see SystemTimeProvider
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface TimeProvider {
+
+    /** Returns the timestamp in microseconds based on {@link Timebase#UPTIME}. */
+    long uptimeUs();
+
+    /** Returns the timestamp in microseconds based on {@link Timebase#REALTIME}. */
+    long realtimeUs();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/CorrectVideoTimeByTimebase.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/CorrectVideoTimeByTimebase.java
deleted file mode 100644
index 7578dd9..0000000
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/CorrectVideoTimeByTimebase.java
+++ /dev/null
@@ -1,79 +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.video.internal.workaround;
-
-import android.media.MediaCodec;
-import android.os.SystemClock;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.Logger;
-import androidx.camera.video.internal.compat.quirk.CameraUseInconsistentTimebaseQuirk;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Corrects the video timestamps if video buffer contains REALTIME timestamp.
- *
- * <p>As described on b/197805856, some Samsung devices use inconsistent timebase for camera
- * frame. The workaround detects and corrects the timestamp by generating a new timestamp.
- * Note: this will sacrifice the precise timestamp of video buffer.
- *
- * @see CameraUseInconsistentTimebaseQuirk
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class CorrectVideoTimeByTimebase {
-    private static final String TAG = "CorrectVideoTimeByTimebase";
-
-    @Nullable
-    private AtomicBoolean mNeedToCorrectVideoTimebase = null;
-
-    /**
-     * Corrects the video timestamp if necessary.
-     *
-     * <p>This method will modify the {@link MediaCodec.BufferInfo#presentationTimeUs} if necessary.
-     *
-     * @param bufferInfo the buffer info.
-     */
-    public void correctTimestamp(@NonNull MediaCodec.BufferInfo bufferInfo) {
-        // For performance concern, only check the requirement once.
-        if (mNeedToCorrectVideoTimebase == null) {
-            // Skip invalid buffer
-            if (bufferInfo.size <= 0 || bufferInfo.presentationTimeUs <= 0L
-                    || (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
-                return;
-            }
-
-            long uptimeUs = TimeUnit.MILLISECONDS.toMicros(SystemClock.uptimeMillis());
-            long realtimeUs = TimeUnit.MILLISECONDS.toMicros(SystemClock.elapsedRealtime());
-            // Expected to be uptime
-            boolean closeToRealTime = Math.abs(bufferInfo.presentationTimeUs - realtimeUs)
-                    < Math.abs(bufferInfo.presentationTimeUs - uptimeUs);
-            if (closeToRealTime) {
-                Logger.w(TAG, "Detected video buffer timestamp is close to real time.");
-            }
-            mNeedToCorrectVideoTimebase = new AtomicBoolean(closeToRealTime);
-        }
-
-        if (mNeedToCorrectVideoTimebase.get()) {
-            bufferInfo.presentationTimeUs -= TimeUnit.MILLISECONDS.toMicros(
-                    SystemClock.elapsedRealtime() - SystemClock.uptimeMillis());
-        }
-    }
-}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/VideoTimebaseConverter.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/VideoTimebaseConverter.java
new file mode 100644
index 0000000..6a14e07
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/VideoTimebaseConverter.java
@@ -0,0 +1,112 @@
+/*
+ * 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.video.internal.workaround;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Timebase;
+import androidx.camera.video.internal.compat.quirk.CameraUseInconsistentTimebaseQuirk;
+import androidx.camera.video.internal.encoder.TimeProvider;
+
+/**
+ * Converts the video timestamps to {@link Timebase#UPTIME} if video buffer contains
+ * {@link Timebase#REALTIME} timestamp.
+ *
+ * <p>The workaround accepts an {@code null} input timebase. This is useful when the timebase is
+ * unknown, such as the problem described in b/197805856. If the input timebase is null, an
+ * automatic detection mechanism is used to determine the timebase, which is by checking the input
+ * timestamp is close to UPTIME or REALTIME. For performance reason, the detection will only check
+ * the first input timestamp.
+ *
+ * @see CameraUseInconsistentTimebaseQuirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class VideoTimebaseConverter {
+    private static final String TAG = "VideoTimebaseConverter";
+
+    private final TimeProvider mTimeProvider;
+
+    private long mUptimeToRealtimeOffsetUs = -1L;
+    private Timebase mInputTimebase;
+
+    /**
+     * Constructs the VideoTimebaseConverter.
+     *
+     * @param timeProvider  the time provider.
+     * @param inputTimebase the input video frame timebase. {@code null} if the timebase is unknown.
+     */
+    public VideoTimebaseConverter(@NonNull TimeProvider timeProvider,
+            @Nullable Timebase inputTimebase) {
+        mTimeProvider = timeProvider;
+        mInputTimebase = inputTimebase;
+    }
+
+    /**
+     * Converts the video timestamp to {@link Timebase#UPTIME} if necessary.
+     *
+     * @param timestampUs the video frame timestamp in micro seconds. The timebase is supposed
+     *                    to be the input timebase in constructor.
+     */
+    public long convertToUptimeUs(long timestampUs) {
+        if (mInputTimebase == null) {
+            if (isCloseToRealtime(timestampUs)) {
+                Logger.w(TAG, "Detected video buffer timestamp is close to realtime.");
+                mInputTimebase = Timebase.REALTIME;
+            } else {
+                mInputTimebase = Timebase.UPTIME;
+            }
+        }
+        switch (mInputTimebase) {
+            case REALTIME:
+                if (mUptimeToRealtimeOffsetUs == -1) {
+                    mUptimeToRealtimeOffsetUs = calculateUptimeToRealtimeOffsetUs();
+                }
+                return timestampUs - mUptimeToRealtimeOffsetUs;
+            case UPTIME:
+                return timestampUs;
+            default:
+                throw new AssertionError("Unknown timebase: " + mInputTimebase);
+        }
+    }
+
+    private boolean isCloseToRealtime(long timeUs) {
+        long uptimeUs = mTimeProvider.uptimeUs();
+        long realtimeUs = mTimeProvider.realtimeUs();
+        return Math.abs(timeUs - realtimeUs) < Math.abs(timeUs - uptimeUs);
+    }
+
+    // The algorithm is from camera framework Camera3Device.cpp
+    private long calculateUptimeToRealtimeOffsetUs() {
+        // Try three times to get the clock offset, choose the one with the minimum gap in
+        // measurements.
+        long bestGap = Long.MAX_VALUE;
+        long measured = 0L;
+        for (int i = 0; i < 3; i++) {
+            long uptime1 = mTimeProvider.uptimeUs();
+            long realtime = mTimeProvider.realtimeUs();
+            long uptime2 = mTimeProvider.uptimeUs();
+            long gap = uptime2 - uptime1;
+            if (i == 0 || gap < bestGap) {
+                bestGap = gap;
+                measured = realtime - ((uptime1 + uptime2) >> 1);
+            }
+        }
+        return Math.max(0, measured);
+    }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index cac814d..a2d7fc1 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -25,16 +25,19 @@
 import android.view.Surface
 import androidx.arch.core.util.Function
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraSelector.LENS_FACING_BACK
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.CamcorderProfileProxy
 import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.MutableStateObservable
 import androidx.camera.core.impl.Observable
 import androidx.camera.core.impl.Timebase
 import androidx.camera.core.impl.utils.TransformUtils.rectToSize
+import androidx.camera.core.impl.utils.TransformUtils.rotateSize
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.core.processing.SurfaceEffectInternal
@@ -94,6 +97,7 @@
     private val context: Context = ApplicationProvider.getApplicationContext()
     private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
     private lateinit var cameraFactory: CameraFactory
+    private lateinit var cameraInfo: CameraInfoInternal
     private lateinit var surfaceManager: FakeCameraDeviceSurfaceManager
     private var surfaceRequestsToRelease = mutableListOf<SurfaceRequest>()
 
@@ -103,6 +107,7 @@
             cameraUseCaseAdapter.apply {
                 detachUseCases()
                 removeUseCases(useCases)
+                shadowOf(Looper.getMainLooper()).idle()
             }
         }
         surfaceRequestsToRelease.forEach {
@@ -132,22 +137,83 @@
     }
 
     @Test
-    fun addUseCases_receiveOnSurfaceRequest() {
-        // Arrange.
-        setupCamera()
+    fun addUseCases_sendCorrectResolution() {
+        testSetRotationWillSendCorrectResolution()
+    }
+
+    @Test
+    fun enableEffect_sensorRotationIs0AndSetTargetRotation_sendCorrectResolution() {
+        testSetRotationWillSendCorrectResolution(
+            sensorRotation = 0,
+            effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor())
+        )
+    }
+
+    @Test
+    fun enableEffect_sensorRotationIs90AndSetTargetRotation_sendCorrectResolution() {
+        testSetRotationWillSendCorrectResolution(
+            sensorRotation = 90,
+            effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor())
+        )
+    }
+
+    @Test
+    fun enableEffect_sensorRotationIs180AndSetTargetRotation_sendCorrectResolution() {
+        testSetRotationWillSendCorrectResolution(
+            sensorRotation = 180,
+            effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor())
+        )
+    }
+
+    @Test
+    fun enableEffect_sensorRotationIs270AndSetTargetRotation_sendCorrectResolution() {
+        testSetRotationWillSendCorrectResolution(
+            sensorRotation = 270,
+            effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor())
+        )
+    }
+
+    private fun testSetRotationWillSendCorrectResolution(
+        sensorRotation: Int = 0,
+        effect: SurfaceEffectInternal? = null
+    ) {
+        setupCamera(sensorRotation = sensorRotation)
         createCameraUseCaseAdapter()
 
-        var surfaceRequest: SurfaceRequest? = null
-        val videoOutput = createVideoOutput(surfaceRequestListener = { request, _ ->
-            surfaceRequest = request
-        })
-        val videoCapture = createVideoCapture(videoOutput)
+        listOf(
+            Surface.ROTATION_0,
+            Surface.ROTATION_90,
+            Surface.ROTATION_180,
+            Surface.ROTATION_270
+        ).forEach { targetRotation ->
+            // Arrange.
+            var surfaceRequest: SurfaceRequest? = null
+            val videoOutput = createVideoOutput(
+                mediaSpec = MediaSpec.builder().configureVideo {
+                    it.setQualitySelector(QualitySelector.from(Quality.HD))
+                }.build(),
+                surfaceRequestListener = { request, _ ->
+                    surfaceRequest = request
+                })
+            val videoCapture = createVideoCapture(videoOutput)
+            effect?.let { videoCapture.setEffect(it) }
+            videoCapture.targetRotation = targetRotation
 
-        // Act.
-        addAndAttachUseCases(videoCapture)
+            // Act.
+            addAndAttachUseCases(videoCapture)
 
-        // Assert.
-        assertThat(surfaceRequest).isNotNull()
+            // Assert.
+            val expectedResolution = if (effect != null) {
+                rotateSize(RESOLUTION_720P, cameraInfo.getSensorRotationDegrees(targetRotation))
+            } else {
+                RESOLUTION_720P
+            }
+            assertThat(surfaceRequest).isNotNull()
+            assertThat(surfaceRequest!!.resolution).isEqualTo(expectedResolution)
+
+            // Clean-up.
+            detachAndRemoveUseCases(videoCapture)
+        }
     }
 
     @Test
@@ -161,7 +227,7 @@
     }
 
     @Test
-    fun addUseCasesWithSurfaceEffect__cameraIsUptime_requestIsUptime() {
+    fun addUseCasesWithSurfaceEffect_cameraIsUptime_requestIsUptime() {
         testTimebase(
             effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor()),
             cameraTimebase = Timebase.UPTIME,
@@ -170,7 +236,7 @@
     }
 
     @Test
-    fun addUseCasesWithSurfaceEffect__cameraIsRealtime_requestIsRealtime() {
+    fun addUseCasesWithSurfaceEffect_cameraIsRealtime_requestIsRealtime() {
         testTimebase(
             effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor()),
             cameraTimebase = Timebase.REALTIME,
@@ -198,7 +264,6 @@
 
         // Act.
         addAndAttachUseCases(videoCapture)
-        shadowOf(Looper.getMainLooper()).idle()
 
         // Assert.
         assertThat(timebase).isEqualTo(expectedTimebase)
@@ -501,7 +566,6 @@
         // Act: bind and provide Surface.
         videoCapture.setEffect(effect)
         addAndAttachUseCases(videoCapture)
-        shadowOf(Looper.getMainLooper()).idle()
 
         // Assert: surfaceOutput received.
         assertThat(effect.surfaceOutput).isNotNull()
@@ -515,7 +579,6 @@
 
         // Act: unbind.
         detachAndRemoveUseCases(videoCapture)
-        shadowOf(Looper.getMainLooper()).idle()
 
         // Assert: effect and effect surface is released.
         assertThat(effect.isReleased).isTrue()
@@ -617,13 +680,12 @@
         val videoCapture = createVideoCapture(
             videoOutput, videoEncoderInfoFinder = { videoEncoderInfo }
         )
-        val effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor(), false)
+        val effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor())
         videoCapture.setEffect(effect)
         videoCapture.setViewPortCropRect(cropRect)
 
         // Act.
         addAndAttachUseCases(videoCapture)
-        shadowOf(Looper.getMainLooper()).idle()
 
         // Assert.
         assertThat(surfaceRequest).isNotNull()
@@ -701,11 +763,13 @@
     private fun addAndAttachUseCases(vararg useCases: UseCase) {
         cameraUseCaseAdapter.addUseCases(useCases.asList())
         cameraUseCaseAdapter.attachUseCases()
+        shadowOf(Looper.getMainLooper()).idle()
     }
 
     private fun detachAndRemoveUseCases(vararg useCases: UseCase) {
         cameraUseCaseAdapter.detachUseCases()
         cameraUseCaseAdapter.removeUseCases(useCases.asList())
+        shadowOf(Looper.getMainLooper()).idle()
     }
 
     private fun createCameraUseCaseAdapter() {
@@ -728,10 +792,11 @@
 
     private fun setupCamera(
         cameraId: String = CAMERA_ID_0,
+        sensorRotation: Int = 0,
         vararg profiles: CamcorderProfileProxy = CAMERA_0_PROFILES,
         timebase: Timebase = Timebase.UPTIME,
     ) {
-        val cameraInfo = FakeCameraInfoInternal(cameraId).apply {
+        cameraInfo = FakeCameraInfoInternal(cameraId, sensorRotation, LENS_FACING_BACK).apply {
             camcorderProfileProvider =
                 FakeCamcorderProfileProvider.Builder().addProfile(*profiles).build()
             setTimebase(timebase)
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/FakeTimeProvider.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/FakeTimeProvider.kt
new file mode 100644
index 0000000..eb2df49
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/FakeTimeProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.workaround
+
+import androidx.camera.video.internal.encoder.TimeProvider
+import java.util.concurrent.TimeUnit
+
+/**
+ * A fake TimeProvider implementation.
+ */
+class FakeTimeProvider(var uptimeNs: Long = 0L, var realtimeNs: Long = 0L) : TimeProvider {
+
+    override fun uptimeUs() = TimeUnit.NANOSECONDS.toMicros(uptimeNs)
+
+    override fun realtimeUs() = TimeUnit.NANOSECONDS.toMicros(realtimeNs)
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/VideoTimebaseConverterTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/VideoTimebaseConverterTest.kt
new file mode 100644
index 0000000..eec1a76
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/VideoTimebaseConverterTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.video.internal.workaround
+
+import android.os.Build
+import androidx.camera.core.impl.Timebase
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import org.junit.Test
+import org.junit.runner.RunWith
+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 VideoTimebaseConverterTest {
+
+    private val systemTimeProvider =
+        FakeTimeProvider(TimeUnit.MICROSECONDS.toNanos(1000L), TimeUnit.MICROSECONDS.toNanos(2000L))
+
+    @Test
+    fun uptimeTimebase_noConversion() {
+        // Arrange.
+        val videoTimebaseConverter = VideoTimebaseConverter(systemTimeProvider, Timebase.UPTIME)
+
+        // Act.
+        val outputTime1 = videoTimebaseConverter.convertToUptimeUs(800L)
+        val outputTime2 = videoTimebaseConverter.convertToUptimeUs(900L)
+
+        // Assert.
+        assertThat(outputTime1).isEqualTo(800L)
+        assertThat(outputTime2).isEqualTo(900L)
+    }
+
+    @Test
+    fun realtimeTimebase_doConversion() {
+        // Arrange.
+        val videoTimebaseConverter = VideoTimebaseConverter(systemTimeProvider, Timebase.REALTIME)
+
+        // Act.
+        val outputTime1 = videoTimebaseConverter.convertToUptimeUs(1800L)
+        val outputTime2 = videoTimebaseConverter.convertToUptimeUs(1900L)
+
+        // Assert.
+        assertThat(outputTime1).isEqualTo(800L)
+        assertThat(outputTime2).isEqualTo(900L)
+    }
+
+    @Test
+    fun unknownTimebase_closeToUptime_noConversion() {
+        // Arrange.
+        val videoTimebaseConverter = VideoTimebaseConverter(systemTimeProvider, null)
+
+        // Act.
+        val outputTime1 = videoTimebaseConverter.convertToUptimeUs(800L)
+        val outputTime2 = videoTimebaseConverter.convertToUptimeUs(900L)
+
+        // Assert.
+        assertThat(outputTime1).isEqualTo(800L)
+        assertThat(outputTime2).isEqualTo(900L)
+    }
+
+    @Test
+    fun unknownTimebase_closeToRealtime_doConversion() {
+        // Arrange.
+        val videoTimebaseConverter = VideoTimebaseConverter(systemTimeProvider, null)
+
+        // Act.
+        val outputTime1 = videoTimebaseConverter.convertToUptimeUs(1800L)
+        val outputTime2 = videoTimebaseConverter.convertToUptimeUs(1900L)
+
+        // Assert.
+        assertThat(outputTime1).isEqualTo(800L)
+        assertThat(outputTime2).isEqualTo(900L)
+    }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
index 423a076..0a8ed8b 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
@@ -32,7 +32,7 @@
  */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP, maxSdk = 32) // maxSdk due to b/247175194
 public class SurfaceViewStretchedQuirkTest {
 
     @Test
diff --git a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/internal/quirk/SurfaceViewStretchedQuirkTest.java b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/internal/quirk/SurfaceViewStretchedQuirkTest.java
index 8f994c9..49ea437 100644
--- a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/internal/quirk/SurfaceViewStretchedQuirkTest.java
+++ b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/internal/quirk/SurfaceViewStretchedQuirkTest.java
@@ -32,7 +32,7 @@
  */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP, maxSdk = 32) // maxSdk due to b/247175194
 public class SurfaceViewStretchedQuirkTest {
 
     @Test
diff --git a/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml
index 542568f..f44bb20 100644
--- a/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml
@@ -17,9 +17,24 @@
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <!-- For accessing current location in PlaceListMapTemplate -->
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <permission-group android:name="android.permission-group.SHOWCASE"
+        android:label="@string/perm_group"
+        android:description="@string/perm_group_description" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
+        android:permissionGroup="android.permission-group.SHOWCASE"
+        android:label="@string/perm_fine_location"
+        android:description="@string/perm_fine_location_desc" />
+
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
+        android:permissionGroup="android.permission-group.SHOWCASE"
+        android:label="@string/perm_coarse_location"
+        android:description="@string/perm_coarse_location_desc" />
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"
+        android:permissionGroup="android.permission-group.SHOWCASE"
+        android:label="@string/perm_record_audio"
+        android:description="@string/perm_record_audio_desc" />
 
     <application
         android:supportsRtl="true">
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java
new file mode 100644
index 0000000..415fd73
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java
@@ -0,0 +1,295 @@
+/*
+ * 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.sample.showcase.common.audio;
+
+import static android.Manifest.permission.RECORD_AUDIO;
+import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+import static android.media.AudioFormat.CHANNEL_OUT_MONO;
+import static android.media.AudioFormat.ENCODING_DEFAULT;
+import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+import static android.os.Build.VERSION.SDK_INT;
+
+import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE;
+import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_SAMPLING_RATE;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
+import androidx.car.app.CarContext;
+import androidx.car.app.CarToast;
+import androidx.car.app.media.CarAudioRecord;
+import androidx.car.app.utils.LogTags;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Manages recording the microphone and accessing the stored data from the microphone. */
+public class VoiceInteraction {
+    private static final String FILE_NAME = "recording.wav";
+
+    private final CarContext mCarContext;
+
+    public VoiceInteraction(@NonNull CarContext carContext) {
+        mCarContext = carContext;
+    }
+
+    /**
+     * Starts recording the car microphone, then plays it back.
+     */
+    @RequiresPermission(RECORD_AUDIO)
+    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
+    public void voiceInteractionDemo() {
+        // Some of the functions for recording require API level 26, so verify that first
+        if (SDK_INT < VERSION_CODES.O) {
+            CarToast.makeText(mCarContext, "API level is less than 26, "
+                            + "cannot use this functionality!",
+                    CarToast.LENGTH_LONG).show();
+            return;
+        }
+
+        // Check if we have permissions to record audio
+        if (!checkAudioPermission()) {
+            return;
+        }
+
+        // Start the thread for recording and playing back the audio
+        createRecordingThread().start();
+    }
+
+    /**
+     * Create thread which executes the record and the playback functions
+     */
+    @NonNull
+    @RequiresApi(api = VERSION_CODES.O)
+    @RequiresPermission(RECORD_AUDIO)
+    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
+    public Thread createRecordingThread() {
+        Thread recordingThread = new Thread(
+                () -> {
+                    // Request audio focus
+                    AudioFocusRequest audioFocusRequest = null;
+                    try {
+                        CarAudioRecord record = CarAudioRecord.create(mCarContext);
+                        // Take audio focus so that user's media is not recorded
+                        AudioAttributes audioAttributes =
+                                new AudioAttributes.Builder()
+                                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+                                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                                        .build();
+
+                        audioFocusRequest = new AudioFocusRequest.Builder(
+                                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
+                                .setAudioAttributes(audioAttributes)
+                                .setOnAudioFocusChangeListener(state -> {
+                                    if (state == AudioManager.AUDIOFOCUS_LOSS) {
+                                        // Stop recording if audio focus is lost
+                                        record.stopRecording();
+                                    }
+                                })
+                                .build();
+
+                        if (mCarContext.getSystemService(AudioManager.class)
+                                .requestAudioFocus(audioFocusRequest)
+                                != AUDIOFOCUS_REQUEST_GRANTED) {
+                            CarToast.makeText(mCarContext, "Audio Focus Request not granted",
+                                    CarToast.LENGTH_LONG).show();
+                            return;
+                        }
+                        recordAudio(record);
+                        playBackAudio();
+                    } catch (Exception e) {
+                        Log.e(LogTags.TAG, "Voice Interaction Error: ", e);
+                        throw new RuntimeException(e);
+                    } finally {
+                        // Abandon the FocusRequest so that user's media can be resumed
+                        mCarContext.getSystemService(AudioManager.class).abandonAudioFocusRequest(
+                                audioFocusRequest);
+                    }
+                },
+                "AudioRecorder Thread");
+        return recordingThread;
+    }
+
+    @RequiresPermission(RECORD_AUDIO)
+    private void playBackAudio() {
+
+        InputStream inputStream;
+        try {
+            inputStream = mCarContext.openFileInput(FILE_NAME);
+        } catch (FileNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+
+        AudioTrack audioTrack = new AudioTrack.Builder()
+                .setAudioAttributes(new AudioAttributes.Builder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setContentType(CONTENT_TYPE_MUSIC)
+                        .build())
+                .setAudioFormat(new AudioFormat.Builder()
+                        .setEncoding(ENCODING_DEFAULT)
+                        .setSampleRate(AUDIO_CONTENT_SAMPLING_RATE)
+                        .setChannelMask(CHANNEL_OUT_MONO)
+                        .build())
+                .setBufferSizeInBytes(AUDIO_CONTENT_BUFFER_SIZE)
+                .build();
+        audioTrack.play();
+        try {
+            byte[] audioData = new byte[AUDIO_CONTENT_BUFFER_SIZE];
+            while (inputStream.available() > 0) {
+                int readByteLength = inputStream.read(audioData, 0, audioData.length);
+
+                if (readByteLength < 0) {
+                    // End of file
+                    break;
+                }
+                audioTrack.write(audioData, 0, readByteLength);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        audioTrack.stop();
+
+    }
+
+    @RequiresApi(api = VERSION_CODES.O)
+    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
+    @RequiresPermission(RECORD_AUDIO)
+    private void recordAudio(CarAudioRecord record) {
+
+        record.startRecording();
+
+        List<Byte> bytes = new ArrayList<>();
+        boolean isRecording = true;
+        while (isRecording) {
+            // gets the voice output from microphone to byte format
+            byte[] bData = new byte[AUDIO_CONTENT_BUFFER_SIZE];
+            int len = record.read(bData, 0, AUDIO_CONTENT_BUFFER_SIZE);
+
+            if (len > 0) {
+                for (int i = 0; i < len; i++) {
+                    bytes.add(bData[i]);
+                }
+            } else {
+                isRecording = false;
+            }
+        }
+
+        try {
+            OutputStream outputStream = mCarContext.openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
+            addHeader(outputStream, bytes.size());
+            for (Byte b : bytes) {
+                outputStream.write(b);
+            }
+
+            outputStream.flush();
+            outputStream.close();
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+        record.stopRecording();
+    }
+
+    private static void addHeader(OutputStream outputStream, int totalAudioLen) throws IOException {
+        int totalDataLen = totalAudioLen + 36;
+        byte[] header = new byte[44];
+        int dataElementSize = 8;
+        long longSampleRate = AUDIO_CONTENT_SAMPLING_RATE;
+
+        // See http://soundfile.sapp.org/doc/WaveFormat/
+        header[0] = 'R';  // RIFF/WAVE header
+        header[1] = 'I';
+        header[2] = 'F';
+        header[3] = 'F';
+        header[4] = (byte) (totalAudioLen & 0xff);
+        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
+        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
+        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
+        header[8] = 'W';
+        header[9] = 'A';
+        header[10] = 'V';
+        header[11] = 'E';
+        header[12] = 'f';  // 'fmt ' chunk
+        header[13] = 'm';
+        header[14] = 't';
+        header[15] = ' ';
+        header[16] = 16;  // 4 bytes: size of 'fmt ' chunk
+        header[17] = 0;
+        header[18] = 0;
+        header[19] = 0;
+        header[20] = 1;  // format = 1 PCM
+        header[21] = 0;
+        header[22] = 1; // Num channels (mono)
+        header[23] = 0;
+        header[24] = (byte) (longSampleRate & 0xff); // sample rate
+        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
+        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
+        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
+        header[28] = (byte) (longSampleRate & 0xff); // byte rate
+        header[29] = (byte) ((longSampleRate >> 8) & 0xff);
+        header[30] = (byte) ((longSampleRate >> 16) & 0xff);
+        header[31] = (byte) ((longSampleRate >> 24) & 0xff);
+        header[32] = 1;  // block align
+        header[33] = 0;
+        header[34] = (byte) (dataElementSize & 0xff);  // bits per sample
+        header[35] = (byte) ((dataElementSize >> 8) & 0xff);
+        header[36] = 'd';
+        header[37] = 'a';
+        header[38] = 't';
+        header[39] = 'a';
+        header[40] = (byte) (totalAudioLen & 0xff);
+        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
+        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
+        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
+
+        outputStream.write(header, 0, 44);
+    }
+
+    // Returns whether or not the user has granted audio permissions
+    private boolean checkAudioPermission() {
+        if (mCarContext.checkSelfPermission(RECORD_AUDIO)
+                != PackageManager.PERMISSION_GRANTED) {
+            CarToast.makeText(mCarContext, "Grant mic permission on device",
+                    CarToast.LENGTH_LONG).show();
+            List<String> permissions = Collections.singletonList(RECORD_AUDIO);
+            mCarContext.requestPermissions(permissions, (grantedPermissions,
+                    rejectedPermissions) -> {
+                if (grantedPermissions.contains(RECORD_AUDIO)) {
+                    voiceInteractionDemo();
+                }
+            });
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
new file mode 100644
index 0000000..8bd55d5
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
@@ -0,0 +1,178 @@
+/*
+ * 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.sample.showcase.common.screens;
+
+import static androidx.car.app.model.Action.BACK;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Item;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.OnClickListener;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.car.app.sample.showcase.common.audio.VoiceInteraction;
+import androidx.car.app.sample.showcase.common.screens.userinteractions.RequestPermissionMenuDemoScreen;
+import androidx.core.graphics.drawable.IconCompat;
+
+/** A screen demonstrating User Interactions */
+public final class UserInteractionsDemoScreen extends Screen {
+    private static final int MAX_STEPS_ALLOWED = 4;
+
+    private final int mStep;
+    private boolean mIsBackOperation;
+
+    public UserInteractionsDemoScreen(int step, @NonNull CarContext carContext) {
+        super(carContext);
+        this.mStep = step;
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+
+        // Last step must either be a PaneTemplate, MessageTemplate or NavigationTemplate.
+        if (mStep == MAX_STEPS_ALLOWED) {
+            return templateForTaskLimitReached();
+        }
+
+        ItemList.Builder builder = new ItemList.Builder();
+
+        builder.addItem(buildRowForVoiceInteractionDemo());
+        builder.addItem(buildRowForRequestPermissionsDemo());
+        builder.addItem(buildRowForTaskRestrictionDemo());
+
+        if (mIsBackOperation) {
+            builder.addItem(buildRowForAdditionalData());
+        }
+
+        return new ListTemplate.Builder()
+                .setSingleList(builder.build())
+                .setTitle(getCarContext().getString(R.string.user_interactions_demo_title))
+                .setHeaderAction(BACK)
+                .setActionStrip(
+                        new ActionStrip.Builder()
+                                .addAction(
+                                        new Action.Builder()
+                                                .setTitle(getCarContext().getString(
+                                                        R.string.home_caps_action_title))
+                                                .setOnClickListener(
+                                                        () -> getScreenManager().popToRoot())
+                                                .build())
+                                .build())
+                .build();
+
+    }
+
+    /**
+     * Returns the row for VoiceInteraction Demo
+     */
+    private Item buildRowForVoiceInteractionDemo() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(R.string.voice_access_demo_title))
+                .setImage(new CarIcon.Builder(
+                        IconCompat.createWithResource(
+                                getCarContext(),
+                                R.drawable.ic_mic))
+                        .build(), Row.IMAGE_TYPE_ICON)
+                .setOnClickListener(new VoiceInteraction(getCarContext())::voiceInteractionDemo)
+                .build();
+    }
+
+    /**
+     * Returns the row for TaskRestriction Demo
+     */
+    private Item buildRowForTaskRestrictionDemo() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(R.string.task_step_of_title, mStep,
+                        MAX_STEPS_ALLOWED))
+                .addText(getCarContext().getString(R.string.task_step_of_text))
+                .setImage(new CarIcon.Builder(
+                        IconCompat.createWithResource(
+                                getCarContext(), R.drawable.baseline_task_24))
+                        .build(), Row.IMAGE_TYPE_ICON)
+                .setOnClickListener(
+                        () ->
+                                getScreenManager()
+                                        .pushForResult(
+                                                new UserInteractionsDemoScreen(
+                                                        mStep + 1, getCarContext()),
+                                                result -> mIsBackOperation = true))
+                .build();
+    }
+
+    /**
+     * Returns the row for RequestPermissions Demo
+     */
+    private Item buildRowForRequestPermissionsDemo() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(
+                        R.string.request_permission_menu_demo_title))
+                .setImage(new CarIcon.Builder(
+                        IconCompat.createWithResource(
+                                getCarContext(),
+                                R.drawable.baseline_question_mark_24))
+                        .build(), Row.IMAGE_TYPE_ICON)
+                .setOnClickListener(() -> getScreenManager().push(
+                        new RequestPermissionMenuDemoScreen(getCarContext())))
+                .build();
+    }
+
+    /**
+     * Returns the row for AdditionalData
+     */
+    private Item buildRowForAdditionalData() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(R.string.additional_data_title))
+                .addText(getCarContext().getString(R.string.additional_data_text))
+                .build();
+    }
+
+    /**
+     * Returns the MessageTemplate with "Task Limit Reached" message after user completes 4 task
+     * steps
+     */
+    private MessageTemplate templateForTaskLimitReached() {
+        OnClickListener onClickListener = () ->
+                getScreenManager()
+                        .pushForResult(
+                                new UserInteractionsDemoScreen(
+                                        mStep + 1,
+                                        getCarContext()),
+                                result ->
+                                        mIsBackOperation = true);
+
+        return new MessageTemplate.Builder(
+                getCarContext().getString(R.string.task_limit_reached_msg))
+                .setHeaderAction(BACK)
+                .addAction(
+                        new Action.Builder()
+                                .setTitle(getCarContext().getString(
+                                        R.string.try_anyway_action_title))
+                                .setOnClickListener(onClickListener)
+                                .build())
+                .build();
+    }
+
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/PreSeedPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/PreSeedPermissionScreen.java
new file mode 100644
index 0000000..f82949d
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/PreSeedPermissionScreen.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.sample.showcase.common.screens.userinteractions;
+
+import static androidx.car.app.model.Action.BACK;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.car.app.sample.showcase.common.ShowcaseService;
+
+/** A screen that demonstrates exiting the app and pre-seeding it with a request for permissions */
+public class PreSeedPermissionScreen extends Screen {
+    public PreSeedPermissionScreen(@NonNull CarContext carContext) {
+        super(carContext);
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+        return new MessageTemplate.Builder(getCarContext().getString(R.string.finish_app_msg))
+                .setTitle(getCarContext().getString(R.string.preseed_permission_app_title))
+                .setHeaderAction(BACK)
+                .addAction(
+                        new Action.Builder()
+                                .setOnClickListener(
+                                        () -> {
+                                            getCarContext()
+                                                    .getSharedPreferences(
+                                                            ShowcaseService.SHARED_PREF_KEY,
+                                                            Context.MODE_PRIVATE)
+                                                    .edit()
+                                                    .putBoolean(
+                                                            ShowcaseService.PRE_SEED_KEY, true)
+                                                    .apply();
+                                            getCarContext().finishCarApp();
+                                        })
+                                .setTitle(getCarContext().getString(R.string.exit_action_title))
+                                .build())
+                .build();
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionMenuDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionMenuDemoScreen.java
new file mode 100644
index 0000000..875d1df
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionMenuDemoScreen.java
@@ -0,0 +1,67 @@
+/*
+ * 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.sample.showcase.common.screens.userinteractions;
+
+import static androidx.car.app.model.Action.BACK;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.lifecycle.DefaultLifecycleObserver;
+
+/** Screen to list different permission demos */
+public final class RequestPermissionMenuDemoScreen extends Screen
+        implements DefaultLifecycleObserver {
+
+    public RequestPermissionMenuDemoScreen(@NonNull CarContext carContext) {
+        super(carContext);
+        getLifecycle().addObserver(this);
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+        ItemList.Builder listBuilder = new ItemList.Builder();
+
+        listBuilder.addItem(
+                new Row.Builder()
+                        .setTitle(getCarContext().getString(R.string.request_permissions_title))
+                        .setOnClickListener(() ->
+                                getScreenManager().push(
+                                        new RequestPermissionScreen(getCarContext())))
+                        .setBrowsable(true)
+                        .build());
+        listBuilder.addItem(
+                new Row.Builder()
+                        .setTitle(getCarContext().getString(R.string.preseed_permission_demo_title))
+                        .setOnClickListener(() ->
+                                getScreenManager().push(
+                                        new PreSeedPermissionScreen(getCarContext())))
+                        .setBrowsable(true)
+                        .build());
+        return new ListTemplate.Builder()
+                .setSingleList(listBuilder.build())
+                .setTitle(getCarContext().getString(R.string.request_permission_menu_demo_title))
+                .setHeaderAction(BACK)
+                .build();
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java
new file mode 100644
index 0000000..91f6583
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java
@@ -0,0 +1,199 @@
+/*
+ * 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.sample.showcase.common.screens.userinteractions;
+
+import static android.content.pm.PackageManager.FEATURE_AUTOMOTIVE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.CarContext;
+import androidx.car.app.CarToast;
+import androidx.car.app.Screen;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.OnClickListener;
+import androidx.car.app.model.ParkedOnlyOnClickListener;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.core.location.LocationManagerCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A screen to show a request for a runtime permission from the user.
+ *
+ * <p>Scans through the possible dangerous permissions and shows which ones have not been
+ * granted in the message. Clicking on the action button will launch the permission request on
+ * the phone.
+ *
+ * <p>If all permissions are granted, corresponding message is displayed with a refresh button which
+ * will scan again when clicked.
+ */
+public class RequestPermissionScreen extends Screen {
+    /**
+     * This field can and should be removed once b/192386096 and/or b/192385602 have been resolved.
+     * Currently it is not possible to know the level of the screen stack and determine the
+     * header action according to that. A boolean flag is added to determine that temporarily.
+     */
+    private final boolean mPreSeedMode;
+    private final String mCarAppPermissionsPrefix = "androidx.car.app";
+
+    /**
+     * Action which invalidates the template.
+     *
+     * <p>This can give the user a chance to revoke the permissions and then refresh will pickup
+     * the permissions that need to be granted.
+     */
+    private final Action mRefreshAction = new Action.Builder()
+            .setTitle(getCarContext().getString(R.string.refresh_action_title))
+            .setBackgroundColor(CarColor.BLUE)
+            .setOnClickListener(this::invalidate)
+            .build();
+
+    public RequestPermissionScreen(@NonNull CarContext carContext) {
+        this(carContext, false);
+    }
+
+    public RequestPermissionScreen(@NonNull CarContext carContext, boolean preSeedMode) {
+        super(carContext);
+        this.mPreSeedMode = preSeedMode;
+    }
+
+    @NonNull
+    @Override
+    @SuppressWarnings("deprecation")
+    public Template onGetTemplate() {
+        Action headerAction = mPreSeedMode ? Action.APP_ICON : Action.BACK;
+        List<String> permissions = new ArrayList<>();
+        String[] declaredPermissions;
+        try {
+            PackageInfo info =
+                    getCarContext().getPackageManager().getPackageInfo(
+                            getCarContext().getPackageName(),
+                            PackageManager.GET_PERMISSIONS);
+            declaredPermissions = info.requestedPermissions;
+        } catch (PackageManager.NameNotFoundException e) {
+            return new MessageTemplate.Builder(
+                    getCarContext().getString(R.string.package_not_found_error_msg))
+                    .setHeaderAction(headerAction)
+                    .addAction(mRefreshAction)
+                    .build();
+        }
+
+        if (declaredPermissions != null) {
+            for (String declaredPermission : declaredPermissions) {
+                // Don't include permissions against the car app host as they are all normal but
+                // show up as ungranted by the system.
+                if (declaredPermission.startsWith(mCarAppPermissionsPrefix)) {
+                    continue;
+                }
+                try {
+                    CarAppPermission.checkHasPermission(getCarContext(), declaredPermission);
+                } catch (SecurityException e) {
+                    permissions.add(declaredPermission);
+                }
+            }
+        }
+        if (permissions.isEmpty()) {
+            return new MessageTemplate.Builder(
+                    getCarContext().getString(R.string.permissions_granted_msg))
+                    .setHeaderAction(headerAction)
+                    .addAction(new Action.Builder()
+                            .setTitle(getCarContext().getString(R.string.close_action_title))
+                            .setOnClickListener(this::finish)
+                            .build())
+                    .build();
+        }
+
+        StringBuilder message = new StringBuilder()
+                .append(getCarContext().getString(R.string.needs_access_msg_prefix));
+        for (String permission : permissions) {
+            message.append(permission);
+            message.append("\n");
+        }
+
+        OnClickListener listener = ParkedOnlyOnClickListener.create(() -> {
+            getCarContext().requestPermissions(
+                    permissions,
+                    (approved, rejected) -> {
+                        CarToast.makeText(
+                                getCarContext(),
+                                String.format("Approved: %s Rejected: %s", approved, rejected),
+                                CarToast.LENGTH_LONG).show();
+                    });
+            if (!getCarContext().getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)) {
+                CarToast.makeText(getCarContext(),
+                        getCarContext().getString(R.string.phone_screen_permission_msg),
+                        CarToast.LENGTH_LONG).show();
+            }
+        });
+
+        Action action = new Action.Builder()
+                .setTitle(getCarContext().getString(R.string.grant_access_action_title))
+                .setBackgroundColor(CarColor.BLUE)
+                .setOnClickListener(listener)
+                .build();
+
+
+        Action action2 = null;
+        LocationManager locationManager =
+                (LocationManager) getCarContext().getSystemService(Context.LOCATION_SERVICE);
+        if (!LocationManagerCompat.isLocationEnabled(locationManager)) {
+            message.append(
+                    getCarContext().getString(R.string.enable_location_permission_on_device_msg));
+            message.append("\n");
+            action2 = new Action.Builder()
+                    .setTitle(getCarContext().getString(R.string.enable_location_action_title))
+                    .setBackgroundColor(CarColor.BLUE)
+                    .setOnClickListener(ParkedOnlyOnClickListener.create(() -> {
+                        getCarContext().startActivity(
+                                new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(
+                                        Intent.FLAG_ACTIVITY_NEW_TASK));
+                        if (!getCarContext().getPackageManager().hasSystemFeature(
+                                FEATURE_AUTOMOTIVE)) {
+                            CarToast.makeText(getCarContext(),
+                                    getCarContext().getString(
+                                            R.string.enable_location_permission_on_phone_msg),
+                                    CarToast.LENGTH_LONG).show();
+                        }
+                    }))
+                    .build();
+        }
+
+
+        LongMessageTemplate.Builder builder = new LongMessageTemplate.Builder(message)
+                .setTitle(getCarContext().getString(R.string.required_permissions_title))
+                .addAction(action)
+                .setHeaderAction(headerAction);
+
+        if (action2 != null) {
+            builder.addAction(action2);
+        }
+
+        return builder.build();
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/res/drawable-hdpi/ic_mic.xml b/car/app/app-samples/showcase/common/src/main/res/drawable-hdpi/ic_mic.xml
new file mode 100644
index 0000000..57d2b5191
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/res/drawable-hdpi/ic_mic.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="40dp"
+    android:height="40dp"
+    android:viewportWidth="40"
+    android:viewportHeight="40">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,22.708Q18.125,22.708 16.833,21.354Q15.542,20 15.542,18.083V7.792Q15.542,5.917 16.833,4.625Q18.125,3.333 20,3.333Q21.875,3.333 23.167,4.625Q24.458,5.917 24.458,7.792V18.083Q24.458,20 23.167,21.354Q21.875,22.708 20,22.708ZM20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042ZM18.625,35V29.5Q14.208,29 11.271,25.75Q8.333,22.5 8.333,18.083H11.125Q11.125,21.75 13.729,24.292Q16.333,26.833 20,26.833Q23.667,26.833 26.271,24.292Q28.875,21.75 28.875,18.083H31.667Q31.667,22.5 28.729,25.75Q25.792,29 21.375,29.5V35ZM20,19.917Q20.75,19.917 21.229,19.375Q21.708,18.833 21.708,18.083V7.792Q21.708,7.083 21.208,6.604Q20.708,6.125 20,6.125Q19.292,6.125 18.792,6.604Q18.292,7.083 18.292,7.792V18.083Q18.292,18.833 18.771,19.375Q19.25,19.917 20,19.917Z"/>
+</vector>
\ No newline at end of file
diff --git a/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_question_mark_24.xml b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_question_mark_24.xml
new file mode 100644
index 0000000..fbfe98d
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_question_mark_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z"/>
+</vector>
diff --git a/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_task_24.xml b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_task_24.xml
new file mode 100644
index 0000000..fce940d
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_task_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M14,2H6C4.9,2 4.01,2.9 4.01,4L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8L14,2zM10.94,18L7.4,14.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L10.94,18zM13,9V3.5L18.5,9H13z"/>
+</vector>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
index c4d6f70..45f07ef 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
@@ -130,6 +130,8 @@
   <string name="finish_app_msg">This will finish the app, and when you return it will pre-seed a permission screen</string>
   <string name="finish_app_title">Finish App Demo</string>
   <string name="finish_app_demo_title">Pre-seed the Permission Screen on next run Demo</string>
+  <string name="preseed_permission_app_title">Pre-seed permission App Demo</string>
+  <string name="preseed_permission_demo_title">Pre-seed the Permission Screen on next run Demo</string>
 
   <!-- LoadingDemoScreen -->
   <string name="loading_demo_title">Loading Demo</string>
@@ -387,6 +389,21 @@
   <string name="cal_api_level_prefix" translatable="false">CAL API Level: %d</string>
   <string name="showcase_demos_title">Showcase Demos</string>
 
+  <!-- User Interactions Screen -->
+  <string name="voice_access_demo_title">Voice Access Demo Screen</string>
+  <string name="user_interactions_demo_title">User Interactions</string>
+  <string name="request_permission_menu_demo_title">Request Permissions Demos</string>
+
+  <!-- Manifest file permissions -->
+  <string name="perm_group">Permission Group</string>
+  <string name="perm_group_description">Permission Group for Showcase App</string>
+  <string name="perm_fine_location">Access to Fine Location</string>
+  <string name="perm_fine_location_desc">Permission for Access to Fine Location</string>
+  <string name="perm_coarse_location">Access to Coarse Location</string>
+  <string name="perm_coarse_location_desc">Permission for Access to Coarse Location</string>
+  <string name="perm_record_audio">Access to Record Audio</string>
+  <string name="perm_record_audio_desc">Permission for Access to Record Audio</string>
+
   <!-- Location Strings -->
   <string name="location_1_title" translatable="false">Google Kirkland</string>
   <string name="location_1_address" translatable="false">747 6th St South, Kirkland, WA 98033</string>
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index e8c82f9..f21850c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -1287,18 +1287,6 @@
         }
     }
 
-    /**
-     * Comparator allowing to sort nodes by zIndex and placement order.
-     */
-    val ZComparator = Comparator<LayoutNode> { node1, node2 ->
-        if (node1.zIndex == node2.zIndex) {
-            // if zIndex is the same we use the placement order
-            node1.placeOrder.compareTo(node2.placeOrder)
-        } else {
-            node1.zIndex.compareTo(node2.zIndex)
-        }
-    }
-
     override val parentInfo: LayoutInfo?
         get() = parent
 
@@ -1339,6 +1327,18 @@
             override val minimumTouchTargetSize: DpSize
                 get() = DpSize.Zero
         }
+
+        /**
+         * Comparator allowing to sort nodes by zIndex and placement order.
+         */
+        internal val ZComparator = Comparator<LayoutNode> { node1, node2 ->
+            if (node1.zIndex == node2.zIndex) {
+                // if zIndex is the same we use the placement order
+                node1.placeOrder.compareTo(node2.placeOrder)
+            } else {
+                node1.zIndex.compareTo(node2.zIndex)
+            }
+        }
     }
 
     /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt
index a104d042..9fd19fe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt
@@ -156,8 +156,13 @@
             return -1
         }
 
+        val zDifference = LayoutNode.ZComparator.compare(node, other.node)
+        if (zDifference != 0) {
+            return -zDifference
+        }
+
         // Break tie somehow
-        return -1
+        return node.semanticsId - other.node.semanticsId
     }
 }
 
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 35b87e2..15519cb 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -2468,7 +2468,7 @@
 }
 
 @OptIn(InternalCoreApi::class)
-private class MockOwner(
+internal class MockOwner(
     val position: IntOffset = IntOffset.Zero,
     override val root: LayoutNode = LayoutNode()
 ) : Owner {
@@ -2691,7 +2691,7 @@
     hitPointerInputFilters.addAll(hitTestResult)
 }
 
-private fun LayoutNode(
+internal fun LayoutNode(
     x: Int,
     y: Int,
     x2: Int,
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/semantics/SemanticsSortTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/semantics/SemanticsSortTest.kt
new file mode 100644
index 0000000..47ac9fe4
--- /dev/null
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/semantics/SemanticsSortTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.semantics
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.MockOwner
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.zIndex
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalComposeUiApi::class)
+@RunWith(JUnit4::class)
+class SemanticsSortTest {
+
+    @Test // regression test for b/207477257
+    fun compareDoesNotViolateComparatorContract() {
+        val root = LayoutNode(0, 0, 720, 1080)
+        repeat(32) { index ->
+            val child = if (index % 2 == 0) {
+                LayoutNode(0, 0, 0, 0)
+            } else {
+                val offset = if (index == 1 || index == 31) 100 else 0
+                LayoutNode(0, 0 - offset, 720, 30 - offset).also {
+                    it.insertAt(0, LayoutNode(0, 0, 100, 100, Modifier.semantics { }))
+                }
+            }
+            root.insertAt(index, child)
+        }
+
+        root.attach(MockOwner())
+        root.findOneLayerOfSemanticsWrappersSortedByBounds()
+
+        // expect - no crash happened
+    }
+
+    @Test
+    fun sortedByZOrderIfHasSameBounds() {
+        val root = LayoutNode(0, 0, 100, 100)
+        repeat(5) { index ->
+            root.insertAt(
+                index,
+                LayoutNode(
+                    0, 0, 100, 100,
+                    Modifier
+                        .semantics { set(LayoutNodeIndex, index) }
+                        .zIndex((index * 3 % 5).toFloat())
+                )
+            )
+        }
+        root.attach(MockOwner())
+        root.remeasure()
+        root.replace()
+        val result = root.findOneLayerOfSemanticsWrappersSortedByBounds()
+
+        assertThat(result[0].layoutNodeIndex()).isEqualTo(3)
+        assertThat(result[1].layoutNodeIndex()).isEqualTo(1)
+        assertThat(result[2].layoutNodeIndex()).isEqualTo(4)
+        assertThat(result[3].layoutNodeIndex()).isEqualTo(2)
+        assertThat(result[4].layoutNodeIndex()).isEqualTo(0)
+    }
+
+    private val LayoutNodeIndex = SemanticsPropertyKey<Int>("LayoutNodeIndex")
+
+    private fun SemanticsModifierNode.layoutNodeIndex(): Int {
+        return semanticsConfiguration[LayoutNodeIndex]
+    }
+}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index e943f94..e686bd5 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -456,8 +456,9 @@
 Run the build with '\-\-info' for more details\.
 # > Task :health:health-connect-client:testDebugUnitTest
 WARNING: An illegal reflective access operation has occurred
-WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers\$[0-9]+ \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/[0-9]+\.[0-9]+\.[0-9]+/shadowapi\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to field java\.io\.FileDescriptor\.fd
-WARNING: Please consider reporting this to the maintainers of org\.robolectric\.util\.ReflectionHelpers\$[0-9]+
+WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers\$[0-9]+ \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/.*/shadowapi\-.*\.jar\) to field java\.io\.FileDescriptor\.fd
+WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/.*/shadowapi\-.*\.jar\) to method java\.lang\.Class\.getDeclaredFields0\(boolean\)
+WARNING: Please consider reporting this to the maintainers of org\.robolectric\.util\.ReflectionHelpers.*
 WARNING: Use \-\-illegal\-access=warn to enable warnings of further illegal reflective access operations
 WARNING: All illegal access operations will be denied in a future release
 # > Task :room:room-compiler-processing-testing:test
diff --git a/development/collection-consumer/.gitignore b/development/collection-consumer/.gitignore
new file mode 100644
index 0000000..e0d53d8
--- /dev/null
+++ b/development/collection-consumer/.gitignore
@@ -0,0 +1,3 @@
+.gradle
+.idea
+build
diff --git a/development/collection-consumer/README.md b/development/collection-consumer/README.md
new file mode 100644
index 0000000..75586e0
--- /dev/null
+++ b/development/collection-consumer/README.md
@@ -0,0 +1,33 @@
+## Testing collection KMP release
+
+This project helps to validate a newly-staged version of the KMP build of androidx.collection.  Here's how!
+
+### Stage the release
+- Create a new global release in production or autopush JetPad (TODO: more info about how)
+- Schedule a library group release attached to the global release for androidx.collect (TODO: ditto)
+- You cannot stage unless the _global_ ADMRS config allowlists the KMP targets.  If necessary, you may need to
+  create, have reviewed, and submit a CL like [this one](https://critique.corp.google.com/cl/474557118).
+  - It can take around 30 minutes between submission and updating the loaded allowlist in ADMRS, so be patient.
+- Then:
+  - Start with [prod](go/jetpad) or [autopush](go/jetpad-autopush)
+  - `Release Dates` > `Browse Release Date`
+  - Click `Release Information` for the global release you created above
+  - `Stage to ADMRS`
+  - `Compose BOM?`  No (not relevant to our test)
+  - Answer "Yes" to "really staging"
+- At this point, you will either see an error in the first ~20 seconds if something is wrong with our stuff, or
+  ADMRS will go quietly do things for a few minutes, resulting in an email to `mdb.jetpad-admins@google.com`.
+
+### Test the staged release
+- Check out this repo (if you haven't): `git clone sso://user/saff`
+- The email has [instructions](go/adt-redir) for how to set up a proxy server for the staged maven repo.
+- Once that's done, if necessary, edit the androidx.collection version in build.gradle.kts
+- To test JVM:
+  - `./gradlew installDist`
+  - `./build/install/collection-consumer/bin/collection-consumer`
+- To test native:
+  - `./gradlew nativeBinaries`
+  - `build/bin/native/releaseExecutable/collection-consumer.kexe`
+- You can look back at the stdout for the adt-redir proxy server to assure yourself that the androidx dependencies
+  are being loaded through the proxy.
+- Profit??!
\ No newline at end of file
diff --git a/development/collection-consumer/build.gradle.kts b/development/collection-consumer/build.gradle.kts
new file mode 100644
index 0000000..beef228
--- /dev/null
+++ b/development/collection-consumer/build.gradle.kts
@@ -0,0 +1,63 @@
+plugins {
+    kotlin("multiplatform") version "1.7.10"
+    application
+}
+
+group = "net.saff"
+version = "1.0-SNAPSHOT"
+
+repositories {
+    maven {
+        url = uri("http://localhost:1480")
+        isAllowInsecureProtocol = true
+    }
+    mavenCentral()
+    google()
+}
+
+kotlin {
+    val hostOs = System.getProperty("os.name")
+    val isMingwX64 = hostOs.startsWith("Windows")
+    val nativeTarget = when {
+        hostOs == "Mac OS X" -> macosX64("native")
+        hostOs == "Linux" -> linuxX64("native")
+        isMingwX64 -> mingwX64("native")
+        else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
+    }
+
+    nativeTarget.apply {
+        binaries {
+            executable {
+                entryPoint = "main"
+            }
+        }
+    }
+    jvm {
+        compilations.all {
+            kotlinOptions.jvmTarget = "1.8"
+        }
+        withJava()
+        testRuns["test"].executionTask.configure {
+            useJUnitPlatform()
+        }
+    }
+    sourceSets {
+        val nativeMain by getting
+        val commonMain by getting {
+            dependencies {
+                implementation("androidx.collection:collection:1.3.0-alpha03")
+            }
+        }
+        val nativeTest by getting
+        val jvmMain by getting
+        val jvmTest by getting {
+            dependencies {
+                implementation(kotlin("test"))
+            }
+        }
+    }
+}
+
+application {
+    mainClass.set("MainKt")
+}
diff --git a/development/collection-consumer/gradle.properties b/development/collection-consumer/gradle.properties
new file mode 100644
index 0000000..415b6be
--- /dev/null
+++ b/development/collection-consumer/gradle.properties
@@ -0,0 +1,5 @@
+kotlin.code.style=official
+kotlin.mpp.enableGranularSourceSetsMetadata=true
+kotlin.native.enableDependencyPropagation=false
+kotlin.js.generate.executable.default=false
+kotlin.native.binary.memoryModel=experimental
\ No newline at end of file
diff --git a/development/collection-consumer/gradle/wrapper/gradle-wrapper.jar b/development/collection-consumer/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..41d9927
--- /dev/null
+++ b/development/collection-consumer/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/development/collection-consumer/gradle/wrapper/gradle-wrapper.properties b/development/collection-consumer/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..aa991fc
--- /dev/null
+++ b/development/collection-consumer/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/development/collection-consumer/gradlew b/development/collection-consumer/gradlew
new file mode 100755
index 0000000..1b6c787
--- /dev/null
+++ b/development/collection-consumer/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# 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
+#
+#      https://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.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/development/collection-consumer/gradlew.bat b/development/collection-consumer/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/development/collection-consumer/gradlew.bat
@@ -0,0 +1,89 @@
+@rem

+@rem Copyright 2015 the original author or authors.

+@rem

+@rem Licensed under the Apache License, Version 2.0 (the "License");

+@rem you may not use this file except in compliance with the License.

+@rem You may obtain a copy of the License at

+@rem

+@rem      https://www.apache.org/licenses/LICENSE-2.0

+@rem

+@rem Unless required by applicable law or agreed to in writing, software

+@rem distributed under the License is distributed on an "AS IS" BASIS,

+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+@rem See the License for the specific language governing permissions and

+@rem limitations under the License.

+@rem

+

+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Resolve any "." and ".." in APP_HOME to make it shorter.

+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto execute

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto execute

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/development/collection-consumer/settings.gradle.kts b/development/collection-consumer/settings.gradle.kts
new file mode 100644
index 0000000..8268b87
--- /dev/null
+++ b/development/collection-consumer/settings.gradle.kts
@@ -0,0 +1,3 @@
+
+rootProject.name = "collection-consumer"
+
diff --git a/development/collection-consumer/src/commonMain/kotlin/list.kt b/development/collection-consumer/src/commonMain/kotlin/list.kt
new file mode 100644
index 0000000..6be7048
--- /dev/null
+++ b/development/collection-consumer/src/commonMain/kotlin/list.kt
@@ -0,0 +1,21 @@
+import androidx.collection.LongSparseArray
+import androidx.collection.LruCache
+
+fun runCache(): String {
+    val cache = object : LruCache<Int, String>(2) {
+        override fun create(key: Int): String? {
+            return "x".repeat(key)
+        }
+    }
+    cache[1]
+    cache[2]
+    cache[3]
+    return cache.snapshot().toString()
+}
+
+fun runSparseArray(): String {
+    val array = LongSparseArray<String>()
+    array.put(0L, "zero")
+    array.put(1L, "one")
+    return array.toString()
+}
\ No newline at end of file
diff --git a/development/collection-consumer/src/jvmMain/kotlin/Main.kt b/development/collection-consumer/src/jvmMain/kotlin/Main.kt
new file mode 100644
index 0000000..64fe0c0
--- /dev/null
+++ b/development/collection-consumer/src/jvmMain/kotlin/Main.kt
@@ -0,0 +1,7 @@
+fun main() {
+    println("Hello, Kotlin/JVM!")
+    println("LongSparseArray:")
+    println(runSparseArray())
+    println("LruCache:")
+    println(runCache())
+}
\ No newline at end of file
diff --git a/development/collection-consumer/src/nativeMain/kotlin/Main.kt b/development/collection-consumer/src/nativeMain/kotlin/Main.kt
new file mode 100644
index 0000000..b5e96e8
--- /dev/null
+++ b/development/collection-consumer/src/nativeMain/kotlin/Main.kt
@@ -0,0 +1,7 @@
+fun main() {
+    println("Hello, Kotlin/Native!")
+    println("LongSparseArray:")
+    println(runSparseArray())
+    println("LruCache:")
+    println(runCache())
+}
\ No newline at end of file
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 59f5adc..7d488c5 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -49,12 +49,8 @@
 # Each directory's spelling must match the library's directory in
 # frameworks/support.
 readonly javaLibraryDirsThatDontUseDackka=(
-  "androidx/draganddrop"
-  "androidx/dynamicanimation"
 )
 readonly kotlinLibraryDirsThatDontUseDackka=(
-  "androidx/dynamicanimation"
-  "androidx/draganddrop"
 )
 
 # Change directory to this script's location and store the directory
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 89ce06f..496d310 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -196,7 +196,7 @@
 reactiveStreams = { module = "org.reactivestreams:reactive-streams", version = "1.0.0" }
 retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
 retrofitConverterWire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" }
-robolectric = { module = "org.robolectric:robolectric", version = "4.8.1" }
+robolectric = { module = "org.robolectric:robolectric", version = "4.9-alpha-1" }
 rxjava2 = { module = "io.reactivex.rxjava2:rxjava", version = "2.2.9" }
 rxjava3 = { module = "io.reactivex.rxjava3:rxjava", version = "3.0.0" }
 shadow = { module = "gradle.plugin.com.github.johnrengelman:shadow", version = "7.1.1" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 3d0c168..46c9570 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -2857,6 +2857,64 @@
 -----END PGP PUBLIC KEY BLOCK-----
 
 
+pub    3F00DB67AE236E2E
+uid    Pete Bentley <prb@google.com>
+
+sub    6B7EF7B18190F4A9
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBF2KLsIBEADgVw/j0Loslv+pBDEfYemeObeKCWBhEdAiGznT23XFb4eOa4oL
+Yk8FTL5SYV+Ylm5Pv4zUGV1JUggzb4mS5+/k0kl2OHzZpJTLz45E9Qe4KI5vk6jT
+zBVJGdB6X1EXeQNozZZwuKHTDoFSTqT+oYpjUB3kRoP04Cm1vL9NdLvYwabv0BfI
+/e63QyJ60B8tTxVzEiN2u4VxSwrW/Vku3LT/wky/jgdwDUrwR7Elf189BPUlchtG
+fLZJJoJwlBd7h/wo7ik+KpUkDrMhMUkPTcC+aferQiAc2S53H7Zeu2S49F34qDLm
+dp3d89ImVgzplpBiGBlryy571YU5dafo/fsVuiB0FINTqzSvs/RLTIFwubmSdXGj
+/UaNZYtRRFG8bkqal8VuDsUikuPMez7VF5/KLGRzL9uonEfFiV7c5uUEk4VDlVSK
+4v6cEw0yyRpxIwh5C9IvLKpplpJajBXLeMKoep8+VP8+VpdrFd/hHW/MOl2uYVpM
+mHhyXoSg+Gf6My7PQw65dC2VrdWoYpGeyVK2BD1wBcw8/HJDJTJT7SQDLJ11oDSf
+JzuwtfVT8sMfl/m1vaJJvkW3RPqkgqiyhr+PwdXALHQLV48tlUVu3uEG6xK+hT24
+8pPqC/vL/IECzd8BQF310Cne2dU3V8ykJQfGg5Vu7LExE8jMfna5Ipz/GQARAQAB
+tB1QZXRlIEJlbnRsZXkgPHByYkBnb29nbGUuY29tPokCTgQTAQoAOBYhBBWXqyMb
+et1+FLHZxD8A22euI24uBQJdii7CAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
+AAoJED8A22euI24uy/0P/jIuaB5KnoBIbZvH8eO7yg5hog4nHP2kT7CF6iYUL5Rt
+v/NEWVbSgcWHdwucoy4aENEA2cSTMfG5vzlkbDfg8BezJyRA0qax1Lc9Q4MgDtGS
+1GDFY46xD6X2Y/vgAYd2uu9EqEugked7VMjbmeoQOz9tcIJwK3Nc5tuHqH8YwGDa
+bXyuWCyNnH1OJwU/0lymsEplgcyo29N2cZfGQUC980maRERzIO90PKWZ4kMoFiIR
+pjLjbN2ZqTKi6JMgGMwXrKxc1BLi9LA6rJAFJHT/FVX8z4D6cyIOsahq+PhL5kN3
+wznzyT3VeapeC+ybSr9+MuYeSPdMEx9sW7j7bdbvr92bkpBfH2IC2SUkQeY5oy4+
+DCIQYV2PxqKhr+Oy14Cg3EzQT+u/JwpQnvIBipn2ISXZCGnMIwYLw3viiK/Jvr+F
+V+fmGvFYMxja5M6/zPpg1fJkNhhCGrrAWydxNfb+YERSSlQis4c4sLp0L6QWWY35
+gAwMpvMGYrYkaOnlI38ZWcvuzKUYlaX2Us01eMAJ9l2zN0bIQOn9Z77D4YcROLhO
+mWOn5yUDaziYPc2mXhoFCkmyBzxqJw0m5z5YRinf80gI4uLtPlLEBBYBxO/nE20j
+vcAVk8sH0CT/1uWj2M6K5NuD1sGfxHV0GXB6yQPPO0cCsVWGDa1nmKa21XouYHTE
+uQINBF2KLsIBEADRy0X4ZSnuFgg3pHmjPJraPsVpCmxDuuRcgpbP8DwS+t99us2w
+bjfzkUjT3glkKz4iVWay65B2uss088vOe9evcH5lssUctLjFYDuSlQOm3SOXwZgD
+4CsmCr82D4PwxuQlevxh/XVQXZ++CnF6f8jNDCtIwBO8+AysdYVV+BEPrRuLS1uG
+ySaE/Vchi7sUPVq79HOAOY55HxVWkzxRVKXdI2AtVol6drx9s8TL1F3wBLcWavyl
+WXNqp5x6zt3n0LbHgBMWKe04dUYKO3VwYddPgPo3n5zfy/D2X4IEf/spKc6RMs2i
+kbTIxRVw6kNGk+hgr9XO4zyEYwIbirfvwp9u8HgFAnR7CdQacVv0cNUEzP6/4hUZ
+5uqtL8QveazqOTWbe3j6W6mVPfR5jKThHWmJgihtmY59hGLTDKewcmaj8QF3Syx0
+b/bwtSU8HyKA3E4iKD7avfX5Ql61+kuIrbfCMMoBsxuYix1r0Q7G3pfHubXBbOM8
+i/AfULEypRMM0LoKZjJMbhaebwKhxlyAf5+9eZDIn2BHUkzg3+g5bNnQoE4n7UMh
+kt3kMR0IvDoyMKwT1dvDoJEa61+FU4Z66k8F18HfjcE/oxnvBOzBUf1KWWm+ZG6a
+XT0wdH9bME2htQKt9s6FWN9QV12nemHaro3ViiEiSB9BrN8jQfgqbhmWpwARAQAB
+iQI2BBgBCgAgFiEEFZerIxt63X4UsdnEPwDbZ64jbi4FAl2KLsICGwwACgkQPwDb
+Z64jbi52whAAgRI6Ag+wKbYh8Soi3Nye65z/E3KOUwCKrHZJnG0bheYHZ48y91d5
+aFeKVZGPdSgL/MQvWIV2Gje1mPLi9KtgNKqTNpWscSN8KsqVc3uOovBliCLRExno
+7jE+3A+42ms6T6yaig2oLXTbmI23Xj7m0C+nP+Q1t0RxSndq+0fzRQTWfybNOMd5
+5Q8d45Kasku5nvdPXSRjXOovJRKherARX2NMt5MImpPTF3SDg8UQ/bmM72VXsrDR
+Dl3iOAGgp6/ie758QfYaa0wYOxAskCWwXIQmPLbP3UFIQFbzgvzSfy8OKutLNe4+
+mr+DLRR/CeOPIFmOWImr0DerD3gq17OWQf0KqCVQl/fXJWJFmglswLqum1A6/Sjr
+Ove2hxaXmrM9GJg5sOv36ldYFwwZEMxxH29BIBMVwpPM5+xydx8l8c4UAwTnIUjP
+4wJtz71d+4DrCskswXkMSLiGJ623y9izHGled3/98vUPVMoM1pT1BML5arjpYOH1
+S+nlOqBXU3TZ0KGijUYh4GBS5MFpFiM8N7Ne8ctBWd4g0uHifv0+3/UDnd5va8da
+rmOUSu1D9cJPP5w6PfopRo9f1ltpPop5pwdyXoQDpyelwvA5XeNLuroOY+3j+xmu
+k4MTJ2V6vm2gqOJY4UhHt5Pw0MQQp7Uya0naw4mtdoQNp4gFtpWBfvo=
+=ZR2N
+-----END PGP PUBLIC KEY BLOCK-----
+
+
 pub    3F36885C24DF4B75
 sub    97859F2FE8EAEB26
 -----BEGIN PGP PUBLIC KEY BLOCK-----
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 3690337..08a256f 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -69,6 +69,7 @@
          <trusted-key id="12d16069219c90212a974d119ae296fd02e9f65b" group="org.apache.commons" name="commons-math3"/>
          <trusted-key id="147b691a19097624902f4ea9689cbe64f4bc997f" group="^org[.]mockito($|([.].*))" regex="true"/>
          <trusted-key id="151ba00a46886a5f95441a0f5d67bffcba1f9a39" group="com.google.gradle" name="osdetector-gradle-plugin"/>
+         <trusted-key id="1597ab231b7add7e14b1d9c43f00db67ae236e2e" group="org.conscrypt"/>
          <trusted-key id="160a7a9cf46221a56b06ad64461a804f2609fd89" group="com.github.shyiko.klob" name="klob"/>
          <trusted-key id="1861c322c56014b2" group="commons-lang"/>
          <trusted-key id="190d5a957ff22273e601f7a7c92c5fec70161c62" group="org.codehaus.mojo"/>
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index deab1f9..4496d11 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -379,7 +379,7 @@
     ctor public PagingSource.LoadResult.Invalid();
   }
 
-  public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
+  public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> implements java.lang.Iterable<Value> kotlin.jvm.internal.markers.KMappedMarker {
     ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey, optional @IntRange(from=androidx.paging.PagingSource.LoadResult.Page.COUNT_UNDEFINED.toLong()) int itemsBefore, optional @IntRange(from=androidx.paging.PagingSource.LoadResult.Page.COUNT_UNDEFINED.toLong()) int itemsAfter);
     ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey);
     method public java.util.List<Value> component1();
@@ -393,6 +393,7 @@
     method public int getItemsBefore();
     method public Key? getNextKey();
     method public Key? getPrevKey();
+    method public java.util.Iterator<Value> iterator();
     property public final java.util.List<Value> data;
     property public final int itemsAfter;
     property public final int itemsBefore;
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 548cd7e..8782062 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -383,7 +383,7 @@
     ctor public PagingSource.LoadResult.Invalid();
   }
 
-  public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
+  public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> implements java.lang.Iterable<Value> kotlin.jvm.internal.markers.KMappedMarker {
     ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey, optional @IntRange(from=androidx.paging.PagingSource.LoadResult.Page.COUNT_UNDEFINED.toLong()) int itemsBefore, optional @IntRange(from=androidx.paging.PagingSource.LoadResult.Page.COUNT_UNDEFINED.toLong()) int itemsAfter);
     ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey);
     method public java.util.List<Value> component1();
@@ -397,6 +397,7 @@
     method public int getItemsBefore();
     method public Key? getNextKey();
     method public Key? getPrevKey();
+    method public java.util.Iterator<Value> iterator();
     property public final java.util.List<Value> data;
     property public final int itemsAfter;
     property public final int itemsBefore;
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index deab1f9..4496d11 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -379,7 +379,7 @@
     ctor public PagingSource.LoadResult.Invalid();
   }
 
-  public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
+  public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> implements java.lang.Iterable<Value> kotlin.jvm.internal.markers.KMappedMarker {
     ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey, optional @IntRange(from=androidx.paging.PagingSource.LoadResult.Page.COUNT_UNDEFINED.toLong()) int itemsBefore, optional @IntRange(from=androidx.paging.PagingSource.LoadResult.Page.COUNT_UNDEFINED.toLong()) int itemsAfter);
     ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey);
     method public java.util.List<Value> component1();
@@ -393,6 +393,7 @@
     method public int getItemsBefore();
     method public Key? getNextKey();
     method public Key? getPrevKey();
+    method public java.util.Iterator<Value> iterator();
     property public final java.util.List<Value> data;
     property public final int itemsAfter;
     property public final int itemsBefore;
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PagingSource.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSource.kt
index 8feda0d..23ff581 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/PagingSource.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSource.kt
@@ -240,6 +240,8 @@
         /**
          * Success result object for [PagingSource.load].
          *
+         * As a convenience, iterating on this object will iterate through its loaded [data].
+         *
          * @sample androidx.paging.samples.pageKeyedPage
          * @sample androidx.paging.samples.pageIndexedPage
          */
@@ -267,7 +269,7 @@
              */
             @IntRange(from = COUNT_UNDEFINED.toLong())
             val itemsAfter: Int = COUNT_UNDEFINED
-        ) : LoadResult<Key, Value>() {
+        ) : LoadResult<Key, Value>(), Iterable<Value> {
 
             /**
              * Success result object for [PagingSource.load].
@@ -293,6 +295,11 @@
                     "itemsAfter cannot be negative"
                 }
             }
+
+            override fun iterator(): Iterator<Value> {
+                return data.listIterator()
+            }
+
             override fun toString(): String {
                 return """LoadResult.Page(
                     |   data size: ${data.size}
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
index 4277314..e2d59c1 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -261,6 +261,42 @@
         assertThat(invalidateCalls).isEqualTo(3)
     }
 
+    @Test
+    fun page_iterator() {
+        val dataSource = ItemDataSource()
+
+        runBlocking {
+            val pages = mutableListOf<LoadResult.Page<Key, Item>>()
+
+            // first page
+            val key = ITEMS_BY_NAME_ID[5].key()
+            val params = LoadParams.Append(key, 5, false)
+            val page = dataSource.load(params) as LoadResult.Page
+            pages.add(page)
+
+            val iterator = page.iterator()
+            var startIndex = 6
+            val endIndex = 11
+            // iterate normally
+            while (iterator.hasNext() && startIndex < endIndex) {
+                val item = iterator.next()
+                assertThat(item).isEqualTo(ITEMS_BY_NAME_ID[startIndex++])
+            }
+
+            // second page
+            val params2 = LoadParams.Append(
+                ITEMS_BY_NAME_ID[10].key(), 5, false
+            )
+            val page2 = dataSource.load(params2) as LoadResult.Page
+            pages.add(page2)
+
+            // iterate through list of pages
+            assertThat(pages.flatten()).containsExactlyElementsIn(
+                ITEMS_BY_NAME_ID.subList(6, 16)
+            ).inOrder()
+        }
+    }
+
     data class Key(val name: String, val id: Int)
 
     data class Item(
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
index 971069d..f1b3668 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -29,9 +29,9 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
@@ -62,7 +62,7 @@
 fun FeaturedCarousel() {
     val carouselState = remember { CarouselState(0) }
     Carousel(
-        modifier = Modifier.height(130.dp).width(950.dp).border(1.dp, Color.Black),
+        modifier = Modifier.height(130.dp).fillMaxWidth().border(1.dp, Color.Black),
         carouselState = carouselState,
         slideCount = mediaItems.size
     ) { SampleFrame(it) }
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
index 45dffab..cab1518 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
@@ -56,7 +56,7 @@
     ImmersiveList(
         modifier = Modifier
             .height(130.dp)
-            .width(950.dp)
+            .fillMaxWidth()
             .border(1.dp, Color.Black),
         background = { index, _ ->
             AnimatedContent(targetState = index) { SampleBackground(it) } },
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
index 3ecc680..16787a1 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
@@ -38,6 +38,7 @@
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -50,6 +51,8 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.test.assertIsDisplayed
@@ -60,8 +63,10 @@
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.onParent
 import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.FlakyTest
+import androidx.test.platform.app.InstrumentationRegistry
 import androidx.tv.material.ExperimentalTvMaterialApi
 import org.junit.Rule
 import org.junit.Test
@@ -333,13 +338,92 @@
         rule.onNodeWithText("PLAY").assertIsFocused()
     }
 
+    @Test
+    fun carousel_manualScrolling_ltr() {
+        rule.setContent {
+            Content {
+                TestButton("Button ${it + 1}")
+            }
+        }
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_manualScrolling_rtl() {
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                Content {
+                    TestButton("Button ${it + 1}")
+                }
+            }
+        }
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+
+    private fun performKeyPress(keyCode: Int, count: Int = 1) {
+        for (i in 1..count) {
+            InstrumentationRegistry
+                .getInstrumentation()
+                .sendKeyDownUpSync(keyCode)
+        }
+    }
+
     @Composable
     fun Content(
         carouselState: CarouselState = remember { CarouselState() },
-        content: @Composable (index: Int) -> Unit = { BasicText(text = "Text ${it + 1}")
-        }
+        slideCount: Int = 3,
+        content: @Composable (index: Int) -> Unit = { BasicText(text = "Text ${it + 1}") }
     ) {
-        val slideCount = 3
         LazyColumn {
             item {
                 Carousel(
@@ -475,7 +559,7 @@
     }
 
     @Test
-    fun carousel_zeroSlideCount_drawsSomething() {
+    fun carousel_zeroSlideCount_doesntCrash() {
         val testTag = "emptyCarousel"
         rule.setContent {
             Carousel(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index d579e94a..cc77d09 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -46,10 +46,16 @@
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.tv.material.ExperimentalTvMaterialApi
 import java.lang.Math.floorMod
@@ -95,24 +101,83 @@
     CarouselStateUpdater(carouselState, slideCount)
     var focusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
+    val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
+    val focusRequester = remember { FocusRequester() }
+    var isAutoScrollActive by remember { mutableStateOf(false) }
 
     AutoScrollSideEffect(
         timeToDisplaySlideMillis,
         slideCount,
         carouselState,
-        focusState)
+        focusState,
+        onAutoScrollChange = { isAutoScrollActive = it })
     Box(modifier = modifier
+        .focusRequester(focusRequester)
         .onFocusChanged {
             focusState = it
-            if (it.isFocused) {
+            if (it.isFocused && isAutoScrollActive) {
                 focusManager.moveFocus(FocusDirection.Enter)
             }
         }
+        .focusProperties {
+            exit = {
+                val showPreviousSlideAndGetFocusRequester = {
+                    if (carouselState
+                            .isFirstSlide()
+                            .not()
+                    ) {
+                        carouselState.moveToPreviousSlide(slideCount)
+                        focusRequester
+                    } else {
+                        FocusRequester.Default
+                    }
+                }
+                val showNextSlideAndGetFocusRequester = {
+                    if (carouselState
+                            .isLastSlide(slideCount)
+                            .not()
+                    ) {
+                        carouselState.moveToNextSlide(slideCount)
+                        focusRequester
+                    } else {
+                        FocusRequester.Default
+                    }
+                }
+                when (it) {
+                    FocusDirection.Left -> {
+                        if (isLtr) {
+                            showPreviousSlideAndGetFocusRequester()
+                        } else {
+                            showNextSlideAndGetFocusRequester()
+                        }
+                    }
+                    FocusDirection.Right -> {
+                        if (isLtr) {
+                            showNextSlideAndGetFocusRequester()
+                        } else {
+                            showPreviousSlideAndGetFocusRequester()
+                        }
+                    }
+                    else -> FocusRequester.Default
+                }
+            }
+        }
         .focusable()) {
         AnimatedContent(
             targetState = carouselState.slideIndex,
             transitionSpec = { enterTransition.with(exitTransition) }
-        ) { content.invoke(it) }
+        ) {
+            Box(
+                modifier = Modifier
+                    .onPlaced {
+                        if (isAutoScrollActive.not()) {
+                            focusManager.moveFocus(FocusDirection.Enter)
+                        }
+                    }
+            ) {
+                content.invoke(it)
+            }
+        }
         this.carouselIndicator()
     }
 }
@@ -123,14 +188,16 @@
     timeToDisplaySlideMillis: Long,
     slideCount: Int,
     carouselState: CarouselState,
-    focusState: FocusState?
+    focusState: FocusState?,
+    onAutoScrollChange: (isAutoScrollActive: Boolean) -> Unit = {},
 ) {
     val currentTimeToDisplaySlideMillis by rememberUpdatedState(timeToDisplaySlideMillis)
     val currentSlideCount by rememberUpdatedState(slideCount)
     val carouselIsFocused = focusState?.isFocused ?: false
     val carouselHasFocus = focusState?.hasFocus ?: false
+    val doAutoScroll = (carouselIsFocused || carouselHasFocus).not()
 
-    if (!(carouselIsFocused || carouselHasFocus)) {
+    if (doAutoScroll) {
         LaunchedEffect(carouselState) {
             while (true) {
                 yield()
@@ -139,10 +206,11 @@
                     snapshotFlow { carouselState.activePauseHandlesCount }
                         .first { pauseHandleCount -> pauseHandleCount == 0 }
                 }
-                carouselState.nextSlide(currentSlideCount)
+                carouselState.moveToNextSlide(currentSlideCount)
             }
         }
     }
+    onAutoScrollChange(doAutoScroll)
 }
 
 @OptIn(ExperimentalTvMaterialApi::class)
@@ -186,10 +254,24 @@
         return ScrollPauseHandleImpl(this)
     }
 
-    internal fun nextSlide(slideCount: Int) {
-        if (slideCount != 0) {
-            slideIndex = floorMod(slideIndex + 1, slideCount)
-        }
+    internal fun isFirstSlide() = slideIndex == 0
+
+    internal fun isLastSlide(slideCount: Int) = slideIndex == slideCount - 1
+
+    internal fun moveToPreviousSlide(slideCount: Int) {
+        // No slides available for carousel
+        if (slideCount == 0) return
+
+        // Go to previous slide
+        slideIndex = floorMod(slideIndex - 1, slideCount)
+    }
+
+    internal fun moveToNextSlide(slideCount: Int) {
+        // No slides available for carousel
+        if (slideCount == 0) return
+
+        // Go to next slide
+        slideIndex = floorMod(slideIndex + 1, slideCount)
     }
 }