[BasicExtenderRefactor] Implement BasicExtenderSessionProcessor

Implement BasciExtenderSessionProcessor which is a SessionProcessor
based on OEM's basic extender implementation.

Bug: 256749443
Test: BasicExtenderSessionProcessorTest
Change-Id: I76864b2a0742126a9db105960665b132c3f1333b
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index df23742..4898c8b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -108,7 +108,6 @@
 import androidx.camera.core.impl.ImmediateSurface;
 import androidx.camera.core.impl.MutableConfig;
 import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.MutableTagBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
@@ -420,49 +419,17 @@
             };
         } else if (isSessionProcessorEnabledInCurrentCamera()) {
             ImageReaderProxy imageReader;
+            // SessionProcessor only outputs JPEG format.
             if (getImageFormat() == ImageFormat.JPEG) {
-                imageReader =
-                        new AndroidImageReaderProxy(ImageReader.newInstance(resolution.getWidth(),
-                                resolution.getHeight(), getImageFormat(), MAX_IMAGES));
-            } else if (getImageFormat() == ImageFormat.YUV_420_888) { // convert it into Jpeg
-                if (Build.VERSION.SDK_INT >= 26) {
-                    // Jpeg rotation / quality will be set to softwareJpegProcessor later in
-                    // ImageCaptureRequestProcessor.
-                    softwareJpegProcessor =
-                            new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
-
-                    ModifiableImageReaderProxy inputReader =
-                            new ModifiableImageReaderProxy(
-                                    ImageReader.newInstance(resolution.getWidth(),
-                                            resolution.getHeight(),
-                                            ImageFormat.YUV_420_888,
-                                            MAX_IMAGES));
-
-                    CaptureBundle captureBundle = CaptureBundles.singleDefaultCaptureBundle();
-                    ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
-                            inputReader,
-                            captureBundle,
-                            softwareJpegProcessor
-                    ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
-
-                    // Ensure the ImageProxy contains the same capture stage id expected from the
-                    // ProcessingImageReader.
-                    MutableTagBundle tagBundle = MutableTagBundle.create();
-                    // Implicit non-null type use for getCaptureStages().
-                    //noinspection ConstantConditions
-                    tagBundle.putTag(processingImageReader.getTagBundleKey(),
-                            captureBundle.getCaptureStages().get(0).getId());
-                    inputReader.setImageTagBundle(tagBundle);
-
-                    imageReader = processingImageReader;
-                } else {
-                    throw new UnsupportedOperationException("Does not support API level < 26");
-                }
+                // SessionProcessor can't guarantee that image and capture result have the same
+                // time stamp. Thus we can't use MetadataImageReader
+                imageReader = ImageReaderProxys.createIsolatedReader(resolution.getWidth(),
+                        resolution.getHeight(), ImageFormat.JPEG, MAX_IMAGES);
+                mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
+                };
             } else {
                 throw new IllegalArgumentException("Unsupported image format:" + getImageFormat());
             }
-            mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
-            };
             mImageReader = new SafeCloseImageReaderProxy(imageReader);
         } else if (mCaptureProcessor != null || mUseSoftwareJpeg) {
             // Capture processor set from configuration takes precedence over software JPEG.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
index ef6c237..e47c72a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
@@ -97,30 +97,30 @@
      * Callback to be invoked during the capture.
      */
     interface Callback {
-        void onCaptureStarted(
+        default void onCaptureStarted(
                 @NonNull Request request,
                 long frameNumber,
-                long timestamp);
+                long timestamp) {}
 
-        void onCaptureProgressed(
+        default void onCaptureProgressed(
                 @NonNull Request request,
-                @NonNull CameraCaptureResult captureResult);
+                @NonNull CameraCaptureResult captureResult) {}
 
-        void onCaptureCompleted(
+        default void onCaptureCompleted(
                 @NonNull Request request,
-                @NonNull CameraCaptureResult captureResult);
+                @NonNull CameraCaptureResult captureResult) {}
 
-        void onCaptureFailed(
+        default void onCaptureFailed(
                 @NonNull Request request,
-                @NonNull CameraCaptureFailure captureFailure);
+                @NonNull CameraCaptureFailure captureFailure) {}
 
-        void onCaptureBufferLost(
+        default void onCaptureBufferLost(
                 @NonNull Request request,
                 long frameNumber,
-                int outputConfigId);
+                int outputConfigId) {}
 
-        void onCaptureSequenceCompleted(int sequenceId, long frameNumber);
+        default void onCaptureSequenceCompleted(int sequenceId, long frameNumber) {}
 
-        void onCaptureSequenceAborted(int sequenceId);
+        default void onCaptureSequenceAborted(int sequenceId) {}
     }
 }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index acd5027..e640f36 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -201,53 +201,6 @@
         verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture, imageAnalysis)
     }
 
-    @SdkSuppress(minSdkVersion = 29) // YUV to JPEG requires api level >= 29
-    @Test
-    fun useCasesCanWork_captureOutputFormatIsYUV() = runBlocking {
-        var captureOutputSurface: Surface? = null
-        var intermediaConfigId = -1
-        var captureOutputSurfaceFormat = 0
-        val fakeSessionProcessImpl = FakeSessionProcessImpl(
-            // Directly use output surface
-            previewConfigBlock = { outputSurfaceImpl ->
-                Camera2OutputConfigImplBuilder
-                    .newSurfaceConfig(outputSurfaceImpl.surface)
-                    .build()
-            },
-            // Has intermediate image reader to process YUV.
-            captureConfigBlock = { outputSurfaceImpl ->
-                captureOutputSurfaceFormat = outputSurfaceImpl.imageFormat
-                captureOutputSurface = outputSurfaceImpl.surface
-                Camera2OutputConfigImplBuilder
-                    .newImageReaderConfig(outputSurfaceImpl.size, ImageFormat.YUV_420_888, 2)
-                    .build()
-                    .also {
-                        intermediaConfigId = it.id
-                    }
-            },
-            onCaptureSessionStarted = {
-                // Emulates the processing, write an image to the output surface.
-                val imageWriter = ImageWriter.newInstance(captureOutputSurface!!, 2)
-                it.setImageProcessor(intermediaConfigId) { _, _, image, _ ->
-                    val inputImage = imageWriter.dequeueInputImage()
-                    imageWriter.queueInputImage(inputImage)
-                    image.decrement()
-                }
-            }
-        )
-
-        val preview = Preview.Builder().build()
-        val imageCapture = ImageCapture.Builder()
-            .setSupportedResolutions(
-                listOf(
-                    android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480)))
-                )
-            )
-            .build()
-        verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture)
-        assertThat(captureOutputSurfaceFormat).isEqualTo(ImageFormat.YUV_420_888)
-    }
-
     private suspend fun assumeAllowsSharedSurface() = withContext(Dispatchers.Main) {
         val imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2)
         val maxSharedSurfaceCount =
@@ -318,7 +271,7 @@
     }
 
     // Test if physicalCameraId is set and returned in the image received in the image processor.
-    @SdkSuppress(minSdkVersion = 29) // YUV to JPEG requires api level >= 29
+    @SdkSuppress(minSdkVersion = 28) // physical camera id is supported in API28+
     @Test
     fun useCasesCanWork_setPhysicalCameraId() = runBlocking {
         assumeAllowsSharedSurface()
@@ -326,9 +279,9 @@
         assumeTrue(physicalCameraIdList.isNotEmpty())
 
         val physicalCameraId = physicalCameraIdList[0]
-        var captureOutputSurface: Surface? = null
-        var intermediaConfigId = -1
+        var analysisOutputSurface: Surface? = null
         var sharedConfigId = -1
+        var intermediaConfigId = -1
         val deferredImagePhysicalCameraId = CompletableDeferred<String?>()
         val deferredSharedImagePhysicalCameraId = CompletableDeferred<String?>()
 
@@ -340,25 +293,29 @@
                     .setPhysicalCameraId(physicalCameraId)
                     .build()
             },
+            // Directly use output surface
+            captureConfigBlock = {
+                Camera2OutputConfigImplBuilder
+                    .newSurfaceConfig(it.surface)
+                    .build()
+            },
             // Has intermediate image reader to process YUV
-            captureConfigBlock = { outputSurfaceImpl ->
-                captureOutputSurface = outputSurfaceImpl.surface
+            analysisConfigBlock = { outputSurfaceImpl ->
+                analysisOutputSurface = outputSurfaceImpl.surface
                 val sharedConfig = Camera2OutputConfigImplBuilder.newImageReaderConfig(
                     outputSurfaceImpl.size, outputSurfaceImpl.imageFormat, 2
-                ).setPhysicalCameraId(physicalCameraId).build()
-                sharedConfigId = sharedConfig.id
+                ).setPhysicalCameraId(physicalCameraId)
+                    .build().also { sharedConfigId = it.id }
 
                 Camera2OutputConfigImplBuilder
                     .newImageReaderConfig(outputSurfaceImpl.size, ImageFormat.YUV_420_888, 2)
                     .setPhysicalCameraId(physicalCameraId)
                     .addSurfaceSharingOutputConfig(sharedConfig)
                     .build()
-                    .also {
-                        intermediaConfigId = it.id
-                    }
+                    .also { intermediaConfigId = it.id }
             },
             onCaptureSessionStarted = { requestProcessor ->
-                val imageWriter = ImageWriter.newInstance(captureOutputSurface!!, 2)
+                val imageWriter = ImageWriter.newInstance(analysisOutputSurface!!, 2)
                 requestProcessor.setImageProcessor(intermediaConfigId) {
                         _, _, image, physicalCameraIdOfImage ->
                     deferredImagePhysicalCameraId.complete(physicalCameraIdOfImage)
@@ -375,12 +332,9 @@
         )
 
         val preview = Preview.Builder().build()
-        val imageCapture = ImageCapture.Builder()
-            .setSupportedResolutions(
-                listOf(android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480))))
-            )
-            .build()
-        verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture)
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture, imageAnalysis)
         assertThat(deferredImagePhysicalCameraId.awaitWithTimeout(2000))
             .isEqualTo(physicalCameraId)
         assertThat(deferredSharedImagePhysicalCameraId.awaitWithTimeout(2000))
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
new file mode 100644
index 0000000..ec04282
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -0,0 +1,542 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.media.ImageWriter
+import android.util.Pair
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraFilter
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+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.UseCaseGroup
+import androidx.camera.core.impl.CameraConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
+import androidx.camera.core.impl.Identifier
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.extensions.impl.CaptureProcessorImpl
+import androidx.camera.extensions.impl.CaptureStageImpl
+import androidx.camera.extensions.impl.ExtenderStateListener
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl
+import androidx.camera.extensions.impl.PreviewExtenderImpl
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_NONE
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl
+import androidx.camera.extensions.impl.ProcessResultImpl
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29) // Extensions supported on API 29+
+@RunWith(Parameterized::class)
+class BasicExtenderSessionProcessorTest(
+    private val hasCaptureProcessor: Boolean,
+    private val previewProcessorType: ProcessorType
+) {
+    companion object {
+        @Parameterized.Parameters(name = "hasCaptureProcessor = {0}, previewProcessorType = {1}")
+        @JvmStatic
+        fun parameters() = listOf(
+            arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_NONE),
+            arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_NONE),
+            arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_REQUEST_UPDATE_ONLY),
+            arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_REQUEST_UPDATE_ONLY),
+            arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_IMAGE_PROCESSOR),
+            arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_IMAGE_PROCESSOR)
+        )
+
+        private fun createCaptureStage(
+            id: Int = 0,
+            parameters: List<Pair<CaptureRequest.Key<Any>, Any>> = mutableListOf()
+        ): CaptureStageImpl {
+            return object : CaptureStageImpl {
+                override fun getId() = id
+                override fun getParameters() = parameters
+            }
+        }
+    }
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var cameraProvider: ProcessCameraProvider
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+    private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+    private lateinit var fakePreviewExtenderImpl: FakePreviewExtenderImpl
+    private lateinit var fakeCaptureExtenderImpl: FakeImageCaptureExtenderImpl
+    private lateinit var basicExtenderSessionProcessor: BasicExtenderSessionProcessor
+
+    @Before
+    fun setUp() = runBlocking {
+        cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner()
+            fakeLifecycleOwner.startAndResume()
+        }
+
+        fakePreviewExtenderImpl = FakePreviewExtenderImpl(previewProcessorType)
+        fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(hasCaptureProcessor)
+        basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+        )
+    }
+
+    @After
+    fun tearDown() = runBlocking {
+        if (::cameraProvider.isInitialized) {
+            withContext(Dispatchers.Main) {
+                cameraProvider.unbindAll()
+                cameraProvider.shutdown()[10, TimeUnit.SECONDS]
+            }
+        }
+    }
+
+    @Test
+    fun canOutputCorrectly(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        val analysisSemaphore = Semaphore(0)
+        verifyUseCasesOutput(
+            preview,
+            imageCapture,
+            imageAnalysis,
+            previewSemaphore,
+            analysisSemaphore
+        )
+    }
+
+    @Test
+    fun imageCaptureError(): Unit = runBlocking {
+        assumeTrue(hasCaptureProcessor)
+        fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(
+            hasCaptureProcessor, throwErrorOnProcess = true
+        )
+        basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+        )
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        assertThrows<ImageCaptureException> {
+            verifyUseCasesOutput(preview, imageCapture, imageAnalysis)
+        }
+    }
+
+    @Test
+    fun canOutputCorrectly_withoutAnalysis(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        verifyUseCasesOutput(
+            preview = preview,
+            imageCapture = imageCapture,
+            previewFrameSemaphore = previewSemaphore
+        )
+    }
+
+    suspend fun getSensorRotationDegrees(rotation: Int): Int {
+        return withContext(Dispatchers.Main) {
+            val camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+            camera.cameraInfo.getSensorRotationDegrees(rotation)
+        }
+    }
+
+    @Test
+    fun canOutputCorrectly_setTargetRotation(): Unit = runBlocking {
+        assumeTrue(hasCaptureProcessor)
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder()
+            .setTargetRotation(Surface.ROTATION_0)
+            .build()
+        val previewSemaphore = Semaphore(0)
+        verifyUseCasesOutput(
+            preview = preview,
+            imageCapture = imageCapture,
+            previewFrameSemaphore = previewSemaphore,
+            expectedExifRotation = getSensorRotationDegrees(Surface.ROTATION_0)
+        )
+    }
+
+    @Test
+    fun canOutputCorrectlyAfterStopStart(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+        val imageCapture = ImageCapture.Builder().build()
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        val previewSemaphore = Semaphore(0)
+        val analysisSemaphore = Semaphore(0)
+
+        verifyUseCasesOutput(
+            preview,
+            imageCapture,
+            imageAnalysis,
+            previewSemaphore,
+            analysisSemaphore
+        )
+
+        fakeLifecycleOwner.pauseAndStop()
+
+        delay(1000)
+        previewSemaphore.drainPermits()
+        analysisSemaphore.drainPermits()
+        fakeLifecycleOwner.startAndResume()
+
+        assertThat(previewSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+
+        imageAnalysis.let {
+            assertThat(analysisSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        }
+
+        verifyStillCapture(imageCapture)
+    }
+
+    /**
+     * Verify if the given use cases have expected output.
+     * 1) Preview frame is received
+     * 2) imageCapture gets a captured JPEG image
+     * 3) imageAnalysis gets a Image in Analyzer.
+     */
+    private suspend fun verifyUseCasesOutput(
+        preview: Preview,
+        imageCapture: ImageCapture,
+        imageAnalysis: ImageAnalysis? = null,
+        previewFrameSemaphore: Semaphore? = null,
+        analysisSemaphore: Semaphore? = null,
+        expectedExifRotation: Int = 0,
+    ) {
+        withContext(Dispatchers.Main) {
+            preview.setSurfaceProvider(
+                SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider {
+                    if (previewFrameSemaphore?.availablePermits() == 0) {
+                        previewFrameSemaphore.release()
+                    }
+                }
+            )
+            imageAnalysis?.setAnalyzer(CameraXExecutors.mainThreadExecutor()) {
+                it.close()
+                if (analysisSemaphore?.availablePermits() == 0) {
+                    analysisSemaphore.release()
+                }
+            }
+            val cameraSelector =
+                getCameraSelectorWithSessionProcessor(
+                    cameraSelector,
+                    basicExtenderSessionProcessor
+                )
+
+            val useCaseGroupBuilder = UseCaseGroup.Builder()
+            useCaseGroupBuilder.addUseCase(preview)
+            useCaseGroupBuilder.addUseCase(imageCapture)
+            imageAnalysis?.let { useCaseGroupBuilder.addUseCase(it) }
+
+            cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                cameraSelector,
+                useCaseGroupBuilder.build()
+            )
+        }
+
+        previewFrameSemaphore?.let {
+            assertThat(it.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        }
+
+        analysisSemaphore?.let {
+            assertThat(analysisSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        }
+
+        verifyStillCapture(imageCapture, expectedExifRotation)
+    }
+
+    private suspend fun verifyStillCapture(
+        imageCapture: ImageCapture,
+        expectExifRotation: Int = 0
+    ) {
+        val deferCapturedImage = CompletableDeferred<ImageProxy>()
+        imageCapture.takePicture(
+            CameraXExecutors.mainThreadExecutor(),
+            object : ImageCapture.OnImageCapturedCallback() {
+                override fun onCaptureSuccess(image: ImageProxy) {
+                    deferCapturedImage.complete(image)
+                }
+
+                override fun onError(exception: ImageCaptureException) {
+                    deferCapturedImage.completeExceptionally(exception)
+                }
+            })
+        withTimeout(6000) {
+            deferCapturedImage.await().use {
+                assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+                if (expectExifRotation != 0) {
+                    val exif = Exif.createFromImageProxy(it)
+                    assertThat(exif.rotation).isEqualTo(expectExifRotation)
+                }
+            }
+        }
+    }
+
+    private fun getCameraSelectorWithSessionProcessor(
+        cameraSelector: CameraSelector,
+        sessionProcessor: SessionProcessor
+    ): CameraSelector {
+        val identifier = Identifier.create("idStr")
+        ExtendedCameraConfigProviderStore.addConfig(identifier) { _, _ ->
+            object : CameraConfig {
+                override fun getConfig(): Config {
+                    return MutableOptionsBundle.create()
+                }
+
+                override fun getCompatibilityId(): Identifier {
+                    return Identifier.create(0)
+                }
+
+                override fun getSessionProcessor(
+                    valueIfMissing: SessionProcessor?
+                ): SessionProcessor {
+                    return sessionProcessor
+                }
+
+                override fun getSessionProcessor(): SessionProcessor {
+                    return sessionProcessor
+                }
+            }
+        }
+        val builder = CameraSelector.Builder.fromSelector(cameraSelector)
+        builder.addCameraFilter(object : CameraFilter {
+            override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
+                val newCameraInfos = mutableListOf<CameraInfo>()
+                newCameraInfos.addAll(cameraInfos)
+                return newCameraInfos
+            }
+
+            override fun getIdentifier(): Identifier {
+                return identifier
+            }
+        })
+        return builder.build()
+    }
+
+    open class FakeExtenderStateListener : ExtenderStateListener {
+        override fun onInit(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics,
+            context: Context
+        ) {
+        }
+
+        override fun onDeInit() {}
+        override fun onPresetSession() = null
+        override fun onEnableSession() = null
+        override fun onDisableSession() = null
+    }
+
+    private class FakePreviewExtenderImpl(
+        private var processorType: ProcessorType = PROCESSOR_TYPE_NONE
+    ) : PreviewExtenderImpl, FakeExtenderStateListener() {
+        var fakePreviewImageProcessorImpl: FakePreviewImageProcessorImpl? = null
+        override fun isExtensionAvailable(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics
+        ): Boolean {
+            return true
+        }
+
+        override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {}
+        override fun getCaptureStage() = createCaptureStage()
+        override fun getProcessorType() = processorType
+        override fun getProcessor() =
+            when (processorType) {
+                PROCESSOR_TYPE_NONE -> null
+                PROCESSOR_TYPE_REQUEST_UPDATE_ONLY -> FakeRequestUpdateProcessor()
+                PROCESSOR_TYPE_IMAGE_PROCESSOR -> {
+                    fakePreviewImageProcessorImpl = FakePreviewImageProcessorImpl()
+                    fakePreviewImageProcessorImpl
+                }
+            }
+
+        override fun getSupportedResolutions() = null
+        override fun onDeInit() {
+            fakePreviewImageProcessorImpl?.close()
+        }
+    }
+
+    private class FakeImageCaptureExtenderImpl(
+        private val hasCaptureProcessor: Boolean = false,
+        private val throwErrorOnProcess: Boolean = false
+    ) : ImageCaptureExtenderImpl, FakeExtenderStateListener() {
+        val fakeCaptureProcessorImpl: FakeCaptureProcessorImpl? by lazy {
+            if (hasCaptureProcessor) {
+                FakeCaptureProcessorImpl(throwErrorOnProcess)
+            } else {
+                null
+            }
+        }
+
+        override fun isExtensionAvailable(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics
+        ): Boolean {
+            return true
+        }
+
+        override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {}
+        override fun getCaptureProcessor() = fakeCaptureProcessorImpl
+
+        override fun getCaptureStages() = listOf(createCaptureStage())
+
+        override fun getMaxCaptureStage(): Int {
+            return 2
+        }
+
+        override fun getSupportedResolutions() = null
+        override fun getEstimatedCaptureLatencyRange(size: Size?) = null
+        override fun getAvailableCaptureRequestKeys(): MutableList<CaptureRequest.Key<Any>> {
+            return mutableListOf()
+        }
+
+        override fun getAvailableCaptureResultKeys(): MutableList<CaptureResult.Key<Any>> {
+            return mutableListOf()
+        }
+
+        override fun onDeInit() {
+            fakeCaptureProcessorImpl?.close()
+        }
+    }
+
+    private class FakeCaptureProcessorImpl(
+        val throwErrorOnProcess: Boolean = false
+    ) : CaptureProcessorImpl {
+        private var imageWriter: ImageWriter? = null
+        override fun process(results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?) {
+            if (throwErrorOnProcess) {
+                throw RuntimeException("Process failed")
+            }
+            val image = imageWriter!!.dequeueInputImage()
+            imageWriter!!.queueInputImage(image)
+        }
+
+        override fun process(
+            results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?,
+            resultCallback: ProcessResultImpl?,
+            executor: Executor?
+        ) {
+            process(results)
+        }
+
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            imageWriter = ImageWriter.newInstance(surface, 2)
+        }
+
+        override fun onResolutionUpdate(size: Size) {}
+        override fun onImageFormatUpdate(imageFormat: Int) {}
+        fun close() {
+            imageWriter?.close()
+            imageWriter = null
+        }
+    }
+
+    private class FakePreviewImageProcessorImpl : PreviewImageProcessorImpl {
+        private var imageWriter: ImageWriter? = null
+        override fun process(image: Image?, result: TotalCaptureResult?) {
+            val emptyImage = imageWriter!!.dequeueInputImage()
+            imageWriter!!.queueInputImage(emptyImage)
+        }
+
+        override fun process(
+            image: Image?,
+            result: TotalCaptureResult?,
+            resultCallback: ProcessResultImpl?,
+            executor: Executor?
+        ) {
+            process(image, result)
+        }
+
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            imageWriter = ImageWriter.newInstance(surface, 2)
+        }
+
+        override fun onResolutionUpdate(size: Size) {}
+        override fun onImageFormatUpdate(imageFormat: Int) {}
+        fun close() {
+            imageWriter?.close()
+            imageWriter = null
+        }
+    }
+
+    private class FakeRequestUpdateProcessor : RequestUpdateProcessorImpl {
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            throw RuntimeException("Should not invoke this")
+        }
+
+        override fun onResolutionUpdate(size: Size) {
+            throw RuntimeException("Should not invoke this")
+        }
+
+        override fun onImageFormatUpdate(imageFormat: Int) {
+            throw RuntimeException("Should not invoke this")
+        }
+
+        override fun process(result: TotalCaptureResult?): CaptureStageImpl {
+            return createCaptureStage()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
new file mode 100644
index 0000000..39594e8
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import static androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR;
+import static androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.util.Pair;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
+import androidx.camera.camera2.interop.CaptureRequestOptions;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraCaptureFailure;
+import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.OutputSurface;
+import androidx.camera.core.impl.RequestProcessor;
+import androidx.camera.core.impl.SessionProcessor;
+import androidx.camera.extensions.impl.CaptureProcessorImpl;
+import androidx.camera.extensions.impl.CaptureStageImpl;
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
+import androidx.camera.extensions.impl.PreviewExtenderImpl;
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A {@link SessionProcessor} based on OEMs' basic extender implementation.
+ */
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
+@RequiresApi(26) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class BasicExtenderSessionProcessor extends SessionProcessorBase {
+    private static final String TAG = "BasicSessionProcessor";
+
+    private static final int PREVIEW_PROCESS_MAX_IMAGES = 2;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final PreviewExtenderImpl mPreviewExtenderImpl;
+    @NonNull
+    private final ImageCaptureExtenderImpl mImageCaptureExtenderImpl;
+
+    final Object mLock = new Object();
+    volatile StillCaptureProcessor mStillCaptureProcessor = null;
+    volatile PreviewProcessor mPreviewProcessor = null;
+    volatile RequestUpdateProcessorImpl mRequestUpdateProcessor = null;
+    private volatile Camera2OutputConfig mPreviewOutputConfig;
+    private volatile Camera2OutputConfig mCaptureOutputConfig;
+    @Nullable
+    private volatile Camera2OutputConfig mAnalysisOutputConfig = null;
+    private volatile OutputSurface mPreviewOutputSurfaceConfig;
+    private volatile OutputSurface mCaptureOutputSurfaceConfig;
+    private volatile RequestProcessor mRequestProcessor;
+    volatile boolean mIsCapturing = false;
+    private final AtomicInteger mNextCaptureSequenceId = new AtomicInteger(0);
+    static AtomicInteger sLastOutputConfigId = new AtomicInteger(0);
+    @GuardedBy("mLock")
+    private final Map<CaptureRequest.Key<?>, Object> mParameters = new LinkedHashMap<>();
+
+    public BasicExtenderSessionProcessor(@NonNull PreviewExtenderImpl previewExtenderImpl,
+            @NonNull ImageCaptureExtenderImpl imageCaptureExtenderImpl,
+            @NonNull Context context) {
+        mPreviewExtenderImpl = previewExtenderImpl;
+        mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
+        mContext = context;
+    }
+
+    @NonNull
+    @Override
+    protected Camera2SessionConfig initSessionInternal(@NonNull String cameraId,
+            @NonNull Map<String, CameraCharacteristics> cameraCharacteristicsMap,
+            @NonNull OutputSurface previewSurfaceConfig,
+            @NonNull OutputSurface imageCaptureSurfaceConfig,
+            @Nullable OutputSurface imageAnalysisSurfaceConfig) {
+        Logger.d(TAG, "PreviewExtenderImpl.onInit");
+        mPreviewExtenderImpl.onInit(cameraId, cameraCharacteristicsMap.get(cameraId),
+                mContext);
+        Logger.d(TAG, "ImageCaptureExtenderImpl.onInit");
+        mImageCaptureExtenderImpl.onInit(cameraId, cameraCharacteristicsMap.get(cameraId),
+                mContext);
+
+        mPreviewOutputSurfaceConfig = previewSurfaceConfig;
+        mCaptureOutputSurfaceConfig = imageCaptureSurfaceConfig;
+
+        // Preview
+        PreviewExtenderImpl.ProcessorType processorType =
+                mPreviewExtenderImpl.getProcessorType();
+        Logger.d(TAG, "preview processorType=" + processorType);
+        if (processorType == PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+            mPreviewOutputConfig = ImageReaderOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    previewSurfaceConfig.getSize(),
+                    ImageFormat.YUV_420_888,
+                    PREVIEW_PROCESS_MAX_IMAGES);
+            PreviewImageProcessorImpl previewImageProcessor =
+                    (PreviewImageProcessorImpl) mPreviewExtenderImpl.getProcessor();
+            mPreviewProcessor = new PreviewProcessor(
+                    previewImageProcessor, mPreviewOutputSurfaceConfig.getSurface(),
+                    mPreviewOutputSurfaceConfig.getSize());
+        } else if (processorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) {
+            mPreviewOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    previewSurfaceConfig.getSurface());
+            mRequestUpdateProcessor =
+                    (RequestUpdateProcessorImpl) mPreviewExtenderImpl.getProcessor();
+        } else {
+            mPreviewOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    previewSurfaceConfig.getSurface());
+        }
+
+        // Image Capture
+        CaptureProcessorImpl captureProcessor = mImageCaptureExtenderImpl.getCaptureProcessor();
+        Logger.d(TAG, "CaptureProcessor=" + captureProcessor);
+
+        if (captureProcessor != null) {
+            mCaptureOutputConfig = ImageReaderOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    imageCaptureSurfaceConfig.getSize(),
+                    ImageFormat.YUV_420_888,
+                    mImageCaptureExtenderImpl.getMaxCaptureStage());
+            mStillCaptureProcessor = new StillCaptureProcessor(
+                    captureProcessor, mCaptureOutputSurfaceConfig.getSurface(),
+                    mCaptureOutputSurfaceConfig.getSize());
+        } else {
+            mCaptureOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    imageCaptureSurfaceConfig.getSurface());
+        }
+
+        // Image Analysis
+        if (imageAnalysisSurfaceConfig != null) {
+            mAnalysisOutputConfig = SurfaceOutputConfig.create(
+                    sLastOutputConfigId.getAndIncrement(),
+                    imageAnalysisSurfaceConfig.getSurface());
+        }
+
+        Camera2SessionConfigBuilder builder =
+                new Camera2SessionConfigBuilder()
+                        .addOutputConfig(mPreviewOutputConfig)
+                        .addOutputConfig(mCaptureOutputConfig)
+                        .setSessionTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+
+        if (mAnalysisOutputConfig != null) {
+            builder.addOutputConfig(mAnalysisOutputConfig);
+        }
+
+        CaptureStageImpl captureStagePreview = mPreviewExtenderImpl.onPresetSession();
+        Logger.d(TAG, "preview onPresetSession:" + captureStagePreview);
+
+        CaptureStageImpl captureStageCapture = mImageCaptureExtenderImpl.onPresetSession();
+        Logger.d(TAG, "capture onPresetSession:" + captureStageCapture);
+
+        if (captureStagePreview != null && captureStagePreview.getParameters() != null) {
+            for (Pair<CaptureRequest.Key, Object> parameter :
+                    captureStagePreview.getParameters()) {
+                builder.addSessionParameter(parameter.first, parameter.second);
+            }
+        }
+
+        if (captureStageCapture != null && captureStageCapture.getParameters() != null) {
+            for (Pair<CaptureRequest.Key, Object> parameter :
+                    captureStageCapture.getParameters()) {
+                builder.addSessionParameter(parameter.first, parameter.second);
+            }
+        }
+        return builder.build();
+    }
+
+    @Override
+    protected void deInitSessionInternal() {
+        Logger.d(TAG, "preview onDeInit");
+        mPreviewExtenderImpl.onDeInit();
+        Logger.d(TAG, "capture onDeInit");
+        mImageCaptureExtenderImpl.onDeInit();
+
+        if (mPreviewProcessor != null) {
+            mPreviewProcessor.close();
+            mPreviewProcessor = null;
+        }
+        if (mStillCaptureProcessor != null) {
+            mStillCaptureProcessor.close();
+            mStillCaptureProcessor = null;
+        }
+    }
+
+    @Override
+    public void setParameters(@NonNull Config config) {
+        synchronized (mLock) {
+            HashMap<CaptureRequest.Key<?>, Object> map = new HashMap<>();
+
+            CaptureRequestOptions options =
+                    CaptureRequestOptions.Builder.from(config).build();
+
+            for (Config.Option<?> option : options.listOptions()) {
+                @SuppressWarnings("unchecked")
+                CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+                map.put(key, options.retrieveOption(option));
+            }
+            mParameters.putAll(map);
+            applyRotationAndJpegQualityToProcessor();
+        }
+    }
+
+    @Override
+    public void onCaptureSessionStart(@NonNull RequestProcessor requestProcessor) {
+        mRequestProcessor = requestProcessor;
+
+        CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onEnableSession();
+        Logger.d(TAG, "preview onEnableSession: " + captureStage1);
+
+        CaptureStageImpl captureStage2 = mImageCaptureExtenderImpl.onEnableSession();
+        Logger.d(TAG, "capture onEnableSession:" + captureStage2);
+
+        if (captureStage1 != null) {
+            submitRequestByCaptureStage(requestProcessor, captureStage1);
+        }
+
+        if (captureStage2 != null) {
+            submitRequestByCaptureStage(requestProcessor, captureStage2);
+        }
+
+        if (mPreviewProcessor != null) {
+            setImageProcessor(mPreviewOutputConfig.getId(),
+                    new ImageProcessor() {
+                        @Override
+                        public void onNextImageAvailable(int outputStreamId, long timestampNs,
+                                @NonNull ImageReference imageReference,
+                                @Nullable String physicalCameraId) {
+                            if (mPreviewProcessor != null) {
+                                mPreviewProcessor.notifyImage(imageReference);
+                            }
+                        }
+                    });
+            mPreviewProcessor.start();
+        }
+    }
+
+    private void applyParameters(RequestBuilder builder) {
+        synchronized (mLock) {
+            for (CaptureRequest.Key<?> key : mParameters.keySet()) {
+                Object value = mParameters.get(key);
+                if (value != null) {
+                    builder.setParameters(key, value);
+                }
+            }
+        }
+    }
+
+    private void applyRotationAndJpegQualityToProcessor() {
+        synchronized (mLock) {
+            if (mStillCaptureProcessor == null) {
+                return;
+            }
+            Integer orientationObj = (Integer) mParameters.get(CaptureRequest.JPEG_ORIENTATION);
+            if (orientationObj != null) {
+                mStillCaptureProcessor.setRotationDegrees(orientationObj);
+            }
+
+            Byte qualityObj = (Byte) mParameters.get(CaptureRequest.JPEG_QUALITY);
+            if (qualityObj != null) {
+                mStillCaptureProcessor.setJpegQuality((int) qualityObj);
+            }
+        }
+    }
+
+
+    private void submitRequestByCaptureStage(RequestProcessor requestProcessor,
+            CaptureStageImpl captureStage) {
+        RequestBuilder builder = new RequestBuilder();
+        builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+        if (mAnalysisOutputConfig != null) {
+            builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+        }
+        for (Pair<CaptureRequest.Key, Object> keyObjectPair : captureStage.getParameters()) {
+            builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+        }
+
+        builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+        requestProcessor.submit(builder.build(), new RequestProcessor.Callback() {
+        });
+    }
+
+    @Override
+    public void onCaptureSessionEnd() {
+        CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onDisableSession();
+        Logger.d(TAG, "preview onDisableSession: " + captureStage1);
+
+        CaptureStageImpl captureStage2 = mImageCaptureExtenderImpl.onDisableSession();
+        Logger.d(TAG, "capture onDisableSession:" + captureStage2);
+        if (captureStage1 != null) {
+            submitRequestByCaptureStage(mRequestProcessor, captureStage1);
+        }
+
+        if (captureStage2 != null) {
+            submitRequestByCaptureStage(mRequestProcessor, captureStage2);
+        }
+        mRequestProcessor = null;
+        mIsCapturing = false;
+    }
+
+    @Override
+    public int startRepeating(@NonNull CaptureCallback captureCallback) {
+        int repeatingCaptureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+        if (mRequestProcessor == null) {
+            captureCallback.onCaptureFailed(repeatingCaptureSequenceId);
+            captureCallback.onCaptureSequenceAborted(repeatingCaptureSequenceId);
+        } else {
+            updateRepeating(repeatingCaptureSequenceId, captureCallback);
+        }
+
+        return repeatingCaptureSequenceId;
+    }
+
+    void updateRepeating(int repeatingCaptureSequenceId, @NonNull CaptureCallback captureCallback) {
+        if (mRequestProcessor == null) {
+            Logger.d(TAG, "mRequestProcessor is null, ignore repeating request");
+            return;
+        }
+        RequestBuilder builder = new RequestBuilder();
+        builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+        if (mAnalysisOutputConfig != null) {
+            builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+        }
+        builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+        applyParameters(builder);
+        CaptureStageImpl captureStage = mPreviewExtenderImpl.getCaptureStage();
+        if (captureStage != null) {
+            for (Pair<CaptureRequest.Key, Object> keyObjectPair :
+                    captureStage.getParameters()) {
+                builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+            }
+        }
+
+        RequestProcessor.Callback callback = new RequestProcessor.Callback() {
+            @Override
+            public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                CaptureResult captureResult =
+                        Camera2CameraCaptureResultConverter.getCaptureResult(
+                                cameraCaptureResult);
+                Preconditions.checkArgument(captureResult instanceof TotalCaptureResult,
+                        "Cannot get TotalCaptureResult from the cameraCaptureResult ");
+                TotalCaptureResult totalCaptureResult = (TotalCaptureResult) captureResult;
+
+                if (mPreviewProcessor != null) {
+                    mPreviewProcessor.notifyCaptureResult(totalCaptureResult);
+                }
+
+                if (mRequestUpdateProcessor != null) {
+                    CaptureStageImpl captureStage =
+                            mRequestUpdateProcessor.process(totalCaptureResult);
+
+                    if (captureStage != null) {
+                        updateRepeating(repeatingCaptureSequenceId, captureCallback);
+                    }
+                }
+
+                captureCallback.onCaptureSequenceCompleted(repeatingCaptureSequenceId);
+            }
+        };
+
+        Logger.d(TAG, "requestProcessor setRepeating");
+        mRequestProcessor.setRepeating(builder.build(), callback);
+    }
+
+    @Override
+    public void stopRepeating() {
+        mRequestProcessor.stopRepeating();
+    }
+
+    @Override
+    public int startCapture(@NonNull CaptureCallback captureCallback) {
+        int captureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+
+        if (mRequestProcessor == null || mIsCapturing) {
+            Logger.d(TAG, "startCapture failed");
+            captureCallback.onCaptureFailed(captureSequenceId);
+            captureCallback.onCaptureSequenceAborted(captureSequenceId);
+            return captureSequenceId;
+        }
+        mIsCapturing = true;
+
+        List<RequestProcessor.Request> requestList = new ArrayList<>();
+        List<CaptureStageImpl> captureStages = mImageCaptureExtenderImpl.getCaptureStages();
+        List<Integer> captureIdList = new ArrayList<>();
+
+        for (CaptureStageImpl captureStage : captureStages) {
+            RequestBuilder builder = new RequestBuilder();
+            builder.addTargetOutputConfigIds(mCaptureOutputConfig.getId());
+            builder.setTemplateId(CameraDevice.TEMPLATE_STILL_CAPTURE);
+            builder.setCaptureStageId(captureStage.getId());
+
+            captureIdList.add(captureStage.getId());
+
+            applyParameters(builder);
+
+            for (Pair<CaptureRequest.Key, Object> keyObjectPair :
+                    captureStage.getParameters()) {
+                builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+            }
+            requestList.add(builder.build());
+        }
+
+        Logger.d(TAG, "Wait for capture stage id: " + captureIdList);
+
+        RequestProcessor.Callback callback = new RequestProcessor.Callback() {
+            boolean mIsCaptureFailed = false;
+            boolean mIsCaptureStarted = false;
+
+            @Override
+            public void onCaptureStarted(@NonNull RequestProcessor.Request request,
+                    long frameNumber, long timestamp) {
+                if (!mIsCaptureStarted) {
+                    mIsCaptureStarted = true;
+                    captureCallback.onCaptureStarted(captureSequenceId, timestamp);
+                }
+            }
+
+            @Override
+            public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                CaptureResult captureResult =
+                        Camera2CameraCaptureResultConverter.getCaptureResult(
+                                cameraCaptureResult);
+                Preconditions.checkArgument(captureResult instanceof TotalCaptureResult,
+                        "Cannot get capture TotalCaptureResult from the cameraCaptureResult ");
+                TotalCaptureResult totalCaptureResult = (TotalCaptureResult) captureResult;
+
+                RequestBuilder.RequestProcessorRequest requestProcessorRequest =
+                        (RequestBuilder.RequestProcessorRequest) request;
+
+                if (mStillCaptureProcessor != null) {
+                    mStillCaptureProcessor.notifyCaptureResult(
+                            totalCaptureResult,
+                            requestProcessorRequest.getCaptureStageId());
+                } else {
+                    captureCallback.onCaptureProcessStarted(captureSequenceId);
+                    captureCallback.onCaptureSequenceCompleted(captureSequenceId);
+                    mIsCapturing = false;
+                }
+            }
+
+            @Override
+            public void onCaptureFailed(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureFailure captureFailure) {
+                if (!mIsCaptureFailed) {
+                    mIsCaptureFailed = true;
+                    captureCallback.onCaptureFailed(captureSequenceId);
+                    captureCallback.onCaptureSequenceAborted(captureSequenceId);
+                    mIsCapturing = false;
+                }
+            }
+
+            @Override
+            public void onCaptureSequenceAborted(int sequenceId) {
+                captureCallback.onCaptureSequenceAborted(captureSequenceId);
+                mIsCapturing = false;
+            }
+        };
+
+        Logger.d(TAG, "startCapture");
+        if (mStillCaptureProcessor != null) {
+            mStillCaptureProcessor.startCapture(captureIdList,
+                    new StillCaptureProcessor.OnCaptureResultCallback() {
+                        @Override
+                        public void onCompleted() {
+                            captureCallback.onCaptureSequenceCompleted(captureSequenceId);
+                            mIsCapturing = false;
+                        }
+
+                        @Override
+                        public void onError(@NonNull Exception e) {
+                            captureCallback.onCaptureFailed(captureSequenceId);
+                            mIsCapturing = false;
+                        }
+                    });
+        }
+        setImageProcessor(mCaptureOutputConfig.getId(),
+                new ImageProcessor() {
+                    boolean mIsFirstFrame = true;
+
+                    @Override
+                    public void onNextImageAvailable(int outputStreamId, long timestampNs,
+                            @NonNull ImageReference imageReference,
+                            @Nullable String physicalCameraId) {
+                        Logger.d(TAG,
+                                "onNextImageAvailable  outputStreamId=" + outputStreamId);
+                        if (mStillCaptureProcessor != null) {
+                            mStillCaptureProcessor.notifyImage(imageReference);
+                        }
+
+                        if (mIsFirstFrame) {
+                            captureCallback.onCaptureProcessStarted(captureSequenceId);
+                            mIsFirstFrame = false;
+                        }
+                    }
+                });
+        mRequestProcessor.submit(requestList, callback);
+        return captureSequenceId;
+    }
+
+    @Override
+    public void abortCapture(int captureSequenceId) {
+        mRequestProcessor.abortCaptures();
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
index 60e13b7..57baff7 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
@@ -55,8 +55,8 @@
      * Sets session parameters.
      */
     @NonNull
-    <T> Camera2SessionConfigBuilder addSessionParameter(
-            @NonNull CaptureRequest.Key<T> key, @Nullable T value) {
+    Camera2SessionConfigBuilder addSessionParameter(
+            @NonNull CaptureRequest.Key key, @Nullable Object value) {
         mSessionParameters.put(key, value);
         return this;
     }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
index 454e4a0..1ae12a1 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
@@ -24,6 +24,7 @@
 
 import com.google.auto.value.AutoValue;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -42,6 +43,12 @@
         return new AutoValue_ImageReaderOutputConfig(id, surfaceGroupId, physicalCameraId,
                 sharedOutputConfigs, size, imageFormat, maxImages);
     }
+
+    static ImageReaderOutputConfig create(
+            int id, @NonNull Size size, int imageFormat, int maxImages) {
+        return new AutoValue_ImageReaderOutputConfig(id, -1, null,
+                Collections.emptyList(), size, imageFormat, maxImages);
+    }
     /**
      * Returns the size of the surface.
      */
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java
new file mode 100644
index 0000000..32d17b9
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.RequestProcessor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A builder for building {@link androidx.camera.core.impl.RequestProcessor.Request}.
+ */
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class RequestBuilder {
+    private List<Integer> mTargetOutputConfigIds = new ArrayList<>();
+    private Map<CaptureRequest.Key<?>, Object> mParameters = new HashMap<>();
+    private int mTemplateId = CameraDevice.TEMPLATE_PREVIEW;
+    int mCaptureStageId;
+
+    RequestBuilder() {
+    }
+
+    @NonNull
+    RequestBuilder addTargetOutputConfigIds(int targetOutputConfigId) {
+        mTargetOutputConfigIds.add(targetOutputConfigId);
+        return this;
+    }
+
+    @NonNull
+    RequestBuilder setParameters(@NonNull CaptureRequest.Key<?> key,
+            @NonNull Object value) {
+        mParameters.put(key, value);
+        return this;
+    }
+
+    @NonNull
+    RequestBuilder setTemplateId(int templateId) {
+        mTemplateId = templateId;
+        return this;
+    }
+
+    @NonNull
+    public RequestBuilder setCaptureStageId(int captureStageId) {
+        mCaptureStageId = captureStageId;
+        return this;
+    }
+
+    @NonNull
+    RequestProcessor.Request build() {
+        return new RequestProcessorRequest(
+                mTargetOutputConfigIds, mParameters, mTemplateId, mCaptureStageId);
+    }
+
+    static class RequestProcessorRequest implements RequestProcessor.Request {
+        final List<Integer> mTargetOutputConfigIds;
+        final Config mParameterConfig;
+        final int mTemplateId;
+        final int mCaptureStageId;
+
+        RequestProcessorRequest(List<Integer> targetOutputConfigIds,
+                Map<CaptureRequest.Key<?>, Object> parameters,
+                int templateId,
+                int captureStageId) {
+            mTargetOutputConfigIds = targetOutputConfigIds;
+            mTemplateId = templateId;
+            mCaptureStageId = captureStageId;
+
+            Camera2ImplConfig.Builder camera2ConfigBuilder = new Camera2ImplConfig.Builder();
+            for (CaptureRequest.Key<?> key : parameters.keySet()) {
+                @SuppressWarnings("unchecked")
+                CaptureRequest.Key<Object> objKey = (CaptureRequest.Key<Object>) key;
+                camera2ConfigBuilder.setCaptureRequestOption(objKey,
+                        parameters.get(objKey));
+            }
+            mParameterConfig = camera2ConfigBuilder.build();
+        }
+
+        @Override
+        @NonNull
+        public List<Integer> getTargetOutputConfigIds() {
+            return mTargetOutputConfigIds;
+        }
+
+        @Override
+        @NonNull
+        public Config getParameters() {
+            return mParameterConfig;
+        }
+
+        @Override
+        public int getTemplateId() {
+            return mTemplateId;
+        }
+
+        public int getCaptureStageId() {
+            return mCaptureStageId;
+        }
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
index 31a7144..4c56295 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
@@ -24,6 +24,7 @@
 
 import com.google.auto.value.AutoValue;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -44,6 +45,10 @@
                 sharedOutputConfigs, surface);
     }
 
+    static SurfaceOutputConfig create(int id, @NonNull Surface surface) {
+        return create(id, -1, null, Collections.emptyList(), surface);
+    }
+
     /**
      * Get the {@link Surface}. It'll return a valid surface only when type is TYPE_SURFACE.
      */
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
index 8aa0f5c..8387b97 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
@@ -30,6 +30,7 @@
 import android.view.TextureView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
@@ -132,6 +133,21 @@
      */
     @NonNull
     public static Preview.SurfaceProvider createAutoDrainingSurfaceTextureProvider() {
+        return createAutoDrainingSurfaceTextureProvider(null);
+    }
+
+    /**
+     * Creates a {@link Preview.SurfaceProvider} that is backed by a {@link SurfaceTexture}.
+     *
+     * <p>This method also creates a backing OpenGL thread that will automatically drain frames
+     * from the SurfaceTexture as they become available.
+     *
+     * @param frameAvailableListener listener to be invoked when frame is updated.
+     */
+    @NonNull
+    public static Preview.SurfaceProvider createAutoDrainingSurfaceTextureProvider(
+            @Nullable SurfaceTexture.OnFrameAvailableListener frameAvailableListener
+    ) {
         return (surfaceRequest) -> {
             HandlerThread handlerThread = new HandlerThread(String.format("CameraX"
                     + "-AutoDrainThread-%x", surfaceRequest.hashCode()));
@@ -147,8 +163,12 @@
                 SurfaceTexture surfaceTexture = new SurfaceTexture(textureIds[0]);
                 surfaceTexture.setDefaultBufferSize(surfaceRequest.getResolution().getWidth(),
                         surfaceRequest.getResolution().getHeight());
-                surfaceTexture.setOnFrameAvailableListener(
-                        SurfaceTexture::updateTexImage, handler);
+                surfaceTexture.setOnFrameAvailableListener((st) -> {
+                    st.updateTexImage();
+                    if (frameAvailableListener != null) {
+                        frameAvailableListener.onFrameAvailable(st);
+                    }
+                }, handler);
 
                 Surface surface = new Surface(surfaceTexture);
                 surfaceRequest.provideSurface(surface,