Camera2CapturePipeline.java

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.camera.camera2.internal;

import static androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY;
import static androidx.camera.core.ImageCapture.CaptureMode;
import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
import static androidx.camera.core.ImageCapture.FLASH_MODE_SCREEN;
import static androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH;
import static androidx.camera.core.ImageCapture.FlashMode;
import static androidx.camera.core.ImageCapture.FlashType;

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.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.workaround.FlashAvailabilityChecker;
import androidx.camera.camera2.internal.compat.workaround.OverrideAeModeForStillCapture;
import androidx.camera.camera2.internal.compat.workaround.UseFlashModeTorchFor3aUpdate;
import androidx.camera.camera2.internal.compat.workaround.UseTorchAsFlash;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureFailure;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraCaptureResults;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.ConvergenceUtils;
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.annotation.ExecutedBy;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureChain;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;

import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Implementation detail of the submitStillCaptures method.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class Camera2CapturePipeline {

    private static final String TAG = "Camera2CapturePipeline";

    @NonNull
    private final Camera2CameraControlImpl mCameraControl;

    @NonNull
    private final UseTorchAsFlash mUseTorchAsFlash;

    private final boolean mHasFlashUnit;

    @NonNull
    private final Quirks mCameraQuirk;

    @NonNull
    @CameraExecutor
    private final Executor mExecutor;

    @NonNull
    private final ScheduledExecutorService mScheduler;

    private final boolean mIsLegacyDevice;

    private int mTemplate = CameraDevice.TEMPLATE_PREVIEW;

    /**
     * Constructs a Camera2CapturePipeline for single capture use.
     */
    Camera2CapturePipeline(@NonNull Camera2CameraControlImpl cameraControl,
            @NonNull CameraCharacteristicsCompat cameraCharacteristics,
            @NonNull Quirks cameraQuirks,
            @CameraExecutor @NonNull Executor executor,
            @NonNull ScheduledExecutorService scheduler) {
        mCameraControl = cameraControl;
        Integer level =
                cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
        mIsLegacyDevice = level != null
                && level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
        mExecutor = executor;
        mScheduler = scheduler;
        mCameraQuirk = cameraQuirks;
        mUseTorchAsFlash = new UseTorchAsFlash(cameraQuirks);
        mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
    }

    @ExecutedBy("mExecutor")
    public void setTemplate(int template) {
        mTemplate = template;
    }

    /**
     * Submit a list of capture configs to the camera, it returns a ListenableFuture
     * which will be completed after all the captures were done.
     *
     * @return the future will be completed after all the captures are completed, It would
     * fail with a {@link androidx.camera.core.ImageCapture#ERROR_CAMERA_CLOSED} when the
     * capture was canceled, or {@link androidx.camera.core.ImageCapture#ERROR_CAPTURE_FAILED}
     * when the capture was failed.
     */
    @ExecutedBy("mExecutor")
    @NonNull
    public ListenableFuture<List<Void>> submitStillCaptures(
            @NonNull List<CaptureConfig> captureConfigs, @CaptureMode int captureMode,
            @FlashMode int flashMode, @FlashType int flashType) {

        OverrideAeModeForStillCapture aeQuirk = new OverrideAeModeForStillCapture(mCameraQuirk);
        Pipeline pipeline = new Pipeline(mTemplate, mExecutor, mScheduler, mCameraControl,
                mIsLegacyDevice, aeQuirk);

        if (captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
            pipeline.addTask(new AfTask(mCameraControl));
        }

        if (flashMode == FLASH_MODE_SCREEN) {
            pipeline.addTask(new ScreenFlashTask(mCameraControl, mExecutor, mScheduler,
                    new UseFlashModeTorchFor3aUpdate(mCameraQuirk)));
        } else {
            if (mHasFlashUnit) {
                if (isTorchAsFlash(flashType)) {
                    pipeline.addTask(
                            new TorchTask(mCameraControl, flashMode, mExecutor, mScheduler));
                } else {
                    pipeline.addTask(new AePreCaptureTask(mCameraControl, flashMode, aeQuirk));
                }
            }
            // If there is no flash unit, skip the flash related task instead of failing the
            // pipeline.
        }

        return Futures.nonCancellationPropagating(
                pipeline.executeCapture(captureConfigs, flashMode));
    }

    /**
     * The pipeline for single capturing.
     */
    @VisibleForTesting
    static class Pipeline {
        private static final long CHECK_3A_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(1);
        private static final long CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(5);

        private final int mTemplate;
        private final Executor mExecutor;
        private final ScheduledExecutorService mScheduler;
        private final Camera2CameraControlImpl mCameraControl;
        private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
        private final boolean mIsLegacyDevice;
        private long mTimeout3A = CHECK_3A_TIMEOUT_IN_NS;
        @SuppressWarnings("WeakerAccess") /* synthetic access */
        final List<PipelineTask> mTasks = new ArrayList<>();

        private final PipelineTask mPipelineSubTask = new PipelineTask() {

            @NonNull
            @Override
            public ListenableFuture<Boolean> preCapture(
                    @Nullable TotalCaptureResult captureResult) {
                ArrayList<ListenableFuture<Boolean>> futures = new ArrayList<>();
                for (PipelineTask task : mTasks) {
                    futures.add(task.preCapture(captureResult));
                }
                return Futures.transform(Futures.allAsList(futures),
                        results -> results.contains(true), CameraXExecutors.directExecutor());
            }

            @Override
            public boolean isCaptureResultNeeded() {
                for (PipelineTask task : mTasks) {
                    if (task.isCaptureResultNeeded()) {
                        return true;
                    }
                }
                return false;
            }

            @Override
            public void postCapture() {
                for (PipelineTask task : mTasks) {
                    task.postCapture();
                }
            }
        };

        Pipeline(int template, @NonNull Executor executor,
                @NonNull ScheduledExecutorService scheduler,
                @NonNull Camera2CameraControlImpl cameraControl, boolean isLegacyDevice,
                @NonNull OverrideAeModeForStillCapture overrideAeModeForStillCapture) {
            mTemplate = template;
            mExecutor = executor;
            mScheduler = scheduler;
            mCameraControl = cameraControl;
            mIsLegacyDevice = isLegacyDevice;
            mOverrideAeModeForStillCapture = overrideAeModeForStillCapture;
        }

        /**
         * Add the AE/AF/Torch tasks if required.
         *
         * @param task implements the PipelineTask interface
         */
        void addTask(@NonNull PipelineTask task) {
            mTasks.add(task);
        }

        /**
         * Set the timeout for the 3A converge.
         *
         * @param timeout3A in nano seconds
         */
        @SuppressWarnings("SameParameterValue")
        private void setTimeout3A(long timeout3A) {
            mTimeout3A = timeout3A;
        }

        @SuppressWarnings("FutureReturnValueIgnored")
        @ExecutedBy("mExecutor")
        @NonNull
        ListenableFuture<List<Void>> executeCapture(@NonNull List<CaptureConfig> captureConfigs,
                @FlashMode int flashMode) {
            ListenableFuture<TotalCaptureResult> preCapture = Futures.immediateFuture(null);
            if (!mTasks.isEmpty()) {
                ListenableFuture<TotalCaptureResult> getResult =
                        mPipelineSubTask.isCaptureResultNeeded() ? waitForResult(mCameraControl,
                                null) : Futures.immediateFuture(null);

                preCapture = FutureChain.from(getResult).transformAsync(captureResult -> {
                    if (isFlashRequired(flashMode, captureResult)) {
                        setTimeout3A(CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS);
                    }
                    return mPipelineSubTask.preCapture(captureResult);
                }, mExecutor).transformAsync(is3aConvergeRequired -> {
                    if (Boolean.TRUE.equals(is3aConvergeRequired)) {
                        return waitForResult(mTimeout3A, mScheduler, mCameraControl,
                                (result) -> is3AConverged(result, false));
                    }
                    return Futures.immediateFuture(null);
                }, mExecutor);
            }

            ListenableFuture<List<Void>> future = FutureChain.from(preCapture).transformAsync(
                    v -> submitConfigsInternal(captureConfigs, flashMode), mExecutor);


            /* Always call postCapture(), it will unlock3A if it was locked in preCapture.*/
            future.addListener(mPipelineSubTask::postCapture, mExecutor);

            return future;
        }

        @ExecutedBy("mExecutor")
        @NonNull
        ListenableFuture<List<Void>> submitConfigsInternal(
                @NonNull List<CaptureConfig> captureConfigs, @FlashMode int flashMode) {
            List<ListenableFuture<Void>> futureList = new ArrayList<>();
            List<CaptureConfig> configsToSubmit = new ArrayList<>();
            for (CaptureConfig captureConfig : captureConfigs) {
                CaptureConfig.Builder configBuilder = CaptureConfig.Builder.from(captureConfig);

                // Dequeue image from buffer and enqueue into image writer for reprocessing. If
                // succeeded, retrieve capture result and set into capture config.
                CameraCaptureResult cameraCaptureResult = null;
                if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
                        && !mCameraControl.getZslControl().isZslDisabledByFlashMode()
                        && !mCameraControl.getZslControl().isZslDisabledByUserCaseConfig()) {
                    ImageProxy imageProxy =
                            mCameraControl.getZslControl().dequeueImageFromBuffer();
                    boolean isSuccess = imageProxy != null
                            && mCameraControl.getZslControl().enqueueImageToImageWriter(
                                        imageProxy);
                    if (isSuccess) {
                        cameraCaptureResult =
                                CameraCaptureResults.retrieveCameraCaptureResult(
                                        imageProxy.getImageInfo());
                    }
                }

                if (cameraCaptureResult != null) {
                    configBuilder.setCameraCaptureResult(cameraCaptureResult);
                } else {
                    // Apply still capture template type for regular still capture case
                    applyStillCaptureTemplate(configBuilder, captureConfig);
                }

                if (mOverrideAeModeForStillCapture.shouldSetAeModeAlwaysFlash(flashMode)) {
                    applyAeModeQuirk(configBuilder);
                }

                futureList.add(CallbackToFutureAdapter.getFuture(completer -> {
                    configBuilder.addCameraCaptureCallback(new CameraCaptureCallback() {
                        @Override
                        public void onCaptureCompleted(int captureConfigId,
                                @NonNull CameraCaptureResult result) {
                            completer.set(null);
                        }

                        @Override
                        public void onCaptureFailed(int captureConfigId,
                                @NonNull CameraCaptureFailure failure) {
                            String msg =
                                    "Capture request failed with reason " + failure.getReason();
                            completer.setException(
                                    new ImageCaptureException(ERROR_CAPTURE_FAILED, msg, null));
                        }

                        @Override
                        public void onCaptureCancelled(int captureConfigId) {
                            String msg = "Capture request is cancelled because camera is closed";
                            completer.setException(
                                    new ImageCaptureException(ERROR_CAMERA_CLOSED, msg, null));
                        }
                    });
                    return "submitStillCapture";
                }));
                configsToSubmit.add(configBuilder.build());
            }
            mCameraControl.submitCaptureRequestsInternal(configsToSubmit);

            return Futures.allAsList(futureList);
        }

        @ExecutedBy("mExecutor")
        private void applyStillCaptureTemplate(@NonNull CaptureConfig.Builder configBuilder,
                @NonNull CaptureConfig captureConfig) {
            int templateToModify = CaptureConfig.TEMPLATE_TYPE_NONE;
            if (mTemplate == CameraDevice.TEMPLATE_RECORD && !mIsLegacyDevice) {
                // Always override template by TEMPLATE_VIDEO_SNAPSHOT when
                // repeating template is TEMPLATE_RECORD. Note:
                // TEMPLATE_VIDEO_SNAPSHOT is not supported on legacy device.
                templateToModify = CameraDevice.TEMPLATE_VIDEO_SNAPSHOT;
            } else if (captureConfig.getTemplateType() == CaptureConfig.TEMPLATE_TYPE_NONE
                    || captureConfig.getTemplateType() == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG) {
                templateToModify = CameraDevice.TEMPLATE_STILL_CAPTURE;
            }

            if (templateToModify != CaptureConfig.TEMPLATE_TYPE_NONE) {
                configBuilder.setTemplateType(templateToModify);
            }
        }

        @ExecutedBy("mExecutor")
        @OptIn(markerClass = ExperimentalCamera2Interop.class)
        private void applyAeModeQuirk(@NonNull CaptureConfig.Builder configBuilder) {
            Camera2ImplConfig.Builder impBuilder = new Camera2ImplConfig.Builder();
            impBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
                    CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
            configBuilder.addImplementationOptions(impBuilder.build());
        }
    }

    /**
     * Waits, with a timeout, for a camera capture result satisfying some criteria defined with the
     * {@code checker} parameter.
     *
     * @param timeoutNanos             The timeout for waiting in nanoseconds.
     * @param scheduledExecutorService The executor service to enforce the timeout.
     * @param cameraControl            The {@link Camera2CameraControlImpl} instance used to
     *                                 listen for capture results.
     * @param checker                  Defines the criteria of camera capture result for which
     *                                 the returned future will be waiting.
     * @return A {@link ListenableFuture} providing the first capture result that satisfies the
     * {@code checker} parameter.
     */
    @ExecutedBy("mExecutor")
    @NonNull
    static ListenableFuture<TotalCaptureResult> waitForResult(long timeoutNanos,
            @NonNull ScheduledExecutorService scheduledExecutorService,
            @NonNull Camera2CameraControlImpl cameraControl,
            @Nullable ResultListener.Checker checker) {
        return Futures.makeTimeoutFuture(TimeUnit.NANOSECONDS.toMillis(timeoutNanos),
                scheduledExecutorService, null, true, waitForResult(cameraControl, checker));
    }

    /**
     * Waits indefinitely for a camera capture result satisfying some criteria defined with the
     * {@code checker} parameter.
     *
     * @param cameraControl The {@link Camera2CameraControlImpl} instance used to listen for
     *                      capture results.
     * @param checker       Defines the criteria of camera capture result for which the returned
     *                      future will be waiting.
     * @return A {@link ListenableFuture} providing the first capture result that satisfies the
     * {@code checker} parameter.
     */
    @ExecutedBy("mExecutor")
    @NonNull
    static ListenableFuture<TotalCaptureResult> waitForResult(
            @NonNull Camera2CameraControlImpl cameraControl,
            @Nullable ResultListener.Checker checker) {
        ResultListener resultListener = new ResultListener(checker);
        cameraControl.addCaptureResultListener(resultListener);

        ListenableFuture<TotalCaptureResult> future = resultListener.getFuture();
        future.addListener(() -> cameraControl.removeCaptureResultListener(resultListener),
                cameraControl.mExecutor);

        return  future;
    }

    static boolean is3AConverged(@Nullable TotalCaptureResult totalCaptureResult,
            boolean isTorchAsFlash) {
        if (totalCaptureResult == null) {
            return false;
        }

        Camera2CameraCaptureResult captureResult = new Camera2CameraCaptureResult(
                totalCaptureResult);
        return ConvergenceUtils.is3AConverged(captureResult, isTorchAsFlash);
    }

    interface PipelineTask {
        /**
         * @return A {@link ListenableFuture} that will be fulfilled with a Boolean result, the
         * result true if it needs to wait for 3A converge after the task is executed, otherwise
         * false.
         */
        @ExecutedBy("mExecutor")
        @NonNull
        ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult);

        /**
         * @return true if the preCapture method requires a CaptureResult. When it return false,
         * that means the {@link #preCapture(TotalCaptureResult)} ()} can accept a null input, we
         * don't need to capture a CaptureResult for this task.
         */
        @ExecutedBy("mExecutor")
        boolean isCaptureResultNeeded();

        @ExecutedBy("mExecutor")
        void postCapture();
    }

    /**
     * Task to triggerAF preCapture if it is required
     */
    static class AfTask implements PipelineTask {

        private final Camera2CameraControlImpl mCameraControl;
        private boolean mIsExecuted = false;

        AfTask(@NonNull Camera2CameraControlImpl cameraControl) {
            mCameraControl = cameraControl;
        }

        @ExecutedBy("mExecutor")
        @NonNull
        @Override
        public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
            // Always return true for this task since we always need to wait for the focused
            // signal after the task is executed.
            ListenableFuture<Boolean> ret = Futures.immediateFuture(true);

            if (captureResult == null) {
                return ret;
            }

            Integer afMode = captureResult.get(CaptureResult.CONTROL_AF_MODE);
            if (afMode == null) {
                return ret;
            }
            switch (afMode) {
                case CaptureResult.CONTROL_AF_MODE_AUTO:
                case CaptureResult.CONTROL_AF_MODE_MACRO:
                    Logger.d(TAG, "TriggerAf? AF mode auto");
                    Integer afState = captureResult.get(CaptureResult.CONTROL_AF_STATE);
                    if (afState != null && afState == CaptureResult.CONTROL_AF_STATE_INACTIVE) {
                        Logger.d(TAG, "Trigger AF");

                        mIsExecuted = true;
                        mCameraControl.getFocusMeteringControl().triggerAf(null, false);
                        return ret;
                    }
                    break;
                default:
                    // fall out
            }

            return ret;
        }

        @ExecutedBy("mExecutor")
        @Override
        public boolean isCaptureResultNeeded() {
            return true;
        }

        @ExecutedBy("mExecutor")
        @Override
        public void postCapture() {
            if (mIsExecuted) {
                Logger.d(TAG, "cancel TriggerAF");
                mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(true, false);
            }
        }
    }

    /**
     * Task to open the Torch if flash is required.
     */
    static class TorchTask implements PipelineTask {
        private static final long CHECK_3A_WITH_TORCH_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(2);

        private final Camera2CameraControlImpl mCameraControl;
        private final @FlashMode int mFlashMode;
        private boolean mIsExecuted = false;
        @CameraExecutor
        private final Executor mExecutor;
        private final ScheduledExecutorService mScheduler;

        TorchTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode,
                @NonNull Executor executor, ScheduledExecutorService scheduler) {
            mCameraControl = cameraControl;
            mFlashMode = flashMode;
            mExecutor = executor;
            mScheduler = scheduler;
        }

        @ExecutedBy("mExecutor")
        @NonNull
        @Override
        public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
            if (isFlashRequired(mFlashMode, captureResult)) {
                if (mCameraControl.isTorchOn()) {
                    Logger.d(TAG, "Torch already on, not turn on");
                } else {
                    Logger.d(TAG, "Turn on torch");
                    mIsExecuted = true;

                    ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
                        mCameraControl.getTorchControl().enableTorchInternal(completer, true);
                        return "TorchOn";
                    });
                    return FutureChain.from(future).transformAsync(
                            input -> waitForResult(CHECK_3A_WITH_TORCH_TIMEOUT_IN_NS, mScheduler,
                                    mCameraControl, (result) -> is3AConverged(result, true)),
                            mExecutor).transform(input -> false, CameraXExecutors.directExecutor());
                }
            }

            return Futures.immediateFuture(false);
        }

        @ExecutedBy("mExecutor")
        @Override
        public boolean isCaptureResultNeeded() {
            return mFlashMode == FLASH_MODE_AUTO;
        }

        @ExecutedBy("mExecutor")
        @Override
        public void postCapture() {
            if (mIsExecuted) {
                mCameraControl.getTorchControl().enableTorchInternal(null, false);
                Logger.d(TAG, "Turn off torch");
            }
        }
    }

    /**
     * Task to trigger AePreCapture if flash is required.
     */
    static class AePreCaptureTask implements PipelineTask {

        private final Camera2CameraControlImpl mCameraControl;
        private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
        private final @FlashMode int mFlashMode;
        private boolean mIsExecuted = false;

        AePreCaptureTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode,
                @NonNull OverrideAeModeForStillCapture overrideAeModeForStillCapture) {
            mCameraControl = cameraControl;
            mFlashMode = flashMode;
            mOverrideAeModeForStillCapture = overrideAeModeForStillCapture;
        }

        @ExecutedBy("mExecutor")
        @NonNull
        @Override
        public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
            if (isFlashRequired(mFlashMode, captureResult)) {
                Logger.d(TAG, "Trigger AE");
                mIsExecuted = true;

                ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
                    mCameraControl.getFocusMeteringControl().triggerAePrecapture(completer);
                    mOverrideAeModeForStillCapture.onAePrecaptureStarted();
                    return "AePreCapture";
                });
                return FutureChain.from(future).transform(input -> true,
                        CameraXExecutors.directExecutor());
            }

            return Futures.immediateFuture(false);
        }

        @ExecutedBy("mExecutor")
        @Override
        public boolean isCaptureResultNeeded() {
            return mFlashMode == FLASH_MODE_AUTO;
        }

        @ExecutedBy("mExecutor")
        @Override
        public void postCapture() {
            if (mIsExecuted) {
                Logger.d(TAG, "cancel TriggerAePreCapture");
                mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(false, true);
                mOverrideAeModeForStillCapture.onAePrecaptureFinished();
            }
        }
    }

    /**
     * Task to trigger ScreenFlashCallback and AePreCapture if screen flash is enabled.
     */
    static class ScreenFlashTask implements PipelineTask {
        private static final long CHECK_3A_WITH_SCREEN_FLASH_TIMEOUT_IN_NS =
                TimeUnit.SECONDS.toNanos(2);

        private final Camera2CameraControlImpl mCameraControl;
        private final Executor mExecutor;
        private final ScheduledExecutorService mScheduler;
        private final ImageCapture.ScreenFlash mScreenFlash;
        private final UseFlashModeTorchFor3aUpdate mUseFlashModeTorchFor3aUpdate;

        ScreenFlashTask(@NonNull Camera2CameraControlImpl cameraControl, @NonNull Executor executor,
                @NonNull ScheduledExecutorService scheduler,
                @NonNull UseFlashModeTorchFor3aUpdate useFlashModeTorchFor3aUpdate) {
            mCameraControl = cameraControl;
            mExecutor = executor;
            mScheduler = scheduler;
            mUseFlashModeTorchFor3aUpdate = useFlashModeTorchFor3aUpdate;

            mScreenFlash = Objects.requireNonNull(mCameraControl.getScreenFlash());
        }

        @ExecutedBy("mExecutor")
        @NonNull
        @Override
        public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
            Logger.d(TAG, "ScreenFlashTask#preCapture");

            AtomicReference<ImageCapture.ScreenFlashListener> screenFlashListener =
                    new AtomicReference<>();

            ListenableFuture<Void> uiAppliedFuture = CallbackToFutureAdapter.getFuture(
                    completer -> {
                        screenFlashListener.set(() -> {
                            Logger.d(TAG, "ScreenFlashTask#preCapture: UI change applied");
                            completer.set(null);
                        });
                        return "OnScreenFlashUiApplied";
                    });

            ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
                CameraXExecutors.mainThreadExecutor().execute(() -> {
                    Logger.d(TAG, "ScreenFlashTask#preCapture: invoking applyScreenFlashUi");
                    mScreenFlash.apply(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(
                                    ImageCapture.SCREEN_FLASH_UI_APPLY_TIMEOUT_SECONDS),
                            screenFlashListener.get());
                    completer.set(null);
                });
                return "OnScreenFlashStart";
            });

            return FutureChain.from(future).transformAsync(
                    input -> mCameraControl.getFocusMeteringControl().enableExternalFlashAeMode(
                            true),
                    mExecutor
            ).transformAsync(
                    input -> CallbackToFutureAdapter.getFuture(
                            completer -> {
                                if (!mUseFlashModeTorchFor3aUpdate.shouldUseFlashModeTorch()) {
                                    completer.set(null);
                                    return "EnableTorchInternal";
                                }
                                Logger.d(TAG, "ScreenFlashTask#preCapture: enable torch");
                                mCameraControl.enableTorchInternal(true);
                                completer.set(null);
                                return "EnableTorchInternal";
                            }),
                    mExecutor
            ).transformAsync(
                    input -> Futures.makeTimeoutFuture(
                            // Not using the previous timestamp here gives users a bit more grace
                            // time before CameraX stops waiting.
                            TimeUnit.SECONDS.toMillis(
                                    ImageCapture.SCREEN_FLASH_UI_APPLY_TIMEOUT_SECONDS),
                            mScheduler, null, true, uiAppliedFuture),
                    mExecutor
            ).transformAsync(
                    input -> mCameraControl.getFocusMeteringControl().triggerAePrecapture(),
                    mExecutor
            ).transformAsync(
                    input -> waitForResult(CHECK_3A_WITH_SCREEN_FLASH_TIMEOUT_IN_NS, mScheduler,
                            mCameraControl, (result) -> is3AConverged(result, false)), mExecutor
            ).transform(input -> false, CameraXExecutors.directExecutor());
        }

        @ExecutedBy("mExecutor")
        @Override
        public boolean isCaptureResultNeeded() {
            return false;
        }

        @ExecutedBy("mExecutor")
        @Override
        public void postCapture() {
            Logger.d(TAG, "ScreenFlashTask#postCapture");
            if (mUseFlashModeTorchFor3aUpdate.shouldUseFlashModeTorch()) {
                mCameraControl.enableTorchInternal(false);
            }
            mCameraControl.getFocusMeteringControl().enableExternalFlashAeMode(false).addListener(
                    () -> Log.d(TAG, "enableExternalFlashAeMode disabled"), mExecutor
            );
            mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(false, true);
            CameraXExecutors.mainThreadExecutor().execute(mScreenFlash::clear);
        }
    }

    static boolean isFlashRequired(@FlashMode int flashMode, @Nullable TotalCaptureResult result) {
        switch (flashMode) {
            case FLASH_MODE_SCREEN:
            case FLASH_MODE_ON:
                return true;
            case FLASH_MODE_AUTO:
                Integer aeState = (result != null) ? result.get(CaptureResult.CONTROL_AE_STATE)
                        : null;
                return aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED;
            case FLASH_MODE_OFF:
                return false;
        }
        throw new AssertionError(flashMode);
    }

    /**
     * A listener receives the result of the repeating request. The results will be sent to the
     * Checker to identify if the mFuture can be completed.
     */
    static class ResultListener implements Camera2CameraControlImpl.CaptureResultListener {

        /**
         * The totalCaptureResults will be sent to the Checker#check() method, return true in the
         * Checker#check() will complete the mFuture.
         */
        interface Checker {
            boolean check(@NonNull TotalCaptureResult totalCaptureResult);
        }

        private CallbackToFutureAdapter.Completer<TotalCaptureResult> mCompleter;
        private final ListenableFuture<TotalCaptureResult> mFuture =
                CallbackToFutureAdapter.getFuture(completer -> {
                    mCompleter = completer;
                    return "waitFor3AResult";
                });
        private final Checker mChecker;

        /**
         * @param checker     the checker to define the condition to complete the mFuture, set null
         *                    will complete the mFuture once it receives any totalCaptureResults.
         */
        ResultListener(@Nullable Checker checker) {
            mChecker = checker;
        }

        @NonNull
        public ListenableFuture<TotalCaptureResult> getFuture() {
            return mFuture;
        }

        @Override
        public boolean onCaptureResult(@NonNull TotalCaptureResult captureResult) {
            if (mChecker != null && !mChecker.check(captureResult)) {
                return false;
            }

            mCompleter.set(captureResult);
            return true;
        }
    }

    private boolean isTorchAsFlash(@FlashType int flashType) {
        return mUseTorchAsFlash.shouldUseTorchAsFlash() || mTemplate == CameraDevice.TEMPLATE_RECORD
                || flashType == FLASH_TYPE_USE_TORCH_AS_FLASH;
    }

}