AdvancedSessionProcessor.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.extensions.internal.sessionprocessor;

import static android.hardware.camera2.CameraExtensionCharacteristics.EXTENSION_AUTOMATIC;
import static android.hardware.camera2.CameraExtensionCharacteristics.EXTENSION_BOKEH;
import static android.hardware.camera2.CameraExtensionCharacteristics.EXTENSION_FACE_RETOUCH;
import static android.hardware.camera2.CameraExtensionCharacteristics.EXTENSION_HDR;
import static android.hardware.camera2.CameraExtensionCharacteristics.EXTENSION_NIGHT;

import android.annotation.SuppressLint;
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.SessionConfiguration;
import android.media.Image;
import android.os.Build;
import android.util.Pair;
import android.util.Size;
import android.view.Surface;

import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CameraCaptureFailure;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.OutputSurface;
import androidx.camera.core.impl.OutputSurfaceConfiguration;
import androidx.camera.core.impl.RequestProcessor;
import androidx.camera.core.impl.SessionProcessor;
import androidx.camera.extensions.ExtensionMode;
import androidx.camera.extensions.impl.advanced.Camera2OutputConfigImpl;
import androidx.camera.extensions.impl.advanced.Camera2SessionConfigImpl;
import androidx.camera.extensions.impl.advanced.ImageProcessorImpl;
import androidx.camera.extensions.impl.advanced.ImageReferenceImpl;
import androidx.camera.extensions.impl.advanced.OutputSurfaceConfigurationImpl;
import androidx.camera.extensions.impl.advanced.OutputSurfaceImpl;
import androidx.camera.extensions.impl.advanced.RequestProcessorImpl;
import androidx.camera.extensions.impl.advanced.SessionProcessorImpl;
import androidx.camera.extensions.internal.ClientVersion;
import androidx.camera.extensions.internal.ExtensionVersion;
import androidx.camera.extensions.internal.RequestOptionConfig;
import androidx.camera.extensions.internal.VendorExtender;
import androidx.camera.extensions.internal.Version;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * A {@link SessionProcessor} based on OEMs' {@link SessionProcessorImpl}.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class AdvancedSessionProcessor extends SessionProcessorBase {
    private static final String TAG = "AdvancedSessionProcessor";
    @NonNull
    private final SessionProcessorImpl mImpl;
    @NonNull
    private final VendorExtender mVendorExtender;
    @NonNull
    private final Context mContext;
    @ExtensionMode.Mode
    private final int mMode;
    @Nullable
    private final MutableLiveData<Integer> mCurrentExtensionTypeLiveData;
    private boolean mIsPostviewConfigured = false;
    // Caches the working capture config so that the new extension strength can be applied on top
    // of the existing config.
    @GuardedBy("mLock")
    private HashMap<CaptureRequest.Key<?>, Object> mWorkingCaptureConfigMap = new HashMap<>();
    // Caches the capture callback adapter so that repeating can be started again to apply the
    // new extension strength setting.
    @GuardedBy("mLock")
    private SessionProcessorImplCaptureCallbackAdapter mRepeatingCaptureCallbackAdapter = null;
    @Nullable
    private final MutableLiveData<Integer> mExtensionStrengthLiveData;
    @Nullable
    private final ExtensionMetadataMonitor mExtensionMetadataMonitor;

    public AdvancedSessionProcessor(@NonNull SessionProcessorImpl impl,
            @NonNull List<CaptureRequest.Key> supportedKeys,
            @NonNull VendorExtender vendorExtender,
            @NonNull Context context) {
        this(impl, supportedKeys, vendorExtender, context, ExtensionMode.NONE);
    }

    public AdvancedSessionProcessor(@NonNull SessionProcessorImpl impl,
            @NonNull List<CaptureRequest.Key> supportedKeys,
            @NonNull VendorExtender vendorExtender,
            @NonNull Context context,
            @ExtensionMode.Mode int mode) {
        super(supportedKeys);
        mImpl = impl;
        mVendorExtender = vendorExtender;
        mContext = context;
        mMode = mode;
        mCurrentExtensionTypeLiveData = isCurrentExtensionModeAvailable() ? new MutableLiveData<>(
                mMode) : null;
        mExtensionStrengthLiveData = isExtensionStrengthAvailable() ? new MutableLiveData<>(100)
                : null;
        if (mCurrentExtensionTypeLiveData != null || mExtensionStrengthLiveData != null) {
            mExtensionMetadataMonitor = new ExtensionMetadataMonitor(mCurrentExtensionTypeLiveData,
                    mExtensionStrengthLiveData);
        } else {
            mExtensionMetadataMonitor = null;
        }
    }

    @NonNull
    @Override
    protected Camera2SessionConfig initSessionInternal(
            @NonNull String cameraId,
            @NonNull Map<String, CameraCharacteristics> cameraCharacteristicsMap,
            @NonNull OutputSurfaceConfiguration outputSurfaceConfig) {
        Camera2SessionConfigImpl sessionConfigImpl = null;
        if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
                && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)) {
            sessionConfigImpl =
                    mImpl.initSession(
                            cameraId,
                            cameraCharacteristicsMap,
                            mContext,
                            new OutputSurfaceConfigurationImplAdapter(outputSurfaceConfig));

        }

        // In case of OEM doesn't implement the v1.4 version of initSession, we fallback to invoke
        // prior version.
        if (sessionConfigImpl == null) {
            sessionConfigImpl =
                    mImpl.initSession(
                            cameraId,
                            cameraCharacteristicsMap,
                            mContext,
                            new OutputSurfaceImplAdapter(
                                    outputSurfaceConfig.getPreviewOutputSurface()),
                            new OutputSurfaceImplAdapter(
                                    outputSurfaceConfig.getImageCaptureOutputSurface()),
                            outputSurfaceConfig.getImageAnalysisOutputSurface() == null
                                    ? null : new OutputSurfaceImplAdapter(
                                    outputSurfaceConfig.getImageAnalysisOutputSurface()));
        }

        mIsPostviewConfigured = outputSurfaceConfig.getPostviewOutputSurface() != null;
        // Resets the extension type and strength result when initializing the session
        if (mCurrentExtensionTypeLiveData != null) {
            mCurrentExtensionTypeLiveData.postValue(mMode);
        }
        if (mExtensionStrengthLiveData != null) {
            mExtensionStrengthLiveData.postValue(100);
        }
        // Convert Camera2SessionConfigImpl(implemented in OEM) into Camera2SessionConfig
        return convertToCamera2SessionConfig(sessionConfigImpl);
    }

    private Camera2SessionConfig convertToCamera2SessionConfig(
            @NonNull Camera2SessionConfigImpl sessionConfigImpl) {
        Camera2SessionConfigBuilder camera2SessionConfigBuilder = new Camera2SessionConfigBuilder();
        for (Camera2OutputConfigImpl outputConfigImpl : sessionConfigImpl.getOutputConfigs()) {
            Camera2OutputConfig outputConfig =
                    Camera2OutputConfigConverter.fromImpl(outputConfigImpl);
            camera2SessionConfigBuilder.addOutputConfig(outputConfig);
        }

        for (CaptureRequest.Key<?> key : sessionConfigImpl.getSessionParameters().keySet()) {
            @SuppressWarnings("unchecked")
            CaptureRequest.Key<Object> objKey = (CaptureRequest.Key<Object>) key;
            camera2SessionConfigBuilder.addSessionParameter(objKey,
                    sessionConfigImpl.getSessionParameters().get(objKey));
        }
        camera2SessionConfigBuilder
                .setSessionTemplateId(sessionConfigImpl.getSessionTemplateId());
        if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
                && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)) {
            try {
                int sessionType = sessionConfigImpl.getSessionType();
                if (sessionType == -1) { // -1 means using default value
                    sessionType = SessionConfiguration.SESSION_REGULAR;
                }
                camera2SessionConfigBuilder.setSessionType(sessionType);
            } catch (NoSuchMethodError e) {
                camera2SessionConfigBuilder.setSessionType(SessionConfiguration.SESSION_REGULAR);
            }
        }
        return camera2SessionConfigBuilder.build();
    }

    @Override
    protected void deInitSessionInternal() {
        synchronized (mLock) {
            // Clears the working config map
            mWorkingCaptureConfigMap = new HashMap<>();
            mRepeatingCaptureCallbackAdapter = null;
        }
        mImpl.deInitSession();
    }

    @Override
    public boolean isCurrentExtensionModeAvailable() {
        return mVendorExtender.isCurrentExtensionModeAvailable();
    }

    @NonNull
    @Override
    public LiveData<Integer> getCurrentExtensionMode() {
        return mCurrentExtensionTypeLiveData;
    }

    @Override
    public boolean isExtensionStrengthAvailable() {
        return mVendorExtender.isExtensionStrengthAvailable();
    }

    @Override
    @SuppressWarnings("unchecked")
    public void setExtensionStrength(@IntRange(from = 0, to = 100) int strength) {
        if (!isExtensionStrengthAvailable()
                || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            return;
        }

        SessionProcessorImplCaptureCallbackAdapter captureCallbackAdapter;
        HashMap<CaptureRequest.Key<?>, Object> captureConfigMap;

        synchronized (mLock) {
            mExtensionStrength = strength;
            mWorkingCaptureConfigMap.put(CaptureRequest.EXTENSION_STRENGTH, mExtensionStrength);
            captureCallbackAdapter = mRepeatingCaptureCallbackAdapter;
            captureConfigMap =
                    (HashMap<CaptureRequest.Key<?>, Object>) mWorkingCaptureConfigMap.clone();
        }

        mImpl.setParameters(captureConfigMap);

        // Starts the repeating again to apply the new strength setting if it has been started.
        // Otherwise, the new strength setting will be applied when the capture session is
        // configured and repeating is started.
        if (captureCallbackAdapter != null) {
            mImpl.startRepeating(captureCallbackAdapter);
        }
    }

    @SuppressLint("KotlinPropertyAccess")
    @NonNull
    @Override
    public LiveData<Integer> getExtensionStrength() {
        return mExtensionStrengthLiveData;
    }

    @Override
    public void setParameters(
            @NonNull Config parameters) {
        HashMap<CaptureRequest.Key<?>, Object> captureConfigMap;

        synchronized (mLock) {
            captureConfigMap = convertConfigToMap(parameters);
            // Applies extension strength setting if it is set via
            // CameraExtensionsControl#setExtensionStrength() API.
            if (mExtensionStrength != EXTENSION_STRENGTH_UNKNOWN
                    && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                captureConfigMap.put(CaptureRequest.EXTENSION_STRENGTH, mExtensionStrength);
            }
            mWorkingCaptureConfigMap = captureConfigMap;
        }

        mImpl.setParameters(captureConfigMap);
    }

    @NonNull
    private static HashMap<CaptureRequest.Key<?>, Object> convertConfigToMap(
            @NonNull Config parameters) {
        HashMap<CaptureRequest.Key<?>, Object> map = new HashMap<>();

        RequestOptionConfig options =
                RequestOptionConfig.Builder.from(parameters).build();

        for (Config.Option<?> option : options.listOptions()) {
            @SuppressWarnings("unchecked")
            CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
            map.put(key, options.retrieveOption(option));
        }
        return map;
    }

    @Override
    public void onCaptureSessionStart(
            @NonNull RequestProcessor requestProcessor) {
        mImpl.onCaptureSessionStart(new RequestProcessorImplAdapter(requestProcessor));
    }

    @Override
    public void onCaptureSessionEnd() {
        mImpl.onCaptureSessionEnd();
    }

    @Override
    public int startCapture(
            boolean postviewEnabled,
            @NonNull SessionProcessor.CaptureCallback callback) {
        SessionProcessorImplCaptureCallbackAdapter stillCaptureCallback =
                new SessionProcessorImplCaptureCallbackAdapter(callback);

        if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
                && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
                && mIsPostviewConfigured && postviewEnabled
                && mVendorExtender.isPostviewAvailable()) {
            Logger.d(TAG, "startCaptureWithPostview");
            return mImpl.startCaptureWithPostview(stillCaptureCallback);
        } else {
            Logger.d(TAG, "startCapture");
            return mImpl.startCapture(stillCaptureCallback);
        }
    }

    @Override
    public int startRepeating(@NonNull SessionProcessor.CaptureCallback callback) {
        SessionProcessorImplCaptureCallbackAdapter captureCallbackAdapter;
        synchronized (mLock) {
            captureCallbackAdapter = new SessionProcessorImplCaptureCallbackAdapter(callback,
                    mExtensionMetadataMonitor);
            mRepeatingCaptureCallbackAdapter = captureCallbackAdapter;
        }
        return mImpl.startRepeating(captureCallbackAdapter);
    }

    @Override
    public int startTrigger(@NonNull Config config, @NonNull CaptureCallback callback) {
        HashMap<CaptureRequest.Key<?>, Object> map = convertConfigToMap(config);
        if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_3)
                && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_3)) {
            return mImpl.startTrigger(map,
                    new SessionProcessorImplCaptureCallbackAdapter(callback));
        }
        return -1;
    }

    @Override
    public void stopRepeating() {
        mImpl.stopRepeating();
        synchronized (mLock) {
            mRepeatingCaptureCallbackAdapter = null;
        }
    }

    @Override
    public void abortCapture(int captureSequenceId) {
        mImpl.abortCapture(captureSequenceId);
    }

    @Nullable
    @Override
    public Pair<Long, Long> getRealtimeCaptureLatency() {
        if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
                && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)) {
            return mImpl.getRealtimeCaptureLatency();
        }
        return null;
    }

    @NonNull
    @Override
    public Map<Integer, List<Size>> getSupportedPostviewSize(@NonNull Size captureSize) {
        return mVendorExtender.getSupportedPostviewResolutions(captureSize);
    }

    /**
     * Adapter to transform a {@link OutputSurface} to a {@link OutputSurfaceImpl}.
     */
    private static class OutputSurfaceImplAdapter implements OutputSurfaceImpl {
        private final OutputSurface mOutputSurface;

        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
        OutputSurfaceImplAdapter(OutputSurface outputSurface) {
            mOutputSurface = outputSurface;
        }

        @NonNull
        @Override
        public Surface getSurface() {
            return mOutputSurface.getSurface();
        }

        @NonNull
        @Override
        public Size getSize() {
            return mOutputSurface.getSize();
        }

        @Override
        public int getImageFormat() {
            return mOutputSurface.getImageFormat();
        }
    }

    private static class OutputSurfaceConfigurationImplAdapter implements
            OutputSurfaceConfigurationImpl {
        private final OutputSurfaceImpl mPreviewOutputSurface;
        private final OutputSurfaceImpl mCaptureOutputSurface;
        private final OutputSurfaceImpl mAnalysisOutputSurface;
        private final OutputSurfaceImpl mPostviewOutputSurface;

        OutputSurfaceConfigurationImplAdapter(
                @NonNull OutputSurfaceConfiguration outputSurfaceConfig) {
            mPreviewOutputSurface = new OutputSurfaceImplAdapter(
                    outputSurfaceConfig.getPreviewOutputSurface());
            mCaptureOutputSurface = new OutputSurfaceImplAdapter(
                    outputSurfaceConfig.getImageCaptureOutputSurface());
            mAnalysisOutputSurface =
                    outputSurfaceConfig.getImageAnalysisOutputSurface() != null
                            ? new OutputSurfaceImplAdapter(
                            outputSurfaceConfig.getImageAnalysisOutputSurface()) : null;
            mPostviewOutputSurface =
                    outputSurfaceConfig.getPostviewOutputSurface() != null
                            ? new OutputSurfaceImplAdapter(
                            outputSurfaceConfig.getPostviewOutputSurface()) : null;
        }

        @NonNull
        @Override
        public OutputSurfaceImpl getPreviewOutputSurface() {
            return mPreviewOutputSurface;
        }

        @NonNull
        @Override
        public OutputSurfaceImpl getImageCaptureOutputSurface() {
            return mCaptureOutputSurface;
        }

        @Nullable
        @Override
        public OutputSurfaceImpl getImageAnalysisOutputSurface() {
            return mAnalysisOutputSurface;
        }

        @Nullable
        @Override
        public OutputSurfaceImpl getPostviewOutputSurface() {
            return mPostviewOutputSurface;
        }
    }

    /**
     * Adapter to transform a {@link RequestProcessor} to {@link RequestProcessorImpl}.
     */
    private class RequestProcessorImplAdapter implements RequestProcessorImpl {
        private final RequestProcessor mRequestProcessor;

        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
        RequestProcessorImplAdapter(@NonNull RequestProcessor requestProcessor) {
            mRequestProcessor = requestProcessor;
        }

        @Override
        public void setImageProcessor(int outputConfigId,
                @NonNull ImageProcessorImpl imageProcessorImpl) {
            AdvancedSessionProcessor.this.setImageProcessor(outputConfigId,
                    new ImageProcessorAdapter(imageProcessorImpl));
        }

        @Override
        public int submit(
                @NonNull RequestProcessorImpl.Request request, @NonNull Callback callback) {
            return mRequestProcessor.submit(new RequestAdapter(request),
                    new CallbackAdapter(callback));
        }

        @Override
        public int submit(
                @NonNull List<RequestProcessorImpl.Request> requests, @NonNull Callback callback) {
            ArrayList<RequestProcessor.Request> outRequests = new ArrayList<>();
            for (RequestProcessorImpl.Request request : requests) {
                outRequests.add(new RequestAdapter(request));
            }
            return mRequestProcessor.submit(outRequests, new CallbackAdapter(callback));
        }

        @Override
        public int setRepeating(
                @NonNull RequestProcessorImpl.Request request, @NonNull Callback callback) {
            return mRequestProcessor.setRepeating(new RequestAdapter(request),
                    new CallbackAdapter(callback));
        }

        @Override
        public void abortCaptures() {
            mRequestProcessor.abortCaptures();
        }

        @Override
        public void stopRepeating() {
            mRequestProcessor.stopRepeating();
        }
    }

    /**
     * Adapter to transform a {@link RequestProcessorImpl.Request} to a
     * {@link RequestProcessor.Request}.
     */
    private static class RequestAdapter implements RequestProcessor.Request {
        private final RequestProcessorImpl.Request mImplRequest;
        private final List<Integer> mTargetOutputConfigIds;
        private final Config mParameters;
        private final int mTemplateId;

        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
        RequestAdapter(@NonNull RequestProcessorImpl.Request implRequest) {
            mImplRequest = implRequest;

            List<Integer> targetOutputConfigIds = new ArrayList<>();
            for (Integer outputConfigId : implRequest.getTargetOutputConfigIds()) {
                targetOutputConfigIds.add(outputConfigId);
            }
            mTargetOutputConfigIds = targetOutputConfigIds;

            RequestOptionConfig.Builder optionBuilder = new RequestOptionConfig.Builder();
            for (CaptureRequest.Key<?> key : implRequest.getParameters().keySet()) {
                @SuppressWarnings("unchecked")
                CaptureRequest.Key<Object> objKey = (CaptureRequest.Key<Object>) key;
                optionBuilder.setCaptureRequestOption(objKey,
                        implRequest.getParameters().get(objKey));
            }
            mParameters = optionBuilder.build();
            mTemplateId = implRequest.getTemplateId();
        }

        @NonNull
        @Override
        public List<Integer> getTargetOutputConfigIds() {
            return mTargetOutputConfigIds;
        }

        @NonNull
        @Override
        public Config getParameters() {
            return mParameters;
        }

        @Override
        public int getTemplateId() {
            return mTemplateId;
        }

        @Nullable
        public RequestProcessorImpl.Request getImplRequest() {
            return mImplRequest;
        }
    }

    /**
     * Adapter to transform a {@link ImageProcessorImpl} to {@link ImageProcessor}.
     */
    private static class ImageProcessorAdapter implements ImageProcessor {
        private final ImageProcessorImpl mImpl;

        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
        ImageProcessorAdapter(ImageProcessorImpl impl) {
            mImpl = impl;
        }

        @Override
        public void onNextImageAvailable(int outputStreamId, long timestampNs,
                @NonNull ImageReference imageReference, @Nullable String physicalCameraId) {
            mImpl.onNextImageAvailable(outputStreamId, timestampNs,
                    new ImageReferenceImplAdapter(imageReference), physicalCameraId);
        }
    }

    /**
     * Adapter to transform a {@link ImageReference} to a {@link ImageReferenceImpl}.
     */
    private static class ImageReferenceImplAdapter implements ImageReferenceImpl {
        private final ImageReference mImageReference;

        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
        ImageReferenceImplAdapter(ImageReference imageReference) {
            mImageReference = imageReference;
        }

        @Override
        public boolean increment() {
            return mImageReference.increment();
        }

        @Override
        public boolean decrement() {
            return mImageReference.decrement();
        }

        @Nullable
        @Override
        public Image get() {
            return mImageReference.get();
        }
    }

    /**
     * Adapter to transform a {@link RequestProcessorImpl.Callback} to a
     * {@link RequestProcessor.Callback}.
     */
    private static class CallbackAdapter implements RequestProcessor.Callback {
        private final RequestProcessorImpl.Callback mCallback;

        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
        CallbackAdapter(@NonNull RequestProcessorImpl.Callback callback) {
            mCallback = callback;
        }

        @Override
        public void onCaptureStarted(
                @NonNull RequestProcessor.Request request,
                long frameNumber, long timestamp) {
            mCallback.onCaptureStarted(getImplRequest(request), frameNumber,
                    timestamp);
        }

        @Override
        public void onCaptureProgressed(
                @NonNull RequestProcessor.Request request,
                @NonNull CameraCaptureResult cameraCaptureResult) {
            CaptureResult captureResult = cameraCaptureResult.getCaptureResult();
            Preconditions.checkArgument(captureResult != null,
                    "Cannot get CaptureResult from the cameraCaptureResult ");
            mCallback.onCaptureProgressed(getImplRequest(request), captureResult);
        }

        @Override
        public void onCaptureCompleted(
                @NonNull RequestProcessor.Request request,
                @Nullable CameraCaptureResult cameraCaptureResult) {
            CaptureResult captureResult = cameraCaptureResult.getCaptureResult();
            Preconditions.checkArgument(captureResult instanceof TotalCaptureResult,
                    "CaptureResult in cameraCaptureResult is not a TotalCaptureResult");
            mCallback.onCaptureCompleted(getImplRequest(request),
                    (TotalCaptureResult) captureResult);
        }

        @Override
        public void onCaptureFailed(
                @NonNull RequestProcessor.Request request,
                @Nullable CameraCaptureFailure cameraCaptureFailure) {
            Object captureFailure = cameraCaptureFailure.getCaptureFailure();
            Preconditions.checkArgument(captureFailure instanceof CaptureFailure,
                    "CameraCaptureFailure does not contain CaptureFailure.");
            mCallback.onCaptureFailed(getImplRequest(request), (CaptureFailure) captureFailure);
        }

        @Override
        public void onCaptureBufferLost(
                @NonNull RequestProcessor.Request request,
                long frameNumber, int outputStreamId) {
            mCallback.onCaptureBufferLost(getImplRequest(request), frameNumber, outputStreamId);
        }

        @Override
        public void onCaptureSequenceCompleted(
                int sequenceId, long frameNumber) {
            mCallback.onCaptureSequenceCompleted(sequenceId, frameNumber);
        }

        @Override
        public void onCaptureSequenceAborted(int sequenceId) {
            mCallback.onCaptureSequenceAborted(sequenceId);
        }

        private RequestProcessorImpl.Request getImplRequest(
                RequestProcessor.Request request) {
            Preconditions.checkArgument(request instanceof RequestAdapter);

            RequestAdapter requestProcessorRequest = (RequestAdapter) request;
            return requestProcessorRequest.getImplRequest();
        }
    }

    /**
     * Adapter to transform a {@link SessionProcessor.CaptureCallback} to a
     * {@link SessionProcessorImpl.CaptureCallback}.
     */
    private static class SessionProcessorImplCaptureCallbackAdapter implements
            SessionProcessorImpl.CaptureCallback {
        private final SessionProcessor.CaptureCallback mCaptureCallback;
        @Nullable
        private final ExtensionMetadataMonitor mExtensionMetadataMonitor;

        SessionProcessorImplCaptureCallbackAdapter(
                @NonNull SessionProcessor.CaptureCallback callback) {
            this(callback, null);
        }

        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
        SessionProcessorImplCaptureCallbackAdapter(
                @NonNull SessionProcessor.CaptureCallback callback,
                @Nullable ExtensionMetadataMonitor extensionMetadataMonitor) {
            mCaptureCallback = callback;
            mExtensionMetadataMonitor = extensionMetadataMonitor;
        }

        @Override
        public void onCaptureStarted(
                int captureSequenceId,
                long timestamp) {
            mCaptureCallback.onCaptureStarted(captureSequenceId, timestamp);
        }

        @Override
        public void onCaptureProcessStarted(
                int captureSequenceId) {
            mCaptureCallback.onCaptureProcessStarted(captureSequenceId);
        }

        @Override
        public void onCaptureFailed(int captureSequenceId) {
            mCaptureCallback.onCaptureFailed(captureSequenceId);
        }

        @Override
        public void onCaptureSequenceCompleted(int captureSequenceId) {
            mCaptureCallback.onCaptureSequenceCompleted(captureSequenceId);
        }

        @Override
        public void onCaptureSequenceAborted(int captureSequenceId) {
            mCaptureCallback.onCaptureSequenceAborted(captureSequenceId);
        }

        @Override
        public void onCaptureCompleted(long timestamp, int captureSequenceId,
                Map<CaptureResult.Key, Object> result) {
            if (mExtensionMetadataMonitor != null) {
                mExtensionMetadataMonitor.checkExtensionMetadata(result);
            }
            mCaptureCallback.onCaptureCompleted(timestamp, captureSequenceId, result);
        }

        @Override
        public void onCaptureProcessProgressed(int progress) {
            mCaptureCallback.onCaptureProcessProgressed(progress);
        }
    }

    /**
     * Monitors the extension metadata (extension strength, type) changes from the capture results.
     */
    private static class ExtensionMetadataMonitor {
        @Nullable
        private final MutableLiveData<Integer> mCurrentExtensionTypeLiveData;
        @Nullable
        private final MutableLiveData<Integer> mExtensionStrengthLiveData;

        ExtensionMetadataMonitor(
                @Nullable MutableLiveData<Integer> currentExtensionTypeLiveData,
                @Nullable MutableLiveData<Integer> extensionStrengthLiveData) {
            mCurrentExtensionTypeLiveData = currentExtensionTypeLiveData;
            mExtensionStrengthLiveData = extensionStrengthLiveData;
        }

        void checkExtensionMetadata(Map<CaptureResult.Key, Object> captureResult) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {

                if (mCurrentExtensionTypeLiveData != null) {
                    // Monitors and update current extension type
                    Object extensionType = captureResult.get(CaptureResult.EXTENSION_CURRENT_TYPE);
                    // The returned type should be the value defined by the Camera2 API.
                    // Needs to
                    // convert it to the value defined by CameraX.
                    if (extensionType != null && !Objects.equals(
                            mCurrentExtensionTypeLiveData.getValue(),
                            convertExtensionMode((int) extensionType))) {
                        mCurrentExtensionTypeLiveData.postValue(
                                convertExtensionMode((int) extensionType));
                    }
                }

                if (mExtensionStrengthLiveData != null) {
                    // Monitors and update current extension strength
                    Object extensionStrength = captureResult.get(CaptureResult.EXTENSION_STRENGTH);
                    if (extensionStrength != null && !Objects.equals(
                            mExtensionStrengthLiveData.getValue(), extensionStrength)) {
                        mExtensionStrengthLiveData.postValue((Integer) extensionStrength);
                    }
                }
            }
        }

        @ExtensionMode.Mode
        private int convertExtensionMode(int camera2ExtensionMode) {
            switch (camera2ExtensionMode) {
                case EXTENSION_AUTOMATIC:
                    return ExtensionMode.AUTO;
                case EXTENSION_FACE_RETOUCH:
                    return ExtensionMode.FACE_RETOUCH;
                case EXTENSION_BOKEH:
                    return ExtensionMode.BOKEH;
                case EXTENSION_HDR:
                    return ExtensionMode.HDR;
                case EXTENSION_NIGHT:
                    return ExtensionMode.NIGHT;
                default:
                    return ExtensionMode.NONE;
            }
        }
    }
}