[BasicExtenderRefactor] Support Tap-To-Focus and Flash on Extensions with SessionProcessor

1. add startTrigger to SessionProcessor and implement it in
   BasicExtenderSessionProcessor
2. modify ProcessingCaptureSession.issueCaptureRequests:
    (a) support trigger type request
    (b) allows concurrent execution if it is supported
        by SessionProcessor.
3. Fix BasicExtenderSessionProcessor does not remove the
    parameters that no longer exists.

After the change,  tap-to-focus (focusAndMetering), zoom,
flash , torch and exposure compensation works normally with
BasicExtenderSessionProcessor. AdvancedSessionProcessor support
will be added when CameraX support extensions-interface v1.3

Bug: 258963943
Test: ProcessingCaptureSessionTest
Change-Id: I5254d20bb73ccf69df10a1e90df9a09210f0c705
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
index 37e02a8..24d785a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
@@ -91,7 +91,7 @@
  */
 @LargeTest
 @RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = 28)  // ImageWriter to PRIVATE format requires API 28
+@SdkSuppress(minSdkVersion = 28) // ImageWriter to PRIVATE format requires API 28
 class ProcessingCaptureSessionTest(
     private var lensFacing: Int,
     // The pair specifies (Output image format to Input image format). SessionProcessor will
@@ -310,8 +310,10 @@
         )
 
         // Assert
+        sessionProcessor.assertStartCaptureInvoked()
         sessionConfigParameters.assertStillCaptureCompleted()
         sessionConfigParameters.assertCaptureImageReceived()
+
         val parametersConfig = sessionProcessor.getLatestParameters()
         assertThat(
             parametersConfig.isParameterSet(
@@ -323,6 +325,41 @@
         ).isTrue()
     }
 
+    @Test
+    fun canIssueAfTrigger(): Unit = runBlocking(Dispatchers.Main) {
+        assertCanIssueTriggerRequest(CaptureRequest.CONTROL_AF_TRIGGER,
+            CaptureRequest.CONTROL_AF_TRIGGER_START)
+    }
+
+    @Test
+    fun canIssueAePrecaptureTrigger(): Unit = runBlocking(Dispatchers.Main) {
+        assertCanIssueTriggerRequest(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+    }
+
+    private suspend fun <T : Any> assertCanIssueTriggerRequest(
+        testKey: CaptureRequest.Key<T>,
+        testValue: T
+    ) {
+        // Arrange
+        val cameraDevice = cameraDeviceHolder.get()!!
+        val captureSession = createProcessingCaptureSession()
+        captureSession.open(
+            sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
+            captureSessionOpenerBuilder.build()
+        ).awaitWithTimeout(3000)
+
+        // Act
+        captureSession.issueCaptureRequests(
+            listOf(sessionConfigParameters.getTriggerCaptureConfig(testKey, testValue))
+        )
+
+        // Assert
+        val triggerConfig = sessionProcessor.assertStartTriggerInvoked()
+        assertThat(triggerConfig.isParameterSet(testKey, testValue)).isTrue()
+        sessionConfigParameters.assertTriggerCompleted()
+    }
+
     private fun <T> Config.isParameterSet(key: CaptureRequest.Key<T>, objValue: T): Boolean {
         val options = CaptureRequestOptions.Builder.from(this).build()
         return Objects.equals(
@@ -396,41 +433,6 @@
     }
 
     @Test
-    fun willCancelRequests_whenIssueMultipleConfigs(): Unit = runBlocking(Dispatchers.Main) {
-        // Arrange
-        val cameraDevice = cameraDeviceHolder.get()!!
-        val captureSession = createProcessingCaptureSession()
-        captureSession.open(
-            sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
-            captureSessionOpenerBuilder.build()
-        ).awaitWithTimeout(3000)
-
-        val cancelCountLatch = CountDownLatch(2)
-        val captureConfig1 = CaptureConfig.Builder().apply {
-            templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
-            addCameraCaptureCallback(object : CameraCaptureCallback() {
-                override fun onCaptureCancelled() {
-                    cancelCountLatch.countDown()
-                }
-            })
-        }.build()
-        val captureConfig2 = CaptureConfig.Builder().apply {
-            templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
-            addCameraCaptureCallback(object : CameraCaptureCallback() {
-                override fun onCaptureCancelled() {
-                    cancelCountLatch.countDown()
-                }
-            })
-        }.build()
-
-        // Act
-        captureSession.issueCaptureRequests(listOf(captureConfig1, captureConfig2))
-
-        // Assert
-        assertThat(cancelCountLatch.await(3, TimeUnit.SECONDS)).isTrue()
-    }
-
-    @Test
     fun willCancelNonStillCaptureRequests(): Unit = runBlocking(Dispatchers.Main) {
         // Arrange
         val cameraDevice = cameraDeviceHolder.get()!!
@@ -457,37 +459,6 @@
     }
 
     @Test
-    fun willCancelRequests_whenPendingRequestNotFinished(): Unit = runBlocking(Dispatchers.Main) {
-        // Arrange
-        val cameraDevice = cameraDeviceHolder.get()!!
-        val captureSession = createProcessingCaptureSession()
-        captureSession.open(
-            sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
-            captureSessionOpenerBuilder.build()
-        ).awaitWithTimeout(3000)
-
-        // Act
-        captureSession.issueCaptureRequests(
-            listOf(sessionConfigParameters.getStillCaptureCaptureConfig())
-        )
-
-        val cancelCountLatch = CountDownLatch(1)
-        val captureConfig = CaptureConfig.Builder().apply {
-            templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
-            addCameraCaptureCallback(object : CameraCaptureCallback() {
-                override fun onCaptureCancelled() {
-                    cancelCountLatch.countDown()
-                }
-            })
-        }.build()
-        // send 2nd request immediately.
-        captureSession.issueCaptureRequests(listOf(captureConfig))
-
-        // Assert
-        assertThat(cancelCountLatch.await(3, TimeUnit.SECONDS)).isTrue()
-    }
-
-    @Test
     fun canExecuteStillCaptureOneByOne(): Unit = runBlocking(Dispatchers.Main) {
         // Arrange
         val cameraDevice = cameraDeviceHolder.get()!!
@@ -761,6 +732,7 @@
         private val previewImageReady = CompletableDeferred<Unit>()
         private val captureImageReady = CompletableDeferred<Unit>()
         private val stillCaptureCompleted = CompletableDeferred<Unit>()
+        private val triggerRequestCompleted = CompletableDeferred<Unit>()
         private val tagKey1 = "KEY1"
         private val tagKey2 = "KEY2"
         private val tagValue1 = "Value1"
@@ -890,6 +862,23 @@
             }.build()
         }
 
+        fun <T : Any> getTriggerCaptureConfig(
+            triggerKey: CaptureRequest.Key<T>,
+            triggerValue: T
+        ): CaptureConfig {
+            return CaptureConfig.Builder().apply {
+                templateType = CameraDevice.TEMPLATE_PREVIEW
+                implementationOptions = CaptureRequestOptions.Builder().apply {
+                    setCaptureRequestOption(triggerKey, triggerValue)
+                }.build()
+                addCameraCaptureCallback(object : CameraCaptureCallback() {
+                    override fun onCaptureCompleted(cameraCaptureResult: CameraCaptureResult) {
+                        triggerRequestCompleted.complete(Unit)
+                    }
+                })
+            }.build()
+        }
+
         fun closeOutputSurfaces() {
             previewOutputDeferrableSurface.close()
             captureOutputDeferrableSurface.close()
@@ -921,6 +910,10 @@
             captureImageReady.awaitWithTimeout(3000)
         }
 
+        suspend fun assertTriggerCompleted() {
+            triggerRequestCompleted.awaitWithTimeout(3000)
+        }
+
         fun tearDown() {
             closeOutputSurfaces()
         }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index 173093a..e530e40 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -100,7 +100,7 @@
     private ProcessorState mProcessorState;
     private static List<DeferrableSurface> sHeldProcessorSurfaces = new ArrayList<>();
     @Nullable
-    private volatile CaptureConfig mPendingCaptureConfig = null;
+    private volatile List<CaptureConfig> mPendingCaptureConfigs = null;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     volatile boolean mIsExecutingStillCaptureRequest = false;
     private final SessionProcessorCaptureCallback mSessionProcessorCaptureCallback;
@@ -280,33 +280,69 @@
         }
     }
 
-    private boolean isStillCapture(@NonNull List<CaptureConfig> captureConfigs) {
-        if (captureConfigs.isEmpty()) {
-            return false;
-        }
-        for (CaptureConfig captureConfig : captureConfigs) {
-            // Don't need to consider TEMPLATE_VIDEO_SNAPSHOT case since extensions does not
-            // support Video Capture yet
-            if (captureConfig.getTemplateType() != CameraDevice.TEMPLATE_STILL_CAPTURE) {
-                return false;
+    /**
+     * Send a trigger request. Currently only CONTROL_AF_TRIGGER and CONTROL_AE_PRECAPTURE_TRIGGER
+     * are supported.
+     */
+    void issueTriggerRequest(@NonNull CaptureConfig captureConfig) {
+        Logger.d(TAG, "issueTriggerRequest");
+        CaptureRequestOptions options =
+                CaptureRequestOptions.Builder.from(
+                        captureConfig.getImplementationOptions()).build();
+
+        boolean hasTriggerParameters = false;
+        for (Config.Option<?> option : options.listOptions()) {
+            @SuppressWarnings("unchecked")
+            CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+            if (key.equals(CaptureRequest.CONTROL_AF_TRIGGER)
+                    || key.equals(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)) {
+                hasTriggerParameters = true;
+                break;
             }
         }
-        return true;
+
+        if (!hasTriggerParameters) {
+            cancelRequests(Arrays.asList(captureConfig));
+            return;
+        }
+        mSessionProcessor.startTrigger(options, new SessionProcessor.CaptureCallback() {
+            @Override
+            public void onCaptureFailed(int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
+                                CameraCaptureFailure.Reason.ERROR));
+                    }
+                });
+            }
+
+            @Override
+            public void onCaptureSequenceCompleted(int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureCompleted(
+                                new CameraCaptureResult.EmptyCameraCaptureResult());
+                    }
+                });
+            }
+        });
     }
 
     /**
-     * Submit a still capture request via
-     * {@link SessionProcessor#startCapture(SessionProcessor.CaptureCallback)}.
+     * Submit a list of capture requests.
      *
-     * <p>The method is more restrictive than {@link CaptureSession#issueCaptureRequests(List)}.
-     * Only one @link CaptureConfig} with {@link CameraDevice#TEMPLATE_STILL_CAPTURE} template is
-     * allowed. If the captureConfigs contain multiple {@link CaptureConfig}s or the contained
-     * {@link CaptureConfig} does not use {@link CameraDevice#TEMPLATE_STILL_CAPTURE}, all
-     * captureConfigs will be cancelled immediately.
+     * <p>Capture requests using {@link CameraDevice#TEMPLATE_STILL_CAPTURE} are executed by.
+     * {@link SessionProcessor#startCapture(SessionProcessor.CaptureCallback)}. Other
+     * capture requests that trigger {@link CaptureRequest#CONTROL_AF_TRIGGER} or
+     * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER} are executed by
+     * {@link SessionProcessor#startTrigger(Config, SessionProcessor.CaptureCallback)}.
      *
-     * <p>Camera2 capture options in {@link CaptureConfig#getImplementationOptions()} will be
+     * <p>For still capture requests, Camera2 capture options in
+     * {@link CaptureConfig#getImplementationOptions()} will be
      * merged with the options in {@link SessionConfig#getImplementationOptions()} set by
-     * {@link #setSessionConfig(SessionConfig)}. The merged parameters set will be passed to
+     * {@link #setSessionConfig(SessionConfig)}. The merged parameters set is passed to
      * {@link SessionProcessor#setParameters(Config)} but it is up to the implementation of the
      * {@link SessionProcessor} to determine which options to apply.
      *
@@ -314,103 +350,30 @@
      * to invoke callbacks of {@link CaptureCallbackContainer} type due to lack of the access to
      * the camera2 {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback}.
      *
-     * <p>Still capture requests are expected to arrive one at a time sequentially by upper layer.
-     * Capture requests will be cancelled if previous request have not finished.
+     * <p>Although it allows concurrent capture requests to be submitted, the session processor
+     * might not support more than one capture request to execute at the same time. The session
+     * processor could fail the request immediately if it can't run multiple requests.
      */
     @Override
     public void issueCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
         if (captureConfigs.isEmpty()) {
             return;
         }
-        if (captureConfigs.size() > 1 || !isStillCapture(captureConfigs)) {
-            cancelRequests(captureConfigs);
-            return;
-        }
-        // Only allows one capture config at a time.
-        if (mPendingCaptureConfig != null || mIsExecutingStillCaptureRequest) {
-            cancelRequests(captureConfigs);
-            return;
-        }
-
-        // captureConfigs should contain exactly one CaptureConfig.
-        CaptureConfig captureConfig = captureConfigs.get(0);
 
         Logger.d(TAG, "issueCaptureRequests (id=" + mInstanceId + ") + state =" + mProcessorState);
-
         switch (mProcessorState) {
             case UNINITIALIZED:
             case SESSION_INITIALIZED:
-                mPendingCaptureConfig = captureConfig;
-
+                mPendingCaptureConfigs = captureConfigs;
                 break;
             case ON_CAPTURE_SESSION_STARTED:
-                mIsExecutingStillCaptureRequest = true;
-                CaptureRequestOptions.Builder builder =
-                        CaptureRequestOptions.Builder.from(
-                                captureConfig.getImplementationOptions());
-
-                if (captureConfig.getImplementationOptions().containsOption(
-                        CaptureConfig.OPTION_ROTATION)) {
-                    builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
-                            captureConfig.getImplementationOptions().retrieveOption(
-                                    CaptureConfig.OPTION_ROTATION));
+                for (CaptureConfig captureConfig : captureConfigs) {
+                    if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_STILL_CAPTURE) {
+                        issueStillCaptureRequest(captureConfig);
+                    } else {
+                        issueTriggerRequest(captureConfig);
+                    }
                 }
-
-                if (captureConfig.getImplementationOptions().containsOption(
-                        CaptureConfig.OPTION_JPEG_QUALITY)) {
-                    builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
-                            captureConfig.getImplementationOptions().retrieveOption(
-                                    CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
-                }
-
-                mStillCaptureOptions = builder.build();
-                updateParameters(mSessionOptions, mStillCaptureOptions);
-                mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
-                    @Override
-                    public void onCaptureStarted(
-                            int captureSequenceId, long timestamp) {
-                    }
-
-                    @Override
-                    public void onCaptureProcessStarted(
-                            int captureSequenceId) {
-                    }
-
-                    @Override
-                    public void onCaptureFailed(
-                            int captureSequenceId) {
-                        mExecutor.execute(() -> {
-                            for (CameraCaptureCallback cameraCaptureCallback :
-                                    captureConfig.getCameraCaptureCallbacks()) {
-                                cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
-                                        CameraCaptureFailure.Reason.ERROR));
-                            }
-                            mIsExecutingStillCaptureRequest = false;
-                        });
-                    }
-
-                    @Override
-                    public void onCaptureSequenceCompleted(int captureSequenceId) {
-                        mExecutor.execute(() -> {
-                            for (CameraCaptureCallback cameraCaptureCallback :
-                                    captureConfig.getCameraCaptureCallbacks()) {
-                                cameraCaptureCallback.onCaptureCompleted(
-                                        new CameraCaptureResult.EmptyCameraCaptureResult());
-                            }
-                            mIsExecutingStillCaptureRequest = false;
-                        });
-                    }
-
-                    @Override
-                    public void onCaptureSequenceAborted(int captureSequenceId) {
-                    }
-
-                    @Override
-                    public void onCaptureCompleted(long timestamp, int captureSequenceId,
-                            @NonNull Map<CaptureResult.Key, Object> result) {
-
-                    }
-                });
                 break;
             case ON_CAPTURE_SESSION_ENDED:
             case CLOSED:
@@ -420,6 +383,52 @@
                 break;
         }
     }
+    void issueStillCaptureRequest(@NonNull CaptureConfig captureConfig) {
+        CaptureRequestOptions.Builder builder =
+                CaptureRequestOptions.Builder.from(
+                        captureConfig.getImplementationOptions());
+
+        if (captureConfig.getImplementationOptions().containsOption(
+                CaptureConfig.OPTION_ROTATION)) {
+            builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
+                    captureConfig.getImplementationOptions().retrieveOption(
+                            CaptureConfig.OPTION_ROTATION));
+        }
+
+        if (captureConfig.getImplementationOptions().containsOption(
+                CaptureConfig.OPTION_JPEG_QUALITY)) {
+            builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
+                    captureConfig.getImplementationOptions().retrieveOption(
+                            CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
+        }
+
+        mStillCaptureOptions = builder.build();
+        updateParameters(mSessionOptions, mStillCaptureOptions);
+        mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
+            @Override
+            public void onCaptureFailed(
+                    int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
+                                CameraCaptureFailure.Reason.ERROR));
+                    }
+                });
+            }
+
+            @Override
+            public void onCaptureSequenceCompleted(int captureSequenceId) {
+                mExecutor.execute(() -> {
+                    for (CameraCaptureCallback cameraCaptureCallback :
+                            captureConfig.getCameraCaptureCallbacks()) {
+                        cameraCaptureCallback.onCaptureCompleted(
+                                new CameraCaptureResult.EmptyCameraCaptureResult());
+                    }
+                });
+            }
+        });
+    }
 
     /**
      * {@inheritDoc}
@@ -457,10 +466,9 @@
             setSessionConfig(mSessionConfig);
         }
 
-        if (mPendingCaptureConfig != null) {
-            List<CaptureConfig> pendingCaptureConfigList = Arrays.asList(mPendingCaptureConfig);
-            mPendingCaptureConfig = null;
-            issueCaptureRequests(pendingCaptureConfigList);
+        if (mPendingCaptureConfigs != null) {
+            issueCaptureRequests(mPendingCaptureConfigs);
+            mPendingCaptureConfigs = null;
         }
     }
 
@@ -479,8 +487,7 @@
     @NonNull
     @Override
     public List<CaptureConfig> getCaptureConfigs() {
-        return mPendingCaptureConfig != null ? Arrays.asList(mPendingCaptureConfig)
-                : Collections.emptyList();
+        return mPendingCaptureConfigs != null ? mPendingCaptureConfigs : Collections.emptyList();
     }
 
     /**
@@ -489,12 +496,14 @@
     @Override
     public void cancelIssuedCaptureRequests() {
         Logger.d(TAG, "cancelIssuedCaptureRequests (id=" + mInstanceId + ")");
-        if (mPendingCaptureConfig != null) {
-            for (CameraCaptureCallback cameraCaptureCallback :
-                    mPendingCaptureConfig.getCameraCaptureCallbacks()) {
-                cameraCaptureCallback.onCaptureCancelled();
+        if (mPendingCaptureConfigs != null) {
+            for (CaptureConfig captureConfig : mPendingCaptureConfigs) {
+                for (CameraCaptureCallback cameraCaptureCallback :
+                        captureConfig.getCameraCaptureCallbacks()) {
+                    cameraCaptureCallback.onCaptureCancelled();
+                }
             }
-            mPendingCaptureConfig = null;
+            mPendingCaptureConfigs = null;
         }
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
index 9237485..f9858f5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
@@ -120,6 +120,13 @@
     void abortCapture(int captureSequenceId);
 
     /**
+     * Sends trigger-type single request such as AF/AE triggers.
+     */
+    default int startTrigger(@NonNull Config config, @NonNull CaptureCallback callback) {
+        return -1;
+    }
+
+    /**
      * Callback for {@link #startRepeating} and {@link #startCapture}.
      */
     interface CaptureCallback {
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
index 9bb522f..74de26b 100644
--- 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
@@ -231,6 +231,7 @@
                 CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
                 map.put(key, options.retrieveOption(option));
             }
+            mParameters.clear();
             mParameters.putAll(map);
             applyRotationAndJpegQualityToProcessor();
         }
@@ -551,4 +552,42 @@
     public void abortCapture(int captureSequenceId) {
         mRequestProcessor.abortCaptures();
     }
+
+    @Override
+    public int startTrigger(@NonNull Config config, @NonNull CaptureCallback callback) {
+        Logger.d(TAG, "startTrigger");
+        int captureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+        RequestBuilder builder = new RequestBuilder();
+        builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+        if (mAnalysisOutputConfig != null) {
+            builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+        }
+        builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+        applyParameters(builder);
+        applyPreviewStagesParameters(builder);
+
+        CaptureRequestOptions options =
+                CaptureRequestOptions.Builder.from(config).build();
+        for (Config.Option<?> option : options.listOptions()) {
+            @SuppressWarnings("unchecked")
+            CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+            builder.setParameters(key, options.retrieveOption(option));
+        }
+
+        mRequestProcessor.submit(builder.build(), new RequestProcessor.Callback() {
+            @Override
+            public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureResult captureResult) {
+                callback.onCaptureSequenceCompleted(captureSequenceId);
+            }
+
+            @Override
+            public void onCaptureFailed(@NonNull RequestProcessor.Request request,
+                    @NonNull CameraCaptureFailure captureFailure) {
+                callback.onCaptureFailed(captureSequenceId);
+            }
+        });
+
+        return captureSequenceId;
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
index 2f98d10..1e57e06 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
@@ -71,6 +71,7 @@
     private val startRepeatingCalled = CompletableDeferred<Long>()
     private val startCaptureCalled = CompletableDeferred<Long>()
     private val setParametersCalled = CompletableDeferred<Config>()
+    private val startTriggerCalled = CompletableDeferred<Config>()
     private var latestParameters: Config = OptionsBundle.emptyBundle()
     private var blockRunAfterInitSession: () -> Unit = {}
 
@@ -314,6 +315,12 @@
         return FAKE_CAPTURE_SEQUENCE_ID
     }
 
+    override fun startTrigger(config: Config, callback: SessionProcessor.CaptureCallback): Int {
+        startTriggerCalled.complete(config)
+        callback.onCaptureSequenceCompleted(FAKE_CAPTURE_SEQUENCE_ID)
+        return FAKE_CAPTURE_SEQUENCE_ID
+    }
+
     override fun abortCapture(captureSequenceId: Int) {
     }
 
@@ -355,6 +362,10 @@
         return setParametersCalled.awaitWithTimeout(3000)
     }
 
+    suspend fun assertStartTriggerInvoked(): Config {
+        return startTriggerCalled.awaitWithTimeout(3000)
+    }
+
     private suspend fun <T> Deferred<T>.awaitWithTimeout(timeMillis: Long): T {
         return withTimeout(timeMillis) {
             await()