camera2: Implement most of CameraCaptureSession

Bug: 14964443
Change-Id: I8203842c77a94a3a6e5f89494fce658b00a4160d
diff --git a/core/java/android/hardware/camera2/CameraCaptureSession.java b/core/java/android/hardware/camera2/CameraCaptureSession.java
index 7738d2d..d62958f 100644
--- a/core/java/android/hardware/camera2/CameraCaptureSession.java
+++ b/core/java/android/hardware/camera2/CameraCaptureSession.java
@@ -54,7 +54,7 @@
 public abstract class CameraCaptureSession implements AutoCloseable {
 
     /**
-     * Get the camera device that this session is created for
+     * Get the camera device that this session is created for.
      */
     public abstract CameraDevice getDevice();
 
@@ -90,8 +90,9 @@
      *
      * @throws CameraAccessException if the camera device is no longer connected or has
      *                               encountered a fatal error
-     * @throws IllegalStateException if this session is no longer active, either because a new
-     *                               session has been created or the camera device has been closed.
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
      * @throws IllegalArgumentException if the request targets Surfaces that are not configured as
      *                                  outputs for this session. Or if the handler is null, the
      *                                  listener is not null, and the calling thread has no looper.
@@ -99,6 +100,7 @@
      * @see #captureBurst
      * @see #setRepeatingRequest
      * @see #setRepeatingBurst
+     * @see #abortCaptures
      */
     public abstract int capture(CaptureRequest request, CaptureListener listener, Handler handler)
             throws CameraAccessException;
@@ -132,8 +134,9 @@
      *
      * @throws CameraAccessException if the camera device is no longer connected or has
      *                               encountered a fatal error
-     * @throws IllegalStateException if this session is no longer active, either because a new
-     *                               session has been created or the camera device has been closed.
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
      * @throws IllegalArgumentException If the requests target Surfaces not currently configured as
      *                                  outputs. Or if the handler is null, the listener is not
      *                                  null, and the calling thread has no looper.
@@ -141,6 +144,7 @@
      * @see #capture
      * @see #setRepeatingRequest
      * @see #setRepeatingBurst
+     * @see #abortCaptures
      */
     public abstract int captureBurst(List<CaptureRequest> requests, CaptureListener listener,
             Handler handler) throws CameraAccessException;
@@ -188,11 +192,13 @@
      *
      * @throws CameraAccessException if the camera device is no longer connected or has
      *                               encountered a fatal error
-     * @throws IllegalStateException if this session is no longer active, either because a new
-     *                               session has been created or the camera device has been closed.
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
      * @throws IllegalArgumentException If the requests reference Surfaces that are not currently
      *                                  configured as outputs. Or if the handler is null, the
      *                                  listener is not null, and the calling thread has no looper.
+     *                                  Or if no requests were passed in.
      *
      * @see #capture
      * @see #captureBurst
@@ -246,11 +252,13 @@
      *
      * @throws CameraAccessException if the camera device is no longer connected or has
      *                               encountered a fatal error
-     * @throws IllegalStateException if this session is no longer active, either because a new
-     *                               session has been created or the camera device has been closed.
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
      * @throws IllegalArgumentException If the requests reference Surfaces not currently configured
      *                                  as outputs. Or if the handler is null, the listener is not
-     *                                  null, and the calling thread has no looper.
+     *                                  null, and the calling thread has no looper. Or if no
+     *                                  requests were passed in.
      *
      * @see #capture
      * @see #captureBurst
@@ -274,8 +282,9 @@
      *
      * @throws CameraAccessException if the camera device is no longer connected or has
      *                               encountered a fatal error
-     * @throws IllegalStateException if this session is no longer active, either because a new
-     *                               session has been created or the camera device has been closed.
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
      *
      * @see #setRepeatingRequest
      * @see #setRepeatingBurst
@@ -308,8 +317,9 @@
      *
      * @throws CameraAccessException if the camera device is no longer connected or has
      *                               encountered a fatal error
-     * @throws IllegalStateException if this session is no longer active, either because a new
-     *                               session has been created or the camera device has been closed.
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
      *
      * @see #setRepeatingRequest
      * @see #setRepeatingBurst
@@ -320,8 +330,8 @@
     /**
      * Close this capture session asynchronously.
      *
-     * <p>Closing a session frees up the target output Surfaces of the session for reuse with either a
-     * new session, or to other APIs that can draw to Surfaces.</p>
+     * <p>Closing a session frees up the target output Surfaces of the session for reuse with either
+     * a new session, or to other APIs that can draw to Surfaces.</p>
      *
      * <p>Note that creating a new capture session with {@link CameraDevice#createCaptureSession}
      * will close any existing capture session automatically, and call the older session listener's
@@ -334,6 +344,8 @@
      * However, any in-progress capture requests submitted to the session will be completed as
      * normal; once all captures have completed and the session has been torn down,
      * {@link StateListener#onClosed} will be called.</p>
+     *
+     * <p>Closing a session is idempotent; closing more than once has no effect.</p>
      */
     @Override
     public abstract void close();
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 6f5099b..f9f617a 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -244,6 +244,7 @@
      * @see StreamConfigurationMap#getOutputSizes(Class)
      * @deprecated Use {@link #createCaptureSession} instead
      */
+    @Deprecated
     public void configureOutputs(List<Surface> outputs) throws CameraAccessException;
 
     /**
@@ -432,6 +433,7 @@
      * @see #setRepeatingBurst
      * @deprecated Use {@link CameraCaptureSession} instead
      */
+    @Deprecated
     public int capture(CaptureRequest request, CaptureListener listener, Handler handler)
             throws CameraAccessException;
 
@@ -470,13 +472,15 @@
      *                               or the camera device has been closed.
      * @throws IllegalArgumentException If the requests target Surfaces not
      * currently configured as outputs. Or if the handler is null, the listener
-     * is not null, and the calling thread has no looper.
+     * is not null, and the calling thread has no looper. Or if no requests were
+     * passed in.
      *
      * @see #capture
      * @see #setRepeatingRequest
      * @see #setRepeatingBurst
      * @deprecated Use {@link CameraCaptureSession} instead
      */
+    @Deprecated
     public int captureBurst(List<CaptureRequest> requests, CaptureListener listener,
             Handler handler) throws CameraAccessException;
 
@@ -536,6 +540,7 @@
      * @see #flush
      * @deprecated Use {@link CameraCaptureSession} instead
      */
+    @Deprecated
     public int setRepeatingRequest(CaptureRequest request, CaptureListener listener,
             Handler handler) throws CameraAccessException;
 
@@ -586,7 +591,8 @@
      *                               or the camera device has been closed.
      * @throws IllegalArgumentException If the requests reference Surfaces not
      * currently configured as outputs. Or if the handler is null, the listener
-     * is not null, and the calling thread has no looper.
+     * is not null, and the calling thread has no looper. Or if no requests were
+     * passed in.
      *
      * @see #capture
      * @see #captureBurst
@@ -595,6 +601,7 @@
      * @see #flush
      * @deprecated Use {@link CameraCaptureSession} instead
      */
+    @Deprecated
     public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener,
             Handler handler) throws CameraAccessException;
 
@@ -620,6 +627,7 @@
      * @see StateListener#onIdle
      * @deprecated Use {@link CameraCaptureSession} instead
      */
+    @Deprecated
     public void stopRepeating() throws CameraAccessException;
 
     /**
@@ -657,6 +665,7 @@
      * @see #configureOutputs
      * @deprecated Use {@link CameraCaptureSession} instead
      */
+    @Deprecated
     public void flush() throws CameraAccessException;
 
     /**
@@ -691,6 +700,7 @@
      * @see #setRepeatingBurst
      * @deprecated Use {@link CameraCaptureSession} instead
      */
+    @Deprecated
     public static abstract class CaptureListener {
 
         /**
@@ -1042,6 +1052,7 @@
          * @param camera the camera device has that become unconfigured
          * @deprecated Use {@link CameraCaptureSession.StateListener} instead.
          */
+        @Deprecated
         public void onUnconfigured(CameraDevice camera) {
             // Default empty implementation
         }
@@ -1072,6 +1083,7 @@
          * @see CameraDevice#setRepeatingRequest
          * @deprecated Use {@link CameraCaptureSession.StateListener} instead.
          */
+        @Deprecated
         public void onActive(CameraDevice camera) {
             // Default empty implementation
         }
@@ -1106,6 +1118,7 @@
          * @see CameraDevice#flush
          * @deprecated Use {@link CameraCaptureSession.StateListener} instead.
          */
+        @Deprecated
         public void onBusy(CameraDevice camera) {
             // Default empty implementation
         }
@@ -1154,6 +1167,7 @@
          * @see CameraDevice#flush
          * @deprecated Use {@link CameraCaptureSession.StateListener} instead.
          */
+        @Deprecated
         public void onIdle(CameraDevice camera) {
             // Default empty implementation
         }
diff --git a/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java b/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java
new file mode 100644
index 0000000..fe575b2
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.dispatch;
+
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * Broadcast a single dispatch into multiple other dispatchables.
+ *
+ * <p>Every time {@link #dispatch} is invoked, all the broadcast targets will
+ * see the same dispatch as well. The first target's return value is returned.</p>
+ *
+ * <p>This enables a single listener to be converted into a multi-listener.</p>
+ */
+public class BroadcastDispatcher<T> implements Dispatchable<T> {
+
+    private final List<Dispatchable<T>> mDispatchTargets;
+
+    /**
+     * Create a broadcast dispatcher from the supplied dispatch targets.
+     *
+     * @param dispatchTargets one or more targets to dispatch to
+     */
+    @SafeVarargs
+    public BroadcastDispatcher(Dispatchable<T>... dispatchTargets) {
+        mDispatchTargets = Arrays.asList(
+                checkNotNull(dispatchTargets, "dispatchTargets must not be null"));
+    }
+
+    @Override
+    public Object dispatch(Method method, Object[] args) throws Throwable {
+        Object result = null;
+        boolean gotResult = false;
+
+        for (Dispatchable<T> dispatchTarget : mDispatchTargets) {
+            Object localResult = dispatchTarget.dispatch(method, args);
+
+            if (!gotResult) {
+                gotResult = true;
+                result = localResult;
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/core/java/android/hardware/camera2/dispatch/Dispatchable.java b/core/java/android/hardware/camera2/dispatch/Dispatchable.java
new file mode 100644
index 0000000..753103f
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/Dispatchable.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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.dispatch;
+
+import java.lang.reflect.Method;
+
+/**
+ * Dynamically dispatch a method and its argument to some object.
+ *
+ * <p>This can be used to intercept method calls and do work around them, redirect work,
+ * or block calls entirely.</p>
+ */
+public interface Dispatchable<T> {
+    /**
+     * Dispatch the method and arguments to this object.
+     * @param method a method defined in class {@code T}
+     * @param args arguments corresponding to said {@code method}
+     * @return the object returned when invoking {@code method}
+     * @throws Throwable any exception that might have been raised while invoking the method
+     */
+    public Object dispatch(Method method, Object[] args) throws Throwable;
+}
diff --git a/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java b/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java
new file mode 100644
index 0000000..75f97e4
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.dispatch;
+
+
+import java.lang.reflect.Method;
+
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * Duck typing dispatcher; converts dispatch methods calls from one class to another by
+ * looking up equivalently methods at runtime by name.
+ *
+ * <p>For example, if two types have identical method names and arguments, but
+ * are not subclasses/subinterfaces of each other, this dispatcher will allow calls to be
+ * made from one type to the other.</p>
+ *
+ * @param <TFrom> source dispatch type, whose methods with {@link #dispatch} will be called
+ * @param <T> destination dispatch type, methods will be converted to the class of {@code T}
+ */
+public class DuckTypingDispatcher<TFrom, T> implements Dispatchable<TFrom> {
+
+    private final MethodNameInvoker<T> mDuck;
+
+    /**
+     * Create a new duck typing dispatcher.
+     *
+     * @param target destination dispatch type, methods will be redirected to this dispatcher
+     * @param targetClass destination dispatch class, methods will be converted to this class's
+     */
+    public DuckTypingDispatcher(Dispatchable<T> target, Class<T> targetClass) {
+        checkNotNull(targetClass, "targetClass must not be null");
+        checkNotNull(target, "target must not be null");
+
+        mDuck = new MethodNameInvoker<T>(target, targetClass);
+    }
+
+    @Override
+    public Object dispatch(Method method, Object[] args) {
+        return mDuck.invoke(method.getName(), args);
+    }
+}
diff --git a/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java b/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java
new file mode 100644
index 0000000..f8e9d49
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 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.dispatch;
+
+import android.hardware.camera2.utils.UncheckedThrow;
+import android.os.Handler;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * Forward all interface calls into a handler by posting it as a {@code Runnable}.
+ *
+ * <p>All calls will return immediately; functions with return values will return a default
+ * value of {@code null}, {@code 0}, or {@code false} where that value is legal.</p>
+ *
+ * <p>Any exceptions thrown on the handler while trying to invoke a method
+ * will be re-thrown. Throwing checked exceptions on a handler which doesn't expect any
+ * checked exceptions to be thrown will result in "undefined" behavior
+ * (although in practice it is usually thrown as normal).</p>
+ */
+public class HandlerDispatcher<T> implements Dispatchable<T> {
+
+    private static final String TAG = "HandlerDispatcher";
+
+    private final Dispatchable<T> mDispatchTarget;
+    private final Handler mHandler;
+
+    /**
+     * Create a dispatcher that forwards it's dispatch calls by posting
+     * them onto the {@code handler} as a {@code Runnable}.
+     *
+     * @param dispatchTarget the destination whose method calls will be redirected into the handler
+     * @param handler all calls into {@code dispatchTarget} will be posted onto this handler
+     * @param <T> the type of the element you want to wrap.
+     * @return a dispatcher that will forward it's dispatch calls to a handler
+     */
+    public HandlerDispatcher(Dispatchable<T> dispatchTarget, Handler handler) {
+        mDispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null");
+        mHandler = checkNotNull(handler, "handler must not be null");
+    }
+
+    @Override
+    public Object dispatch(final Method method, final Object[] args) throws Throwable {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mDispatchTarget.dispatch(method, args);
+                } catch (InvocationTargetException e) {
+                    Throwable t = e.getTargetException();
+                    // Potential UB. Hopefully 't' is a runtime exception.
+                    UncheckedThrow.throwAnyException(t);
+                } catch (IllegalAccessException e) {
+                    // Impossible
+                    Log.wtf(TAG, "IllegalAccessException while invoking " + method, e);
+                } catch (IllegalArgumentException e) {
+                    // Impossible
+                    Log.wtf(TAG, "IllegalArgumentException while invoking " + method, e);
+                } catch (Throwable e) {
+                    UncheckedThrow.throwAnyException(e);
+                }
+            }
+        });
+
+        // TODO handle primitive return values that would avoid NPE if unboxed
+        return null;
+    }
+}
diff --git a/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java b/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java
new file mode 100644
index 0000000..ac5f526
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.dispatch;
+
+import android.hardware.camera2.utils.UncheckedThrow;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import static com.android.internal.util.Preconditions.*;
+
+
+public class InvokeDispatcher<T> implements Dispatchable<T> {
+
+    private static final String TAG = "InvocationSink";
+    private final T mTarget;
+
+    public InvokeDispatcher(T target) {
+        mTarget = checkNotNull(target, "target must not be null");
+    }
+
+    @Override
+    public Object dispatch(Method method, Object[] args) {
+        try {
+            return method.invoke(mTarget, args);
+        } catch (InvocationTargetException e) {
+            Throwable t = e.getTargetException();
+            // Potential UB. Hopefully 't' is a runtime exception.
+            UncheckedThrow.throwAnyException(t);
+        } catch (IllegalAccessException e) {
+            // Impossible
+            Log.wtf(TAG, "IllegalAccessException while invoking " + method, e);
+        } catch (IllegalArgumentException e) {
+            // Impossible
+            Log.wtf(TAG, "IllegalArgumentException while invoking " + method, e);
+        }
+
+        // unreachable
+        return null;
+    }
+}
diff --git a/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java b/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java
new file mode 100644
index 0000000..02c3d87
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 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.dispatch;
+
+import android.hardware.camera2.utils.UncheckedThrow;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * Invoke a method on a dispatchable by its name (without knowing the {@code Method} ahead of time).
+ *
+ * @param <T> destination dispatch type, methods will be looked up in the class of {@code T}
+ */
+public class MethodNameInvoker<T> {
+
+    private final Dispatchable<T> mTarget;
+    private final Class<T> mTargetClass;
+    private final ConcurrentHashMap<String, Method> mMethods =
+            new ConcurrentHashMap<>();
+
+    /**
+     * Create a new method name invoker.
+     *
+     * @param target destination dispatch type, invokes will be redirected to this dispatcher
+     * @param targetClass destination dispatch class, the invoked methods will be from this class
+     */
+    public MethodNameInvoker(Dispatchable<T> target, Class<T> targetClass) {
+        mTargetClass = targetClass;
+        mTarget = target;
+    }
+
+    /**
+     * Invoke a method by its name.
+     *
+     * <p>If more than one method exists in {@code targetClass}, the first method will be used.</p>
+     *
+     * @param methodName
+     *          The name of the method, which will be matched 1:1 to the destination method
+     * @param params
+     *          Variadic parameter list.
+     * @return
+     *          The same kind of value that would normally be returned by calling {@code methodName}
+     *          statically.
+     *
+     * @throws IllegalArgumentException if {@code methodName} does not exist on the target class
+     * @throws Throwable will rethrow anything that the target method would normally throw
+     */
+    @SuppressWarnings("unchecked")
+    public <K> K invoke(String methodName, Object... params) {
+        checkNotNull(methodName, "methodName must not be null");
+
+        Method targetMethod = mMethods.get(methodName);
+        if (targetMethod == null) {
+            for (Method method : mTargetClass.getMethods()) {
+                // TODO future: match by # of params and types of params if possible
+                if (method.getName().equals(methodName)) {
+                    targetMethod = method;
+                    mMethods.put(methodName, targetMethod);
+                    break;
+                }
+            }
+
+            if (targetMethod == null) {
+                throw new IllegalArgumentException(
+                        "Method " + methodName + " does not exist on class " + mTargetClass);
+            }
+        }
+
+        try {
+            return (K) mTarget.dispatch(targetMethod, params);
+        } catch (Throwable e) {
+            UncheckedThrow.throwAnyException(e);
+            // unreachable
+            return null;
+        }
+    }
+}
diff --git a/core/java/android/hardware/camera2/dispatch/NullDispatcher.java b/core/java/android/hardware/camera2/dispatch/NullDispatcher.java
new file mode 100644
index 0000000..fada075
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/NullDispatcher.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 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.dispatch;
+
+
+import java.lang.reflect.Method;
+
+/**
+ * Do nothing when dispatching; follows the null object pattern.
+ */
+public class NullDispatcher<T> implements Dispatchable<T> {
+    /**
+     * Create a dispatcher that does nothing when dispatched to.
+     */
+    public NullDispatcher() {
+    }
+
+    /**
+     * Do nothing; all parameters are ignored.
+     */
+    @Override
+    public Object dispatch(Method method, Object[] args) {
+        return null;
+    }
+}
diff --git a/core/java/android/hardware/camera2/dispatch/package.html b/core/java/android/hardware/camera2/dispatch/package.html
new file mode 100644
index 0000000..783d0a1
--- /dev/null
+++ b/core/java/android/hardware/camera2/dispatch/package.html
@@ -0,0 +1,3 @@
+<body>
+{@hide}
+</body>
diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
new file mode 100644
index 0000000..e129783
--- /dev/null
+++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2014 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 android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.dispatch.BroadcastDispatcher;
+import android.hardware.camera2.dispatch.Dispatchable;
+import android.hardware.camera2.dispatch.DuckTypingDispatcher;
+import android.hardware.camera2.dispatch.HandlerDispatcher;
+import android.hardware.camera2.dispatch.InvokeDispatcher;
+import android.hardware.camera2.dispatch.NullDispatcher;
+import android.hardware.camera2.utils.TaskDrainer;
+import android.hardware.camera2.utils.TaskSingleDrainer;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Surface;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static android.hardware.camera2.impl.CameraDevice.checkHandler;
+import static com.android.internal.util.Preconditions.*;
+
+public class CameraCaptureSessionImpl extends CameraCaptureSession {
+    private static final String TAG = "CameraCaptureSession";
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+
+    /** User-specified set of surfaces used as the configuration outputs */
+    private final List<Surface> mOutputs;
+    /**
+     * User-specified state listener, used for outgoing events; calls to this object will be
+     * automatically {@link Handler#post(Runnable) posted} to {@code mStateHandler}.
+     */
+    private final CameraCaptureSession.StateListener mStateListener;
+    /** User-specified state handler used for outgoing state listener events */
+    private final Handler mStateHandler;
+
+    /** Internal camera device; used to translate calls into existing deprecated API */
+    private final android.hardware.camera2.impl.CameraDevice mDeviceImpl;
+    /** Internal handler; used for all incoming events to preserve total order */
+    private final Handler mDeviceHandler;
+
+    /** Drain Sequence IDs which have been queued but not yet finished with aborted/completed */
+    private final TaskDrainer<Integer> mSequenceDrainer;
+    /** Drain state transitions from ACTIVE -> IDLE */
+    private final TaskSingleDrainer mIdleDrainer;
+    /** Drain state transitions from BUSY -> IDLE */
+    private final TaskSingleDrainer mAbortDrainer;
+    /** Drain the UNCONFIGURED state transition */
+    private final TaskSingleDrainer mUnconfigureDrainer;
+
+    /** This session is closed; all further calls will throw ISE */
+    private boolean mClosed = false;
+    /** Do not unconfigure if this is set; another session will overwrite configuration */
+    private boolean mSkipUnconfigure = false;
+
+    /** Is the session in the process of aborting? Pay attention to BUSY->IDLE transitions. */
+    private boolean mAborting;
+
+    /**
+     * Create a new CameraCaptureSession.
+     *
+     * <p>The camera device must already be in the {@code IDLE} state when this is invoked.
+     * There must be no pending actions
+     * (e.g. no pending captures, no repeating requests, no flush).</p>
+     */
+    CameraCaptureSessionImpl(List<Surface> outputs,
+            CameraCaptureSession.StateListener listener, Handler stateHandler,
+            android.hardware.camera2.impl.CameraDevice deviceImpl,
+            Handler deviceStateHandler, boolean configureSuccess) {
+        if (outputs == null || outputs.isEmpty()) {
+            throw new IllegalArgumentException("outputs must be a non-null, non-empty list");
+        } else if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+
+        // TODO: extra verification of outputs
+        mOutputs = outputs;
+        mStateHandler = checkHandler(stateHandler);
+        mStateListener = createUserStateListenerProxy(mStateHandler, listener);
+
+        mDeviceHandler = checkNotNull(deviceStateHandler, "deviceStateHandler must not be null");
+        mDeviceImpl = checkNotNull(deviceImpl, "deviceImpl must not be null");
+
+        /*
+         * Use the same handler as the device's StateListener for all the internal coming events
+         *
+         * This ensures total ordering between CameraDevice.StateListener and
+         * CameraDevice.CaptureListener events.
+         */
+        mSequenceDrainer = new TaskDrainer<>(mDeviceHandler, new SequenceDrainListener(),
+                /*name*/"seq");
+        mIdleDrainer = new TaskSingleDrainer(mDeviceHandler, new IdleDrainListener(),
+                /*name*/"idle");
+        mAbortDrainer = new TaskSingleDrainer(mDeviceHandler, new AbortDrainListener(),
+                /*name*/"abort");
+        mUnconfigureDrainer = new TaskSingleDrainer(mDeviceHandler, new UnconfigureDrainListener(),
+                /*name*/"unconf");
+
+        // CameraDevice should call configureOutputs and have it finish before constructing us
+
+        if (configureSuccess) {
+            mStateListener.onConfigured(this);
+        } else {
+            mStateListener.onConfigureFailed(this);
+            mClosed = true; // do not fire any other callbacks, do not allow any other work
+        }
+    }
+
+    @Override
+    public CameraDevice getDevice() {
+        return mDeviceImpl;
+    }
+
+    @Override
+    public synchronized int capture(CaptureRequest request, CaptureListener listener,
+            Handler handler) throws CameraAccessException {
+        checkNotClosed();
+        checkLegalToCapture();
+
+        handler = checkHandler(handler);
+
+        if (VERBOSE) {
+            Log.v(TAG, "capture - request " + request + ", listener " + listener + " handler" +
+                    "" + handler);
+        }
+
+        return addPendingSequence(mDeviceImpl.capture(request,
+                createCaptureListenerProxy(handler, listener), mDeviceHandler));
+    }
+
+    @Override
+    public synchronized int captureBurst(List<CaptureRequest> requests, CaptureListener listener,
+            Handler handler) throws CameraAccessException {
+        checkNotClosed();
+        checkLegalToCapture();
+
+        handler = checkHandler(handler);
+
+        if (VERBOSE) {
+            CaptureRequest[] requestArray = requests.toArray(new CaptureRequest[0]);
+            Log.v(TAG, "captureBurst - requests " + Arrays.toString(requestArray) + ", listener " +
+                    listener + " handler" + "" + handler);
+        }
+
+        return addPendingSequence(mDeviceImpl.captureBurst(requests,
+                createCaptureListenerProxy(handler, listener), mDeviceHandler));
+    }
+
+    @Override
+    public synchronized int setRepeatingRequest(CaptureRequest request, CaptureListener listener,
+            Handler handler) throws CameraAccessException {
+        checkNotClosed();
+        checkLegalToCapture();
+
+        handler = checkHandler(handler);
+
+        return addPendingSequence(mDeviceImpl.setRepeatingRequest(request,
+                createCaptureListenerProxy(handler, listener), mDeviceHandler));
+    }
+
+    @Override
+    public synchronized int setRepeatingBurst(List<CaptureRequest> requests,
+            CaptureListener listener, Handler handler) throws CameraAccessException {
+        checkNotClosed();
+        checkLegalToCapture();
+
+        handler = checkHandler(handler);
+
+        if (VERBOSE) {
+            CaptureRequest[] requestArray = requests.toArray(new CaptureRequest[0]);
+            Log.v(TAG, "setRepeatingBurst - requests " + Arrays.toString(requestArray) +
+                    ", listener " + listener + " handler" + "" + handler);
+        }
+
+        return addPendingSequence(mDeviceImpl.setRepeatingBurst(requests,
+                createCaptureListenerProxy(handler, listener), mDeviceHandler));
+    }
+
+    @Override
+    public synchronized void stopRepeating() throws CameraAccessException {
+        checkNotClosed();
+
+        if (VERBOSE) {
+            Log.v(TAG, "stopRepeating");
+        }
+
+        mDeviceImpl.stopRepeating();
+    }
+
+    @Override
+    public synchronized void abortCaptures() throws CameraAccessException {
+        checkNotClosed();
+
+        if (VERBOSE) {
+            Log.v(TAG, "abortCaptures");
+        }
+
+        if (mAborting) {
+            Log.w(TAG, "abortCaptures - Session is already aborting; doing nothing");
+            return;
+        }
+
+        mAborting = true;
+        mAbortDrainer.taskStarted();
+
+        mDeviceImpl.flush();
+        // The next BUSY -> IDLE set of transitions will mark the end of the abort.
+    }
+
+    /**
+     * Replace this session with another session.
+     *
+     * <p>This is an optimization to avoid unconfiguring and then immediately having to
+     * reconfigure again.</p>
+     *
+     * <p>The semantics are identical to {@link #close}, except that unconfiguring will be skipped.
+     * <p>
+     *
+     * @see CameraCaptureSession#close
+     */
+    synchronized void replaceSessionClose(CameraCaptureSession other) {
+        /*
+         * In order for creating new sessions to be fast, the new session should be created
+         * before the old session is closed.
+         *
+         * Otherwise the old session will always unconfigure if there is no new session to
+         * replace it.
+         *
+         * Unconfiguring could add hundreds of milliseconds of delay. We could race and attempt
+         * to skip unconfigure if a new session is created before the captures are all drained,
+         * but this would introduce nondeterministic behavior.
+         */
+
+        // #close was already called explicitly, keep going the slow route
+        if (mClosed) {
+            return;
+        }
+
+        mSkipUnconfigure = true;
+        close();
+    }
+
+    @Override
+    public synchronized void close() {
+        if (mClosed) {
+            return;
+        }
+
+        mClosed = true;
+
+        /*
+         * Flush out any repeating request. Since camera is closed, no new requests
+         * can be queued, and eventually the entire request queue will be drained.
+         *
+         * Once this is done, wait for camera to idle, then unconfigure the camera.
+         * Once that's done, fire #onClosed.
+         */
+        try {
+            mDeviceImpl.stopRepeating();
+        } catch (CameraAccessException e) {
+            // OK: close does not throw checked exceptions.
+            Log.e(TAG, "Exception while stopping repeating: ", e);
+
+            // TODO: call onError instead of onClosed if this happens
+        }
+
+        // If no sequences are pending, fire #onClosed immediately
+        mSequenceDrainer.beginDrain();
+    }
+
+    /**
+     * Post calls into a CameraCaptureSession.StateListener to the user-specified {@code handler}.
+     */
+    private StateListener createUserStateListenerProxy(Handler handler, StateListener listener) {
+        InvokeDispatcher<StateListener> userListenerSink = new InvokeDispatcher<>(listener);
+        HandlerDispatcher<StateListener> handlerPassthrough =
+                new HandlerDispatcher<>(userListenerSink, handler);
+
+        return new ListenerProxies.SessionStateListenerProxy(handlerPassthrough);
+    }
+
+    /**
+     * Forward callbacks from
+     * CameraDevice.CaptureListener to the CameraCaptureSession.CaptureListener.
+     *
+     * <p>In particular, all calls are automatically split to go both to our own
+     * internal listener, and to the user-specified listener (by transparently posting
+     * to the user-specified handler).</p>
+     *
+     * <p>When a capture sequence finishes, update the pending checked sequences set.</p>
+     */
+    @SuppressWarnings("deprecation")
+    private CameraDevice.CaptureListener createCaptureListenerProxy(
+            Handler handler, CaptureListener listener) {
+        CameraDevice.CaptureListener localListener = new CameraDevice.CaptureListener() {
+            @Override
+            public void onCaptureSequenceCompleted(CameraDevice camera,
+                    int sequenceId, long frameNumber) {
+                finishPendingSequence(sequenceId);
+            }
+
+            @Override
+            public void onCaptureSequenceAborted(CameraDevice camera,
+                    int sequenceId) {
+                finishPendingSequence(sequenceId);
+            }
+        };
+
+        /*
+         * Split the calls from the device listener into local listener and the following chain:
+         * - duck type from device listener to session listener
+         * - then forward the call to a handler
+         * - then finally invoke the destination method on the session listener object
+         */
+        Dispatchable<CaptureListener> userListenerSink;
+        if (listener == null) { // OK: API allows the user to not specify a listener
+            userListenerSink = new NullDispatcher<>();
+        } else {
+            userListenerSink = new InvokeDispatcher<>(listener);
+        }
+
+        InvokeDispatcher<CameraDevice.CaptureListener> localSink =
+                new InvokeDispatcher<>(localListener);
+        HandlerDispatcher<CaptureListener> handlerPassthrough =
+                new HandlerDispatcher<>(userListenerSink, handler);
+        DuckTypingDispatcher<CameraDevice.CaptureListener, CaptureListener> duckToSessionCaptureListener
+                = new DuckTypingDispatcher<>(handlerPassthrough, CaptureListener.class);
+
+        BroadcastDispatcher<CameraDevice.CaptureListener> broadcaster =
+                new BroadcastDispatcher<CameraDevice.CaptureListener>(
+                        duckToSessionCaptureListener,
+                        localSink);
+
+        return new ListenerProxies.DeviceCaptureListenerProxy(broadcaster);
+    }
+
+    /**
+     *
+     * Create an internal state listener, to be invoked on the mDeviceHandler
+     *
+     * <p>It has a few behaviors:
+     * <ul>
+     * <li>Convert device state changes into session state changes.
+     * <li>Keep track of async tasks that the session began (idle, abort).
+     * </ul>
+     * </p>
+     * */
+    CameraDevice.StateListener getDeviceStateListener() {
+        final CameraCaptureSession session = this;
+
+        return new CameraDevice.StateListener() {
+            private boolean mBusy = false;
+            private boolean mActive = false;
+
+            @Override
+            public void onOpened(CameraDevice camera) {
+                throw new AssertionError("Camera must already be open before creating a session");
+            }
+
+            @Override
+            public void onDisconnected(CameraDevice camera) {
+                close();
+            }
+
+            @Override
+            public void onError(CameraDevice camera, int error) {
+                // TODO: Handle errors somehow.
+                Log.wtf(TAG, "Got device error " + error);
+            }
+
+            @Override
+            public void onActive(CameraDevice camera) {
+                mIdleDrainer.taskStarted();
+                mActive = true;
+
+                mStateListener.onActive(session);
+            }
+
+            @Override
+            public void onIdle(CameraDevice camera) {
+                boolean isAborting;
+                synchronized (session) {
+                    isAborting = mAborting;
+                }
+
+                /*
+                 * Check which states we transitioned through:
+                 *
+                 * (ACTIVE -> IDLE)
+                 * (BUSY -> IDLE)
+                 *
+                 * Note that this is also legal:
+                 * (ACTIVE -> BUSY -> IDLE)
+                 *
+                 * and mark those tasks as finished
+                 */
+                if (mBusy && isAborting) {
+                    mAbortDrainer.taskFinished();
+
+                    synchronized (session) {
+                        mAborting = false;
+                    }
+                }
+
+                if (mActive) {
+                    mIdleDrainer.taskFinished();
+                }
+
+                mBusy = false;
+                mActive = false;
+
+                mStateListener.onReady(session);
+            }
+
+            @Override
+            public void onBusy(CameraDevice camera) {
+                mBusy = true;
+
+                // TODO: Queue captures during abort instead of failing them
+                // since the app won't be able to distinguish the two actives
+                Log.w(TAG, "Device is now busy; do not submit new captures (TODO: allow this)");
+                mStateListener.onActive(session);
+            }
+
+            @Override
+            public void onUnconfigured(CameraDevice camera) {
+                mUnconfigureDrainer.taskFinished();
+            }
+        };
+
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    private void checkLegalToCapture() {
+        if (mAborting) {
+            throw new IllegalStateException(
+                    "Session is aborting captures; new captures are not permitted");
+        }
+    }
+
+    private void checkNotClosed() {
+        if (mClosed) {
+            throw new IllegalStateException(
+                    "Session has been closed; further changes are illegal.");
+        }
+    }
+
+    /**
+     * Notify the session that a pending capture sequence has just been queued.
+     *
+     * <p>During a shutdown/close, the session waits until all pending sessions are finished
+     * before taking any further steps to shut down itself.</p>
+     *
+     * @see #finishPendingSequence
+     */
+    private int addPendingSequence(int sequenceId) {
+        mSequenceDrainer.taskStarted(sequenceId);
+        return sequenceId;
+    }
+
+    /**
+     * Notify the session that a pending capture sequence is now finished.
+     *
+     * <p>During a shutdown/close, once all pending sequences finish, it is safe to
+     * close the camera further by unconfiguring and then firing {@code onClosed}.</p>
+     */
+    private void finishPendingSequence(int sequenceId) {
+        mSequenceDrainer.taskFinished(sequenceId);
+    }
+
+    private class SequenceDrainListener implements TaskDrainer.DrainListener {
+        @Override
+        public void onDrained() {
+            /*
+             * No repeating request is set; and the capture queue has fully drained.
+             *
+             * If no captures were queued to begin with, and an abort was queued,
+             * it's still possible to get another BUSY before the last IDLE.
+             *
+             * If the camera is already "IDLE" and no aborts are pending,
+             * then the drain immediately finishes.
+             */
+            mAbortDrainer.beginDrain();
+        }
+    }
+
+    private class AbortDrainListener implements TaskDrainer.DrainListener {
+        @Override
+        public void onDrained() {
+            synchronized (CameraCaptureSessionImpl.this) {
+                /*
+                 * Any queued aborts have now completed.
+                 *
+                 * It's now safe to wait to receive the final "IDLE" event, as the camera device
+                 * will no longer again transition to "ACTIVE" by itself.
+                 *
+                 * If the camera is already "IDLE", then the drain immediately finishes.
+                 */
+                mIdleDrainer.beginDrain();
+            }
+        }
+    }
+
+    private class IdleDrainListener implements TaskDrainer.DrainListener {
+        @Override
+        public void onDrained() {
+            synchronized (CameraCaptureSessionImpl.this) {
+                /*
+                 * The device is now IDLE, and has settled. It will not transition to
+                 * ACTIVE or BUSY again by itself.
+                 *
+                 * It's now safe to unconfigure the outputs and after it's done invoke #onClosed.
+                 *
+                 * This operation is idempotent; a session will not be closed twice.
+                 */
+
+                // Fast path: A new capture session has replaced this one; don't unconfigure.
+                if (mSkipUnconfigure) {
+                    mStateListener.onClosed(CameraCaptureSessionImpl.this);
+                    return;
+                }
+
+                // Slow path: #close was called explicitly on this session; unconfigure first
+
+                try {
+                    mUnconfigureDrainer.taskStarted();
+                    mDeviceImpl.configureOutputs(null); // begin transition to unconfigured state
+                } catch (CameraAccessException e) {
+                    // OK: do not throw checked exceptions.
+                    Log.e(TAG, "Exception while configuring outputs: ", e);
+
+                    // TODO: call onError instead of onClosed if this happens
+                }
+
+                mUnconfigureDrainer.beginDrain();
+            }
+        }
+    }
+
+    private class UnconfigureDrainListener implements TaskDrainer.DrainListener {
+        @Override
+        public void onDrained() {
+            synchronized (CameraCaptureSessionImpl.this) {
+                // The device has finished unconfiguring. It's now fully closed.
+                mStateListener.onClosed(CameraCaptureSessionImpl.this);
+            }
+        }
+    }
+}
diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDevice.java
index 9a4c531..e9d4b0f 100644
--- a/core/java/android/hardware/camera2/impl/CameraDevice.java
+++ b/core/java/android/hardware/camera2/impl/CameraDevice.java
@@ -62,6 +62,7 @@
     private final CameraDeviceCallbacks mCallbacks = new CameraDeviceCallbacks();
 
     private final StateListener mDeviceListener;
+    private volatile StateListener mSessionStateListener;
     private final Handler mDeviceHandler;
 
     private boolean mIdle = true;
@@ -90,6 +91,8 @@
      */
     private final FrameNumberTracker mFrameNumberTracker = new FrameNumberTracker();
 
+    private CameraCaptureSessionImpl mCurrentSession;
+
     // Runnables for all state transitions, except error, which needs the
     // error code argument
 
@@ -98,6 +101,10 @@
         public void run() {
             if (!CameraDevice.this.isClosed()) {
                 mDeviceListener.onOpened(CameraDevice.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onOpened(CameraDevice.this);
+                }
             }
         }
     };
@@ -107,6 +114,10 @@
         public void run() {
             if (!CameraDevice.this.isClosed()) {
                 mDeviceListener.onUnconfigured(CameraDevice.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onUnconfigured(CameraDevice.this);
+                }
             }
         }
     };
@@ -116,6 +127,10 @@
         public void run() {
             if (!CameraDevice.this.isClosed()) {
                 mDeviceListener.onActive(CameraDevice.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onActive(CameraDevice.this);
+                }
             }
         }
     };
@@ -125,6 +140,10 @@
         public void run() {
             if (!CameraDevice.this.isClosed()) {
                 mDeviceListener.onBusy(CameraDevice.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onBusy(CameraDevice.this);
+                }
             }
         }
     };
@@ -133,6 +152,10 @@
         @Override
         public void run() {
             mDeviceListener.onClosed(CameraDevice.this);
+            StateListener sessionListener = mSessionStateListener;
+            if (sessionListener != null) {
+                sessionListener.onClosed(CameraDevice.this);
+            }
         }
     };
 
@@ -141,6 +164,10 @@
         public void run() {
             if (!CameraDevice.this.isClosed()) {
                 mDeviceListener.onIdle(CameraDevice.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onIdle(CameraDevice.this);
+                }
             }
         }
     };
@@ -150,6 +177,10 @@
         public void run() {
             if (!CameraDevice.this.isClosed()) {
                 mDeviceListener.onDisconnected(CameraDevice.this);
+                StateListener sessionListener = mSessionStateListener;
+                if (sessionListener != null) {
+                    sessionListener.onDisconnected(CameraDevice.this);
+                }
             }
         }
     };
@@ -170,7 +201,6 @@
             tag = tag.substring(0, MAX_TAG_LEN);
         }
         TAG = tag;
-
         DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     }
 
@@ -263,7 +293,43 @@
     public void createCaptureSession(List<Surface> outputs,
             CameraCaptureSession.StateListener listener, Handler handler)
             throws CameraAccessException {
-        // TODO
+        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
@@ -275,7 +341,7 @@
             CameraMetadataNative templatedRequest = new CameraMetadataNative();
 
             try {
-                mRemoteDevice.createDefaultRequest(templateType, /* out */templatedRequest);
+                mRemoteDevice.createDefaultRequest(templateType, /*out*/templatedRequest);
             } catch (CameraRuntimeException e) {
                 throw e.asChecked();
             } catch (RemoteException e) {
@@ -304,10 +370,8 @@
     @Override
     public int captureBurst(List<CaptureRequest> requests, CaptureListener listener,
             Handler handler) throws CameraAccessException {
-        // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc.
-        if (requests.isEmpty()) {
-            Log.w(TAG, "Capture burst request list is empty, do nothing!");
-            return -1;
+        if (requests == null || requests.isEmpty()) {
+            throw new IllegalArgumentException("At least one request must be given");
         }
         return submitCaptureRequest(requests, listener, handler, /*streaming*/false);
     }
@@ -454,10 +518,8 @@
     @Override
     public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener,
             Handler handler) throws CameraAccessException {
-        // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc.
-        if (requests.isEmpty()) {
-            Log.w(TAG, "Set Repeating burst request list is empty, do nothing!");
-            return -1;
+        if (requests == null || requests.isEmpty()) {
+            throw new IllegalArgumentException("At least one request must be given");
         }
         return submitCaptureRequest(requests, listener, handler, /*streaming*/true);
     }
@@ -951,10 +1013,14 @@
     }
 
     /**
-     * Default handler management. If handler is null, get the current thread's
-     * Looper to create a Handler with. If no looper exists, throw exception.
+     * 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>
      */
-    private Handler checkHandler(Handler handler) {
+    static Handler checkHandler(Handler handler) {
         if (handler == null) {
             Looper looper = Looper.myLooper();
             if (looper == null) {
diff --git a/core/java/android/hardware/camera2/impl/ListenerProxies.java b/core/java/android/hardware/camera2/impl/ListenerProxies.java
new file mode 100644
index 0000000..04c43e3
--- /dev/null
+++ b/core/java/android/hardware/camera2/impl/ListenerProxies.java
@@ -0,0 +1,168 @@
+package android.hardware.camera2.impl;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.dispatch.Dispatchable;
+import android.hardware.camera2.dispatch.MethodNameInvoker;
+
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * Proxy out invocations to the camera2 API listeners into a {@link Dispatchable}.
+ *
+ * <p>Since abstract classes do not support Java's dynamic {@code Proxy}, we have to
+ * to use our own proxy mechanism.</p>
+ */
+public class ListenerProxies {
+
+    // TODO: replace with codegen
+
+    public static class DeviceStateListenerProxy extends CameraDevice.StateListener {
+        private final MethodNameInvoker<CameraDevice.StateListener> mProxy;
+
+        public DeviceStateListenerProxy(
+                Dispatchable<CameraDevice.StateListener> dispatchTarget) {
+            dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null");
+            mProxy = new MethodNameInvoker<>(dispatchTarget, CameraDevice.StateListener.class);
+        }
+
+        @Override
+        public void onOpened(CameraDevice camera) {
+            mProxy.invoke("onOpened", camera);
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice camera) {
+            mProxy.invoke("onDisconnected", camera);
+        }
+
+        @Override
+        public void onError(CameraDevice camera, int error) {
+            mProxy.invoke("onError", camera, error);
+        }
+
+        @Override
+        public void onUnconfigured(CameraDevice camera) {
+            mProxy.invoke("onUnconfigured", camera);
+        }
+
+        @Override
+        public void onActive(CameraDevice camera) {
+            mProxy.invoke("onActive", camera);
+        }
+
+        @Override
+        public void onBusy(CameraDevice camera) {
+            mProxy.invoke("onBusy", camera);
+        }
+
+        @Override
+        public void onClosed(CameraDevice camera) {
+            mProxy.invoke("onClosed", camera);
+        }
+
+        @Override
+        public void onIdle(CameraDevice camera) {
+            mProxy.invoke("onIdle", camera);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    public static class DeviceCaptureListenerProxy extends CameraDevice.CaptureListener {
+        private final MethodNameInvoker<CameraDevice.CaptureListener> mProxy;
+
+        public DeviceCaptureListenerProxy(
+                Dispatchable<CameraDevice.CaptureListener> dispatchTarget) {
+            dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null");
+            mProxy = new MethodNameInvoker<>(dispatchTarget, CameraDevice.CaptureListener.class);
+        }
+
+        @Override
+        public void onCaptureStarted(CameraDevice camera,
+                CaptureRequest request, long timestamp) {
+            mProxy.invoke("onCaptureStarted", camera, request, timestamp);
+        }
+
+        @Override
+        public void onCapturePartial(CameraDevice camera,
+                CaptureRequest request, CaptureResult result) {
+            mProxy.invoke("onCapturePartial", camera, request, result);
+        }
+
+        @Override
+        public void onCaptureProgressed(CameraDevice camera,
+                CaptureRequest request, CaptureResult partialResult) {
+            mProxy.invoke("onCaptureProgressed", camera, request, partialResult);
+        }
+
+        @Override
+        public void onCaptureCompleted(CameraDevice camera,
+                CaptureRequest request, TotalCaptureResult result) {
+            mProxy.invoke("onCaptureCompleted", camera, request, result);
+        }
+
+        @Override
+        public void onCaptureFailed(CameraDevice camera,
+                CaptureRequest request, CaptureFailure failure) {
+            mProxy.invoke("onCaptureFailed", camera, request, failure);
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(CameraDevice camera,
+                int sequenceId, long frameNumber) {
+            mProxy.invoke("onCaptureSequenceCompleted", camera, sequenceId, frameNumber);
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraDevice camera,
+                int sequenceId) {
+            mProxy.invoke("onCaptureSequenceAborted", camera, sequenceId);
+        }
+    }
+
+    public static class SessionStateListenerProxy
+            extends CameraCaptureSession.StateListener {
+        private final MethodNameInvoker<CameraCaptureSession.StateListener> mProxy;
+
+        public SessionStateListenerProxy(
+                Dispatchable<CameraCaptureSession.StateListener> dispatchTarget) {
+            dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null");
+            mProxy = new MethodNameInvoker<>(dispatchTarget,
+                    CameraCaptureSession.StateListener.class);
+        }
+
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            mProxy.invoke("onConfigured", session);
+        }
+
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            mProxy.invoke("onConfigureFailed", session);
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            mProxy.invoke("onReady", session);
+        }
+
+        @Override
+        public void onActive(CameraCaptureSession session) {
+            mProxy.invoke("onActive", session);
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            mProxy.invoke("onClosed", session);
+        }
+    }
+
+    private ListenerProxies() {
+        throw new AssertionError();
+    }
+}
diff --git a/core/java/android/hardware/camera2/utils/TaskDrainer.java b/core/java/android/hardware/camera2/utils/TaskDrainer.java
new file mode 100644
index 0000000..3cba9a1
--- /dev/null
+++ b/core/java/android/hardware/camera2/utils/TaskDrainer.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2014 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.utils;
+
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * Keep track of multiple concurrent tasks starting and finishing by their key;
+ * allow draining existing tasks and figuring out when all tasks have finished
+ * (and new ones won't begin).
+ *
+ * <p>The initial state is to allow all tasks to be started and finished. A task may only be started
+ * once, after which it must be finished before starting again. Likewise, finishing a task
+ * that hasn't been started is also not allowed.</p>
+ *
+ * <p>When draining begins, no more new tasks can be started. This guarantees that at some
+ * point when all the tasks are finished there will be no more collective new tasks,
+ * at which point the {@link DrainListener#onDrained} callback will be invoked.</p>
+ *
+ *
+ * @param <T>
+ *          a type for the key that will represent tracked tasks;
+ *          must implement {@code Object#equals}
+ */
+public class TaskDrainer<T> {
+    /**
+     * Fired asynchronously after draining has begun with {@link TaskDrainer#beginDrain}
+     * <em>and</em> all tasks that were started have finished.
+     */
+    public interface DrainListener {
+        /** All tasks have fully finished draining; there will be no more pending tasks. */
+        public void onDrained();
+    }
+
+    private static final String TAG = "TaskDrainer";
+    private static final boolean VERBOSE = false;
+
+    private final Handler mHandler;
+    private final DrainListener mListener;
+    private final String mName;
+
+    /** Set of tasks which have been started but not yet finished with #taskFinished */
+    private final Set<T> mTaskSet = new HashSet<T>();
+    private final Object mLock = new Object();
+
+    private boolean mDraining = false;
+    private boolean mDrainFinished = false;
+
+    /**
+     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
+     * via the {@code handler}.
+     *
+     * @param handler a non-{@code null} handler to use to post runnables to
+     * @param listener a non-{@code null} listener where {@code onDrained} will be called
+     */
+    public TaskDrainer(Handler handler, DrainListener listener) {
+        mHandler = checkNotNull(handler, "handler must not be null");
+        mListener = checkNotNull(listener, "listener must not be null");
+        mName = null;
+    }
+
+    /**
+     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
+     * via the {@code handler}.
+     *
+     * @param handler a non-{@code null} handler to use to post runnables to
+     * @param listener a non-{@code null} listener where {@code onDrained} will be called
+     * @param name an optional name used for debug logging
+     */
+    public TaskDrainer(Handler handler, DrainListener listener, String name) {
+        // XX: Probably don't need a handler at all here
+        mHandler = checkNotNull(handler, "handler must not be null");
+        mListener = checkNotNull(listener, "listener must not be null");
+        mName = name;
+    }
+
+    /**
+     * Mark an asynchronous task as having started.
+     *
+     * <p>A task cannot be started more than once without first having finished. Once
+     * draining begins with {@link #beginDrain}, no new tasks can be started.</p>
+     *
+     * @param task a key to identify a task
+     *
+     * @see #taskFinished
+     * @see #beginDrain
+     *
+     * @throws IllegalStateException
+     *          If attempting to start a task which is already started (and not finished),
+     *          or if attempting to start a task after draining has begun.
+     */
+    public void taskStarted(T task) {
+        synchronized (mLock) {
+            if (VERBOSE) {
+                Log.v(TAG + "[" + mName + "]", "taskStarted " + task);
+            }
+
+            if (mDraining) {
+                throw new IllegalStateException("Can't start more tasks after draining has begun");
+            }
+
+            if (!mTaskSet.add(task)) {
+                throw new IllegalStateException("Task " + task + " was already started");
+            }
+        }
+    }
+
+
+    /**
+     * Mark an asynchronous task as having finished.
+     *
+     * <p>A task cannot be finished if it hasn't started. Once finished, a task
+     * cannot be finished again (unless it's started again).</p>
+     *
+     * @param task a key to identify a task
+     *
+     * @see #taskStarted
+     * @see #beginDrain
+     *
+     * @throws IllegalStateException
+     *          If attempting to start a task which is already finished (and not re-started),
+     */
+    public void taskFinished(T task) {
+        synchronized (mLock) {
+            if (VERBOSE) {
+                Log.v(TAG + "[" + mName + "]", "taskFinished " + task);
+            }
+
+            if (!mTaskSet.remove(task)) {
+                throw new IllegalStateException("Task " + task + " was already finished");
+            }
+
+            // If this is the last finished task and draining has already begun, fire #onDrained
+            checkIfDrainFinished();
+        }
+    }
+
+    /**
+     * Do not allow any more tasks to be started; once all existing started tasks are finished,
+     * fire the {@link DrainListener#onDrained} callback asynchronously.
+     *
+     * <p>This operation is idempotent; calling it more than once has no effect.</p>
+     */
+    public void beginDrain() {
+        synchronized (mLock) {
+            if (!mDraining) {
+                if (VERBOSE) {
+                    Log.v(TAG + "[" + mName + "]", "beginDrain started");
+                }
+
+                mDraining = true;
+
+                // If all tasks that had started had already finished by now, fire #onDrained
+                checkIfDrainFinished();
+            } else {
+                if (VERBOSE) {
+                    Log.v(TAG + "[" + mName + "]", "beginDrain ignored");
+                }
+            }
+        }
+    }
+
+    private void checkIfDrainFinished() {
+        if (mTaskSet.isEmpty() && mDraining && !mDrainFinished) {
+            mDrainFinished = true;
+            postDrained();
+        }
+    }
+
+    private void postDrained() {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (VERBOSE) {
+                    Log.v(TAG + "[" + mName + "]", "onDrained");
+                }
+
+                mListener.onDrained();
+            }
+        });
+    }
+}
diff --git a/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java b/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java
new file mode 100644
index 0000000..f6272c9
--- /dev/null
+++ b/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2014 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.utils;
+
+import android.hardware.camera2.utils.TaskDrainer.DrainListener;
+import android.os.Handler;
+
+/**
+ * Keep track of a single concurrent task starting and finishing;
+ * allow draining the existing task and figuring out when the task has finished
+ * (and won't restart).
+ *
+ * <p>The initial state is to allow all tasks to be started and finished. A task may only be started
+ * once, after which it must be finished before starting again. Likewise, finishing a task
+ * that hasn't been started is also not allowed.</p>
+ *
+ * <p>When draining begins, the task cannot be started again. This guarantees that at some
+ * point the task will be finished forever, at which point the {@link DrainListener#onDrained}
+ * callback will be invoked.</p>
+ */
+public class TaskSingleDrainer {
+
+    private final TaskDrainer<Object> mTaskDrainer;
+    private final Object mSingleTask = new Object();
+
+    /**
+     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
+     * via the {@code handler}.
+     *
+     * @param handler a non-{@code null} handler to use to post runnables to
+     * @param listener a non-{@code null} listener where {@code onDrained} will be called
+     */
+    public TaskSingleDrainer(Handler handler, DrainListener listener) {
+        mTaskDrainer = new TaskDrainer<>(handler, listener);
+    }
+
+    /**
+     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
+     * via the {@code handler}.
+     *
+     * @param handler a non-{@code null} handler to use to post runnables to
+     * @param listener a non-{@code null} listener where {@code onDrained} will be called
+     * @param name an optional name used for debug logging
+     */
+    public TaskSingleDrainer(Handler handler, DrainListener listener, String name) {
+        mTaskDrainer = new TaskDrainer<>(handler, listener, name);
+    }
+
+    /**
+     * Mark this asynchronous task as having started.
+     *
+     * <p>The task cannot be started more than once without first having finished. Once
+     * draining begins with {@link #beginDrain}, no new tasks can be started.</p>
+     *
+     * @see #taskFinished
+     * @see #beginDrain
+     *
+     * @throws IllegalStateException
+     *          If attempting to start a task which is already started (and not finished),
+     *          or if attempting to start a task after draining has begun.
+     */
+    public void taskStarted() {
+        mTaskDrainer.taskStarted(mSingleTask);
+    }
+
+    /**
+     * Do not allow any more task re-starts; once the existing task is finished,
+     * fire the {@link DrainListener#onDrained} callback asynchronously.
+     *
+     * <p>This operation is idempotent; calling it more than once has no effect.</p>
+     */
+    public void beginDrain() {
+        mTaskDrainer.beginDrain();
+    }
+
+    /**
+     * Mark this asynchronous task as having finished.
+     *
+     * <p>The task cannot be finished if it hasn't started. Once finished, a task
+     * cannot be finished again (unless it's started again).</p>
+     *
+     * @see #taskStarted
+     * @see #beginDrain
+     *
+     * @throws IllegalStateException
+     *          If attempting to start a task which is already finished (and not re-started),
+     */
+    public void taskFinished() {
+        mTaskDrainer.taskFinished(mSingleTask);
+    }
+}
diff --git a/core/java/android/hardware/camera2/utils/UncheckedThrow.java b/core/java/android/hardware/camera2/utils/UncheckedThrow.java
index 8224fed..ffcb78b 100644
--- a/core/java/android/hardware/camera2/utils/UncheckedThrow.java
+++ b/core/java/android/hardware/camera2/utils/UncheckedThrow.java
@@ -33,8 +33,20 @@
         UncheckedThrow.<RuntimeException>throwAnyImpl(e);
     }
 
+    /**
+     * Throw any kind of throwable without needing it to be checked
+     * @param e any instance of a Throwable
+     */
+    public static void throwAnyException(Throwable e) {
+        /**
+         *  Abuse type erasure by making the compiler think we are throwing RuntimeException,
+         *  which is unchecked, but then inserting any exception in there.
+         */
+        UncheckedThrow.<RuntimeException>throwAnyImpl(e);
+    }
+
     @SuppressWarnings("unchecked")
-    private static<T extends Exception> void throwAnyImpl(Exception e) throws T {
+    private static<T extends Throwable> void throwAnyImpl(Throwable e) throws T {
         throw (T) e;
     }
 }