camera2: Change CaptureListener callbacks to include the session (api)

* Also makes CameraDevice an abstract class (API)

Bug: 15428219
Bug: 14964443
Change-Id: I0e8c8309186aad59570aac7e0f998bb615405f0a
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
new file mode 100644
index 0000000..81bd2fd
--- /dev/null
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -0,0 +1,1050 @@
+/*
+ * 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 android.hardware.camera2.CameraAccessException.CAMERA_IN_USE;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.ICameraDeviceCallbacks;
+import android.hardware.camera2.ICameraDeviceUser;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.utils.CameraBinderDecorator;
+import android.hardware.camera2.utils.CameraRuntimeException;
+import android.hardware.camera2.utils.LongParcelable;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Surface;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * HAL2.1+ implementation of CameraDevice. Use CameraManager#open to instantiate
+ */
+public class CameraDeviceImpl extends android.hardware.camera2.CameraDevice {
+
+    private final String TAG;
+    private final boolean DEBUG;
+
+    private static final int REQUEST_ID_NONE = -1;
+
+    // TODO: guard every function with if (!mRemoteDevice) check (if it was closed)
+    private ICameraDeviceUser mRemoteDevice;
+
+    private final Object mLock = new Object();
+    private final CameraDeviceCallbacks mCallbacks = new CameraDeviceCallbacks();
+
+    private final StateListener mDeviceListener;
+    private volatile StateListener mSessionStateListener;
+    private final Handler mDeviceHandler;
+
+    private boolean mIdle = true;
+
+    /** map request IDs to listener/request data */
+    private final SparseArray<CaptureListenerHolder> mCaptureListenerMap =
+            new SparseArray<CaptureListenerHolder>();
+
+    private int mRepeatingRequestId = REQUEST_ID_NONE;
+    private final ArrayList<Integer> mRepeatingRequestIdDeletedList = new ArrayList<Integer>();
+    // Map stream IDs to Surfaces
+    private final SparseArray<Surface> mConfiguredOutputs = new SparseArray<Surface>();
+
+    private final String mCameraId;
+    private final CameraCharacteristics mCharacteristics;
+
+    /**
+     * A list tracking request and its expected last frame.
+     * Updated when calling ICameraDeviceUser methods.
+     */
+    private final List<SimpleEntry</*frameNumber*/Long, /*requestId*/Integer>>
+            mFrameNumberRequestPairs = new ArrayList<SimpleEntry<Long, Integer>>();
+
+    /**
+     * An object tracking received frame numbers.
+     * Updated when receiving callbacks from ICameraDeviceCallbacks.
+     */
+    private final FrameNumberTracker mFrameNumberTracker = new FrameNumberTracker();
+
+    private CameraCaptureSessionImpl mCurrentSession;
+
+    // Runnables for all state transitions, except error, which needs the
+    // error code argument
+
+    private final Runnable mCallOnOpened = new Runnable() {
+        @Override
+        public void run() {
+            if (!CameraDeviceImpl.this.isClosed()) {
+                mDeviceListener.onOpened(CameraDeviceImpl.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onOpened(CameraDeviceImpl.this);
+                }
+            }
+        }
+    };
+
+    private final Runnable mCallOnUnconfigured = new Runnable() {
+        @Override
+        public void run() {
+            if (!CameraDeviceImpl.this.isClosed()) {
+                mDeviceListener.onUnconfigured(CameraDeviceImpl.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onUnconfigured(CameraDeviceImpl.this);
+                }
+            }
+        }
+    };
+
+    private final Runnable mCallOnActive = new Runnable() {
+        @Override
+        public void run() {
+            if (!CameraDeviceImpl.this.isClosed()) {
+                mDeviceListener.onActive(CameraDeviceImpl.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onActive(CameraDeviceImpl.this);
+                }
+            }
+        }
+    };
+
+    private final Runnable mCallOnBusy = new Runnable() {
+        @Override
+        public void run() {
+            if (!CameraDeviceImpl.this.isClosed()) {
+                mDeviceListener.onBusy(CameraDeviceImpl.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onBusy(CameraDeviceImpl.this);
+                }
+            }
+        }
+    };
+
+    private final Runnable mCallOnClosed = new Runnable() {
+        @Override
+        public void run() {
+            mDeviceListener.onClosed(CameraDeviceImpl.this);
+            StateListener sessionListener = mSessionStateListener;
+            if (sessionListener != null) {
+                sessionListener.onClosed(CameraDeviceImpl.this);
+            }
+        }
+    };
+
+    private final Runnable mCallOnIdle = new Runnable() {
+        @Override
+        public void run() {
+            if (!CameraDeviceImpl.this.isClosed()) {
+                mDeviceListener.onIdle(CameraDeviceImpl.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onIdle(CameraDeviceImpl.this);
+                }
+            }
+        }
+    };
+
+    private final Runnable mCallOnDisconnected = new Runnable() {
+        @Override
+        public void run() {
+            if (!CameraDeviceImpl.this.isClosed()) {
+                mDeviceListener.onDisconnected(CameraDeviceImpl.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onDisconnected(CameraDeviceImpl.this);
+                }
+            }
+        }
+    };
+
+    public CameraDeviceImpl(String cameraId, StateListener listener, Handler handler,
+                        CameraCharacteristics characteristics) {
+        if (cameraId == null || listener == null || handler == null) {
+            throw new IllegalArgumentException("Null argument given");
+        }
+        mCameraId = cameraId;
+        mDeviceListener = listener;
+        mDeviceHandler = handler;
+        mCharacteristics = characteristics;
+
+        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;
+        DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    }
+
+    public CameraDeviceCallbacks getCallbacks() {
+        return mCallbacks;
+    }
+
+    public void setRemoteDevice(ICameraDeviceUser remoteDevice) {
+        // TODO: Move from decorator to direct binder-mediated exceptions
+        synchronized(mLock) {
+            mRemoteDevice = CameraBinderDecorator.newInstance(remoteDevice);
+
+            mDeviceHandler.post(mCallOnOpened);
+            mDeviceHandler.post(mCallOnUnconfigured);
+        }
+    }
+
+    @Override
+    public String getId() {
+        return mCameraId;
+    }
+
+    @Override
+    public void configureOutputs(List<Surface> outputs) throws CameraAccessException {
+        // Treat a null input the same an empty list
+        if (outputs == null) {
+            outputs = new ArrayList<Surface>();
+        }
+        synchronized (mLock) {
+            checkIfCameraClosed();
+
+            HashSet<Surface> addSet = new HashSet<Surface>(outputs);    // Streams to create
+            List<Integer> deleteList = new ArrayList<Integer>();        // Streams to delete
+
+            // Determine which streams need to be created, which to be deleted
+            for (int i = 0; i < mConfiguredOutputs.size(); ++i) {
+                int streamId = mConfiguredOutputs.keyAt(i);
+                Surface s = mConfiguredOutputs.valueAt(i);
+
+                if (!outputs.contains(s)) {
+                    deleteList.add(streamId);
+                } else {
+                    addSet.remove(s);  // Don't create a stream previously created
+                }
+            }
+
+            mDeviceHandler.post(mCallOnBusy);
+            stopRepeating();
+
+            try {
+                waitUntilIdle();
+
+                mRemoteDevice.beginConfigure();
+                // Delete all streams first (to free up HW resources)
+                for (Integer streamId : deleteList) {
+                    mRemoteDevice.deleteStream(streamId);
+                    mConfiguredOutputs.delete(streamId);
+                }
+
+                // Add all new streams
+                for (Surface s : addSet) {
+                    // TODO: remove width,height,format since we are ignoring
+                    // it.
+                    int streamId = mRemoteDevice.createStream(0, 0, 0, s);
+                    mConfiguredOutputs.put(streamId, s);
+                }
+
+                mRemoteDevice.endConfigure();
+            } catch (CameraRuntimeException e) {
+                if (e.getReason() == CAMERA_IN_USE) {
+                    throw new IllegalStateException("The camera is currently busy." +
+                            " You must wait until the previous operation completes.");
+                }
+
+                throw e.asChecked();
+            } catch (RemoteException e) {
+                // impossible
+                return;
+            }
+
+            if (outputs.size() > 0) {
+                mDeviceHandler.post(mCallOnIdle);
+            } else {
+                mDeviceHandler.post(mCallOnUnconfigured);
+            }
+        }
+    }
+
+    @Override
+    public void createCaptureSession(List<Surface> outputs,
+            CameraCaptureSession.StateListener listener, Handler handler)
+            throws CameraAccessException {
+        synchronized (mLock) {
+            if (DEBUG) {
+                Log.d(TAG, "createCaptureSession");
+            }
+
+            checkIfCameraClosed();
+
+            // TODO: we must be in UNCONFIGURED mode to begin with, or using another session
+
+            // TODO: dont block for this
+            boolean configureSuccess = true;
+            CameraAccessException pendingException = null;
+            try {
+                configureOutputs(outputs); // and then block until IDLE
+            } catch (CameraAccessException e) {
+                configureSuccess = false;
+                pendingException = e;
+            }
+
+            // Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise.
+            CameraCaptureSessionImpl newSession =
+                    new CameraCaptureSessionImpl(outputs, listener, handler, this, mDeviceHandler,
+                            configureSuccess);
+
+            if (mCurrentSession != null) {
+                mCurrentSession.replaceSessionClose(newSession);
+            }
+
+            // TODO: wait until current session closes, then create the new session
+            mCurrentSession = newSession;
+
+            if (pendingException != null) {
+                throw pendingException;
+            }
+
+            mSessionStateListener = mCurrentSession.getDeviceStateListener();
+        }
+    }
+
+    @Override
+    public CaptureRequest.Builder createCaptureRequest(int templateType)
+            throws CameraAccessException {
+        synchronized (mLock) {
+            checkIfCameraClosed();
+
+            CameraMetadataNative templatedRequest = new CameraMetadataNative();
+
+            try {
+                mRemoteDevice.createDefaultRequest(templateType, /*out*/templatedRequest);
+            } catch (CameraRuntimeException e) {
+                throw e.asChecked();
+            } catch (RemoteException e) {
+                // impossible
+                return null;
+            }
+
+            CaptureRequest.Builder builder =
+                    new CaptureRequest.Builder(templatedRequest);
+
+            return builder;
+        }
+    }
+
+    @Override
+    public int capture(CaptureRequest request, CaptureListener listener, Handler handler)
+            throws CameraAccessException {
+        if (DEBUG) {
+            Log.d(TAG, "calling capture");
+        }
+        List<CaptureRequest> requestList = new ArrayList<CaptureRequest>();
+        requestList.add(request);
+        return submitCaptureRequest(requestList, listener, handler, /*streaming*/false);
+    }
+
+    @Override
+    public int captureBurst(List<CaptureRequest> requests, CaptureListener listener,
+            Handler handler) throws CameraAccessException {
+        if (requests == null || requests.isEmpty()) {
+            throw new IllegalArgumentException("At least one request must be given");
+        }
+        return submitCaptureRequest(requests, listener, handler, /*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 onCaptureSequenceCompleted is immediately triggered.
+     * If lastFrameNumber is non-negative, then the requestId and lastFrameNumber pair
+     * is added to the list mFrameNumberRequestPairs.</p>
+     *
+     * @param requestId the request ID of the current repeating request.
+     *
+     * @param lastFrameNumber last frame number returned from binder.
+     */
+    private void checkEarlyTriggerSequenceComplete(
+            final int requestId, final long lastFrameNumber) {
+        // lastFrameNumber being equal to NO_FRAMES_CAPTURED means that the request
+        // was never sent to HAL. Should trigger onCaptureSequenceCompleted immediately.
+        if (lastFrameNumber == CaptureListener.NO_FRAMES_CAPTURED) {
+            final CaptureListenerHolder holder;
+            int index = mCaptureListenerMap.indexOfKey(requestId);
+            holder = (index >= 0) ? mCaptureListenerMap.valueAt(index) : null;
+            if (holder != null) {
+                mCaptureListenerMap.removeAt(index);
+                if (DEBUG) {
+                    Log.v(TAG, String.format(
+                            "remove holder for requestId %d, "
+                            + "because lastFrame is %d.",
+                            requestId, lastFrameNumber));
+                }
+            }
+
+            if (holder != null) {
+                if (DEBUG) {
+                    Log.v(TAG, "immediately trigger onCaptureSequenceCompleted 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));
+                            }
+                            if (lastFrameNumber < Integer.MIN_VALUE
+                                    || lastFrameNumber > Integer.MAX_VALUE) {
+                                throw new AssertionError(lastFrameNumber + " cannot be cast to int");
+                            }
+                            holder.getListener().onCaptureSequenceCompleted(
+                                    CameraDeviceImpl.this,
+                                    requestId,
+                                    lastFrameNumber);
+                        }
+                    }
+                };
+                holder.getHandler().post(resultDispatch);
+            } else {
+                Log.w(TAG, String.format(
+                        "did not register listener to request %d",
+                        requestId));
+            }
+        } else {
+            mFrameNumberRequestPairs.add(
+                    new SimpleEntry<Long, Integer>(lastFrameNumber,
+                            requestId));
+        }
+    }
+
+    private int submitCaptureRequest(List<CaptureRequest> requestList, CaptureListener listener,
+            Handler handler, boolean repeating) throws CameraAccessException {
+
+        // Need a valid handler, or current thread needs to have a looper, if
+        // listener is valid
+        if (listener != null) {
+            handler = checkHandler(handler);
+        }
+
+        synchronized (mLock) {
+            checkIfCameraClosed();
+            int requestId;
+
+            if (repeating) {
+                stopRepeating();
+            }
+
+            LongParcelable lastFrameNumberRef = new LongParcelable();
+            try {
+                requestId = mRemoteDevice.submitRequestList(requestList, repeating,
+                        /*out*/lastFrameNumberRef);
+                if (DEBUG) {
+                    Log.v(TAG, "last frame number " + lastFrameNumberRef.getNumber());
+                }
+            } catch (CameraRuntimeException e) {
+                throw e.asChecked();
+            } catch (RemoteException e) {
+                // impossible
+                return -1;
+            }
+
+            if (listener != null) {
+                mCaptureListenerMap.put(requestId, new CaptureListenerHolder(listener,
+                        requestList, handler, repeating));
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "Listen for request " + requestId + " is null");
+                }
+            }
+
+            long lastFrameNumber = lastFrameNumberRef.getNumber();
+
+            if (repeating) {
+                if (mRepeatingRequestId != REQUEST_ID_NONE) {
+                    checkEarlyTriggerSequenceComplete(mRepeatingRequestId, lastFrameNumber);
+                }
+                mRepeatingRequestId = requestId;
+            } else {
+                mFrameNumberRequestPairs.add(
+                        new SimpleEntry<Long, Integer>(lastFrameNumber, requestId));
+            }
+
+            if (mIdle) {
+                mDeviceHandler.post(mCallOnActive);
+            }
+            mIdle = false;
+
+            return requestId;
+        }
+    }
+
+    @Override
+    public int setRepeatingRequest(CaptureRequest request, CaptureListener listener,
+            Handler handler) throws CameraAccessException {
+        List<CaptureRequest> requestList = new ArrayList<CaptureRequest>();
+        requestList.add(request);
+        return submitCaptureRequest(requestList, listener, handler, /*streaming*/true);
+    }
+
+    @Override
+    public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener,
+            Handler handler) throws CameraAccessException {
+        if (requests == null || requests.isEmpty()) {
+            throw new IllegalArgumentException("At least one request must be given");
+        }
+        return submitCaptureRequest(requests, listener, handler, /*streaming*/true);
+    }
+
+    @Override
+    public void stopRepeating() throws CameraAccessException {
+
+        synchronized (mLock) {
+            checkIfCameraClosed();
+            if (mRepeatingRequestId != REQUEST_ID_NONE) {
+
+                int requestId = mRepeatingRequestId;
+                mRepeatingRequestId = REQUEST_ID_NONE;
+
+                // Queue for deletion after in-flight requests finish
+                if (mCaptureListenerMap.get(requestId) != null) {
+                    mRepeatingRequestIdDeletedList.add(requestId);
+                }
+
+                try {
+                    LongParcelable lastFrameNumberRef = new LongParcelable();
+                    mRemoteDevice.cancelRequest(requestId, /*out*/lastFrameNumberRef);
+                    long lastFrameNumber = lastFrameNumberRef.getNumber();
+
+                    checkEarlyTriggerSequenceComplete(requestId, lastFrameNumber);
+
+                } catch (CameraRuntimeException e) {
+                    throw e.asChecked();
+                } catch (RemoteException e) {
+                    // impossible
+                    return;
+                }
+            }
+        }
+    }
+
+    private void waitUntilIdle() throws CameraAccessException {
+
+        synchronized (mLock) {
+            checkIfCameraClosed();
+            if (mRepeatingRequestId != REQUEST_ID_NONE) {
+                throw new IllegalStateException("Active repeating request ongoing");
+            }
+
+            try {
+                mRemoteDevice.waitUntilIdle();
+            } catch (CameraRuntimeException e) {
+                throw e.asChecked();
+            } catch (RemoteException e) {
+                // impossible
+                return;
+            }
+
+            mRepeatingRequestId = REQUEST_ID_NONE;
+        }
+    }
+
+    @Override
+    public void flush() throws CameraAccessException {
+        synchronized (mLock) {
+            checkIfCameraClosed();
+
+            mDeviceHandler.post(mCallOnBusy);
+            try {
+                LongParcelable lastFrameNumberRef = new LongParcelable();
+                mRemoteDevice.flush(/*out*/lastFrameNumberRef);
+                if (mRepeatingRequestId != REQUEST_ID_NONE) {
+                    long lastFrameNumber = lastFrameNumberRef.getNumber();
+                    checkEarlyTriggerSequenceComplete(mRepeatingRequestId, lastFrameNumber);
+                    mRepeatingRequestId = REQUEST_ID_NONE;
+                }
+            } catch (CameraRuntimeException e) {
+                throw e.asChecked();
+            } catch (RemoteException e) {
+                // impossible
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+
+            try {
+                if (mRemoteDevice != null) {
+                    mRemoteDevice.disconnect();
+                }
+            } catch (CameraRuntimeException e) {
+                Log.e(TAG, "Exception while closing: ", e.asChecked());
+            } catch (RemoteException e) {
+                // impossible
+            }
+
+            if (mRemoteDevice != null) {
+                mDeviceHandler.post(mCallOnClosed);
+            }
+
+            mRemoteDevice = null;
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            close();
+        }
+        finally {
+            super.finalize();
+        }
+    }
+
+    static class CaptureListenerHolder {
+
+        private final boolean mRepeating;
+        private final CaptureListener mListener;
+        private final List<CaptureRequest> mRequestList;
+        private final Handler mHandler;
+
+        CaptureListenerHolder(CaptureListener listener, List<CaptureRequest> requestList,
+                Handler handler, boolean repeating) {
+            if (listener == null || handler == null) {
+                throw new UnsupportedOperationException(
+                    "Must have a valid handler and a valid listener");
+            }
+            mRepeating = repeating;
+            mHandler = handler;
+            mRequestList = new ArrayList<CaptureRequest>(requestList);
+            mListener = listener;
+        }
+
+        public boolean isRepeating() {
+            return mRepeating;
+        }
+
+        public CaptureListener getListener() {
+            return mListener;
+        }
+
+        public CaptureRequest getRequest(int subsequenceId) {
+            if (subsequenceId >= mRequestList.size()) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Requested subsequenceId %d is larger than request list size %d.",
+                                subsequenceId, mRequestList.size()));
+            } else {
+                if (subsequenceId < 0) {
+                    throw new IllegalArgumentException(String.format(
+                            "Requested subsequenceId %d is negative", subsequenceId));
+                } else {
+                    return mRequestList.get(subsequenceId);
+                }
+            }
+        }
+
+        public CaptureRequest getRequest() {
+            return getRequest(0);
+        }
+
+        public Handler getHandler() {
+            return mHandler;
+        }
+
+    }
+
+    /**
+     * This class tracks the last frame number for submitted requests.
+     */
+    public class FrameNumberTracker {
+
+        private long mCompletedFrameNumber = -1;
+        private final TreeSet<Long> mFutureErrorSet = new TreeSet<Long>();
+
+        private void update() {
+            Iterator<Long> iter = mFutureErrorSet.iterator();
+            while (iter.hasNext()) {
+                long errorFrameNumber = iter.next();
+                if (errorFrameNumber == mCompletedFrameNumber + 1) {
+                    mCompletedFrameNumber++;
+                    iter.remove();
+                } else {
+                    break;
+                }
+            }
+        }
+
+        /**
+         * This function is called every time when a result or an error is received.
+         * @param frameNumber: the frame number corresponding to the result or error
+         * @param isError: true if it is an error, false if it is not an error
+         */
+        public void updateTracker(long frameNumber, boolean isError) {
+            if (isError) {
+                mFutureErrorSet.add(frameNumber);
+            } else {
+                /**
+                 * HAL cannot send an OnResultReceived for frame N unless it knows for
+                 * sure that all frames prior to N have either errored out or completed.
+                 * So if the current frame is not an error, then all previous frames
+                 * should have arrived. The following line checks whether this holds.
+                 */
+                if (frameNumber != mCompletedFrameNumber + 1) {
+                    Log.e(TAG, String.format(
+                            "result frame number %d comes out of order, should be %d + 1",
+                            frameNumber, mCompletedFrameNumber));
+                }
+                mCompletedFrameNumber++;
+            }
+            update();
+        }
+
+        public long getCompletedFrameNumber() {
+            return mCompletedFrameNumber;
+        }
+
+    }
+
+    private void checkAndFireSequenceComplete() {
+        long completedFrameNumber = mFrameNumberTracker.getCompletedFrameNumber();
+        Iterator<SimpleEntry<Long, Integer> > iter = mFrameNumberRequestPairs.iterator();
+        while (iter.hasNext()) {
+            final SimpleEntry<Long, Integer> frameNumberRequestPair = iter.next();
+            if (frameNumberRequestPair.getKey() <= completedFrameNumber) {
+
+                // remove request from mCaptureListenerMap
+                final int requestId = frameNumberRequestPair.getValue();
+                final CaptureListenerHolder holder;
+                synchronized (mLock) {
+                    int index = mCaptureListenerMap.indexOfKey(requestId);
+                    holder = (index >= 0) ? mCaptureListenerMap.valueAt(index)
+                            : null;
+                    if (holder != null) {
+                        mCaptureListenerMap.removeAt(index);
+                        if (DEBUG) {
+                            Log.v(TAG, String.format(
+                                    "remove holder for requestId %d, "
+                                    + "because lastFrame %d is <= %d",
+                                    requestId, frameNumberRequestPair.getKey(),
+                                    completedFrameNumber));
+                        }
+                    }
+                }
+                iter.remove();
+
+                // Call onCaptureSequenceCompleted
+                if (holder != null) {
+                    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));
+                                }
+
+                                long lastFrameNumber = frameNumberRequestPair.getKey();
+                                if (lastFrameNumber < Integer.MIN_VALUE
+                                        || lastFrameNumber > Integer.MAX_VALUE) {
+                                    throw new AssertionError(lastFrameNumber
+                                            + " cannot be cast to int");
+                                }
+                                holder.getListener().onCaptureSequenceCompleted(
+                                    CameraDeviceImpl.this,
+                                    requestId,
+                                    lastFrameNumber);
+                            }
+                        }
+                    };
+                    holder.getHandler().post(resultDispatch);
+                }
+
+            }
+        }
+    }
+
+    public class CameraDeviceCallbacks extends ICameraDeviceCallbacks.Stub {
+
+        //
+        // Constants below need to be kept up-to-date with
+        // frameworks/av/include/camera/camera2/ICameraDeviceCallbacks.h
+        //
+
+        //
+        // Error codes for onCameraError
+        //
+
+        /**
+         * Camera has been disconnected
+         */
+        static final int ERROR_CAMERA_DISCONNECTED = 0;
+
+        /**
+         * Camera has encountered a device-level error
+         * Matches CameraDevice.StateListener#ERROR_CAMERA_DEVICE
+         */
+        static final int ERROR_CAMERA_DEVICE = 1;
+
+        /**
+         * Camera has encountered a service-level error
+         * Matches CameraDevice.StateListener#ERROR_CAMERA_SERVICE
+         */
+        static final int ERROR_CAMERA_SERVICE = 2;
+
+        @Override
+        public IBinder asBinder() {
+            return this;
+        }
+
+        @Override
+        public void onCameraError(final int errorCode, CaptureResultExtras resultExtras) {
+            Runnable r = null;
+            if (isClosed()) return;
+
+            synchronized(mLock) {
+                switch (errorCode) {
+                    case ERROR_CAMERA_DISCONNECTED:
+                        r = mCallOnDisconnected;
+                        break;
+                    default:
+                        Log.e(TAG, "Unknown error from camera device: " + errorCode);
+                        // no break
+                    case ERROR_CAMERA_DEVICE:
+                    case ERROR_CAMERA_SERVICE:
+                        r = new Runnable() {
+                            @Override
+                            public void run() {
+                                if (!CameraDeviceImpl.this.isClosed()) {
+                                    mDeviceListener.onError(CameraDeviceImpl.this, errorCode);
+                                }
+                            }
+                        };
+                        break;
+                }
+                CameraDeviceImpl.this.mDeviceHandler.post(r);
+            }
+
+            // Fire onCaptureSequenceCompleted
+            if (DEBUG) {
+                Log.v(TAG, String.format("got error frame %d", resultExtras.getFrameNumber()));
+            }
+            mFrameNumberTracker.updateTracker(resultExtras.getFrameNumber(), /*error*/true);
+            checkAndFireSequenceComplete();
+
+        }
+
+        @Override
+        public void onCameraIdle() {
+            if (isClosed()) return;
+
+            if (DEBUG) {
+                Log.d(TAG, "Camera now idle");
+            }
+            synchronized (mLock) {
+                if (!CameraDeviceImpl.this.mIdle) {
+                    CameraDeviceImpl.this.mDeviceHandler.post(mCallOnIdle);
+                }
+                CameraDeviceImpl.this.mIdle = true;
+            }
+        }
+
+        @Override
+        public void onCaptureStarted(final CaptureResultExtras resultExtras, final long timestamp) {
+            int requestId = resultExtras.getRequestId();
+            if (DEBUG) {
+                Log.d(TAG, "Capture started for id " + requestId);
+            }
+            final CaptureListenerHolder holder;
+
+            // Get the listener for this frame ID, if there is one
+            synchronized (mLock) {
+                holder = CameraDeviceImpl.this.mCaptureListenerMap.get(requestId);
+            }
+
+            if (holder == null) {
+                return;
+            }
+
+            if (isClosed()) return;
+
+            // Dispatch capture start notice
+            holder.getHandler().post(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!CameraDeviceImpl.this.isClosed()) {
+                            holder.getListener().onCaptureStarted(
+                                CameraDeviceImpl.this,
+                                holder.getRequest(resultExtras.getSubsequenceId()),
+                                timestamp);
+                        }
+                    }
+                });
+        }
+
+        @Override
+        public void onResultReceived(CameraMetadataNative result,
+                CaptureResultExtras resultExtras) throws RemoteException {
+
+            int requestId = resultExtras.getRequestId();
+            if (DEBUG) {
+                Log.v(TAG, "Received result frame " + resultExtras.getFrameNumber() + " for id "
+                        + requestId);
+            }
+
+
+            // TODO: Handle CameraCharacteristics access from CaptureResult correctly.
+            result.set(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE,
+                    getCharacteristics().get(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE));
+
+            final CaptureListenerHolder holder;
+            synchronized (mLock) {
+                holder = CameraDeviceImpl.this.mCaptureListenerMap.get(requestId);
+            }
+
+            Boolean quirkPartial = result.get(CaptureResult.QUIRKS_PARTIAL_RESULT);
+            boolean quirkIsPartialResult = (quirkPartial != null && quirkPartial);
+
+            // Update tracker (increment counter) when it's not a partial result.
+            if (!quirkIsPartialResult) {
+                mFrameNumberTracker.updateTracker(resultExtras.getFrameNumber(), /*error*/false);
+            }
+
+            // Check if we have a listener for this
+            if (holder == null) {
+                if (DEBUG) {
+                    Log.d(TAG,
+                            "holder is null, early return at frame "
+                                    + resultExtras.getFrameNumber());
+                }
+                return;
+            }
+
+            if (isClosed()) {
+                if (DEBUG) {
+                    Log.d(TAG,
+                            "camera is closed, early return at frame "
+                                    + resultExtras.getFrameNumber());
+                }
+                return;
+            }
+
+            final CaptureRequest request = holder.getRequest(resultExtras.getSubsequenceId());
+
+
+            Runnable resultDispatch = null;
+
+            // Either send a partial result or the final capture completed result
+            if (quirkIsPartialResult) {
+                final CaptureResult resultAsCapture =
+                        new CaptureResult(result, request, requestId);
+
+                // Partial result
+                resultDispatch = new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!CameraDeviceImpl.this.isClosed()){
+                            holder.getListener().onCapturePartial(
+                                CameraDeviceImpl.this,
+                                request,
+                                resultAsCapture);
+                        }
+                    }
+                };
+            } else {
+                final TotalCaptureResult resultAsCapture =
+                        new TotalCaptureResult(result, request, requestId);
+
+                // Final capture result
+                resultDispatch = new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!CameraDeviceImpl.this.isClosed()){
+                            holder.getListener().onCaptureCompleted(
+                                CameraDeviceImpl.this,
+                                request,
+                                resultAsCapture);
+                        }
+                    }
+                };
+            }
+
+            holder.getHandler().post(resultDispatch);
+
+            // Fire onCaptureSequenceCompleted
+            if (!quirkIsPartialResult) {
+                checkAndFireSequenceComplete();
+            }
+        }
+
+    }
+
+    /**
+     * 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;
+    }
+
+    private void checkIfCameraClosed() {
+        if (mRemoteDevice == null) {
+            throw new IllegalStateException("CameraDevice was already closed");
+        }
+    }
+
+    private boolean isClosed() {
+        synchronized(mLock) {
+            return (mRemoteDevice == null);
+        }
+    }
+
+    private CameraCharacteristics getCharacteristics() {
+        return mCharacteristics;
+    }
+}