/*
 * Copyright (C) 2013 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 android.hardware.camera2.impl;

import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.Context;
import android.graphics.ImageFormat;
import android.hardware.ICameraService;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraExtensionCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CameraOfflineSession;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.ICameraDeviceCallbacks;
import android.hardware.camera2.ICameraDeviceUser;
import android.hardware.camera2.ICameraOfflineSession;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.ExtensionSessionConfiguration;
import android.hardware.camera2.params.InputConfiguration;
import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
import android.hardware.camera2.params.MultiResolutionStreamInfo;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.hardware.camera2.utils.SubmitInfo;
import android.hardware.camera2.utils.SurfaceUtils;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.util.Log;
import android.util.Range;
import android.util.Size;
import android.util.SparseArray;
import android.view.Surface;

import com.android.internal.camera.flags.Flags;

import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * HAL2.1+ implementation of CameraDevice. Use CameraManager#open to instantiate
 */
public class CameraDeviceImpl extends CameraDevice
        implements IBinder.DeathRecipient {
    private final String TAG;
    private final boolean DEBUG = false;

    private static final int REQUEST_ID_NONE = -1;

    /**
     * Starting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM},
     * {@link #isSessionConfigurationSupported} also checks for compatibility of session parameters
     * when supported by the HAL. This ChangeId guards enabling that functionality for apps
     * that target {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
    static final long CHECK_PARAMS_IN_IS_SESSION_CONFIGURATION_SUPPORTED = 320741775;

    // TODO: guard every function with if (!mRemoteDevice) check (if it was closed)
    private ICameraDeviceUserWrapper mRemoteDevice;
    private boolean mRemoteDeviceInit = false;

    // CameraDeviceSetup object to delegate some of the newer calls to.
    @Nullable private final CameraDeviceSetup mCameraDeviceSetup;

    // Lock to synchronize cross-thread access to device public interface
    final Object mInterfaceLock = new Object(); // access from this class and Session only!
    private final CameraDeviceCallbacks mCallbacks = new CameraDeviceCallbacks();

    private final StateCallback mDeviceCallback;
    private volatile StateCallbackKK mSessionStateCallback;
    private final Executor mDeviceExecutor;

    private final AtomicBoolean mClosing = new AtomicBoolean();
    private boolean mInError = false;
    private boolean mIdle = true;

    /** map request IDs to callback/request data */
    private SparseArray<CaptureCallbackHolder> mCaptureCallbackMap =
            new SparseArray<CaptureCallbackHolder>();

    /** map request IDs which have batchedOutputs to requestCount*/
    private HashMap<Integer, Integer> mBatchOutputMap = new HashMap<>();

    private int mRepeatingRequestId = REQUEST_ID_NONE;
    // Latest repeating request list's types
    private int[] mRepeatingRequestTypes;

    // Cache failed requests to process later in case of a repeating error callback
    private int mFailedRepeatingRequestId = REQUEST_ID_NONE;
    private int[] mFailedRepeatingRequestTypes;

    // Map stream IDs to input/output configurations
    private SimpleEntry<Integer, InputConfiguration> mConfiguredInput =
            new SimpleEntry<>(REQUEST_ID_NONE, null);
    private final SparseArray<OutputConfiguration> mConfiguredOutputs =
            new SparseArray<>();

    // Cache all stream IDs capable of supporting offline mode.
    private final HashSet<Integer> mOfflineSupport = new HashSet<>();

    private final String mCameraId;
    private final CameraCharacteristics mCharacteristics;
    private Map<String, CameraCharacteristics> mPhysicalIdsToChars;
    private final CameraManager mCameraManager;
    private final int mTotalPartialCount;
    private final Context mContext;

    private static final long NANO_PER_SECOND = 1000000000; //ns

    /**
     * A list tracking request and its expected last regular/reprocess/zslStill frame
     * number. Updated when calling ICameraDeviceUser methods.
     */
    private final List<RequestLastFrameNumbersHolder> mRequestLastFrameNumbersList =
            new ArrayList<>();

    /**
     * An object tracking received frame numbers.
     * Updated when receiving callbacks from ICameraDeviceCallbacks.
     */
    private FrameNumberTracker mFrameNumberTracker = new FrameNumberTracker();

    private CameraCaptureSessionCore mCurrentSession;
    private CameraExtensionSessionImpl mCurrentExtensionSession;
    private CameraAdvancedExtensionSessionImpl mCurrentAdvancedExtensionSession;
    private int mNextSessionId = 0;

    private final int mAppTargetSdkVersion;

    private ExecutorService mOfflineSwitchService;
    private CameraOfflineSessionImpl mOfflineSessionImpl;

    // Runnables for all state transitions, except error, which needs the
    // error code argument

    private final Runnable mCallOnOpened = new Runnable() {
        @Override
        public void run() {
            StateCallbackKK sessionCallback = null;
            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed

                sessionCallback = mSessionStateCallback;
            }
            if (sessionCallback != null) {
                sessionCallback.onOpened(CameraDeviceImpl.this);
            }
            mDeviceCallback.onOpened(CameraDeviceImpl.this);
        }
    };

    private final Runnable mCallOnUnconfigured = new Runnable() {
        @Override
        public void run() {
            StateCallbackKK sessionCallback = null;
            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed

                sessionCallback = mSessionStateCallback;
            }
            if (sessionCallback != null) {
                sessionCallback.onUnconfigured(CameraDeviceImpl.this);
            }
        }
    };

    private final Runnable mCallOnActive = new Runnable() {
        @Override
        public void run() {
            StateCallbackKK sessionCallback = null;
            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed

                sessionCallback = mSessionStateCallback;
            }
            if (sessionCallback != null) {
                sessionCallback.onActive(CameraDeviceImpl.this);
            }
        }
    };

    private final Runnable mCallOnBusy = new Runnable() {
        @Override
        public void run() {
            StateCallbackKK sessionCallback = null;
            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed

                sessionCallback = mSessionStateCallback;
            }
            if (sessionCallback != null) {
                sessionCallback.onBusy(CameraDeviceImpl.this);
            }
        }
    };

    private final Runnable mCallOnClosed = new Runnable() {
        private boolean mClosedOnce = false;

        @Override
        public void run() {
            if (mClosedOnce) {
                throw new AssertionError("Don't post #onClosed more than once");
            }
            StateCallbackKK sessionCallback = null;
            synchronized(mInterfaceLock) {
                sessionCallback = mSessionStateCallback;
            }
            if (sessionCallback != null) {
                sessionCallback.onClosed(CameraDeviceImpl.this);
            }
            mDeviceCallback.onClosed(CameraDeviceImpl.this);
            mClosedOnce = true;
        }
    };

    private final Runnable mCallOnIdle = new Runnable() {
        @Override
        public void run() {
            StateCallbackKK sessionCallback = null;
            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed

                sessionCallback = mSessionStateCallback;
            }
            if (sessionCallback != null) {
                sessionCallback.onIdle(CameraDeviceImpl.this);
            }
        }
    };

    private final Runnable mCallOnDisconnected = new Runnable() {
        @Override
        public void run() {
            StateCallbackKK sessionCallback = null;
            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed

                sessionCallback = mSessionStateCallback;
            }
            if (sessionCallback != null) {
                sessionCallback.onDisconnected(CameraDeviceImpl.this);
            }
            mDeviceCallback.onDisconnected(CameraDeviceImpl.this);
        }
    };

    private class ClientStateCallback extends StateCallback {
        private final Executor mClientExecutor;
        private final StateCallback mClientStateCallback;

        private ClientStateCallback(@NonNull Executor clientExecutor,
                @NonNull StateCallback clientStateCallback) {
            mClientExecutor = clientExecutor;
            mClientStateCallback = clientStateCallback;
        }

        public void onClosed(@NonNull CameraDevice camera) {
            mClientExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    mClientStateCallback.onClosed(camera);
                }
            });
        }
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mClientExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    mClientStateCallback.onOpened(camera);
                }
            });
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            mClientExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    mClientStateCallback.onDisconnected(camera);
                }
            });
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            mClientExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    mClientStateCallback.onError(camera, error);
                }
            });
        }
    }

    public CameraDeviceImpl(String cameraId, StateCallback callback, Executor executor,
                        CameraCharacteristics characteristics,
                        @NonNull CameraManager manager,
                        int appTargetSdkVersion,
                        Context ctx,
                        @Nullable CameraDevice.CameraDeviceSetup cameraDeviceSetup) {
        if (cameraId == null || callback == null || executor == null || characteristics == null
                || manager == null) {
            throw new IllegalArgumentException("Null argument given");
        }
        mCameraId = cameraId;
        if (Flags.singleThreadExecutor()) {
            mDeviceCallback = new ClientStateCallback(executor, callback);
            mDeviceExecutor = Executors.newSingleThreadExecutor();
        } else {
            mDeviceCallback = callback;
            mDeviceExecutor = executor;
        }
        mCharacteristics = characteristics;
        mCameraManager = manager;
        mAppTargetSdkVersion = appTargetSdkVersion;
        mContext = ctx;
        mCameraDeviceSetup = cameraDeviceSetup;

        final int MAX_TAG_LEN = 23;
        String tag = String.format("CameraDevice-JV-%s", mCameraId);
        if (tag.length() > MAX_TAG_LEN) {
            tag = tag.substring(0, MAX_TAG_LEN);
        }
        TAG = tag;

        Integer partialCount =
                mCharacteristics.get(CameraCharacteristics.REQUEST_PARTIAL_RESULT_COUNT);
        if (partialCount == null) {
            // 1 means partial result is not supported.
            mTotalPartialCount = 1;
        } else {
            mTotalPartialCount = partialCount;
        }
    }

    private Map<String, CameraCharacteristics> getPhysicalIdToChars() {
        if (mPhysicalIdsToChars == null) {
            try {
                mPhysicalIdsToChars = mCameraManager.getPhysicalIdToCharsMap(mCharacteristics);
            } catch (CameraAccessException e) {
                Log.e(TAG, "Unable to query the physical characteristics map!");
            }
        }

        return mPhysicalIdsToChars;
    }

    public CameraDeviceCallbacks getCallbacks() {
        return mCallbacks;
    }

    /**
     * Set remote device, which triggers initial onOpened/onUnconfigured callbacks
     *
     * <p>This function may post onDisconnected and throw CAMERA_DISCONNECTED if remoteDevice dies
     * during setup.</p>
     *
     */
    public void setRemoteDevice(ICameraDeviceUser remoteDevice) throws CameraAccessException {
        synchronized(mInterfaceLock) {
            // TODO: Move from decorator to direct binder-mediated exceptions
            // If setRemoteFailure already called, do nothing
            if (mInError) return;

            mRemoteDevice = new ICameraDeviceUserWrapper(remoteDevice);

            IBinder remoteDeviceBinder = remoteDevice.asBinder();
            // For legacy camera device, remoteDevice is in the same process, and
            // asBinder returns NULL.
            if (remoteDeviceBinder != null) {
                try {
                    remoteDeviceBinder.linkToDeath(this, /*flag*/ 0);
                } catch (RemoteException e) {
                    CameraDeviceImpl.this.mDeviceExecutor.execute(mCallOnDisconnected);

                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
                            "The camera device has encountered a serious error");
                }
            }

            mDeviceExecutor.execute(mCallOnOpened);
            mDeviceExecutor.execute(mCallOnUnconfigured);

            mRemoteDeviceInit = true;
        }
    }

    /**
     * Call to indicate failed connection to a remote camera device.
     *
     * <p>This places the camera device in the error state and informs the callback.
     * Use in place of setRemoteDevice() when startup fails.</p>
     */
    public void setRemoteFailure(final ServiceSpecificException failure) {
        int failureCode = StateCallback.ERROR_CAMERA_DEVICE;
        boolean failureIsError = true;

        switch (failure.errorCode) {
            case ICameraService.ERROR_CAMERA_IN_USE:
                failureCode = StateCallback.ERROR_CAMERA_IN_USE;
                break;
            case ICameraService.ERROR_MAX_CAMERAS_IN_USE:
                failureCode = StateCallback.ERROR_MAX_CAMERAS_IN_USE;
                break;
            case ICameraService.ERROR_DISABLED:
                failureCode = StateCallback.ERROR_CAMERA_DISABLED;
                break;
            case ICameraService.ERROR_DISCONNECTED:
                failureIsError = false;
                break;
            case ICameraService.ERROR_INVALID_OPERATION:
                failureCode = StateCallback.ERROR_CAMERA_DEVICE;
                break;
            default:
                Log.e(TAG, "Unexpected failure in opening camera device: " + failure.errorCode +
                        failure.getMessage());
                break;
        }
        final int code = failureCode;
        final boolean isError = failureIsError;
        synchronized(mInterfaceLock) {
            mInError = true;
            mDeviceExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    if (isError) {
                        mDeviceCallback.onError(CameraDeviceImpl.this, code);
                    } else {
                        mDeviceCallback.onDisconnected(CameraDeviceImpl.this);
                    }
                }
            });
        }
    }

    @Override
    public String getId() {
        return mCameraId;
    }

    public void configureOutputs(List<Surface> outputs) throws CameraAccessException {
        // Leave this here for backwards compatibility with older code using this directly
        ArrayList<OutputConfiguration> outputConfigs = new ArrayList<>(outputs.size());
        for (Surface s : outputs) {
            outputConfigs.add(new OutputConfiguration(s));
        }
        configureStreamsChecked(/*inputConfig*/null, outputConfigs,
                /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null,
                SystemClock.uptimeMillis());

    }

    /**
     * Attempt to configure the input and outputs; the device goes to idle and then configures the
     * new input and outputs if possible.
     *
     * <p>The configuration may gracefully fail, if input configuration is not supported,
     * if there are too many outputs, if the formats are not supported, or if the sizes for that
     * format is not supported. In this case this function will return {@code false} and the
     * unconfigured callback will be fired.</p>
     *
     * <p>If the configuration succeeds (with 1 or more outputs with or without an input),
     * then the idle callback is fired. Unconfiguring the device always fires the idle callback.</p>
     *
     * @param inputConfig input configuration or {@code null} for no input
     * @param outputs a list of one or more surfaces, or {@code null} to unconfigure
     * @param operatingMode If the stream configuration is for a normal session,
     *     a constrained high speed session, or something else.
     * @param sessionParams Session parameters.
     * @param createSessionStartTimeMs The timestamp when session creation starts, measured by
     *     uptimeMillis().
     * @return whether or not the configuration was successful
     *
     * @throws CameraAccessException if there were any unexpected problems during configuration
     */
    public boolean configureStreamsChecked(InputConfiguration inputConfig,
            List<OutputConfiguration> outputs, int operatingMode, CaptureRequest sessionParams,
            long createSessionStartTime)
                    throws CameraAccessException {
        // Treat a null input the same an empty list
        if (outputs == null) {
            outputs = new ArrayList<OutputConfiguration>();
        }
        if (outputs.size() == 0 && inputConfig != null) {
            throw new IllegalArgumentException("cannot configure an input stream without " +
                    "any output streams");
        }

        checkInputConfiguration(inputConfig);

        boolean success = false;

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            // Streams to create
            HashSet<OutputConfiguration> addSet = new HashSet<OutputConfiguration>(outputs);
            // Streams to delete
            List<Integer> deleteList = new ArrayList<Integer>();

            // Determine which streams need to be created, which to be deleted
            for (int i = 0; i < mConfiguredOutputs.size(); ++i) {
                int streamId = mConfiguredOutputs.keyAt(i);
                OutputConfiguration outConfig = mConfiguredOutputs.valueAt(i);

                if (!outputs.contains(outConfig) || outConfig.isDeferredConfiguration()) {
                    // Always delete the deferred output configuration when the session
                    // is created, as the deferred output configuration doesn't have unique surface
                    // related identifies.
                    deleteList.add(streamId);
                } else {
                    addSet.remove(outConfig);  // Don't create a stream previously created
                }
            }

            mDeviceExecutor.execute(mCallOnBusy);
            stopRepeating();

            try {
                waitUntilIdle();

                mRemoteDevice.beginConfigure();

                // reconfigure the input stream if the input configuration is different.
                InputConfiguration currentInputConfig = mConfiguredInput.getValue();
                if (inputConfig != currentInputConfig &&
                        (inputConfig == null || !inputConfig.equals(currentInputConfig))) {
                    if (currentInputConfig != null) {
                        mRemoteDevice.deleteStream(mConfiguredInput.getKey());
                        mConfiguredInput = new SimpleEntry<Integer, InputConfiguration>(
                                REQUEST_ID_NONE, null);
                    }
                    if (inputConfig != null) {
                        int streamId = mRemoteDevice.createInputStream(inputConfig.getWidth(),
                                inputConfig.getHeight(), inputConfig.getFormat(),
                                inputConfig.isMultiResolution());
                        mConfiguredInput = new SimpleEntry<Integer, InputConfiguration>(
                                streamId, inputConfig);
                    }
                }

                // Delete all streams first (to free up HW resources)
                for (Integer streamId : deleteList) {
                    mRemoteDevice.deleteStream(streamId);
                    mConfiguredOutputs.delete(streamId);
                }

                // Add all new streams
                for (OutputConfiguration outConfig : outputs) {
                    if (addSet.contains(outConfig)) {
                        int streamId = mRemoteDevice.createStream(outConfig);
                        mConfiguredOutputs.put(streamId, outConfig);
                    }
                }

                int offlineStreamIds[];
                if (sessionParams != null) {
                    offlineStreamIds = mRemoteDevice.endConfigure(operatingMode,
                            sessionParams.getNativeCopy(), createSessionStartTime);
                } else {
                    offlineStreamIds = mRemoteDevice.endConfigure(operatingMode, null,
                            createSessionStartTime);
                }

                mOfflineSupport.clear();
                if ((offlineStreamIds != null) && (offlineStreamIds.length > 0)) {
                    for (int offlineStreamId : offlineStreamIds) {
                        mOfflineSupport.add(offlineStreamId);
                    }
                }

                success = true;
            } catch (IllegalArgumentException e) {
                // OK. camera service can reject stream config if it's not supported by HAL
                // This is only the result of a programmer misusing the camera2 api.
                Log.w(TAG, "Stream configuration failed due to: " + e.getMessage());
                return false;
            } catch (CameraAccessException e) {
                if (e.getReason() == CameraAccessException.CAMERA_IN_USE) {
                    throw new IllegalStateException("The camera is currently busy." +
                            " You must wait until the previous operation completes.", e);
                }
                throw e;
            } finally {
                if (success && outputs.size() > 0) {
                    mDeviceExecutor.execute(mCallOnIdle);
                } else {
                    // Always return to the 'unconfigured' state if we didn't hit a fatal error
                    mDeviceExecutor.execute(mCallOnUnconfigured);
                }
            }
        }

        return success;
    }

    @Override
    public void createCaptureSession(List<Surface> outputs,
            CameraCaptureSession.StateCallback callback, Handler handler)
            throws CameraAccessException {
        List<OutputConfiguration> outConfigurations = new ArrayList<>(outputs.size());
        for (Surface surface : outputs) {
            outConfigurations.add(new OutputConfiguration(surface));
        }
        createCaptureSessionInternal(null, outConfigurations, callback,
                checkAndWrapHandler(handler), /*operatingMode*/ICameraDeviceUser.NORMAL_MODE,
                /*sessionParams*/ null);
    }

    @Override
    public void createCaptureSessionByOutputConfigurations(
            List<OutputConfiguration> outputConfigurations,
            CameraCaptureSession.StateCallback callback, Handler handler)
            throws CameraAccessException {
        if (DEBUG) {
            Log.d(TAG, "createCaptureSessionByOutputConfigurations");
        }

        // OutputConfiguration objects are immutable, but need to have our own array
        List<OutputConfiguration> currentOutputs = new ArrayList<>(outputConfigurations);

        createCaptureSessionInternal(null, currentOutputs, callback, checkAndWrapHandler(handler),
                /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/null);
    }

    @Override
    public void createReprocessableCaptureSession(InputConfiguration inputConfig,
            List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)
            throws CameraAccessException {
        if (DEBUG) {
            Log.d(TAG, "createReprocessableCaptureSession");
        }

        if (inputConfig == null) {
            throw new IllegalArgumentException("inputConfig cannot be null when creating a " +
                    "reprocessable capture session");
        }
        List<OutputConfiguration> outConfigurations = new ArrayList<>(outputs.size());
        for (Surface surface : outputs) {
            outConfigurations.add(new OutputConfiguration(surface));
        }
        createCaptureSessionInternal(inputConfig, outConfigurations, callback,
                checkAndWrapHandler(handler), /*operatingMode*/ICameraDeviceUser.NORMAL_MODE,
                /*sessionParams*/ null);
    }

    @Override
    public void createReprocessableCaptureSessionByConfigurations(InputConfiguration inputConfig,
            List<OutputConfiguration> outputs,
            android.hardware.camera2.CameraCaptureSession.StateCallback callback, Handler handler)
                    throws CameraAccessException {
        if (DEBUG) {
            Log.d(TAG, "createReprocessableCaptureSessionWithConfigurations");
        }

        if (inputConfig == null) {
            throw new IllegalArgumentException("inputConfig cannot be null when creating a " +
                    "reprocessable capture session");
        }

        if (outputs == null) {
            throw new IllegalArgumentException("Output configurations cannot be null when " +
                    "creating a reprocessable capture session");
        }

        // OutputConfiguration objects aren't immutable, make a copy before using.
        List<OutputConfiguration> currentOutputs = new ArrayList<OutputConfiguration>();
        for (OutputConfiguration output : outputs) {
            currentOutputs.add(new OutputConfiguration(output));
        }
        createCaptureSessionInternal(inputConfig, currentOutputs,
                callback, checkAndWrapHandler(handler),
                /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null);
    }

    @Override
    public void createConstrainedHighSpeedCaptureSession(List<Surface> outputs,
            android.hardware.camera2.CameraCaptureSession.StateCallback callback, Handler handler)
            throws CameraAccessException {
        if (outputs == null || outputs.size() == 0 || outputs.size() > 2) {
            throw new IllegalArgumentException(
                    "Output surface list must not be null and the size must be no more than 2");
        }
        List<OutputConfiguration> outConfigurations = new ArrayList<>(outputs.size());
        for (Surface surface : outputs) {
            outConfigurations.add(new OutputConfiguration(surface));
        }
        createCaptureSessionInternal(null, outConfigurations, callback,
                checkAndWrapHandler(handler),
                /*operatingMode*/ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE,
                /*sessionParams*/ null);
    }

    @Override
    public void createCustomCaptureSession(InputConfiguration inputConfig,
            List<OutputConfiguration> outputs,
            int operatingMode,
            android.hardware.camera2.CameraCaptureSession.StateCallback callback,
            Handler handler) throws CameraAccessException {
        List<OutputConfiguration> currentOutputs = new ArrayList<OutputConfiguration>();
        for (OutputConfiguration output : outputs) {
            currentOutputs.add(new OutputConfiguration(output));
        }
        createCaptureSessionInternal(inputConfig, currentOutputs, callback,
                checkAndWrapHandler(handler), operatingMode, /*sessionParams*/ null);
    }

    @Override
    public void createCaptureSession(SessionConfiguration config)
            throws CameraAccessException {
        if (config == null) {
            throw new IllegalArgumentException("Invalid session configuration");
        }

        List<OutputConfiguration> outputConfigs = config.getOutputConfigurations();
        if (outputConfigs == null) {
            throw new IllegalArgumentException("Invalid output configurations");
        }
        if (config.getExecutor() == null) {
            throw new IllegalArgumentException("Invalid executor");
        }
        createCaptureSessionInternal(config.getInputConfiguration(), outputConfigs,
                config.getStateCallback(), config.getExecutor(), config.getSessionType(),
                config.getSessionParameters());
    }

    private void createCaptureSessionInternal(InputConfiguration inputConfig,
            List<OutputConfiguration> outputConfigurations,
            CameraCaptureSession.StateCallback callback, Executor executor,
            int operatingMode, CaptureRequest sessionParams) throws CameraAccessException {
        long createSessionStartTime = SystemClock.uptimeMillis();
        synchronized(mInterfaceLock) {
            if (DEBUG) {
                Log.d(TAG, "createCaptureSessionInternal");
            }

            checkIfCameraClosedOrInError();

            boolean isConstrainedHighSpeed =
                    (operatingMode == ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE);
            if (isConstrainedHighSpeed && inputConfig != null) {
                throw new IllegalArgumentException("Constrained high speed session doesn't support"
                        + " input configuration yet.");
            }

            if (mCurrentExtensionSession != null) {
                mCurrentExtensionSession.commitStats();
            }

            if (mCurrentAdvancedExtensionSession != null) {
                mCurrentAdvancedExtensionSession.commitStats();
            }

            // Notify current session that it's going away, before starting camera operations
            // After this call completes, the session is not allowed to call into CameraDeviceImpl
            if (mCurrentSession != null) {
                mCurrentSession.replaceSessionClose();
            }

            if (mCurrentExtensionSession != null) {
                mCurrentExtensionSession.release(false /*skipCloseNotification*/);
                mCurrentExtensionSession = null;
            }

            if (mCurrentAdvancedExtensionSession != null) {
                mCurrentAdvancedExtensionSession.release(false /*skipCloseNotification*/);
                mCurrentAdvancedExtensionSession = null;
            }

            // TODO: dont block for this
            boolean configureSuccess = true;
            CameraAccessException pendingException = null;
            Surface input = null;
            try {
                // configure streams and then block until IDLE
                configureSuccess = configureStreamsChecked(inputConfig, outputConfigurations,
                        operatingMode, sessionParams, createSessionStartTime);
                if (configureSuccess == true && inputConfig != null) {
                    input = mRemoteDevice.getInputSurface();
                }
            } catch (CameraAccessException e) {
                configureSuccess = false;
                pendingException = e;
                input = null;
                if (DEBUG) {
                    Log.v(TAG, "createCaptureSession - failed with exception ", e);
                }
            }

            // Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise.
            CameraCaptureSessionCore newSession = null;
            if (isConstrainedHighSpeed) {
                ArrayList<Surface> surfaces = new ArrayList<>(outputConfigurations.size());
                for (OutputConfiguration outConfig : outputConfigurations) {
                    surfaces.add(outConfig.getSurface());
                }
                StreamConfigurationMap config =
                    getCharacteristics().get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                SurfaceUtils.checkConstrainedHighSpeedSurfaces(surfaces, /*fpsRange*/null, config);

                newSession = new CameraConstrainedHighSpeedCaptureSessionImpl(mNextSessionId++,
                        callback, executor, this, mDeviceExecutor, configureSuccess,
                        mCharacteristics);
            } else {
                newSession = new CameraCaptureSessionImpl(mNextSessionId++, input,
                        callback, executor, this, mDeviceExecutor, configureSuccess);
            }

            // TODO: wait until current session closes, then create the new session
            mCurrentSession = newSession;

            if (pendingException != null) {
                throw pendingException;
            }

            mSessionStateCallback = mCurrentSession.getDeviceStateCallback();
        }
    }

    @Override
    public boolean isSessionConfigurationSupported(
            @NonNull SessionConfiguration sessionConfig) throws CameraAccessException,
            UnsupportedOperationException, IllegalArgumentException {
        synchronized (mInterfaceLock) {
            checkIfCameraClosedOrInError();
            if (CompatChanges.isChangeEnabled(CHECK_PARAMS_IN_IS_SESSION_CONFIGURATION_SUPPORTED)
                    && Flags.cameraDeviceSetup()
                    && mCameraDeviceSetup != null) {
                return mCameraDeviceSetup.isSessionConfigurationSupported(sessionConfig);
            }
            return mRemoteDevice.isSessionConfigurationSupported(sessionConfig);
        }
    }

    /**
     * For use by backwards-compatibility code only.
     */
    public void setSessionListener(StateCallbackKK sessionCallback) {
        synchronized(mInterfaceLock) {
            mSessionStateCallback = sessionCallback;
        }
    }

    /**
     * Disable CONTROL_ENABLE_ZSL based on targetSdkVersion and capture template.
     */
    public static void disableZslIfNeeded(CameraMetadataNative request,
            int targetSdkVersion, int templateType) {
        // If targetSdkVersion is at least O, no need to set ENABLE_ZSL to false
        // for STILL_CAPTURE template.
        if (targetSdkVersion >= Build.VERSION_CODES.O
                && templateType == TEMPLATE_STILL_CAPTURE) {
            return;
        }

        Boolean enableZsl = request.get(CaptureRequest.CONTROL_ENABLE_ZSL);
        if (enableZsl == null) {
            // If enableZsl is not available, don't override.
            return;
        }

        request.set(CaptureRequest.CONTROL_ENABLE_ZSL, false);
    }

    @Override
    public CaptureRequest.Builder createCaptureRequest(int templateType,
            Set<String> physicalCameraIdSet)
            throws CameraAccessException {
        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();

            for (String physicalId : physicalCameraIdSet) {
                if (Objects.equals(physicalId, getId())) {
                    throw new IllegalStateException("Physical id matches the logical id!");
                }
            }

            CameraMetadataNative templatedRequest = null;

            templatedRequest = mRemoteDevice.createDefaultRequest(templateType);

            disableZslIfNeeded(templatedRequest, mAppTargetSdkVersion, templateType);

            CaptureRequest.Builder builder = new CaptureRequest.Builder(
                    templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
                    getId(), physicalCameraIdSet);

            return builder;
        }
    }

    @Override
    public CaptureRequest.Builder createCaptureRequest(int templateType)
            throws CameraAccessException {
        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();

            CameraMetadataNative templatedRequest = null;

            templatedRequest = mRemoteDevice.createDefaultRequest(templateType);

            disableZslIfNeeded(templatedRequest, mAppTargetSdkVersion, templateType);

            CaptureRequest.Builder builder = new CaptureRequest.Builder(
                    templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
                    getId(), /*physicalCameraIdSet*/ null);

            return builder;
        }
    }

    @Override
    public CaptureRequest.Builder createReprocessCaptureRequest(TotalCaptureResult inputResult)
            throws CameraAccessException {
        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();

            CameraMetadataNative resultMetadata = new
                    CameraMetadataNative(inputResult.getNativeCopy());

            CaptureRequest.Builder builder = new CaptureRequest.Builder(resultMetadata,
                    /*reprocess*/true, inputResult.getSessionId(), getId(),
                    /*physicalCameraIdSet*/ null);
            builder.set(CaptureRequest.CONTROL_CAPTURE_INTENT,
                    CameraMetadata.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);

            return builder;
        }
    }

    public void prepare(Surface surface) throws CameraAccessException {
        if (surface == null) throw new IllegalArgumentException("Surface is null");

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            int streamId = -1;
            for (int i = 0; i < mConfiguredOutputs.size(); i++) {
                final List<Surface> surfaces = mConfiguredOutputs.valueAt(i).getSurfaces();
                if (surfaces.contains(surface)) {
                    streamId = mConfiguredOutputs.keyAt(i);
                    break;
                }
            }
            if (streamId == -1) {
                throw new IllegalArgumentException("Surface is not part of this session");
            }

            mRemoteDevice.prepare(streamId);
        }
    }

    public void prepare(int maxCount, Surface surface) throws CameraAccessException {
        if (surface == null) throw new IllegalArgumentException("Surface is null");
        if (maxCount <= 0) throw new IllegalArgumentException("Invalid maxCount given: " +
                maxCount);

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            int streamId = -1;
            for (int i = 0; i < mConfiguredOutputs.size(); i++) {
                if (surface == mConfiguredOutputs.valueAt(i).getSurface()) {
                    streamId = mConfiguredOutputs.keyAt(i);
                    break;
                }
            }
            if (streamId == -1) {
                throw new IllegalArgumentException("Surface is not part of this session");
            }

            mRemoteDevice.prepare2(maxCount, streamId);
        }
    }

    public void updateOutputConfiguration(OutputConfiguration config)
            throws CameraAccessException {
        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            int streamId = -1;
            for (int i = 0; i < mConfiguredOutputs.size(); i++) {
                if (config.getSurface() == mConfiguredOutputs.valueAt(i).getSurface()) {
                    streamId = mConfiguredOutputs.keyAt(i);
                    break;
                }
            }
            if (streamId == -1) {
                throw new IllegalArgumentException("Invalid output configuration");
            }

            mRemoteDevice.updateOutputConfiguration(streamId, config);
            mConfiguredOutputs.put(streamId, config);
        }
    }

    public CameraOfflineSession switchToOffline(
            @NonNull Collection<Surface> offlineOutputs, @NonNull Executor executor,
            @NonNull CameraOfflineSession.CameraOfflineSessionCallback listener)
            throws CameraAccessException {
        if (offlineOutputs.isEmpty()) {
            throw new IllegalArgumentException("Invalid offline surfaces!");
        }

        HashSet<Integer> offlineStreamIds = new HashSet<Integer>();
        SparseArray<OutputConfiguration> offlineConfiguredOutputs =
                new SparseArray<OutputConfiguration>();
        CameraOfflineSession ret;

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            if (mOfflineSessionImpl != null) {
                throw new IllegalStateException("Switch to offline mode already in progress");
            }

            for (Surface surface : offlineOutputs) {
                int streamId = -1;
                for (int i = 0; i < mConfiguredOutputs.size(); i++) {
                    if (surface == mConfiguredOutputs.valueAt(i).getSurface()) {
                        streamId = mConfiguredOutputs.keyAt(i);
                        offlineConfiguredOutputs.append(streamId, mConfiguredOutputs.valueAt(i));
                        break;
                    }
                }
                if (streamId == -1) {
                    throw new IllegalArgumentException("Offline surface is not part of this" +
                            " session");
                }

                if (!mOfflineSupport.contains(streamId)) {
                    throw new IllegalArgumentException("Surface: "  + surface + " does not " +
                            " support offline mode");
                }

                offlineStreamIds.add(streamId);
            }
            stopRepeating();

            mOfflineSessionImpl = new CameraOfflineSessionImpl(mCameraId,
                    mCharacteristics, executor, listener, offlineConfiguredOutputs,
                    mConfiguredInput, mConfiguredOutputs, mFrameNumberTracker, mCaptureCallbackMap,
                    mRequestLastFrameNumbersList);
            ret = mOfflineSessionImpl;

            mOfflineSwitchService = Executors.newSingleThreadExecutor();
            mConfiguredOutputs.clear();
            mConfiguredInput = new SimpleEntry<Integer, InputConfiguration>(REQUEST_ID_NONE, null);
            mIdle = true;
            mCaptureCallbackMap = new SparseArray<CaptureCallbackHolder>();
            mBatchOutputMap = new HashMap<>();
            mFrameNumberTracker = new FrameNumberTracker();

            mCurrentSession.closeWithoutDraining();
            mCurrentSession = null;
        }

        mOfflineSwitchService.execute(new Runnable() {
            @Override
            public void run() {
                // We cannot hold 'mInterfaceLock' during the remote IPC in 'switchToOffline'.
                // The call will block until all non-offline requests are completed and/or flushed.
                // The results/errors need to be handled by 'CameraDeviceCallbacks' which also sync
                // on 'mInterfaceLock'.
                try {
                    ICameraOfflineSession remoteOfflineSession = mRemoteDevice.switchToOffline(
                            mOfflineSessionImpl.getCallbacks(),
                            Arrays.stream(offlineStreamIds.toArray(
                                    new Integer[offlineStreamIds.size()])).mapToInt(
                                            Integer::intValue).toArray());
                    mOfflineSessionImpl.setRemoteSession(remoteOfflineSession);
                } catch (CameraAccessException e) {
                    mOfflineSessionImpl.notifyFailedSwitch();
                } finally {
                    mOfflineSessionImpl = null;
                }
            }
        });

        return ret;
    }

    public boolean supportsOfflineProcessing(Surface surface) {
        if (surface == null) throw new IllegalArgumentException("Surface is null");

        synchronized(mInterfaceLock) {
            int streamId = -1;
            for (int i = 0; i < mConfiguredOutputs.size(); i++) {
                if (surface == mConfiguredOutputs.valueAt(i).getSurface()) {
                    streamId = mConfiguredOutputs.keyAt(i);
                    break;
                }
            }
            if (streamId == -1) {
                throw new IllegalArgumentException("Surface is not part of this session");
            }

            return mOfflineSupport.contains(streamId);
        }
    }

    public void tearDown(Surface surface) throws CameraAccessException {
        if (surface == null) throw new IllegalArgumentException("Surface is null");

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            int streamId = -1;
            for (int i = 0; i < mConfiguredOutputs.size(); i++) {
                if (surface == mConfiguredOutputs.valueAt(i).getSurface()) {
                    streamId = mConfiguredOutputs.keyAt(i);
                    break;
                }
            }
            if (streamId == -1) {
                throw new IllegalArgumentException("Surface is not part of this session");
            }

            mRemoteDevice.tearDown(streamId);
        }
    }

    public void finalizeOutputConfigs(List<OutputConfiguration> outputConfigs)
            throws CameraAccessException {
        if (outputConfigs == null || outputConfigs.size() == 0) {
            throw new IllegalArgumentException("deferred config is null or empty");
        }

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();

            for (OutputConfiguration config : outputConfigs) {
                int streamId = -1;
                for (int i = 0; i < mConfiguredOutputs.size(); i++) {
                    // Have to use equal here, as createCaptureSessionByOutputConfigurations() and
                    // createReprocessableCaptureSessionByConfigurations() do a copy of the configs.
                    if (config.equals(mConfiguredOutputs.valueAt(i))) {
                        streamId = mConfiguredOutputs.keyAt(i);
                        break;
                    }
                }
                if (streamId == -1) {
                    throw new IllegalArgumentException("Deferred config is not part of this "
                            + "session");
                }

                if (config.getSurfaces().size() == 0) {
                    throw new IllegalArgumentException("The final config for stream " + streamId
                            + " must have at least 1 surface");
                }
                mRemoteDevice.finalizeOutputConfigurations(streamId, config);
                mConfiguredOutputs.put(streamId, config);
            }
        }
    }

    public int capture(CaptureRequest request, CaptureCallback callback, Executor executor)
            throws CameraAccessException {
        if (DEBUG) {
            Log.d(TAG, "calling capture");
        }
        List<CaptureRequest> requestList = new ArrayList<CaptureRequest>();
        requestList.add(request);
        return submitCaptureRequest(requestList, callback, executor, /*streaming*/false);
    }

    public int captureBurst(List<CaptureRequest> requests, CaptureCallback callback,
            Executor executor) throws CameraAccessException {
        if (requests == null || requests.isEmpty()) {
            throw new IllegalArgumentException("At least one request must be given");
        }
        return submitCaptureRequest(requests, callback, executor, /*streaming*/false);
    }

    /**
     * This method checks lastFrameNumber returned from ICameraDeviceUser methods for
     * starting and stopping repeating request and flushing.
     *
     * <p>If lastFrameNumber is NO_FRAMES_CAPTURED, it means that the request was never
     * sent to HAL. Then onCaptureSequenceAborted is immediately triggered.
     * If lastFrameNumber is non-negative, then the requestId and lastFrameNumber as the last
     * regular frame number will be added to the list mRequestLastFrameNumbersList.</p>
     *
     * @param requestId the request ID of the current repeating request.
     * @param lastFrameNumber last frame number returned from binder.
     * @param repeatingRequestTypes the repeating requests' types.
     */
    private void checkEarlyTriggerSequenceCompleteLocked(
            final int requestId, final long lastFrameNumber,
            final int[] repeatingRequestTypes) {
        // lastFrameNumber being equal to NO_FRAMES_CAPTURED means that the request
        // was never sent to HAL. Should trigger onCaptureSequenceAborted immediately.
        if (lastFrameNumber == CameraCaptureSession.CaptureCallback.NO_FRAMES_CAPTURED) {
            final CaptureCallbackHolder holder;
            int index = mCaptureCallbackMap.indexOfKey(requestId);
            holder = (index >= 0) ? mCaptureCallbackMap.valueAt(index) : null;
            if (holder != null) {
                mCaptureCallbackMap.removeAt(index);
                if (DEBUG) {
                    Log.v(TAG, String.format(
                            "remove holder for requestId %d, "
                            + "because lastFrame is %d.",
                            requestId, lastFrameNumber));

                    Log.v(TAG, "immediately trigger onCaptureSequenceAborted because"
                            + " request did not reach HAL");
                }

                Runnable resultDispatch = new Runnable() {
                    @Override
                    public void run() {
                        if (!CameraDeviceImpl.this.isClosed()) {
                            if (DEBUG) {
                                Log.d(TAG, String.format(
                                        "early trigger sequence complete for request %d",
                                        requestId));
                            }
                            holder.getCallback().onCaptureSequenceAborted(
                                    CameraDeviceImpl.this,
                                    requestId);
                        }
                    }
                };
                final long ident = Binder.clearCallingIdentity();
                try {
                    holder.getExecutor().execute(resultDispatch);
                } finally {
                    Binder.restoreCallingIdentity(ident);
                }
            } else {
                Log.w(TAG, String.format(
                        "did not register callback to request %d",
                        requestId));
            }
        } else {
            // This function is only called for regular/ZslStill request so lastFrameNumber is the
            // last regular/ZslStill frame number.
            mRequestLastFrameNumbersList.add(new RequestLastFrameNumbersHolder(requestId,
                    lastFrameNumber, repeatingRequestTypes));

            // It is possible that the last frame has already arrived, so we need to check
            // for sequence completion right away
            checkAndFireSequenceComplete();
        }
    }

    private int[] getRequestTypes(final CaptureRequest[] requestArray) {
        int[] requestTypes = new int[requestArray.length];
        for (int i = 0; i < requestArray.length; i++) {
            requestTypes[i] = requestArray[i].getRequestType();
        }
        return requestTypes;
    }

    private boolean hasBatchedOutputs(List<CaptureRequest> requestList) {
        boolean hasBatchedOutputs = true;
        for (int i = 0; i < requestList.size(); i++) {
            CaptureRequest request = requestList.get(i);
            if (!request.isPartOfCRequestList()) {
                hasBatchedOutputs = false;
                break;
            }
            if (i == 0) {
                Collection<Surface> targets = request.getTargets();
                if (targets.size() != 2) {
                    hasBatchedOutputs = false;
                    break;
                }
            }
        }
        return hasBatchedOutputs;
    }

    private void updateTracker(int requestId, long frameNumber,
            int requestType, CaptureResult result, boolean isPartialResult) {
        int requestCount = 1;
        // If the request has batchedOutputs update each frame within the batch.
        if (mBatchOutputMap.containsKey(requestId)) {
            requestCount = mBatchOutputMap.get(requestId);
            for (int i = 0; i < requestCount; i++) {
                mFrameNumberTracker.updateTracker(frameNumber - (requestCount - 1 - i),
                        result, isPartialResult, requestType);
            }
        } else {
            mFrameNumberTracker.updateTracker(frameNumber, result,
                    isPartialResult, requestType);
        }
    }

    private int submitCaptureRequest(List<CaptureRequest> requestList, CaptureCallback callback,
            Executor executor, boolean repeating) throws CameraAccessException {

        // Need a valid executor, or current thread needs to have a looper, if
        // callback is valid
        executor = checkExecutor(executor, callback);

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();

            // Make sure that there all requests have at least 1 surface; all surfaces are non-null;
            for (CaptureRequest request : requestList) {
                if (request.getTargets().isEmpty()) {
                    throw new IllegalArgumentException(
                            "Each request must have at least one Surface target");
                }

                for (Surface surface : request.getTargets()) {
                    if (surface == null) {
                        throw new IllegalArgumentException("Null Surface targets are not allowed");
                    }
                }
            }

            if (repeating) {
                stopRepeating();
            }

            SubmitInfo requestInfo;

            CaptureRequest[] requestArray = requestList.toArray(new CaptureRequest[requestList.size()]);
            // Convert Surface to streamIdx and surfaceIdx
            for (CaptureRequest request : requestArray) {
                request.convertSurfaceToStreamId(mConfiguredOutputs);
            }

            requestInfo = mRemoteDevice.submitRequestList(requestArray, repeating);
            if (DEBUG) {
                Log.v(TAG, "last frame number " + requestInfo.getLastFrameNumber());
            }

            for (CaptureRequest request : requestArray) {
                request.recoverStreamIdToSurface();
            }

            // If the request has batched outputs, then store the
            // requestCount and requestId in the map.
            boolean hasBatchedOutputs = hasBatchedOutputs(requestList);
            if (hasBatchedOutputs) {
                int requestCount = requestList.size();
                mBatchOutputMap.put(requestInfo.getRequestId(), requestCount);
            }

            if (callback != null) {
                mCaptureCallbackMap.put(requestInfo.getRequestId(),
                        new CaptureCallbackHolder(
                            callback, requestList, executor, repeating, mNextSessionId - 1));
            } else {
                if (DEBUG) {
                    Log.d(TAG, "Listen for request " + requestInfo.getRequestId() + " is null");
                }
            }

            if (repeating) {
                if (mRepeatingRequestId != REQUEST_ID_NONE) {
                    checkEarlyTriggerSequenceCompleteLocked(mRepeatingRequestId,
                            requestInfo.getLastFrameNumber(),
                            mRepeatingRequestTypes);
                }
                mRepeatingRequestId = requestInfo.getRequestId();
                mRepeatingRequestTypes = getRequestTypes(requestArray);
            } else {
                mRequestLastFrameNumbersList.add(
                    new RequestLastFrameNumbersHolder(requestList, requestInfo));
            }

            if (mIdle) {
                mDeviceExecutor.execute(mCallOnActive);
            }
            mIdle = false;

            return requestInfo.getRequestId();
        }
    }

    public int setRepeatingRequest(CaptureRequest request, CaptureCallback callback,
            Executor executor) throws CameraAccessException {
        List<CaptureRequest> requestList = new ArrayList<CaptureRequest>();
        requestList.add(request);
        return submitCaptureRequest(requestList, callback, executor, /*streaming*/true);
    }

    public int setRepeatingBurst(List<CaptureRequest> requests, CaptureCallback callback,
            Executor executor) throws CameraAccessException {
        if (requests == null || requests.isEmpty()) {
            throw new IllegalArgumentException("At least one request must be given");
        }
        return submitCaptureRequest(requests, callback, executor, /*streaming*/true);
    }

    public void stopRepeating() throws CameraAccessException {

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            if (mRepeatingRequestId != REQUEST_ID_NONE) {

                int requestId = mRepeatingRequestId;
                mRepeatingRequestId = REQUEST_ID_NONE;
                mFailedRepeatingRequestId = REQUEST_ID_NONE;
                int[] requestTypes = mRepeatingRequestTypes;
                mRepeatingRequestTypes = null;
                mFailedRepeatingRequestTypes = null;

                long lastFrameNumber;
                try {
                    lastFrameNumber = mRemoteDevice.cancelRequest(requestId);
                } catch (IllegalArgumentException e) {
                    if (DEBUG) {
                        Log.v(TAG, "Repeating request was already stopped for request " +
                                requestId);
                    }
                    // Cache request id and request types in case of a race with
                    // "onRepeatingRequestError" which may no yet be scheduled on another thread
                    // or blocked by us.
                    mFailedRepeatingRequestId = requestId;
                    mFailedRepeatingRequestTypes = requestTypes;

                    // Repeating request was already stopped. Nothing more to do.
                    return;
                }

                checkEarlyTriggerSequenceCompleteLocked(requestId, lastFrameNumber, requestTypes);
            }
        }
    }

    private void waitUntilIdle() throws CameraAccessException {

        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();

            if (mRepeatingRequestId != REQUEST_ID_NONE) {
                throw new IllegalStateException("Active repeating request ongoing");
            }

            mRemoteDevice.waitUntilIdle();
        }
    }

    public void flush() throws CameraAccessException {
        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();

            mDeviceExecutor.execute(mCallOnBusy);

            // If already idle, just do a busy->idle transition immediately, don't actually
            // flush.
            if (mIdle) {
                mDeviceExecutor.execute(mCallOnIdle);
                return;
            }

            long lastFrameNumber = mRemoteDevice.flush();
            if (mRepeatingRequestId != REQUEST_ID_NONE) {
                checkEarlyTriggerSequenceCompleteLocked(mRepeatingRequestId, lastFrameNumber,
                        mRepeatingRequestTypes);
                mRepeatingRequestId = REQUEST_ID_NONE;
                mRepeatingRequestTypes = null;
            }
        }
    }

    @Override
    public void close() {
        synchronized (mInterfaceLock) {
            if (mClosing.getAndSet(true)) {
                return;
            }

            if (mOfflineSwitchService != null) {
                mOfflineSwitchService.shutdownNow();
                mOfflineSwitchService = null;
            }

            // Let extension sessions commit stats before disconnecting remoteDevice
            if (mCurrentExtensionSession != null) {
                mCurrentExtensionSession.commitStats();
            }

            if (mCurrentAdvancedExtensionSession != null) {
                mCurrentAdvancedExtensionSession.commitStats();
            }

            if (mRemoteDevice != null) {
                mRemoteDevice.disconnect();
                mRemoteDevice.unlinkToDeath(this, /*flags*/0);
            }

            if (mCurrentExtensionSession != null) {
                mCurrentExtensionSession.release(true /*skipCloseNotification*/);
                mCurrentExtensionSession = null;
            }

            if (mCurrentAdvancedExtensionSession != null) {
                mCurrentAdvancedExtensionSession.release(true /*skipCloseNotification*/);
                mCurrentAdvancedExtensionSession = null;
            }

            // Only want to fire the onClosed callback once;
            // either a normal close where the remote device is valid
            // or a close after a startup error (no remote device but in error state)
            if (mRemoteDevice != null || mInError) {
                mDeviceExecutor.execute(mCallOnClosed);
            }

            mRemoteDevice = null;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            close();
        }
        finally {
            super.finalize();
        }
    }

    private boolean checkInputConfigurationWithStreamConfigurationsAs(
            InputConfiguration inputConfig, StreamConfigurationMap configMap) {
        int[] inputFormats = configMap.getInputFormats();
        boolean validFormat = false;
        int inputFormat = inputConfig.getFormat();
        for (int format : inputFormats) {
            if (format == inputFormat) {
                validFormat = true;
            }
        }

        // Allow RAW formats, even when not advertised.
        if (isRawFormat(inputFormat)) {
            return true;
        }

        if (validFormat == false) {
            return false;
        }

        boolean validSize = false;
        Size[] inputSizes = configMap.getInputSizes(inputFormat);
        for (Size s : inputSizes) {
            if (inputConfig.getWidth() == s.getWidth() &&
                    inputConfig.getHeight() == s.getHeight()) {
                validSize = true;
            }
        }

        if (validSize == false) {
            return false;
        }
        return true;
    }

    private boolean checkInputConfigurationWithStreamConfigurations(
            InputConfiguration inputConfig, boolean maxResolution) {
        // Check if either this logical camera or any of its physical cameras support the
        // input config. If they do, the input config is valid.
        CameraCharacteristics.Key<StreamConfigurationMap> ck =
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP;

        if (maxResolution) {
            ck = CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION;
        }

        StreamConfigurationMap configMap = mCharacteristics.get(ck);

        if (configMap != null &&
                checkInputConfigurationWithStreamConfigurationsAs(inputConfig, configMap)) {
            return true;
        }

        for (Map.Entry<String, CameraCharacteristics> entry : getPhysicalIdToChars().entrySet()) {
            configMap = entry.getValue().get(ck);

            if (configMap != null &&
                    checkInputConfigurationWithStreamConfigurationsAs(inputConfig, configMap)) {
                // Input config supported.
                return true;
            }
        }
        return false;
    }

    private void checkInputConfiguration(InputConfiguration inputConfig) {
        if (inputConfig == null) {
            return;
        }
        int inputFormat = inputConfig.getFormat();
        if (inputConfig.isMultiResolution()) {
            MultiResolutionStreamConfigurationMap configMap = mCharacteristics.get(
                    CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP);

            int[] inputFormats = configMap.getInputFormats();
            boolean validFormat = false;
            for (int format : inputFormats) {
                if (format == inputFormat) {
                    validFormat = true;
                }
            }

            // Allow RAW formats, even when not advertised.
            if (Flags.multiResRawReprocessing() && isRawFormat(inputFormat)) {
                return;
            }

            if (validFormat == false) {
                throw new IllegalArgumentException("multi-resolution input format " +
                        inputFormat + " is not valid");
            }

            boolean validSize = false;
            Collection<MultiResolutionStreamInfo> inputStreamInfo =
                    configMap.getInputInfo(inputFormat);
            for (MultiResolutionStreamInfo info : inputStreamInfo) {
                if (inputConfig.getWidth() == info.getWidth() &&
                        inputConfig.getHeight() == info.getHeight()) {
                    validSize = true;
                }
            }

            if (validSize == false) {
                throw new IllegalArgumentException("Multi-resolution input size " +
                        inputConfig.getWidth() + "x" + inputConfig.getHeight() + " is not valid");
            }
        } else {
            if (!checkInputConfigurationWithStreamConfigurations(inputConfig, /*maxRes*/false) &&
                    !checkInputConfigurationWithStreamConfigurations(inputConfig, /*maxRes*/true)) {
                throw new IllegalArgumentException("Input config with format " +
                        inputFormat + " and size " + inputConfig.getWidth() + "x" +
                        inputConfig.getHeight() + " not supported by camera id " + mCameraId);
            }
        }
    }

    /**
     * A callback for notifications about the state of a camera device, adding in the callbacks that
     * were part of the earlier KK API design, but now only used internally.
     */
    public static abstract class StateCallbackKK extends StateCallback {
        /**
         * The method called when a camera device has no outputs configured.
         *
         */
        public void onUnconfigured(CameraDevice camera) {
            // Default empty implementation
        }

        /**
         * The method called when a camera device begins processing
         * {@link CaptureRequest capture requests}.
         *
         */
        public void onActive(CameraDevice camera) {
            // Default empty implementation
        }

        /**
         * The method called when a camera device is busy.
         *
         */
        public void onBusy(CameraDevice camera) {
            // Default empty implementation
        }

        /**
         * The method called when a camera device has finished processing all
         * submitted capture requests and has reached an idle state.
         *
         */
        public void onIdle(CameraDevice camera) {
            // Default empty implementation
        }

        /**
         * This method is called when camera device's non-repeating request queue is empty,
         * and is ready to start capturing next image.
         */
        public void onRequestQueueEmpty() {
            // Default empty implementation
        }

        /**
         * The method called when the camera device has finished preparing
         * an output Surface
         */
        public void onSurfacePrepared(Surface surface) {
            // Default empty implementation
        }
    }

    private void checkAndFireSequenceComplete() {
        long completedFrameNumber = mFrameNumberTracker.getCompletedFrameNumber();
        long completedReprocessFrameNumber = mFrameNumberTracker.getCompletedReprocessFrameNumber();
        long completedZslStillFrameNumber = mFrameNumberTracker.getCompletedZslStillFrameNumber();

        Iterator<RequestLastFrameNumbersHolder> iter = mRequestLastFrameNumbersList.iterator();
        while (iter.hasNext()) {
            final RequestLastFrameNumbersHolder requestLastFrameNumbers = iter.next();
            final int requestId = requestLastFrameNumbers.getRequestId();
            final CaptureCallbackHolder holder;
            if (mRemoteDevice == null) {
                Log.w(TAG, "Camera closed while checking sequences");
                return;
            }
            if (!requestLastFrameNumbers.isSequenceCompleted()) {
                long lastRegularFrameNumber =
                        requestLastFrameNumbers.getLastRegularFrameNumber();
                long lastReprocessFrameNumber =
                        requestLastFrameNumbers.getLastReprocessFrameNumber();
                long lastZslStillFrameNumber =
                        requestLastFrameNumbers.getLastZslStillFrameNumber();
                if (lastRegularFrameNumber <= completedFrameNumber
                        && lastReprocessFrameNumber <= completedReprocessFrameNumber
                        && lastZslStillFrameNumber <= completedZslStillFrameNumber) {
                    if (DEBUG) {
                        Log.v(TAG, String.format(
                                "Mark requestId %d as completed, because lastRegularFrame %d "
                                + "is <= %d, lastReprocessFrame %d is <= %d, "
                                + "lastZslStillFrame %d is <= %d", requestId,
                                lastRegularFrameNumber, completedFrameNumber,
                                lastReprocessFrameNumber, completedReprocessFrameNumber,
                                lastZslStillFrameNumber, completedZslStillFrameNumber));
                    }
                    requestLastFrameNumbers.markSequenceCompleted();
                }

                // Call onCaptureSequenceCompleted
                int index = mCaptureCallbackMap.indexOfKey(requestId);
                holder = (index >= 0) ?
                        mCaptureCallbackMap.valueAt(index) : null;
                if (holder != null && requestLastFrameNumbers.isSequenceCompleted()) {
                    Runnable resultDispatch = new Runnable() {
                        @Override
                        public void run() {
                            if (!CameraDeviceImpl.this.isClosed()){
                                if (DEBUG) {
                                    Log.d(TAG, String.format(
                                            "fire sequence complete for request %d",
                                            requestId));
                                }

                                holder.getCallback().onCaptureSequenceCompleted(
                                    CameraDeviceImpl.this,
                                    requestId,
                                    requestLastFrameNumbers.getLastFrameNumber());
                            }
                        }
                    };
                    final long ident = Binder.clearCallingIdentity();
                    try {
                        holder.getExecutor().execute(resultDispatch);
                    } finally {
                        Binder.restoreCallingIdentity(ident);
                    }
                }
            }

            if (requestLastFrameNumbers.isSequenceCompleted() &&
                    requestLastFrameNumbers.isInflightCompleted()) {
                int index = mCaptureCallbackMap.indexOfKey(requestId);
                if (index >= 0) {
                    mCaptureCallbackMap.removeAt(index);
                }
                if (DEBUG) {
                    Log.v(TAG, String.format(
                            "Remove holder for requestId %d", requestId));
                }
                iter.remove();
            }
        }
    }

    private void removeCompletedCallbackHolderLocked(long lastCompletedRegularFrameNumber,
            long lastCompletedReprocessFrameNumber, long lastCompletedZslStillFrameNumber) {
        if (DEBUG) {
            Log.v(TAG, String.format("remove completed callback holders for "
                    + "lastCompletedRegularFrameNumber %d, "
                    + "lastCompletedReprocessFrameNumber %d, "
                    + "lastCompletedZslStillFrameNumber %d",
                    lastCompletedRegularFrameNumber,
                    lastCompletedReprocessFrameNumber,
                    lastCompletedZslStillFrameNumber));
        }

        Iterator<RequestLastFrameNumbersHolder> iter = mRequestLastFrameNumbersList.iterator();
        while (iter.hasNext()) {
            final RequestLastFrameNumbersHolder requestLastFrameNumbers = iter.next();
            final int requestId = requestLastFrameNumbers.getRequestId();
            final CaptureCallbackHolder holder;
            if (mRemoteDevice == null) {
                Log.w(TAG, "Camera closed while removing completed callback holders");
                return;
            }

            long lastRegularFrameNumber =
                    requestLastFrameNumbers.getLastRegularFrameNumber();
            long lastReprocessFrameNumber =
                    requestLastFrameNumbers.getLastReprocessFrameNumber();
            long lastZslStillFrameNumber =
                    requestLastFrameNumbers.getLastZslStillFrameNumber();

            if (lastRegularFrameNumber <= lastCompletedRegularFrameNumber
                        && lastReprocessFrameNumber <= lastCompletedReprocessFrameNumber
                        && lastZslStillFrameNumber <= lastCompletedZslStillFrameNumber) {

                if (requestLastFrameNumbers.isSequenceCompleted()) {
                    int index = mCaptureCallbackMap.indexOfKey(requestId);
                    if (index >= 0) {
                        mCaptureCallbackMap.removeAt(index);
                    }
                    if (DEBUG) {
                        Log.v(TAG, String.format(
                                "Remove holder for requestId %d, because lastRegularFrame %d "
                                + "is <= %d, lastReprocessFrame %d is <= %d, "
                                + "lastZslStillFrame %d is <= %d", requestId,
                                lastRegularFrameNumber, lastCompletedRegularFrameNumber,
                                lastReprocessFrameNumber, lastCompletedReprocessFrameNumber,
                                lastZslStillFrameNumber, lastCompletedZslStillFrameNumber));
                    }
                    iter.remove();
                } else {
                    if (DEBUG) {
                        Log.v(TAG, "Sequence not yet completed for request id " + requestId);
                    }
                    requestLastFrameNumbers.markInflightCompleted();
                }
            }
        }
    }

    public void onDeviceError(final int errorCode, CaptureResultExtras resultExtras) {
        if (DEBUG) {
            Log.d(TAG, String.format(
                    "Device error received, code %d, frame number %d, request ID %d, subseq ID %d",
                    errorCode, resultExtras.getFrameNumber(), resultExtras.getRequestId(),
                    resultExtras.getSubsequenceId()));
        }

        synchronized(mInterfaceLock) {
            if (mRemoteDevice == null && mRemoteDeviceInit) {
                return; // Camera already closed, user is not interested in errors anymore.
            }

            // Redirect device callback to the offline session in case we are in the middle
            // of an offline switch
            if (mOfflineSessionImpl != null) {
                mOfflineSessionImpl.getCallbacks().onDeviceError(errorCode, resultExtras);
                return;
            }

            switch (errorCode) {
                case CameraDeviceCallbacks.ERROR_CAMERA_DISCONNECTED: {
                    final long ident = Binder.clearCallingIdentity();
                    try {
                        mDeviceExecutor.execute(mCallOnDisconnected);
                    } finally {
                        Binder.restoreCallingIdentity(ident);
                    }
                    break;
                }
                case CameraDeviceCallbacks.ERROR_CAMERA_REQUEST:
                case CameraDeviceCallbacks.ERROR_CAMERA_RESULT:
                case CameraDeviceCallbacks.ERROR_CAMERA_BUFFER:
                    onCaptureErrorLocked(errorCode, resultExtras);
                    break;
                case CameraDeviceCallbacks.ERROR_CAMERA_DEVICE:
                    scheduleNotifyError(StateCallback.ERROR_CAMERA_DEVICE);
                    break;
                case CameraDeviceCallbacks.ERROR_CAMERA_DISABLED:
                    scheduleNotifyError(StateCallback.ERROR_CAMERA_DISABLED);
                    break;
                default:
                    Log.e(TAG, "Unknown error from camera device: " + errorCode);
                    scheduleNotifyError(StateCallback.ERROR_CAMERA_SERVICE);
            }
        }
    }

    private void scheduleNotifyError(int code) {
        mInError = true;
        final long ident = Binder.clearCallingIdentity();
        try {
            mDeviceExecutor.execute(obtainRunnable(
                        CameraDeviceImpl::notifyError, this, code).recycleOnUse());
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    private void notifyError(int code) {
        if (!CameraDeviceImpl.this.isClosed()) {
            mDeviceCallback.onError(CameraDeviceImpl.this, code);
        }
    }

    /**
     * Called by onDeviceError for handling single-capture failures.
     */
    private void onCaptureErrorLocked(int errorCode, CaptureResultExtras resultExtras) {

        final int requestId = resultExtras.getRequestId();
        final int subsequenceId = resultExtras.getSubsequenceId();
        final long frameNumber = resultExtras.getFrameNumber();
        final String errorPhysicalCameraId = resultExtras.getErrorPhysicalCameraId();
        final CaptureCallbackHolder holder = mCaptureCallbackMap.get(requestId);

        if (holder == null) {
            Log.e(TAG, String.format("Receive capture error on unknown request ID %d",
                    requestId));
            return;
        }

        final CaptureRequest request = holder.getRequest(subsequenceId);

        Runnable failureDispatch = null;
        if (errorCode == CameraDeviceCallbacks.ERROR_CAMERA_BUFFER) {
            // Because 1 stream id could map to multiple surfaces, we need to specify both
            // streamId and surfaceId.
            OutputConfiguration config = mConfiguredOutputs.get(
                    resultExtras.getErrorStreamId());
            if (config == null) {
                Log.v(TAG, String.format(
                        "Stream %d has been removed. Skipping buffer lost callback",
                        resultExtras.getErrorStreamId()));
                return;
            }
            for (Surface surface : config.getSurfaces()) {
                if (!request.containsTarget(surface)) {
                    continue;
                }
                if (DEBUG) {
                    Log.v(TAG, String.format(
                            "Lost output buffer reported for frame %d, target %s",
                            frameNumber, surface));
                }
                failureDispatch = new Runnable() {
                    @Override
                    public void run() {
                        if (!isClosed()){
                            holder.getCallback().onCaptureBufferLost(CameraDeviceImpl.this, request,
                                    surface, frameNumber);
                        }
                    }
                };
                // Dispatch the failure callback
                final long ident = Binder.clearCallingIdentity();
                try {
                    holder.getExecutor().execute(failureDispatch);
                } finally {
                    Binder.restoreCallingIdentity(ident);
                }
            }
        } else {
            boolean mayHaveBuffers = (errorCode == CameraDeviceCallbacks.ERROR_CAMERA_RESULT);

            // This is only approximate - exact handling needs the camera service and HAL to
            // disambiguate between request failures to due abort and due to real errors.  For
            // now, assume that if the session believes we're mid-abort, then the error is due
            // to abort.
            int reason = (mCurrentSession != null && mCurrentSession.isAborting()) ?
                    CaptureFailure.REASON_FLUSHED :
                    CaptureFailure.REASON_ERROR;

            final CaptureFailure failure = new CaptureFailure(
                request,
                reason,
                mayHaveBuffers,
                requestId,
                frameNumber,
                errorPhysicalCameraId);

            failureDispatch = new Runnable() {
                @Override
                public void run() {
                    if (!isClosed()){
                        holder.getCallback().onCaptureFailed(CameraDeviceImpl.this, request,
                                failure);
                    }
                }
            };

            // Fire onCaptureSequenceCompleted if appropriate
            if (DEBUG) {
                Log.v(TAG, String.format("got error frame %d", frameNumber));
            }

            // Do not update frame number tracker for physical camera result error.
            if (errorPhysicalCameraId == null) {
                // Update FrameNumberTracker for every frame during HFR mode.
                if (mBatchOutputMap.containsKey(requestId)) {
                    for (int i = 0; i < mBatchOutputMap.get(requestId); i++) {
                        mFrameNumberTracker.updateTracker(frameNumber - (subsequenceId - i),
                                /*error*/true, request.getRequestType());
                    }
                } else {
                    mFrameNumberTracker.updateTracker(frameNumber,
                            /*error*/true, request.getRequestType());
                }

                checkAndFireSequenceComplete();
            }

            // Dispatch the failure callback
            final long ident = Binder.clearCallingIdentity();
            try {
                holder.getExecutor().execute(failureDispatch);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

    }

    public void onDeviceIdle() {
        if (DEBUG) {
            Log.d(TAG, "Camera now idle");
        }
        synchronized(mInterfaceLock) {
            if (mRemoteDevice == null) return; // Camera already closed

            // Redirect device callback to the offline session in case we are in the middle
            // of an offline switch
            if (mOfflineSessionImpl != null) {
                mOfflineSessionImpl.getCallbacks().onDeviceIdle();
                return;
            }

            // Remove all capture callbacks now that device has gone to IDLE state.
            removeCompletedCallbackHolderLocked(
                    Long.MAX_VALUE, /*lastCompletedRegularFrameNumber*/
                    Long.MAX_VALUE, /*lastCompletedReprocessFrameNumber*/
                    Long.MAX_VALUE /*lastCompletedZslStillFrameNumber*/);

            if (!CameraDeviceImpl.this.mIdle) {
                final long ident = Binder.clearCallingIdentity();
                try {
                    mDeviceExecutor.execute(mCallOnIdle);
                } finally {
                    Binder.restoreCallingIdentity(ident);
                }
            }
            mIdle = true;
        }
    }

    public class CameraDeviceCallbacks extends ICameraDeviceCallbacks.Stub {

        @Override
        public IBinder asBinder() {
            return this;
        }

        @Override
        public void onDeviceError(final int errorCode, CaptureResultExtras resultExtras) {
            CameraDeviceImpl.this.onDeviceError(errorCode, resultExtras);
        }

        @Override
        public void onRepeatingRequestError(long lastFrameNumber, int repeatingRequestId) {
            if (DEBUG) {
                Log.d(TAG, "Repeating request error received. Last frame number is " +
                        lastFrameNumber);
            }

            synchronized(mInterfaceLock) {
                // Camera is already closed or no repeating request is present.
                if (mRemoteDevice == null || mRepeatingRequestId == REQUEST_ID_NONE) {
                    if ((mFailedRepeatingRequestId == repeatingRequestId) &&
                            (mFailedRepeatingRequestTypes != null) && (mRemoteDevice != null)) {
                        Log.v(TAG, "Resuming stop of failed repeating request with id: " +
                                mFailedRepeatingRequestId);

                        checkEarlyTriggerSequenceCompleteLocked(mFailedRepeatingRequestId,
                                lastFrameNumber, mFailedRepeatingRequestTypes);
                        mFailedRepeatingRequestId = REQUEST_ID_NONE;
                        mFailedRepeatingRequestTypes = null;
                    }
                    return;
                }

                // Redirect device callback to the offline session in case we are in the middle
                // of an offline switch
                if (mOfflineSessionImpl != null) {
                    mOfflineSessionImpl.getCallbacks().onRepeatingRequestError(
                           lastFrameNumber, repeatingRequestId);
                    return;
                }

                checkEarlyTriggerSequenceCompleteLocked(mRepeatingRequestId, lastFrameNumber,
                        mRepeatingRequestTypes);
                // Check if there is already a new repeating request
                if (mRepeatingRequestId == repeatingRequestId) {
                    mRepeatingRequestId = REQUEST_ID_NONE;
                    mRepeatingRequestTypes = null;
                }
            }
        }

        @Override
        public void onDeviceIdle() {
            CameraDeviceImpl.this.onDeviceIdle();
        }

        @Override
        public void onCaptureStarted(final CaptureResultExtras resultExtras, final long timestamp) {
            int requestId = resultExtras.getRequestId();
            final long frameNumber = resultExtras.getFrameNumber();
            final long lastCompletedRegularFrameNumber =
                    resultExtras.getLastCompletedRegularFrameNumber();
            final long lastCompletedReprocessFrameNumber =
                    resultExtras.getLastCompletedReprocessFrameNumber();
            final long lastCompletedZslFrameNumber =
                    resultExtras.getLastCompletedZslFrameNumber();
            final boolean hasReadoutTimestamp = resultExtras.hasReadoutTimestamp();
            final long readoutTimestamp = resultExtras.getReadoutTimestamp();

            if (DEBUG) {
                Log.d(TAG, "Capture started for id " + requestId + " frame number " + frameNumber
                        + ": completedRegularFrameNumber " + lastCompletedRegularFrameNumber
                        + ", completedReprocessFrameNUmber " + lastCompletedReprocessFrameNumber
                        + ", completedZslFrameNumber " + lastCompletedZslFrameNumber
                        + ", hasReadoutTimestamp " + hasReadoutTimestamp
                        + (hasReadoutTimestamp ? ", readoutTimestamp " + readoutTimestamp : "")) ;
            }
            final CaptureCallbackHolder holder;

            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed


                // Redirect device callback to the offline session in case we are in the middle
                // of an offline switch
                if (mOfflineSessionImpl != null) {
                    mOfflineSessionImpl.getCallbacks().onCaptureStarted(resultExtras,
                            timestamp);
                    return;
                }

                // Check if it's okay to remove completed callbacks from mCaptureCallbackMap.
                // A callback is completed if the corresponding inflight request has been removed
                // from the inflight queue in cameraservice.
                removeCompletedCallbackHolderLocked(lastCompletedRegularFrameNumber,
                        lastCompletedReprocessFrameNumber, lastCompletedZslFrameNumber);

                // Get the callback for this frame ID, if there is one
                holder = CameraDeviceImpl.this.mCaptureCallbackMap.get(requestId);

                if (holder == null) {
                    return;
                }

                if (isClosed()) return;

                // Dispatch capture start notice
                final long ident = Binder.clearCallingIdentity();
                try {
                    holder.getExecutor().execute(
                        new Runnable() {
                            @Override
                            public void run() {
                                if (!CameraDeviceImpl.this.isClosed()) {
                                    final int subsequenceId = resultExtras.getSubsequenceId();
                                    final CaptureRequest request = holder.getRequest(subsequenceId);

                                    if (holder.hasBatchedOutputs()) {
                                        // Send derived onCaptureStarted for requests within the
                                        // batch
                                        final Range<Integer> fpsRange =
                                            request.get(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE);
                                        for (int i = 0; i < holder.getRequestCount(); i++) {
                                            holder.getCallback().onCaptureStarted(
                                                CameraDeviceImpl.this,
                                                holder.getRequest(i),
                                                timestamp - (subsequenceId - i) *
                                                NANO_PER_SECOND / fpsRange.getUpper(),
                                                frameNumber - (subsequenceId - i));
                                            if (hasReadoutTimestamp) {
                                                holder.getCallback().onReadoutStarted(
                                                    CameraDeviceImpl.this,
                                                    holder.getRequest(i),
                                                    readoutTimestamp - (subsequenceId - i) *
                                                    NANO_PER_SECOND / fpsRange.getUpper(),
                                                    frameNumber - (subsequenceId - i));
                                            }
                                        }
                                    } else {
                                        holder.getCallback().onCaptureStarted(
                                            CameraDeviceImpl.this, request,
                                            timestamp, frameNumber);
                                        if (hasReadoutTimestamp) {
                                            holder.getCallback().onReadoutStarted(
                                                CameraDeviceImpl.this, request,
                                                readoutTimestamp, frameNumber);
                                        }
                                    }
                                }
                            }
                        });
                } finally {
                    Binder.restoreCallingIdentity(ident);
                }
            }
        }

        @Override
        public void onResultReceived(CameraMetadataNative result,
                CaptureResultExtras resultExtras, PhysicalCaptureResultInfo physicalResults[])
                throws RemoteException {
            int requestId = resultExtras.getRequestId();
            long frameNumber = resultExtras.getFrameNumber();

            if (DEBUG) {
                Log.v(TAG, "Received result frame " + frameNumber + " for id "
                        + requestId);
            }

            synchronized(mInterfaceLock) {
                if (mRemoteDevice == null) return; // Camera already closed


                // Redirect device callback to the offline session in case we are in the middle
                // of an offline switch
                if (mOfflineSessionImpl != null) {
                    mOfflineSessionImpl.getCallbacks().onResultReceived(result, resultExtras,
                            physicalResults);
                    return;
                }

                // TODO: Handle CameraCharacteristics access from CaptureResult correctly.
                result.set(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE,
                        getCharacteristics().get(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE));

                final CaptureCallbackHolder holder =
                        CameraDeviceImpl.this.mCaptureCallbackMap.get(requestId);

                boolean isPartialResult =
                        (resultExtras.getPartialResultCount() < mTotalPartialCount);

                // Check if we have a callback for this
                if (holder == null) {
                    if (DEBUG) {
                        Log.d(TAG,
                                "holder is null, early return at frame "
                                        + frameNumber);
                    }

                    return;
                }

                final CaptureRequest request = holder.getRequest(resultExtras.getSubsequenceId());
                int requestType = request.getRequestType();
                if (isClosed()) {
                    if (DEBUG) {
                        Log.d(TAG,
                                "camera is closed, early return at frame "
                                        + frameNumber);
                    }

                    updateTracker(requestId, frameNumber, requestType, /*result*/null,
                            isPartialResult);

                    return;
                }


                Runnable resultDispatch = null;

                CaptureResult finalResult;
                // Make a copy of the native metadata before it gets moved to a CaptureResult
                // object.
                final CameraMetadataNative resultCopy;
                if (holder.hasBatchedOutputs()) {
                    resultCopy = new CameraMetadataNative(result);
                } else {
                    resultCopy = null;
                }

                // Either send a partial result or the final capture completed result
                if (isPartialResult) {
                    final CaptureResult resultAsCapture =
                            new CaptureResult(getId(), result, request, resultExtras);
                    // Partial result
                    resultDispatch = new Runnable() {
                        @Override
                        public void run() {
                            if (!CameraDeviceImpl.this.isClosed()) {
                                if (holder.hasBatchedOutputs()) {
                                    // Send derived onCaptureProgressed for requests within
                                    // the batch.
                                    for (int i = 0; i < holder.getRequestCount(); i++) {
                                        CameraMetadataNative resultLocal =
                                                new CameraMetadataNative(resultCopy);
                                        CaptureResult resultInBatch = new CaptureResult(getId(),
                                                resultLocal, holder.getRequest(i), resultExtras);

                                        holder.getCallback().onCaptureProgressed(
                                            CameraDeviceImpl.this,
                                            holder.getRequest(i),
                                            resultInBatch);
                                    }
                                } else {
                                    holder.getCallback().onCaptureProgressed(
                                        CameraDeviceImpl.this,
                                        request,
                                        resultAsCapture);
                                }
                            }
                        }
                    };
                    finalResult = resultAsCapture;
                } else {
                    List<CaptureResult> partialResults =
                            mFrameNumberTracker.popPartialResults(frameNumber);
                    if (mBatchOutputMap.containsKey(requestId)) {
                        int requestCount = mBatchOutputMap.get(requestId);
                        for (int i = 1; i < requestCount; i++) {
                            mFrameNumberTracker.popPartialResults(frameNumber - (requestCount - i));
                        }
                    }

                    final long sensorTimestamp =
                            result.get(CaptureResult.SENSOR_TIMESTAMP);
                    final Range<Integer> fpsRange =
                            request.get(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE);
                    final int subsequenceId = resultExtras.getSubsequenceId();
                    final TotalCaptureResult resultAsCapture = new TotalCaptureResult(getId(),
                            result, request, resultExtras, partialResults, holder.getSessionId(),
                            physicalResults);
                    // Final capture result
                    resultDispatch = new Runnable() {
                        @Override
                        public void run() {
                            if (!CameraDeviceImpl.this.isClosed()){
                                if (holder.hasBatchedOutputs()) {
                                    // Send derived onCaptureCompleted for requests within
                                    // the batch.
                                    for (int i = 0; i < holder.getRequestCount(); i++) {
                                        resultCopy.set(CaptureResult.SENSOR_TIMESTAMP,
                                                sensorTimestamp - (subsequenceId - i) *
                                                NANO_PER_SECOND/fpsRange.getUpper());
                                        CameraMetadataNative resultLocal =
                                                new CameraMetadataNative(resultCopy);
                                        // No logical multi-camera support for batched output mode.
                                        TotalCaptureResult resultInBatch = new TotalCaptureResult(
                                                getId(), resultLocal, holder.getRequest(i),
                                                resultExtras, partialResults, holder.getSessionId(),
                                                new PhysicalCaptureResultInfo[0]);

                                        holder.getCallback().onCaptureCompleted(
                                            CameraDeviceImpl.this,
                                            holder.getRequest(i),
                                            resultInBatch);
                                    }
                                } else {
                                    holder.getCallback().onCaptureCompleted(
                                        CameraDeviceImpl.this,
                                        request,
                                        resultAsCapture);
                                }
                            }
                        }
                    };
                    finalResult = resultAsCapture;
                }

                final long ident = Binder.clearCallingIdentity();
                try {
                    holder.getExecutor().execute(resultDispatch);
                } finally {
                    Binder.restoreCallingIdentity(ident);
                }

                updateTracker(requestId, frameNumber, requestType, finalResult, isPartialResult);

                // Fire onCaptureSequenceCompleted
                if (!isPartialResult) {
                    checkAndFireSequenceComplete();
                }
            }
        }

        @Override
        public void onPrepared(int streamId) {
            final OutputConfiguration output;
            final StateCallbackKK sessionCallback;

            if (DEBUG) {
                Log.v(TAG, "Stream " + streamId + " is prepared");
            }

            synchronized(mInterfaceLock) {
                // Redirect device callback to the offline session in case we are in the middle
                // of an offline switch
                if (mOfflineSessionImpl != null) {
                    mOfflineSessionImpl.getCallbacks().onPrepared(streamId);
                    return;
                }

                output = mConfiguredOutputs.get(streamId);
                sessionCallback = mSessionStateCallback;
            }

            if (sessionCallback == null) return;

            if (output == null) {
                Log.w(TAG, "onPrepared invoked for unknown output Surface");
                return;
            }
            final List<Surface> surfaces = output.getSurfaces();
            for (Surface surface : surfaces) {
                sessionCallback.onSurfacePrepared(surface);
            }
        }

        @Override
        public void onRequestQueueEmpty() {
            final StateCallbackKK sessionCallback;

            if (DEBUG) {
                Log.v(TAG, "Request queue becomes empty");
            }

            synchronized(mInterfaceLock) {
                // Redirect device callback to the offline session in case we are in the middle
                // of an offline switch
                if (mOfflineSessionImpl != null) {
                    mOfflineSessionImpl.getCallbacks().onRequestQueueEmpty();
                    return;
                }

                sessionCallback = mSessionStateCallback;
            }

            if (sessionCallback == null) return;

            sessionCallback.onRequestQueueEmpty();
        }

    } // public class CameraDeviceCallbacks

    /**
     * A camera specific adapter {@link Executor} that posts all executed tasks onto the given
     * {@link Handler}.
     *
     * @hide
     */
    private static class CameraHandlerExecutor implements Executor {
        private final Handler mHandler;

        public CameraHandlerExecutor(@NonNull Handler handler) {
            mHandler = Objects.requireNonNull(handler);
        }

        @Override
        public void execute(Runnable command) {
            // Return value of 'post()' will be ignored in order to keep the
            // same camera behavior. For further details see b/74605221 .
            mHandler.post(command);
        }
    }

    /**
     * Default executor management.
     *
     * <p>
     * If executor is null, get the current thread's
     * Looper to create a Executor with. If no looper exists, throw
     * {@code IllegalArgumentException}.
     * </p>
     */
    static Executor checkExecutor(Executor executor) {
        return (executor == null) ? checkAndWrapHandler(null) : executor;
    }

    /**
     * Default executor management.
     *
     * <p>If the callback isn't null, check the executor, otherwise pass it through.</p>
     */
    public static <T> Executor checkExecutor(Executor executor, T callback) {
        return (callback != null) ? checkExecutor(executor) : executor;
    }

    /**
     * Wrap Handler in Executor.
     *
     * <p>
     * If handler is null, get the current thread's
     * Looper to create a Executor with. If no looper exists, throw
     * {@code IllegalArgumentException}.
     * </p>
     */
    public static Executor checkAndWrapHandler(Handler handler) {
        return new CameraHandlerExecutor(checkHandler(handler));
    }

    /**
     * Default handler management.
     *
     * <p>
     * If handler is null, get the current thread's
     * Looper to create a Handler with. If no looper exists, throw {@code IllegalArgumentException}.
     * </p>
     */
    static Handler checkHandler(Handler handler) {
        if (handler == null) {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalArgumentException(
                    "No handler given, and current thread has no looper!");
            }
            handler = new Handler(looper);
        }
        return handler;
    }

    /**
     * Default handler management, conditional on there being a callback.
     *
     * <p>If the callback isn't null, check the handler, otherwise pass it through.</p>
     */
    static <T> Handler checkHandler(Handler handler, T callback) {
        if (callback != null) {
            return checkHandler(handler);
        }
        return handler;
    }

    private void checkIfCameraClosedOrInError() throws CameraAccessException {
        if (mRemoteDevice == null) {
            throw new IllegalStateException("CameraDevice was already closed");
        }
        if (mInError) {
            throw new CameraAccessException(CameraAccessException.CAMERA_ERROR,
                    "The camera device has encountered a serious error");
        }
    }

    /** Whether the camera device has started to close (may not yet have finished) */
    private boolean isClosed() {
        return mClosing.get();
    }

    private CameraCharacteristics getCharacteristics() {
        return mCharacteristics;
    }

    private boolean isRawFormat(int format) {
        return (format == ImageFormat.RAW_PRIVATE || format == ImageFormat.RAW10
                || format == ImageFormat.RAW12 || format == ImageFormat.RAW_SENSOR);
    }

    /**
     * Listener for binder death.
     *
     * <p> Handle binder death for ICameraDeviceUser. Trigger onError.</p>
     */
    @Override
    public void binderDied() {
        Log.w(TAG, "CameraDevice " + mCameraId + " died unexpectedly");

        if (mRemoteDevice == null) {
            return; // Camera already closed
        }

        mInError = true;
        Runnable r = new Runnable() {
            @Override
            public void run() {
                if (!isClosed()) {
                    mDeviceCallback.onError(CameraDeviceImpl.this,
                            StateCallback.ERROR_CAMERA_SERVICE);
                }
            }
        };
        final long ident = Binder.clearCallingIdentity();
        try {
            CameraDeviceImpl.this.mDeviceExecutor.execute(r);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    @Override
    public void setCameraAudioRestriction(
            @CAMERA_AUDIO_RESTRICTION int mode) throws CameraAccessException {
        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            mRemoteDevice.setCameraAudioRestriction(mode);
        }
    }

    @Override
    public @CAMERA_AUDIO_RESTRICTION int getCameraAudioRestriction() throws CameraAccessException {
        synchronized(mInterfaceLock) {
            checkIfCameraClosedOrInError();
            return mRemoteDevice.getGlobalAudioRestriction();
        }
    }

    @Override
    public void createExtensionSession(ExtensionSessionConfiguration extensionConfiguration)
            throws CameraAccessException {
        HashMap<String, CameraCharacteristics> characteristicsMap = new HashMap<>(
                getPhysicalIdToChars());
        characteristicsMap.put(mCameraId, mCharacteristics);
        boolean initializationFailed = true;
        IBinder token = new Binder(TAG + " : " + mNextSessionId++);
        try {
            boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token,
                    extensionConfiguration.getExtension(), mCameraId,
                    CameraExtensionUtils.getCharacteristicsMapNative(characteristicsMap));
            if (!ret) {
                token = null;
                throw new UnsupportedOperationException("Unsupported extension!");
            }

            if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported(
                        extensionConfiguration.getExtension())) {
                mCurrentAdvancedExtensionSession =
                        CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession(
                                this, characteristicsMap, mContext, extensionConfiguration,
                                mNextSessionId, token);
            } else {
                mCurrentExtensionSession = CameraExtensionSessionImpl.createCameraExtensionSession(
                        this, characteristicsMap, mContext, extensionConfiguration,
                        mNextSessionId, token);
            }
            initializationFailed = false;
        } catch (RemoteException e) {
            throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
        } finally {
            if (initializationFailed && (token != null)) {
                CameraExtensionCharacteristics.unregisterClient(mContext, token,
                        extensionConfiguration.getExtension());
            }
        }
    }
}
