public class

Camera2CameraControlImpl

extends java.lang.Object

implements CameraControlInternal

 java.lang.Object

↳androidx.camera.camera2.internal.Camera2CameraControlImpl

Gradle dependencies

compile group: 'androidx.camera', name: 'camera-camera2', version: '1.2.0-alpha01'

  • groupId: androidx.camera
  • artifactId: camera-camera2
  • version: 1.2.0-alpha01

Artifact androidx.camera:camera-camera2:1.2.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

A Camera2 implementation for CameraControlInternal interface

There are 2 states in the control, use count and active boolean. Use count controls whether the user can submit new requests, it can be increased or decreased via Camera2CameraControlImpl.incrementUseCount() and Camera2CameraControlImpl.decrementUseCount(). Before sending the request to the control, it must increase use count, otherwise the request will be dropped. Active state controls whether the requests are sent to the camera. It can be set via Camera2CameraControlImpl.setActive(boolean). The transition of active boolean from true to false may also reset state.

There are 4 possible state combinations when processing a request.

  • Use count >= 1 but active boolean == false: the control can accept new requests for changing parameters, but won't attempt to send them to the camera device yet. New requests can be either cached and replace old requests, or may end with ImmediateFailedFuture directly, depending on whether the type of request needs to be cached reasonably.
  • Use count >= 1 and active boolean is true: the control now sends cached requests to the camera. Any new requests are also sent directly to the camera.
  • Use count == 0 and active boolean is true: This state may not be possible or may be very short lived depending on how we want to use it. the control does not accept new requests; all requests end in ImmediateFailedFuture. Previously cached requests may continue processing.
  • Use count == 0 and active boolean is false: the control does not accept new requests; all requests end in ImmediateFailedFuture. Any cached requests are dropped.

Summary

Methods
public voidaddInteropConfig(Config config)

public voidaddZslConfig(Size resolution, SessionConfig.Builder sessionConfigBuilder)

public <any>cancelFocusAndMetering()

public voidclearInteropConfig()

public <any>enableTorch(boolean torch)

public Camera2CameraControlgetCamera2CameraControl()

public ExposureControlgetExposureControl()

public intgetFlashMode()

public androidx.camera.camera2.internal.FocusMeteringControlgetFocusMeteringControl()

public ConfiggetInteropConfig()

public RectgetSensorRect()

public SessionConfiggetSessionConfig()

public androidx.camera.camera2.internal.TorchControlgetTorchControl()

public androidx.camera.camera2.internal.ZoomControlgetZoomControl()

public androidx.camera.camera2.internal.ZslControlgetZslControl()

public <any>setExposureCompensationIndex(int exposure)

public voidsetFlashMode(int flashMode)

public <any>setLinearZoom(float linearZoom)

public voidsetPreviewAspectRatio(Rational previewAspectRatio)

public <any>setZoomRatio(float ratio)

public voidsetZslDisabled(boolean disabled)

public <any>startFocusAndMetering(FocusMeteringAction action)

public <any>submitStillCaptureRequests(java.util.List<CaptureConfig> captureConfigs, int captureMode, int flashType)

public voidupdateSessionConfig()

Triggers an update to the session.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public androidx.camera.camera2.internal.ZoomControl getZoomControl()

public androidx.camera.camera2.internal.FocusMeteringControl getFocusMeteringControl()

public androidx.camera.camera2.internal.TorchControl getTorchControl()

public ExposureControl getExposureControl()

public androidx.camera.camera2.internal.ZslControl getZslControl()

public Camera2CameraControl getCamera2CameraControl()

public void addInteropConfig(Config config)

public void clearInteropConfig()

public Config getInteropConfig()

public void setPreviewAspectRatio(Rational previewAspectRatio)

public <any> startFocusAndMetering(FocusMeteringAction action)

public <any> cancelFocusAndMetering()

public <any> setZoomRatio(float ratio)

public <any> setLinearZoom(float linearZoom)

public int getFlashMode()

public void setFlashMode(int flashMode)

public void addZslConfig(Size resolution, SessionConfig.Builder sessionConfigBuilder)

public void setZslDisabled(boolean disabled)

public <any> enableTorch(boolean torch)

public <any> setExposureCompensationIndex(int exposure)

public <any> submitStillCaptureRequests(java.util.List<CaptureConfig> captureConfigs, int captureMode, int flashType)

public SessionConfig getSessionConfig()

public void updateSessionConfig()

Triggers an update to the session.

public Rect getSensorRect()

Source

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

package androidx.camera.camera2.internal;

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 android.graphics.Rect;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import android.util.ArrayMap;
import android.util.Rational;
import android.util.Size;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.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.AeFpsRange;
import androidx.camera.camera2.internal.compat.workaround.AutoFlashAEModeDisabler;
import androidx.camera.camera2.interop.Camera2CameraControl;
import androidx.camera.camera2.interop.CaptureRequestOptions;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture;
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.CameraControlInternal;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.TagBundle;
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 androidx.core.util.Preconditions;

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

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicLong;

/**
 * A Camera2 implementation for CameraControlInternal interface
 *
 * <p>There are 2 states in the control, use count and active boolean. Use count controls
 * whether the user can submit new requests, it can be increased or decreased via
 * {@link #incrementUseCount()} and {@link #decrementUseCount()}. Before sending the request to
 * the control, it must increase use count, otherwise the request will be dropped. Active state
 * controls whether the requests are sent to the camera. It can be set via
 * {@link #setActive(boolean)}. The transition of active boolean from {@code true} to {@code
 * false} may also reset state.
 *
 * <p>There are 4 possible state combinations when processing a request.
 *
 * <ul>
 * <li>Use count >= 1 but active boolean == false: the control can accept new requests for
 * changing parameters, but won't attempt to send them to the camera device yet. New requests can
 * be either cached and replace old requests, or may end with {@code ImmediateFailedFuture}
 * directly, depending on whether the type of request needs to be cached reasonably.</li>
 * <li>Use count >= 1 and active boolean is true: the control now sends cached requests to the
 * camera. Any new requests are also sent directly to the camera.</li>
 * <li>Use count == 0 and active boolean is true: This state may not be possible or may be very
 * short lived depending on how we want to use it. the control does not accept new requests;
 * all requests end in {@code ImmediateFailedFuture}. Previously cached requests may continue
 * processing.</li>
 * <li>Use count == 0 and active boolean is false: the control does not accept new requests; all
 * requests end in {@code ImmediateFailedFuture}. Any cached requests are dropped.</li>
 * </ul>
 */
@OptIn(markerClass = ExperimentalCamera2Interop.class)
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class Camera2CameraControlImpl implements CameraControlInternal {
    private static final String TAG = "Camera2CameraControlImp";
    private static final int DEFAULT_TEMPLATE = CameraDevice.TEMPLATE_PREVIEW;
    @VisibleForTesting
    final CameraControlSessionCallback mSessionCallback;
    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    @CameraExecutor
    final Executor mExecutor;
    private final Object mLock = new Object();
    private final CameraCharacteristicsCompat mCameraCharacteristics;
    private final ControlUpdateCallback mControlUpdateCallback;

    private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
    private final FocusMeteringControl mFocusMeteringControl;
    private final ZoomControl mZoomControl;
    private final TorchControl mTorchControl;
    private final ExposureControl mExposureControl;
    @VisibleForTesting
    ZslControl mZslControl;
    private final Camera2CameraControl mCamera2CameraControl;
    private final Camera2CapturePipeline mCamera2CapturePipeline;
    @GuardedBy("mLock")
    private int mUseCount = 0;
    // use volatile modifier to make these variables in sync in all threads.
    private volatile boolean mIsTorchOn = false;
    @ImageCapture.FlashMode
    private volatile int mFlashMode = FLASH_MODE_OFF;

    // Workarounds
    private final AeFpsRange mAeFpsRange;
    private final AutoFlashAEModeDisabler mAutoFlashAEModeDisabler;

    static final String TAG_SESSION_UPDATE_ID = "CameraControlSessionUpdateId";
    private final AtomicLong mNextSessionUpdateId = new AtomicLong(0);
    @NonNull
    private volatile ListenableFuture<Void> mFlashModeChangeSessionUpdateFuture =
            Futures.immediateFuture(null);
    //******************** Should only be accessed by executor *****************************//
    private int mTemplate = DEFAULT_TEMPLATE;
    // SessionUpdateId will auto-increment every time session updates.
    private long mCurrentSessionUpdateId = 0;
    private final CameraCaptureCallbackSet mCameraCaptureCallbackSet =
            new CameraCaptureCallbackSet();
    //**************************************************************************************//

    @VisibleForTesting
    Camera2CameraControlImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristics,
            @NonNull ScheduledExecutorService scheduler,
            @NonNull @CameraExecutor Executor executor,
            @NonNull ControlUpdateCallback controlUpdateCallback) {
        this(cameraCharacteristics, scheduler, executor, controlUpdateCallback,
                new Quirks(new ArrayList<>()));
    }

    /**
     * Constructor for a Camera2CameraControlImpl.
     *
     * <p>All {@code controlUpdateListener} invocations will be on the provided {@code executor}.
     *
     * <p>All tasks scheduled by {@code scheduler} will be immediately executed by {@code executor}.
     *
     * @param cameraCharacteristics Characteristics for the camera being controlled.
     * @param scheduler             Scheduler used for scheduling tasks in the future.
     * @param executor              Camera executor for synchronizing and offloading all commands.
     * @param controlUpdateCallback Listener which will be notified of control changes.
     * @param cameraQuirks          Camera-related quirks of the camera being controlled
     */
    Camera2CameraControlImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristics,
            @NonNull ScheduledExecutorService scheduler,
            @NonNull @CameraExecutor Executor executor,
            @NonNull ControlUpdateCallback controlUpdateCallback,
            @NonNull final Quirks cameraQuirks) {
        mCameraCharacteristics = cameraCharacteristics;
        mControlUpdateCallback = controlUpdateCallback;
        mExecutor = executor;
        mSessionCallback = new CameraControlSessionCallback(mExecutor);
        mSessionConfigBuilder.setTemplateType(mTemplate);
        mSessionConfigBuilder.addRepeatingCameraCaptureCallback(
                CaptureCallbackContainer.create(mSessionCallback));
        // Adding a callback via SessionConfigBuilder requires a expensive updateSessionConfig
        // call. mCameraCaptureCallbackset is for enabling dynamically add/remove
        // CameraCaptureCallback efficiently.
        mSessionConfigBuilder.addRepeatingCameraCaptureCallback(mCameraCaptureCallbackSet);

        mExposureControl = new ExposureControl(this, mCameraCharacteristics, mExecutor);
        mFocusMeteringControl = new FocusMeteringControl(
                this, scheduler, mExecutor, cameraQuirks);
        mZoomControl = new ZoomControl(this, mCameraCharacteristics, mExecutor);
        mTorchControl = new TorchControl(this, mCameraCharacteristics, mExecutor);
        if (Build.VERSION.SDK_INT >= 23) {
            mZslControl = new ZslControlImpl(mCameraCharacteristics);
        } else {
            mZslControl = new ZslControlNoOpImpl();
        }

        // Workarounds
        mAeFpsRange = new AeFpsRange(cameraQuirks);
        mAutoFlashAEModeDisabler = new AutoFlashAEModeDisabler(cameraQuirks);
        mCamera2CameraControl = new Camera2CameraControl(this, mExecutor);
        mCamera2CapturePipeline = new Camera2CapturePipeline(this, mCameraCharacteristics,
                cameraQuirks, mExecutor);
        mExecutor.execute(
                () -> addCaptureResultListener(mCamera2CameraControl.getCaptureRequestListener()));
    }

    /** Increments the use count of the control. */
    void incrementUseCount() {
        synchronized (mLock) {
            mUseCount++;
        }
    }

    /**
     * Decrements the use count of the control.
     *
     * @throws IllegalStateException if try to decrement the use count to less than zero
     */
    void decrementUseCount() {
        synchronized (mLock) {
            if (mUseCount == 0) {
                throw new IllegalStateException("Decrementing use count occurs more times than "
                        + "incrementing");
            }
            mUseCount--;
        }
    }

    /**
     * Returns the use count of the control.
     *
     * <p>Use count can be increased and decreased via {@link #incrementUseCount()} and
     * {@link #decrementUseCount()}. Camera control only accepts requests when the use count is
     * greater than 0.
     */
    @VisibleForTesting
    int getUseCount() {
        synchronized (mLock) {
            return mUseCount;
        }
    }

    @NonNull
    public ZoomControl getZoomControl() {
        return mZoomControl;
    }

    @NonNull
    public FocusMeteringControl getFocusMeteringControl() {
        return mFocusMeteringControl;
    }

    @NonNull
    public TorchControl getTorchControl() {
        return mTorchControl;
    }

    @NonNull
    public ExposureControl getExposureControl() {
        return mExposureControl;
    }

    @NonNull
    public ZslControl getZslControl() {
        return mZslControl;
    }

    @NonNull
    public Camera2CameraControl getCamera2CameraControl() {
        return mCamera2CameraControl;
    }

    @Override
    public void addInteropConfig(@NonNull Config config) {
        ListenableFuture<Void> future = mCamera2CameraControl.addCaptureRequestOptions(
                CaptureRequestOptions.Builder.from(config).build());
        future.addListener(() -> {
        }, CameraXExecutors.directExecutor());
    }

    @Override
    public void clearInteropConfig() {
        ListenableFuture<Void> future = mCamera2CameraControl.clearCaptureRequestOptions();
        future.addListener(() -> {
        }, CameraXExecutors.directExecutor());
    }

    @NonNull
    @Override
    public Config getInteropConfig() {
        return mCamera2CameraControl.getCamera2ImplConfig();
    }

    /**
     * Set current active state. Set active if it is ready to trigger camera control operation.
     *
     * <p>Most operations during inactive state do nothing. Some states are reset to default
     * once it is changed to inactive state.
     */
    @ExecutedBy("mExecutor")
    void setActive(boolean isActive) {
        mFocusMeteringControl.setActive(isActive);
        mZoomControl.setActive(isActive);
        mTorchControl.setActive(isActive);
        mExposureControl.setActive(isActive);
        mCamera2CameraControl.setActive(isActive);
    }

    @ExecutedBy("mExecutor")
    public void setPreviewAspectRatio(@Nullable Rational previewAspectRatio) {
        mFocusMeteringControl.setPreviewAspectRatio(previewAspectRatio);
    }

    @NonNull
    @Override
    public ListenableFuture<FocusMeteringResult> startFocusAndMetering(
            @NonNull FocusMeteringAction action) {
        if (!isControlInUse()) {
            return Futures.immediateFailedFuture(
                    new OperationCanceledException("Camera is not active."));
        }
        return Futures.nonCancellationPropagating(
                mFocusMeteringControl.startFocusAndMetering(action));
    }

    @NonNull
    @Override
    public ListenableFuture<Void> cancelFocusAndMetering() {
        if (!isControlInUse()) {
            return Futures.immediateFailedFuture(
                    new OperationCanceledException("Camera is not active."));
        }
        return Futures.nonCancellationPropagating(mFocusMeteringControl.cancelFocusAndMetering());
    }

    @NonNull
    @Override
    public ListenableFuture<Void> setZoomRatio(float ratio) {
        if (!isControlInUse()) {
            return Futures.immediateFailedFuture(
                    new OperationCanceledException("Camera is not active."));
        }
        return Futures.nonCancellationPropagating(mZoomControl.setZoomRatio(ratio));
    }

    @NonNull
    @Override
    public ListenableFuture<Void> setLinearZoom(float linearZoom) {
        if (!isControlInUse()) {
            return Futures.immediateFailedFuture(
                    new OperationCanceledException("Camera is not active."));
        }
        return Futures.nonCancellationPropagating(mZoomControl.setLinearZoom(linearZoom));
    }

    @ImageCapture.FlashMode
    @Override
    public int getFlashMode() {
        return mFlashMode;
    }

    /** {@inheritDoc} */
    @Override
    public void setFlashMode(@ImageCapture.FlashMode int flashMode) {
        if (!isControlInUse()) {
            Logger.w(TAG, "Camera is not active.");
            return;
        }
        // update mFlashMode immediately so that following getFlashMode() returns correct value.
        mFlashMode = flashMode;

        // On some devices, AE precapture may not work properly if the repeating request to change
        // the flash mode is not completed. We need to store the future so that AE precapture can
        // wait for it.
        mFlashModeChangeSessionUpdateFuture = updateSessionConfigAsync();
    }

    @Override
    public void addZslConfig(@NonNull Size resolution,
            @NonNull SessionConfig.Builder sessionConfigBuilder) {
        mZslControl.addZslConfig(resolution, sessionConfigBuilder);
    }

    @Override
    public void setZslDisabled(boolean disabled) {
        mZslControl.setZslDisabled(disabled);
    }

    /** {@inheritDoc} */
    @Override
    @NonNull
    public ListenableFuture<Void> enableTorch(final boolean torch) {
        if (!isControlInUse()) {
            return Futures.immediateFailedFuture(
                    new OperationCanceledException("Camera is not active."));
        }
        return Futures.nonCancellationPropagating(mTorchControl.enableTorch(torch));
    }

    @ExecutedBy("mExecutor")
    @NonNull
    private ListenableFuture<Void> waitForSessionUpdateId(long sessionUpdateIdToWait) {
        return CallbackToFutureAdapter.getFuture(completer -> {
            addCaptureResultListener(captureResult -> {
                boolean updated = isSessionUpdated(captureResult, sessionUpdateIdToWait);
                if (updated) {
                    completer.set(null);
                    return true; // remove the callback
                }
                return false; // continue checking
            });
            return "waitForSessionUpdateId:" + sessionUpdateIdToWait;
        });
    }

    /**
     * Check if the sessionUpdateId in capture result is larger than the given sessionUpdateId.
     */
    static boolean isSessionUpdated(@NonNull TotalCaptureResult captureResult,
            long sessionUpdateId) {
        if (captureResult.getRequest() == null) {
            return false;
        }
        Object tag = captureResult.getRequest().getTag();
        if (tag instanceof TagBundle) {
            Long tagLong =
                    (Long) ((TagBundle) tag).getTag(Camera2CameraControlImpl.TAG_SESSION_UPDATE_ID);
            if (tagLong == null) {
                return false;
            }
            long sessionUpdateIdInCaptureResult = tagLong.longValue();
            // Check if session update is already done.
            if (sessionUpdateIdInCaptureResult >= sessionUpdateId) {
                return true;
            }
        }
        return false;
    }

    @NonNull
    @Override
    public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
        if (!isControlInUse()) {
            return Futures.immediateFailedFuture(
                    new OperationCanceledException("Camera is not active."));
        }
        return mExposureControl.setExposureCompensationIndex(exposure);
    }

    /** {@inheritDoc} */
    @NonNull
    @Override
    public ListenableFuture<List<Void>> submitStillCaptureRequests(
            @NonNull List<CaptureConfig> captureConfigs,
            @ImageCapture.CaptureMode int captureMode,
            @ImageCapture.FlashType int flashType) {
        if (!isControlInUse()) {
            Logger.w(TAG, "Camera is not active.");
            return Futures.immediateFailedFuture(
                    new OperationCanceledException("Camera is not active."));
        }

        // Prior to submitStillCaptures, wait until the pending flash mode session change is
        // completed. On some devices, AE precapture triggered in submitStillCaptures may not
        // work properly if the repeating request to change the flash mode is not completed.
        int flashMode = getFlashMode();
        return FutureChain.from(mFlashModeChangeSessionUpdateFuture).transformAsync(
                v -> mCamera2CapturePipeline.submitStillCaptures(
                        captureConfigs, captureMode, flashMode, flashType), mExecutor);
    }

    /** {@inheritDoc} */
    @Override
    @NonNull
    @ExecutedBy("mExecutor")
    public SessionConfig getSessionConfig() {
        mSessionConfigBuilder.setTemplateType(mTemplate);
        mSessionConfigBuilder.setImplementationOptions(getSessionOptions());
        Object tag = mCamera2CameraControl.getCamera2ImplConfig().getCaptureRequestTag(null);
        if (tag != null && tag instanceof Integer) {
            mSessionConfigBuilder.addTag(Camera2CameraControl.TAG_KEY, tag);
        }
        mSessionConfigBuilder.addTag(TAG_SESSION_UPDATE_ID, mCurrentSessionUpdateId);
        return mSessionConfigBuilder.build();
    }

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

        mFocusMeteringControl.setTemplate(mTemplate);
        mCamera2CapturePipeline.setTemplate(mTemplate);
    }

    @ExecutedBy("mExecutor")
    void resetTemplate() {
        setTemplate(DEFAULT_TEMPLATE);
    }

    private boolean isControlInUse() {
        return getUseCount() > 0;
    }

    /**
     * Triggers an update to the session.
     */
    public void updateSessionConfig() {
        mExecutor.execute(this::updateSessionConfigSynchronous);
    }

    /**
     * Triggers an update to the session and returns a ListenableFuture which completes when the
     * session is updated successfully.
     */
    @NonNull
    ListenableFuture<Void> updateSessionConfigAsync() {
        ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
            mExecutor.execute(() -> {
                long sessionUpdateId = updateSessionConfigSynchronous();
                Futures.propagate(waitForSessionUpdateId(sessionUpdateId), completer);
            });
            return "updateSessionConfigAsync";
        });

        return Futures.nonCancellationPropagating(future);
    }

    /**
     * Triggers an update to the session synchronously.
     *
     * <p>It will return an auto-incremented ID representing the session update request. The ID
     * will be put in the tag of SessionConfig using key {@link #TAG_SESSION_UPDATE_ID}. It can
     * then retrieve the ID in {@link TotalCaptureResult} to check if the session update is done or
     * not.
     */
    @ExecutedBy("mExecutor")
    long updateSessionConfigSynchronous() {
        mCurrentSessionUpdateId = mNextSessionUpdateId.getAndIncrement();
        mControlUpdateCallback.onCameraControlUpdateSessionConfig();
        return mCurrentSessionUpdateId;
    }

    @ExecutedBy("mExecutor")
    @NonNull
    Rect getCropSensorRegion() {
        return mZoomControl.getCropSensorRegion();
    }

    @Override
    @ExecutedBy("mExecutor")
    @NonNull
    public Rect getSensorRect() {
        return Preconditions.checkNotNull(
                mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE));
    }

    @ExecutedBy("mExecutor")
    void removeCaptureResultListener(@NonNull CaptureResultListener listener) {
        mSessionCallback.removeListener(listener);
    }

    @ExecutedBy("mExecutor")
    void addCaptureResultListener(@NonNull CaptureResultListener listener) {
        mSessionCallback.addListener(listener);
    }

    /** Adds a session {@link CameraCaptureCallback dynamically */
    void addSessionCameraCaptureCallback(@NonNull Executor executor,
            @NonNull CameraCaptureCallback cameraCaptureCallback) {
        mExecutor.execute(() -> {
            mCameraCaptureCallbackSet.addCaptureCallback(executor, cameraCaptureCallback);
        });
    }

    /** Removes the {@link CameraCaptureCallback} that was added previously */
    void removeSessionCameraCaptureCallback(@NonNull CameraCaptureCallback cameraCaptureCallback) {
        mExecutor.execute(() -> {
            mCameraCaptureCallbackSet.removeCaptureCallback(cameraCaptureCallback);
        });
    }

    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    @ExecutedBy("mExecutor")
    void enableTorchInternal(boolean torch) {
        mIsTorchOn = torch;
        if (!torch) {
            // Send capture request with AE_MODE_ON + FLASH_MODE_OFF to turn off torch.
            CaptureConfig.Builder singleRequestBuilder = new CaptureConfig.Builder();
            singleRequestBuilder.setTemplateType(mTemplate);
            singleRequestBuilder.setUseRepeatingSurface(true);
            Camera2ImplConfig.Builder configBuilder = new Camera2ImplConfig.Builder();
            configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
                    getSupportedAeMode(CaptureRequest.CONTROL_AE_MODE_ON));
            configBuilder.setCaptureRequestOption(CaptureRequest.FLASH_MODE,
                    CaptureRequest.FLASH_MODE_OFF);
            singleRequestBuilder.addImplementationOptions(configBuilder.build());
            submitCaptureRequestsInternal(
                    Collections.singletonList(singleRequestBuilder.build()));
        }
        updateSessionConfigSynchronous();
    }

    @ExecutedBy("mExecutor")
    boolean isTorchOn() {
        return mIsTorchOn;
    }

    @ExecutedBy("mExecutor")
    void submitCaptureRequestsInternal(final List<CaptureConfig> captureConfigs) {
        mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs);
    }

    /**
     * Gets session options by current status.
     *
     * <p>The session options are based on the current torch status, flash mode, focus area, crop
     * area, etc... They should be appended to the repeat request.
     */
    @VisibleForTesting
    @ExecutedBy("mExecutor")
    Config getSessionOptions() {
        Camera2ImplConfig.Builder builder = new Camera2ImplConfig.Builder();
        builder.setCaptureRequestOption(
                CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);

        // AF Mode is assigned in mFocusMeteringControl.
        mFocusMeteringControl.addFocusMeteringOptions(builder);

        mAeFpsRange.addAeFpsRangeOptions(builder);

        mZoomControl.addZoomOption(builder);

        int aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
        if (mIsTorchOn) {
            builder.setCaptureRequestOption(CaptureRequest.FLASH_MODE,
                    CaptureRequest.FLASH_MODE_TORCH);
        } else {
            switch (mFlashMode) {
                case FLASH_MODE_OFF:
                    aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
                    break;
                case FLASH_MODE_ON:
                    aeMode = CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
                    break;
                case FLASH_MODE_AUTO:
                    aeMode = mAutoFlashAEModeDisabler.getCorrectedAeMode(
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    break;
            }
        }
        builder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, getSupportedAeMode(aeMode));

        builder.setCaptureRequestOption(
                CaptureRequest.CONTROL_AWB_MODE,
                getSupportedAwbMode(CaptureRequest.CONTROL_AWB_MODE_AUTO));

        mExposureControl.setCaptureRequestOption(builder);

        Config currentConfig = mCamera2CameraControl.getCamera2ImplConfig();
        for (Config.Option<?> option : currentConfig.listOptions()) {
            @SuppressWarnings("unchecked")
            Config.Option<Object> objectOpt = (Config.Option<Object>) option;
            builder.getMutableConfig().insertOption(objectOpt,
                    Config.OptionPriority.ALWAYS_OVERRIDE,
                    currentConfig.retrieveOption(objectOpt));
        }

        return builder.build();
    }

    /**
     * Returns a supported AF mode which will be preferredMode if it is supported.
     *
     * <p><pre>If preferredMode is not supported, fallback with the following priority (highest to
     * lowest).
     * 1) {@link CaptureRequest#CONTROL_AF_MODE_CONTINUOUS_PICTURE}
     * 2) {@link CaptureRequest#CONTROL_AF_MODE_AUTO)}
     * 3) {@link CaptureRequest#CONTROL_AF_MODE_OFF}
     * </pre>
     */
    @ExecutedBy("mExecutor")
    int getSupportedAfMode(int preferredMode) {
        int[] modes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
        if (modes == null) {
            return CaptureRequest.CONTROL_AF_MODE_OFF;
        }

        // if preferredMode is supported, use it
        if (isModeInList(preferredMode, modes)) {
            return preferredMode;
        }

        // if not found, priority is CONTINUOUS_PICTURE > AUTO > OFF
        if (isModeInList(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE, modes)) {
            return CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE;
        } else if (isModeInList(CaptureRequest.CONTROL_AF_MODE_AUTO, modes)) {
            return CaptureRequest.CONTROL_AF_MODE_AUTO;
        }

        return CaptureRequest.CONTROL_AF_MODE_OFF;
    }

    /**
     * Returns a supported AE mode which will be preferredMode if it is supported.
     *
     * <p><pre>If preferredMode is not supported, fallback with the following priority (highest to
     * lowest).
     * 1) {@link CaptureRequest#CONTROL_AE_MODE_ON}
     * 2) {@link CaptureRequest#CONTROL_AE_MODE_OFF)}
     * </pre>
     */
    @ExecutedBy("mExecutor")
    int getSupportedAeMode(int preferredMode) {
        int[] modes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);

        if (modes == null) {
            return CaptureRequest.CONTROL_AE_MODE_OFF;
        }

        // if preferredMode is supported, use it
        if (isModeInList(preferredMode, modes)) {
            return preferredMode;
        }

        // if not found, priority is AE_ON > AE_OFF
        if (isModeInList(CaptureRequest.CONTROL_AE_MODE_ON, modes)) {
            return CaptureRequest.CONTROL_AE_MODE_ON;
        }

        return CaptureRequest.CONTROL_AE_MODE_OFF;
    }

    /**
     * Returns a supported AWB mode which will be preferredMode if it is supported.
     *
     * <p><pre>If preferredMode is not supported, fallback with the following priority (highest to
     * lowest).
     * 1) {@link CaptureRequest#CONTROL_AWB_MODE_AUTO}
     * 2) {@link CaptureRequest#CONTROL_AWB_MODE_OFF)}
     * </pre>
     */
    @ExecutedBy("mExecutor")
    private int getSupportedAwbMode(int preferredMode) {
        int[] modes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);

        if (modes == null) {
            return CaptureRequest.CONTROL_AWB_MODE_OFF;
        }

        // if preferredMode is supported, use it
        if (isModeInList(preferredMode, modes)) {
            return preferredMode;
        }

        // if not found, priority is AWB_AUTO > AWB_OFF
        if (isModeInList(CaptureRequest.CONTROL_AWB_MODE_AUTO, modes)) {
            return CaptureRequest.CONTROL_AWB_MODE_AUTO;
        }

        return CaptureRequest.CONTROL_AWB_MODE_OFF;
    }

    @ExecutedBy("mExecutor")
    private boolean isModeInList(int mode, int[] modeList) {
        for (int m : modeList) {
            if (mode == m) {
                return true;
            }
        }
        return false;
    }

    int getMaxAfRegionCount() {
        Integer count = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF);
        return count == null ? 0 : count;
    }

    int getMaxAeRegionCount() {
        Integer count = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE);
        return count == null ? 0 : count;
    }

    int getMaxAwbRegionCount() {
        Integer count = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB);
        return count == null ? 0 : count;
    }

    @VisibleForTesting
    long getCurrentSessionUpdateId() {
        return mCurrentSessionUpdateId;
    }

    /** An interface to listen to camera capture results. */
    public interface CaptureResultListener {
        /**
         * Callback to handle camera capture results.
         *
         * @param captureResult camera capture result.
         * @return true to finish listening, false to continue listening.
         */
        boolean onCaptureResult(@NonNull TotalCaptureResult captureResult);
    }

    static final class CameraControlSessionCallback extends CaptureCallback {

        /* synthetic accessor */final Set<CaptureResultListener> mResultListeners = new HashSet<>();
        @CameraExecutor
        private final Executor mExecutor;

        CameraControlSessionCallback(@NonNull @CameraExecutor Executor executor) {
            mExecutor = executor;
        }

        @ExecutedBy("mExecutor")
        void addListener(@NonNull CaptureResultListener listener) {
            mResultListeners.add(listener);
        }

        @ExecutedBy("mExecutor")
        void removeListener(@NonNull CaptureResultListener listener) {
            mResultListeners.remove(listener);
        }

        @Override
        public void onCaptureCompleted(
                @NonNull CameraCaptureSession session,
                @NonNull CaptureRequest request,
                @NonNull final TotalCaptureResult result) {

            mExecutor.execute(() -> {
                Set<CaptureResultListener> removeSet = new HashSet<>();
                for (CaptureResultListener listener : mResultListeners) {
                    boolean isFinished = listener.onCaptureResult(result);
                    if (isFinished) {
                        removeSet.add(listener);
                    }
                }

                if (!removeSet.isEmpty()) {
                    mResultListeners.removeAll(removeSet);
                }
            });
        }
    }

    /**
     * A set of {@link CameraCaptureCallback}s which is capable of adding/removing callbacks
     * dynamically.
     */
    @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
    static final class CameraCaptureCallbackSet extends CameraCaptureCallback {
        Set<CameraCaptureCallback> mCallbacks = new HashSet<>();
        Map<CameraCaptureCallback, Executor> mCallbackExecutors = new ArrayMap<>();

        @ExecutedBy("mExecutor")
        void addCaptureCallback(@NonNull Executor executor,
                @NonNull CameraCaptureCallback callback) {
            mCallbacks.add(callback);
            mCallbackExecutors.put(callback, executor);
        }

        @ExecutedBy("mExecutor")
        void removeCaptureCallback(@NonNull CameraCaptureCallback callback) {
            mCallbacks.remove(callback);
            mCallbackExecutors.remove(callback);
        }

        @ExecutedBy("mExecutor")
        @Override
        public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
            for (CameraCaptureCallback callback : mCallbacks) {
                try {
                    mCallbackExecutors.get(callback).execute(() -> {
                        callback.onCaptureCompleted(cameraCaptureResult);
                    });
                } catch (RejectedExecutionException e) {
                    Logger.e(TAG, "Executor rejected to invoke onCaptureCompleted.", e);
                }
            }
        }

        @ExecutedBy("mExecutor")
        @Override
        public void onCaptureFailed(@NonNull CameraCaptureFailure failure) {
            for (CameraCaptureCallback callback : mCallbacks) {
                try {
                    mCallbackExecutors.get(callback).execute(() -> {
                        callback.onCaptureFailed(failure);
                    });
                } catch (RejectedExecutionException e) {
                    Logger.e(TAG, "Executor rejected to invoke onCaptureFailed.", e);
                }
            }
        }

        @ExecutedBy("mExecutor")
        @Override
        public void onCaptureCancelled() {
            for (CameraCaptureCallback callback : mCallbacks) {
                try {
                    mCallbackExecutors.get(callback).execute(() -> {
                        callback.onCaptureCancelled();
                    });
                } catch (RejectedExecutionException e) {
                    Logger.e(TAG, "Executor rejected to invoke onCaptureCancelled.", e);
                }
            }
        }
    }
}