Initial frameserver/OneCamera integration

Change-Id: I2fe0d8acf9ce927a6a0a1dea599299c715503462
diff --git a/src/com/android/camera/one/v2/components/ImageSaver.java b/src/com/android/camera/async/BlockingCloseable.java
similarity index 62%
copy from src/com/android/camera/one/v2/components/ImageSaver.java
copy to src/com/android/camera/async/BlockingCloseable.java
index 7138029..90ef168 100644
--- a/src/com/android/camera/one/v2/components/ImageSaver.java
+++ b/src/com/android/camera/async/BlockingCloseable.java
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.camera.one.v2.components;
-
-import com.android.camera.one.v2.camera2proxy.ImageProxy;
+package com.android.camera.async;
 
 /**
- * Interface for an image-saving object.
+ * An {@link AutoCloseable} which blocks until its associated resources are
+ * released.
  */
-public interface ImageSaver {
+public interface BlockingCloseable extends AutoCloseable {
     /**
-     * Implementations should save the image to disk and close it.
+     * Implementations must tolerate multiple calls to close(), and may block
+     * until their associated resources are released.
      */
-    public void saveAndCloseImage(ImageProxy image);
+    @Override
+    public void close() throws InterruptedException;
 }
diff --git a/src/com/android/camera/async/BufferQueueController.java b/src/com/android/camera/async/BufferQueueController.java
index 89dd2e5..e8629c0 100644
--- a/src/com/android/camera/async/BufferQueueController.java
+++ b/src/com/android/camera/async/BufferQueueController.java
@@ -20,18 +20,20 @@
  * An output stream of objects which can be closed from either the producer or
  * the consumer.
  */
-public interface BufferQueueController<T> extends SafeCloseable {
+public interface BufferQueueController<T> extends Updatable<T>, SafeCloseable {
     /**
      * Adds the given element to the stream. Streams must support calling this
      * even after closed.
      *
      * @param element The element to add.
      */
-    public void append(T element);
+    @Override
+    public void update(T element);
 
     /**
      * Closes the stream. Implementations must tolerate multiple calls to close.
      */
+    @Override
     public void close();
 
     /**
diff --git a/src/com/android/camera/async/CallbackRunnable.java b/src/com/android/camera/async/CallbackRunnable.java
new file mode 100644
index 0000000..f515043
--- /dev/null
+++ b/src/com/android/camera/async/CallbackRunnable.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.android.camera.async;
+
+import com.android.camera.util.Callback;
+
+/**
+ * A {@link Callback} which just invokes a {@link Runnable}.
+ *
+ * @param <T>
+ */
+public class CallbackRunnable<T> implements Callback<T> {
+    private final Runnable mRunnable;
+
+    public CallbackRunnable(Runnable runnable) {
+        mRunnable = runnable;
+    }
+
+    @Override
+    public void onCallback(T result) {
+        mRunnable.run();
+    }
+}
diff --git a/src/com/android/camera/async/CloseableHandlerThread.java b/src/com/android/camera/async/CloseableHandlerThread.java
new file mode 100644
index 0000000..f351b82
--- /dev/null
+++ b/src/com/android/camera/async/CloseableHandlerThread.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.android.camera.async;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * Creates a new Handler thread which can be safely destroyed when the object is
+ * closed.
+ */
+public class CloseableHandlerThread implements SafeCloseable {
+    private final HandlerThread mThread;
+    private final Handler mHandler;
+
+    public CloseableHandlerThread(String threadName) {
+        mThread = new HandlerThread(threadName);
+        mThread.start();
+        mHandler = new Handler(mThread.getLooper());
+    }
+
+    public Handler get() {
+        return mHandler;
+    }
+
+    @Override
+    public void close() {
+        mThread.quitSafely();
+    }
+}
diff --git a/src/com/android/camera/async/ConcurrentBufferQueue.java b/src/com/android/camera/async/ConcurrentBufferQueue.java
index 2347af2..8cac688 100644
--- a/src/com/android/camera/async/ConcurrentBufferQueue.java
+++ b/src/com/android/camera/async/ConcurrentBufferQueue.java
@@ -130,7 +130,7 @@
     }
 
     @Override
-    public void append(T element) {
+    public void update(T element) {
         boolean closed = false;
         synchronized (mLock) {
             closed = mClosed.get();
diff --git a/src/com/android/camera/async/ConcurrentState.java b/src/com/android/camera/async/ConcurrentState.java
new file mode 100644
index 0000000..97bd581
--- /dev/null
+++ b/src/com/android/camera/async/ConcurrentState.java
@@ -0,0 +1,143 @@
+/*
+ * 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 com.android.camera.async;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+import com.android.camera.util.Callback;
+
+/**
+ * Generic asynchronous state wrapper which supports two methods of interaction:
+ * polling for the latest value and listening for updates.
+ * <p>
+ * Note that this class only supports polling and using listeners. If
+ * synchronous consumption of state changes is required, see
+ * {@link FutureResult} or {@link BufferQueue} and its implementations.
+ * </p>
+ */
+public class ConcurrentState<T> implements Updatable<T>, Pollable<T> {
+
+    private static class ExecutorListenerPair<T> {
+        private final Executor mExecutor;
+        private final Callback<T> mListener;
+
+        public ExecutorListenerPair(Executor executor, Callback<T> listener) {
+            mExecutor = executor;
+            mListener = listener;
+        }
+
+        /**
+         * Runs the callback on the executor with the given value.
+         */
+        public void run(final T t) {
+            mExecutor.execute(new Runnable() {
+                public void run() {
+                    mListener.onCallback(t);
+                }
+            });
+        }
+    }
+
+    private final Object mLock;
+    private final Set<ExecutorListenerPair<T>> mListeners;
+    private boolean mValueSet;
+    private T mValue;
+
+    public ConcurrentState() {
+        mLock = new Object();
+        mListeners = new HashSet<ExecutorListenerPair<T>>();
+        mValueSet = false;
+    }
+
+    /**
+     * Updates the state to the latest value, notifying all listeners.
+     */
+    @Override
+    public void update(T newValue) {
+        List<ExecutorListenerPair<T>> listeners = new ArrayList<ExecutorListenerPair<T>>();
+        synchronized (mLock) {
+            mValueSet = true;
+            mValue = newValue;
+            // Copy listeners out here so we can iterate over the list outside
+            // the critical section.
+            listeners.addAll(mListeners);
+        }
+        for (ExecutorListenerPair<T> pair : listeners) {
+            pair.run(newValue);
+        }
+    }
+
+    /**
+     * Adds the given callback, returning a token to be closed when the callback
+     * is no longer needed.
+     *
+     * @param callback The callback to add.
+     * @param executor The executor on which the callback will be invoked.
+     * @return A {@link SafeCloseable} token to be closed when the callback must
+     *         be removed.
+     */
+    public SafeCloseable addCallback(Callback callback, Executor executor) {
+        synchronized (mLock) {
+            final ExecutorListenerPair<T> pair = new ExecutorListenerPair<>(executor, callback);
+            mListeners.add(pair);
+
+            return new SafeCloseable() {
+                @Override
+                public void close() {
+                    synchronized (mLock) {
+                        mListeners.remove(pair);
+                    }
+                }
+            };
+        }
+    }
+
+    /**
+     * Polls for the latest value.
+     *
+     * @return The latest state, or defaultValue if no state has been set yet.
+     */
+    @Override
+    public T get(T defaultValue) {
+        try {
+            return get();
+        } catch (Pollable.NoValueSetException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Polls for the latest value.
+     *
+     * @return The latest state.
+     * @throws com.android.camera.async.Pollable.NoValueSetException If no value has been set yet.
+     */
+    @Override
+    public T get() throws Pollable.NoValueSetException {
+        synchronized (mLock) {
+            if (mValueSet) {
+                return mValue;
+            } else {
+                throw new Pollable.NoValueSetException();
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/async/ConstantPollable.java b/src/com/android/camera/async/ConstantPollable.java
new file mode 100644
index 0000000..4ef37d2
--- /dev/null
+++ b/src/com/android/camera/async/ConstantPollable.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.android.camera.async;
+
+/**
+ * A {@link Pollable} which always returns a constant value.
+ *
+ * @param <T>
+ */
+public class ConstantPollable<T> implements Pollable<T> {
+    private final T mValue;
+
+    public ConstantPollable(T value) {
+        mValue = value;
+    }
+
+    @Override
+    public T get(T defaultValue) {
+        return mValue;
+    }
+
+    @Override
+    public T get() throws NoValueSetException {
+        return mValue;
+    }
+}
diff --git a/src/com/android/camera/async/FilteredUpdatable.java b/src/com/android/camera/async/FilteredUpdatable.java
new file mode 100644
index 0000000..b931101
--- /dev/null
+++ b/src/com/android/camera/async/FilteredUpdatable.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.android.camera.async;
+
+/**
+ * Wraps an {@link com.android.camera.async.Updatable} by filtering out
+ * duplicate updates.
+ */
+public class FilteredUpdatable<T> implements Updatable<T> {
+    private final Updatable<T> mUpdatable;
+    private final Object mLock;
+    private boolean mValueSet;
+    private T mLatestValue;
+
+    public FilteredUpdatable(Updatable<T> updatable) {
+        mUpdatable = updatable;
+        mLock = new Object();
+        mValueSet = false;
+        mLatestValue = null;
+    }
+
+    @Override
+    public void update(T t) {
+        synchronized (mLock) {
+            if (!mValueSet) {
+                setNewValue(t);
+            } else {
+                if (t == null && mLatestValue != null) {
+                    setNewValue(t);
+                } else if (t != null) {
+                    if (!t.equals(mLatestValue)) {
+                        setNewValue(t);
+                    }
+                }
+            }
+        }
+    }
+
+    private void setNewValue(T value) {
+        synchronized (mLock) {
+            mUpdatable.update(value);
+            mLatestValue = value;
+            mValueSet = true;
+        }
+    }
+}
diff --git a/src/com/android/camera/async/FutureResult.java b/src/com/android/camera/async/FutureResult.java
index e53d263..74e7771 100644
--- a/src/com/android/camera/async/FutureResult.java
+++ b/src/com/android/camera/async/FutureResult.java
@@ -40,12 +40,25 @@
     private V mValue;
     private boolean mCancelled;
     private Exception mException;
+    private final Updatable<Future<V>> mDoneUpdatable;
 
-    public FutureResult() {
+    /**
+     * @param doneUpdatable An updatable to be notified when the future is done.
+     */
+    public FutureResult(Updatable<Future<V>> doneUpdatable) {
         mDone = new AtomicBoolean();
         mDoneCondition = new CountDownLatch(1);
         mValue = null;
         mCancelled = false;
+        mDoneUpdatable = doneUpdatable;
+    }
+
+    public FutureResult() {
+        this(new Updatable<Future<V>>() {
+            @Override
+            public void update(Future<V> vFutureResult) {
+            }
+        });
     }
 
     /**
@@ -117,6 +130,7 @@
 
         mCancelled = true;
         mDoneCondition.countDown();
+        mDoneUpdatable.update(this);
         return true;
     }
 
@@ -135,6 +149,7 @@
 
         mValue = value;
         mDoneCondition.countDown();
+        mDoneUpdatable.update(this);
         return true;
     }
 
@@ -153,6 +168,7 @@
 
         mException = e;
         mDoneCondition.countDown();
+        mDoneUpdatable.update(this);
         return true;
     }
 
diff --git a/src/com/android/camera/async/HandlerExecutor.java b/src/com/android/camera/async/HandlerExecutor.java
new file mode 100644
index 0000000..87c8c0c
--- /dev/null
+++ b/src/com/android/camera/async/HandlerExecutor.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.android.camera.async;
+
+import java.util.concurrent.Executor;
+
+import android.os.Handler;
+
+/**
+ * An {@link Executor} which posts to a {@link Handler}.
+ */
+public class HandlerExecutor implements Executor {
+    private final Handler mHandler;
+
+    public HandlerExecutor(Handler handler) {
+        mHandler = handler;
+    }
+
+    @Override
+    public void execute(Runnable runnable) {
+        mHandler.post(runnable);
+    }
+}
diff --git a/src/com/android/camera/async/Listenable.java b/src/com/android/camera/async/Listenable.java
new file mode 100644
index 0000000..03eb017
--- /dev/null
+++ b/src/com/android/camera/async/Listenable.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 com.android.camera.async;
+
+import com.android.camera.util.Callback;
+
+/**
+ * Note: This interface, alone, does not provide a means of guaranteeing which
+ * thread the callback will be invoked on. Use
+ * {@link com.android.camera.async.ConcurrentState}, {@link BufferQueue}, or
+ * {@link java.util.concurrent.Future} instead to guarantee thread-safety.
+ */
+public interface Listenable<T> extends SafeCloseable {
+    /**
+     * Sets the callback, removing any existing callback first.
+     */
+    public void setCallback(Callback<T> callback);
+
+    /**
+     * Removes any existing callback.
+     */
+    @Override
+    public void close();
+}
diff --git a/src/com/android/camera/async/ListenableConcurrentState.java b/src/com/android/camera/async/ListenableConcurrentState.java
new file mode 100644
index 0000000..b54763c
--- /dev/null
+++ b/src/com/android/camera/async/ListenableConcurrentState.java
@@ -0,0 +1,69 @@
+/*
+ * 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 com.android.camera.async;
+
+import java.util.concurrent.Executor;
+
+import com.android.camera.util.Callback;
+
+/**
+ * Wraps {@link ConcurrentState} with {@link #setCallback} semantics which
+ * overwrite any existing callback.
+ */
+public class ListenableConcurrentState<T> implements Listenable<T> {
+    private final ConcurrentState<T> mState;
+    private final Executor mExecutor;
+    private final Object mLock;
+    private boolean mClosed;
+    private SafeCloseable mExistingCallbackHandle;
+
+    public ListenableConcurrentState(ConcurrentState<T> state, Executor executor) {
+        mState = state;
+        mExecutor = executor;
+        mLock = new Object();
+        mClosed = false;
+        mExistingCallbackHandle = null;
+    }
+
+    /**
+     * Sets the callback, removing any existing callback first.
+     */
+    @Override
+    public void setCallback(Callback<T> callback) {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+            if (mExistingCallbackHandle != null) {
+                // Unregister any existing callback
+                mExistingCallbackHandle.close();
+            }
+            mExistingCallbackHandle = mState.addCallback(callback, mExecutor);
+        }
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            mClosed = true;
+            if (mExistingCallbackHandle != null) {
+                // Unregister any existing callback
+                mExistingCallbackHandle.close();
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/async/Pollable.java b/src/com/android/camera/async/Pollable.java
new file mode 100644
index 0000000..1cea227
--- /dev/null
+++ b/src/com/android/camera/async/Pollable.java
@@ -0,0 +1,43 @@
+/*
+ * 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 com.android.camera.async;
+
+/**
+ * An interface for state which can be polled for the latest value.
+ */
+public interface Pollable<T> {
+    /**
+     * Indicates that no value has been set yet.
+     */
+    public static class NoValueSetException extends Exception {
+    }
+
+    /**
+     * Polls for the latest value.
+     *
+     * @return The latest state, or defaultValue if no state has been set yet.
+     */
+    public T get(T defaultValue);
+
+    /**
+     * Polls for the latest value.
+     *
+     * @return The latest state.
+     * @throws NoValueSetException If no value has been set yet.
+     */
+    public T get() throws NoValueSetException;
+}
diff --git a/src/com/android/camera/async/RefCountedBufferQueueController.java b/src/com/android/camera/async/RefCountedBufferQueueController.java
index 66a6807..971af68 100644
--- a/src/com/android/camera/async/RefCountedBufferQueueController.java
+++ b/src/com/android/camera/async/RefCountedBufferQueueController.java
@@ -27,8 +27,8 @@
     }
 
     @Override
-    public void append(T element) {
-        mBuffer.get().append(element);
+    public void update(T element) {
+        mBuffer.get().update(element);
     }
 
     @Override
diff --git a/src/com/android/camera/async/ResettingDelayedExecutor.java b/src/com/android/camera/async/ResettingDelayedExecutor.java
new file mode 100644
index 0000000..0ecbdf9
--- /dev/null
+++ b/src/com/android/camera/async/ResettingDelayedExecutor.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.camera.async;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An executor which executes with a delay, discarding pending executions such
+ * that at most one task is queued at any time.
+ */
+public class ResettingDelayedExecutor implements Executor, SafeCloseable {
+    private final ScheduledExecutorService mExecutor;
+    private final long mDelay;
+    private final TimeUnit mDelayUnit;
+    /**
+     * Lock for all mutable state: {@link #mLatestRunRequest} and
+     * {@link #mClosed}.
+     */
+    private final Object mLock;
+    private ScheduledFuture<?> mLatestRunRequest;
+    private boolean mClosed;
+
+    public ResettingDelayedExecutor(ScheduledExecutorService executor, long delay, TimeUnit
+            delayUnit) {
+        mExecutor = executor;
+        mDelay = delay;
+        mDelayUnit = delayUnit;
+        mLock = new Object();
+    }
+
+    @Override
+    public void execute(Runnable runnable) {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+            // Cancel any existing, queued task before scheduling another.
+            if (mLatestRunRequest != null) {
+                boolean mayInterruptIfRunning = false;
+                mLatestRunRequest.cancel(mayInterruptIfRunning);
+            }
+            mLatestRunRequest = mExecutor.schedule(runnable, mDelay, mDelayUnit);
+        }
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+            mExecutor.shutdownNow();
+        }
+    }
+}
diff --git a/src/com/android/camera/async/SafeCloseable.java b/src/com/android/camera/async/SafeCloseable.java
index 8b61348..3a5822c 100644
--- a/src/com/android/camera/async/SafeCloseable.java
+++ b/src/com/android/camera/async/SafeCloseable.java
@@ -20,5 +20,9 @@
  * An {@link AutoCloseable} which should not throw in {@link #close}.
  */
 public interface SafeCloseable extends AutoCloseable {
+    /**
+     * Implementations must tolerate multiple calls to close().
+     */
+    @Override
     public void close();
 }
diff --git a/src/com/android/camera/async/Updatable.java b/src/com/android/camera/async/Updatable.java
new file mode 100644
index 0000000..0caff9c
--- /dev/null
+++ b/src/com/android/camera/async/Updatable.java
@@ -0,0 +1,46 @@
+/*
+ * 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 com.android.camera.async;
+
+/**
+ * An interface for updating thread-shared state from producer threads with
+ * real-time requirements.
+ * <p>
+ * Depending on how consumers need to access updates, the following
+ * implementations may be used:
+ * <ul>
+ * <li>Synchronous access to every update: {@link ConcurrentBufferQueue}</li>
+ * <li>Synchronous access to a single update: {@link UpdatableCountDownLatch}</li>
+ * <li>Polling: {@link ConcurrentState}</li>
+ * <li>Callbacks: {@link ConcurrentState}</li>
+ * </ul>
+ * </p>
+ */
+public interface Updatable<T> {
+    /**
+     * Implementations MUST ALWAYS satisfy the following constraints:
+     * <ul>
+     * <li>Return quickly, without performing any expensive work.</li>
+     * <li>Execute in a predictable, stable, amount of time.</li>
+     * <li>Never block.</li>
+     * <li>Be thread-safe.</li>
+     * <li>Never leak control of the thread on which they are invoked. (e.g.
+     * invoke callbacks which may violate the above constraints)</li>
+     * </ul>
+     */
+    public void update(T t);
+}
diff --git a/src/com/android/camera/one/v2/components/ImageSaver.java b/src/com/android/camera/async/UpdatableCountDownLatch.java
similarity index 64%
copy from src/com/android/camera/one/v2/components/ImageSaver.java
copy to src/com/android/camera/async/UpdatableCountDownLatch.java
index 7138029..cbcb7a5 100644
--- a/src/com/android/camera/one/v2/components/ImageSaver.java
+++ b/src/com/android/camera/async/UpdatableCountDownLatch.java
@@ -14,16 +14,20 @@
  * limitations under the License.
  */
 
-package com.android.camera.one.v2.components;
+package com.android.camera.async;
 
-import com.android.camera.one.v2.camera2proxy.ImageProxy;
+import java.util.concurrent.CountDownLatch;
 
 /**
- * Interface for an image-saving object.
+ * Counts down on each update.
  */
-public interface ImageSaver {
-    /**
-     * Implementations should save the image to disk and close it.
-     */
-    public void saveAndCloseImage(ImageProxy image);
+public class UpdatableCountDownLatch extends CountDownLatch implements Updatable<Void> {
+    public UpdatableCountDownLatch(int count) {
+        super(count);
+    }
+
+    @Override
+    public void update(Void v) {
+        countDown();
+    }
 }
diff --git a/src/com/android/camera/one/AbstractOneCamera.java b/src/com/android/camera/one/AbstractOneCamera.java
index 6342fcf..15276ec 100644
--- a/src/com/android/camera/one/AbstractOneCamera.java
+++ b/src/com/android/camera/one/AbstractOneCamera.java
@@ -30,7 +30,6 @@
  * class instead.
  */
 public abstract class AbstractOneCamera implements OneCamera {
-    protected CameraErrorListener mCameraErrorListener;
     protected FocusStateListener mFocusStateListener;
     protected ReadyStateChangedListener mReadyStateChangedListener;
 
@@ -41,11 +40,6 @@
     static final int DEBUG_FOLDER_SERIAL_LENGTH = 4;
 
     @Override
-    public final void setCameraErrorListener(CameraErrorListener listener) {
-        mCameraErrorListener = listener;
-    }
-
-    @Override
     public final void setFocusStateListener(FocusStateListener listener) {
         mFocusStateListener = listener;
     }
diff --git a/src/com/android/camera/one/OneCamera.java b/src/com/android/camera/one/OneCamera.java
index a15bdb7..a45f834 100644
--- a/src/com/android/camera/one/OneCamera.java
+++ b/src/com/android/camera/one/OneCamera.java
@@ -193,15 +193,6 @@
     }
 
     /**
-     * Classes implementing this interface will be called whenever the camera
-     * encountered an error.
-     */
-    public static interface CameraErrorListener {
-        /** Called when the camera encountered an error. */
-        public void onCameraError();
-    }
-
-    /**
      * Classes implementing this interface will be called when the state of the
      * focus changes. Guaranteed not to stay stuck in scanning state past some
      * reasonable timeout even if Camera API is stuck.
@@ -336,12 +327,6 @@
     public void stopBurst();
 
     /**
-     * Sets or replaces a listener that is called whenever the camera encounters
-     * an error.
-     */
-    public void setCameraErrorListener(CameraErrorListener listener);
-
-    /**
      * Sets or replaces a listener that is called whenever the focus state of
      * the camera changes.
      */
diff --git a/src/com/android/camera/one/v2/SimpleJpegOneCameraFactory.java b/src/com/android/camera/one/v2/SimpleJpegOneCameraFactory.java
new file mode 100644
index 0000000..813ca9c
--- /dev/null
+++ b/src/com/android/camera/one/v2/SimpleJpegOneCameraFactory.java
@@ -0,0 +1,543 @@
+/*
+ * 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 com.android.camera.one.v2;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.view.Surface;
+
+import com.android.camera.app.OrientationManager;
+import com.android.camera.async.BufferQueue;
+import com.android.camera.async.CallbackRunnable;
+import com.android.camera.async.CloseableHandlerThread;
+import com.android.camera.async.ConcurrentBufferQueue;
+import com.android.camera.async.ConcurrentState;
+import com.android.camera.async.ConstantPollable;
+import com.android.camera.async.FilteredUpdatable;
+import com.android.camera.async.FutureResult;
+import com.android.camera.async.HandlerExecutor;
+import com.android.camera.async.Listenable;
+import com.android.camera.async.ListenableConcurrentState;
+import com.android.camera.async.Pollable;
+import com.android.camera.async.ResettingDelayedExecutor;
+import com.android.camera.async.SafeCloseable;
+import com.android.camera.async.Updatable;
+import com.android.camera.one.CameraDirectionProvider;
+import com.android.camera.one.OneCamera;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionProxy;
+import com.android.camera.one.v2.camera2proxy.CameraDeviceProxy;
+import com.android.camera.one.v2.camera2proxy.CameraDeviceRequestBuilderFactory;
+import com.android.camera.one.v2.camera2proxy.ImageProxy;
+import com.android.camera.one.v2.commands.AFScanHoldReset;
+import com.android.camera.one.v2.commands.CameraCommand;
+import com.android.camera.one.v2.commands.CameraCommandExecutor;
+import com.android.camera.one.v2.commands.FullAFScanCommand;
+import com.android.camera.one.v2.commands.LoggingCameraCommand;
+import com.android.camera.one.v2.commands.PreviewCommand;
+import com.android.camera.one.v2.commands.RunnableCameraCommand;
+import com.android.camera.one.v2.commands.StaticPictureCommand;
+import com.android.camera.one.v2.common.CaptureSessionCreator;
+import com.android.camera.one.v2.common.DeferredManualAutoFocus;
+import com.android.camera.one.v2.common.DeferredPictureTaker;
+import com.android.camera.one.v2.common.FullSizeAspectRatioProvider;
+import com.android.camera.one.v2.common.GenericOneCameraImpl;
+import com.android.camera.one.v2.common.ManualAutoFocusImpl;
+import com.android.camera.one.v2.common.MeteringParameters;
+import com.android.camera.one.v2.common.PictureCallbackAdaptor;
+import com.android.camera.one.v2.common.PollableAEMode;
+import com.android.camera.one.v2.common.PollableAERegion;
+import com.android.camera.one.v2.common.PollableAFRegion;
+import com.android.camera.one.v2.common.PollableZoomedCropRegion;
+import com.android.camera.one.v2.common.PreviewSizeSelector;
+import com.android.camera.one.v2.common.SensorOrientationProvider;
+import com.android.camera.one.v2.common.SupportedPreviewSizeProvider;
+import com.android.camera.one.v2.core.DecoratingRequestBuilderBuilder;
+import com.android.camera.one.v2.core.FrameServer;
+import com.android.camera.one.v2.core.MetadataResponseListener;
+import com.android.camera.one.v2.core.RequestBuilder;
+import com.android.camera.one.v2.core.SimpleCaptureStream;
+import com.android.camera.one.v2.core.TagDispatchCaptureSession;
+import com.android.camera.one.v2.core.TimestampResponseListener;
+import com.android.camera.one.v2.sharedimagereader.ImageDistributor;
+import com.android.camera.one.v2.sharedimagereader.ImageDistributorOnImageAvailableListener;
+import com.android.camera.one.v2.sharedimagereader.SharedImageReader;
+import com.android.camera.session.CaptureSession;
+import com.android.camera.util.ScopedFactory;
+import com.android.camera.util.Size;
+
+/**
+ */
+public class SimpleJpegOneCameraFactory {
+    /**
+     * All of the variables available when the CameraDevice is available.
+     */
+    private static class CameraScope {
+        public final CameraDevice device;
+        public final CameraCharacteristics characteristics;
+        public final Handler mainHandler;
+        public final FutureResult<GenericOneCameraImpl.PictureTaker> pictureTaker;
+        public final FutureResult<GenericOneCameraImpl.ManualAutoFocus> manualAutoFocus;
+        public final ConcurrentState<Integer> afState;
+        public final ConcurrentState<Boolean> readyState;
+        public final ConcurrentState<Float> zoomState;
+        public final ConcurrentState<Boolean> previewStartSuccess;
+        public final Size pictureSize;
+
+        private CameraScope(
+                CameraDevice device,
+                CameraCharacteristics characteristics,
+                Handler mainHandler,
+                FutureResult<GenericOneCameraImpl.PictureTaker> pictureTaker,
+                FutureResult<GenericOneCameraImpl.ManualAutoFocus> manualAutoFocus,
+                ConcurrentState<Integer> afState,
+                ConcurrentState<Boolean> readyState,
+                ConcurrentState<Float> zoomState,
+                ConcurrentState<Boolean> previewStartSuccess, Size pictureSize) {
+            this.device = device;
+            this.characteristics = characteristics;
+            this.mainHandler = mainHandler;
+            this.pictureTaker = pictureTaker;
+            this.manualAutoFocus = manualAutoFocus;
+            this.afState = afState;
+            this.readyState = readyState;
+            this.zoomState = zoomState;
+            this.previewStartSuccess = previewStartSuccess;
+            this.pictureSize = pictureSize;
+        }
+    }
+
+    private static class PreviewSurfaceScope {
+        public final CameraScope cameraScope;
+        public final Surface previewSurface;
+        public final ImageReader imageReader;
+        public final CloseableHandlerThread captureSessionOpenHandler;
+
+        private PreviewSurfaceScope(CameraScope cameraScope,
+                Surface previewSurface,
+                ImageReader imageReader,
+                CloseableHandlerThread captureSessionOpenHandler) {
+            this.cameraScope = cameraScope;
+            this.previewSurface = previewSurface;
+            this.imageReader = imageReader;
+            this.captureSessionOpenHandler = captureSessionOpenHandler;
+        }
+    }
+
+    private static class CameraCaptureSessionScope {
+        public final Runnable startPreviewRunnable;
+        public final GenericOneCameraImpl.PictureTaker pictureTaker;
+        public final GenericOneCameraImpl.ManualAutoFocus manualAutoFocus;
+
+        private CameraCaptureSessionScope(Runnable startPreviewRunnable,
+                GenericOneCameraImpl.PictureTaker pictureTaker,
+                GenericOneCameraImpl.ManualAutoFocus manualAutoFocus) {
+            this.startPreviewRunnable = startPreviewRunnable;
+            this.pictureTaker = pictureTaker;
+            this.manualAutoFocus = manualAutoFocus;
+        }
+    }
+
+    private static CameraScope provideCameraScope(CameraDevice device, CameraCharacteristics
+            characteristics, Handler mainHandler, Size pictureSize) {
+        FutureResult<GenericOneCameraImpl.PictureTaker> pictureTakerFutureResult = new FutureResult<>();
+        FutureResult<GenericOneCameraImpl.ManualAutoFocus> manualAutoFocusFutureResult = new FutureResult<>();
+        ConcurrentState<Integer> afState = new ConcurrentState<>();
+        ConcurrentState<Boolean> readyState = new ConcurrentState<>();
+        ConcurrentState<Float> zoomState = new ConcurrentState<>();
+        ConcurrentState<Boolean> previewStartSuccess = new ConcurrentState<>();
+
+        return new CameraScope(device, characteristics, mainHandler, pictureTakerFutureResult,
+                manualAutoFocusFutureResult, afState,
+                readyState, zoomState, previewStartSuccess, pictureSize);
+    }
+
+    private static Set<SafeCloseable> provideCloseListeners(final CameraScope scope) {
+        // FIXME Something in here must close() the CameraDevice, ImageReader,
+        // and CameraCaptureSession.
+        // TODO Maybe replace this with two things:
+        // 1. A blocking AutoCloseable which will wait until the device is
+        // closed.
+        // 2. A ConcurrentState<> for other things to subscribe to, or poll, for
+        // closing state.
+        // - Close the session
+        // - Close all CloseableHandlerThreads
+        // - Close the global timestamp stream
+        // - Close the CameraCommandExecutor
+        // - Close the SharedImageReader (on a separate thread to only release
+        // it when all consumers release any outstanding images.)
+        Set<SafeCloseable> closeables = new HashSet<>();
+        closeables.add(new SafeCloseable() {
+            @Override
+            public void close() {
+                scope.device.close();
+            }
+        });
+        return closeables;
+    }
+
+    private static Executor provideMainHandlerExecutor(CameraScope scope) {
+        return new HandlerExecutor(scope.mainHandler);
+    }
+
+    private static Listenable<Integer> provideAFStateListenable(CameraScope scope) {
+        return new ListenableConcurrentState<>(scope.afState, provideMainHandlerExecutor(scope));
+    }
+
+    private static Listenable<Boolean> provideReadyStateListenable(CameraScope scope) {
+        return new ListenableConcurrentState<>(scope.readyState, provideMainHandlerExecutor(scope));
+    }
+
+    private static CameraDirectionProvider provideCameraDirectionProvider(CameraScope scope) {
+        return new CameraDirectionProvider(scope.characteristics);
+    }
+
+    private static OneCamera.Facing provideCameraDirection(CameraScope scope) {
+        return provideCameraDirectionProvider(scope).getDirection();
+    }
+
+    private static float provideMaxZoom(CameraCharacteristics characteristics) {
+        return characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
+    }
+
+    private static Updatable<Float> provideZoom(CameraScope scope) {
+        return scope.zoomState;
+    }
+
+    private static ScopedFactory<Surface, Runnable> providePreviewStarter(final CameraScope
+            cameraScope) {
+        return new ScopedFactory<Surface, Runnable>() {
+            @Override
+            public Runnable get(Surface previewSurface) {
+                PreviewSurfaceScope previewSurfaceScope = providePreviewSurfaceScope(cameraScope,
+                        previewSurface);
+                return provideCaptureSessionCreator(previewSurfaceScope);
+            }
+        };
+    }
+
+    private static List<Surface> provideSurfaceList(PreviewSurfaceScope scope) {
+        List<Surface> surfaces = new ArrayList<>();
+        surfaces.add(scope.imageReader.getSurface());
+        surfaces.add(scope.previewSurface);
+        return surfaces;
+    }
+
+    private static CaptureSessionCreator provideCaptureSessionCreator(PreviewSurfaceScope scope) {
+        CameraDeviceProxy device = new CameraDeviceProxy(scope.cameraScope.device);
+        Handler cameraHandler = scope.captureSessionOpenHandler.get();
+        List<Surface> surfaces = provideSurfaceList(scope);
+        FutureResult<CameraCaptureSessionProxy> sessionFuture = provideCaptureSessionFuture(scope);
+        ScopedFactory<CameraCaptureSessionProxy, Runnable> captureSessionScopeEntrance =
+                provideCaptureSessionScopeEntrance(scope);
+        return new CaptureSessionCreator(device, cameraHandler, surfaces, sessionFuture,
+                captureSessionScopeEntrance);
+    }
+
+    private static ScopedFactory<CameraCaptureSessionProxy, Runnable>
+            provideCaptureSessionScopeEntrance(
+                    final PreviewSurfaceScope previewSurfaceScope) {
+        return new ScopedFactory<CameraCaptureSessionProxy, Runnable>() {
+            @Override
+            public Runnable get(CameraCaptureSessionProxy cameraCaptureSession) {
+                final CameraCaptureSessionScope scope = provideCameraCaptureSessionScope
+                        (previewSurfaceScope, cameraCaptureSession);
+                return new Runnable() {
+                    @Override
+                    public void run() {
+                        // Update the future for image capture
+                        previewSurfaceScope.cameraScope.pictureTaker.setValue(scope.pictureTaker);
+                        // Update the future for tap-to-focus
+                        previewSurfaceScope.cameraScope.manualAutoFocus.setValue(scope
+                                .manualAutoFocus);
+
+                        // Dispatch to startPreviewRunnable
+                        scope.startPreviewRunnable.run();
+                    }
+                };
+            }
+        };
+    }
+
+    private static CameraCaptureSessionScope provideCameraCaptureSessionScope(
+            final PreviewSurfaceScope previewSurfaceScope, CameraCaptureSessionProxy
+            cameraCaptureSession) {
+        ConcurrentBufferQueue<Long> globalTimestampStream = new ConcurrentBufferQueue<>();
+        // FIXME Wire this up to be closed when done.
+        CloseableHandlerThread imageDistributorThread = new CloseableHandlerThread
+                ("ImageDistributor");
+        ImageDistributor imageDistributor = provideImageDistributor(previewSurfaceScope
+                .imageReader, globalTimestampStream, imageDistributorThread.get());
+        final SharedImageReader sharedImageReader = provideSharedImageReader(previewSurfaceScope,
+                imageDistributor);
+        final FrameServer frameServer = new FrameServer(new TagDispatchCaptureSession
+                (cameraCaptureSession, previewSurfaceScope.captureSessionOpenHandler.get()));
+
+        ExecutorService miscThreadPool = Executors.newCachedThreadPool();
+
+        final CameraCommandExecutor commandExecutor = new CameraCommandExecutor(miscThreadPool);
+
+        SimpleCaptureStream previewSurfaceStream = new SimpleCaptureStream(
+                previewSurfaceScope.previewSurface);
+        RequestBuilder.Factory rootRequestBuilder = new DecoratingRequestBuilderBuilder(
+                new CameraDeviceRequestBuilderFactory(previewSurfaceScope.cameraScope.device))
+                .withResponseListener(new MetadataResponseListener<Integer>(CaptureResult
+                        .CONTROL_AF_STATE, new FilteredUpdatable<Integer>(previewSurfaceScope
+                                .cameraScope.afState)))
+                .withResponseListener(new TimestampResponseListener
+                        (globalTimestampStream));
+        ConcurrentState<MeteringParameters> meteringState = new ConcurrentState<>();
+        final Pollable<Rect> cropRegion = new PollableZoomedCropRegion
+                (previewSurfaceScope.cameraScope.characteristics,
+                        previewSurfaceScope.cameraScope.zoomState);
+        OrientationManager.DeviceOrientation sensorOrientation = new SensorOrientationProvider
+                (previewSurfaceScope.cameraScope
+                        .characteristics).getSensorOrientation();
+        final Pollable<MeteringRectangle[]> afRegionState = new PollableAFRegion(meteringState,
+                cropRegion, sensorOrientation);
+        final Pollable<MeteringRectangle[]> aeRegionState = new PollableAERegion(meteringState,
+                cropRegion, sensorOrientation);
+        // FIXME Use settings to retrieve current flash mode.
+        final Pollable<Integer> aeModeState = new PollableAEMode(new ConstantPollable<>(OneCamera
+                .PhotoCaptureParameters.Flash.OFF));
+        final RequestBuilder.Factory zoomedRequestBuilderFactory = new DecoratingRequestBuilderBuilder(
+                rootRequestBuilder)
+                .withParam(CaptureRequest.SCALER_CROP_REGION, cropRegion);
+        final RequestBuilder.Factory meteredZoomedRequestBuilderFactory = new
+                DecoratingRequestBuilderBuilder(zoomedRequestBuilderFactory)
+                        .withParam(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
+                        .withParam(CaptureRequest.CONTROL_AF_REGIONS, afRegionState)
+                        .withParam(CaptureRequest.CONTROL_AE_REGIONS, aeRegionState)
+                        .withParam(CaptureRequest.CONTROL_AE_MODE, aeModeState);
+        final RequestBuilder.Factory previewRequestBuilder =
+                new DecoratingRequestBuilderBuilder(meteredZoomedRequestBuilderFactory)
+                        .withStream(previewSurfaceStream);
+        // TODO Implement Manual Exposure: Decorate with current manual
+        // exposure level, or Auto.
+        final RequestBuilder.Factory continuousPreviewRequestBuilder =
+                new DecoratingRequestBuilderBuilder(previewRequestBuilder)
+                        .withParam(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
+                        .withParam(CaptureRequest.CONTROL_AF_MODE, CaptureRequest
+                                .CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+        int templateType = CameraDevice.TEMPLATE_PREVIEW;
+        PreviewCommand previewCommand = new PreviewCommand(frameServer,
+                continuousPreviewRequestBuilder, templateType);
+        final RunnableCameraCommand previewRunner = new RunnableCameraCommand(commandExecutor,
+                new LoggingCameraCommand(previewCommand, "preview"));
+        // Restart the preview whenever the zoom, ae regions, or af regions
+        // changes.
+        previewSurfaceScope.cameraScope.zoomState.addCallback(new CallbackRunnable(previewRunner)
+                , miscThreadPool);
+
+        CameraCommand afScanCommand = new LoggingCameraCommand(new FullAFScanCommand(frameServer,
+                previewRequestBuilder, templateType), "AF Scan");
+        // TODO Ensure that this is closed
+        ResettingDelayedExecutor afResetDelayedExecutor = new ResettingDelayedExecutor(Executors
+                .newSingleThreadScheduledExecutor(), 3L, TimeUnit.SECONDS);
+        AFScanHoldReset afScanHoldResetCommand = new AFScanHoldReset(afScanCommand,
+                afResetDelayedExecutor, previewRunner, meteringState);
+        final RunnableCameraCommand afRunner = new RunnableCameraCommand(commandExecutor,
+                afScanHoldResetCommand);
+
+        // TODO Implement Manual Exposure: Add a separate listener to the
+        // current exposure state to run previewRunner on each update.
+        // TODO Implement ready-state: Add a listener for shared-image-reader
+        // availability and frame-server availability, AND the results together
+        // and update the ready-state.
+        final Updatable<ImageProxy> imageSaver = new Updatable<ImageProxy>() {
+            @Override
+            public void update(ImageProxy imageProxy) {
+                // FIXME Replace stub with actual implementation.
+                imageProxy.close();
+            }
+        };
+
+        GenericOneCameraImpl.PictureTaker pictureTaker = new GenericOneCameraImpl.PictureTaker() {
+            @Override
+            public void takePicture(OneCamera.PhotoCaptureParameters params, CaptureSession session) {
+                RequestBuilder.Factory requestBuilderFactory = new
+                        DecoratingRequestBuilderBuilder(previewRequestBuilder);
+                Runnable pictureTakingRunnable = providePictureRunnable(params, session,
+                        commandExecutor, frameServer, requestBuilderFactory, sharedImageReader,
+                        imageSaver, provideMainHandlerExecutor(previewSurfaceScope.cameraScope));
+                pictureTakingRunnable.run();
+            }
+        };
+        GenericOneCameraImpl.ManualAutoFocus manualAutoFocus = new ManualAutoFocusImpl(
+                meteringState, afRunner);
+        return new CameraCaptureSessionScope(previewRunner, pictureTaker, manualAutoFocus);
+    }
+
+    private static Runnable providePictureRunnable(OneCamera.PhotoCaptureParameters params,
+            CaptureSession session, CameraCommandExecutor cameraCommandExecutor,
+            FrameServer frameServer, RequestBuilder.Factory requestBuilderFactory,
+            SharedImageReader sharedImageReader, Updatable<ImageProxy> imageSaver,
+            Executor mainExecutor) {
+        // TODO Add Flash support via PhotoCommand & FlashCommand
+        PictureCallbackAdaptor pictureCallbackAdaptor = new PictureCallbackAdaptor(params
+                .callback, mainExecutor);
+        Updatable<Void> imageExposureUpdatable = pictureCallbackAdaptor
+                .provideQuickExposeUpdatable();
+        CameraCommand photoCommand = new StaticPictureCommand(frameServer, requestBuilderFactory,
+                sharedImageReader, imageSaver, imageExposureUpdatable);
+        return new RunnableCameraCommand(cameraCommandExecutor, new LoggingCameraCommand
+                (photoCommand, "Static Picture"));
+    }
+
+    private static CameraCommand providePhotoCommand(FrameServer frameServer, RequestBuilder
+            .Factory builder, SharedImageReader imageReader, Updatable<ImageProxy> imageSaver,
+            Updatable<Void> imageExposeUpdatable) {
+        return new StaticPictureCommand(frameServer, builder, imageReader, imageSaver,
+                imageExposeUpdatable);
+    }
+
+    private static SharedImageReader provideSharedImageReader(PreviewSurfaceScope scope,
+            ImageDistributor imageDistributor) {
+        return new SharedImageReader(scope.imageReader.getSurface(),
+                scope.imageReader.getMaxImages() - 1, imageDistributor);
+    }
+
+    private static ImageDistributor provideImageDistributor(ImageReader imageReader,
+            BufferQueue<Long> globalTimestampStream,
+            Handler imageDistributorHandler) {
+        ImageDistributor imageDistributor = new ImageDistributor(globalTimestampStream);
+        imageReader.setOnImageAvailableListener(new ImageDistributorOnImageAvailableListener
+                (imageDistributor), imageDistributorHandler);
+        return imageDistributor;
+    }
+
+    private static FutureResult<CameraCaptureSessionProxy> provideCaptureSessionFuture(
+            final PreviewSurfaceScope scope) {
+        return new FutureResult<>(new Updatable<Future<CameraCaptureSessionProxy>>() {
+            @Override
+            public void update(Future<CameraCaptureSessionProxy> cameraCaptureSessionFuture) {
+                try {
+                    cameraCaptureSessionFuture.get();
+                    scope.cameraScope.previewStartSuccess.update(true);
+                } catch (InterruptedException | ExecutionException | CancellationException e) {
+                    scope.cameraScope.previewStartSuccess.update(false);
+                }
+            }
+        });
+    }
+
+    private static PreviewSurfaceScope providePreviewSurfaceScope(CameraScope cameraScope,
+            Surface previewSurface) {
+        CloseableHandlerThread captureSessionOpenHandler = new CloseableHandlerThread
+                ("CaptureSessionStateHandler");
+        ImageReader imageReader = provideImageReader(cameraScope.pictureSize);
+        return new PreviewSurfaceScope(cameraScope, previewSurface, imageReader,
+                captureSessionOpenHandler);
+    }
+
+    private static ImageReader provideImageReader(Size size) {
+        return ImageReader.newInstance(size.getWidth(), size.getHeight(), ImageFormat.JPEG,
+                provideImageReaderSize());
+    }
+
+    private static int provideImageReaderSize() {
+        return 10;
+    }
+
+    private static SupportedPreviewSizeProvider provideSupportedPreviewSizesProvider(
+            CameraScope scope) {
+        return new SupportedPreviewSizeProvider(scope.characteristics);
+    }
+
+    private static Size[] provideSupportedPreviewSizes(CameraScope scope) {
+        return provideSupportedPreviewSizesProvider(scope).getSupportedPreviewSizes();
+    }
+
+    private static FullSizeAspectRatioProvider provideFullSizeAspectRatioProvider(CameraScope scope) {
+        return new FullSizeAspectRatioProvider(scope.characteristics);
+    }
+
+    private static float provideFullSizeAspectRatio(CameraScope scope) {
+        return provideFullSizeAspectRatioProvider(scope).get();
+    }
+
+    private static OneCamera.Facing provideDirection(CameraScope scope) {
+        return new CameraDirectionProvider(scope.characteristics).getDirection();
+    }
+
+    private static GenericOneCameraImpl.ManualAutoFocus provideManualAutoFocus(CameraScope scope) {
+        return new DeferredManualAutoFocus(scope.manualAutoFocus);
+    }
+
+    private static GenericOneCameraImpl.PictureTaker providePictureTaker(CameraScope scope) {
+        return new DeferredPictureTaker(scope.pictureTaker);
+    }
+
+    private static Listenable<Boolean> providePreviewStartSuccessListenable(CameraScope scope) {
+        return new ListenableConcurrentState<Boolean>(scope.previewStartSuccess,
+                provideMainHandlerExecutor(scope));
+    }
+
+    private static PreviewSizeSelector providePreviewSizeSelector(CameraScope scope) {
+        return new PreviewSizeSelector(provideImageFormat(), provideSupportedPreviewSizes(scope));
+    }
+
+    private static int provideImageFormat() {
+        return ImageFormat.JPEG;
+    }
+
+    private static OneCamera provideOneCamera(CameraScope scope) {
+        Set<SafeCloseable> closeListeners = provideCloseListeners(scope);
+        GenericOneCameraImpl.PictureTaker pictureTaker = providePictureTaker(scope);
+        GenericOneCameraImpl.ManualAutoFocus manualAutoFocus = provideManualAutoFocus(scope);
+        Listenable<Integer> afStateListenable = provideAFStateListenable(scope);
+        Listenable<Boolean> readyStateListenable = provideReadyStateListenable(scope);
+        float maxZoom = provideMaxZoom(scope.characteristics);
+        Updatable<Float> zoom = provideZoom(scope);
+        ScopedFactory<Surface, Runnable> surfaceRunnableScopedFactory = providePreviewStarter(scope);
+        Size[] supportedPreviewSizes = provideSupportedPreviewSizes(scope);
+        float fullSizeAspectRatio = provideFullSizeAspectRatio(scope);
+        OneCamera.Facing direction = provideDirection(scope);
+        PreviewSizeSelector previewSizeSelector = providePreviewSizeSelector(scope);
+        Listenable<Boolean> previewStartSuccessListenable = providePreviewStartSuccessListenable(scope);
+
+        return new GenericOneCameraImpl(closeListeners, pictureTaker, manualAutoFocus,
+                afStateListenable, readyStateListenable, maxZoom, zoom,
+                supportedPreviewSizes, fullSizeAspectRatio, direction, previewSizeSelector,
+                previewStartSuccessListenable, surfaceRunnableScopedFactory);
+    }
+
+    public static OneCamera provideOneCamera(CameraDevice device, CameraCharacteristics
+            characteristics, Handler mainHandler, Size pictureSize) {
+        return provideOneCamera(provideCameraScope(device, characteristics, mainHandler,
+                pictureSize));
+    }
+}
diff --git a/src/com/android/camera/one/v2/camera2proxy/AndroidCameraCaptureSessionProxy.java b/src/com/android/camera/one/v2/camera2proxy/AndroidCameraCaptureSessionProxy.java
new file mode 100644
index 0000000..20f1ec2
--- /dev/null
+++ b/src/com/android/camera/one/v2/camera2proxy/AndroidCameraCaptureSessionProxy.java
@@ -0,0 +1,126 @@
+/*
+ * 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 com.android.camera.one.v2.camera2proxy;
+
+import java.util.List;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Handler;
+
+public class AndroidCameraCaptureSessionProxy implements CameraCaptureSessionProxy {
+    private class AndroidCaptureCallback extends CameraCaptureSession.CaptureCallback {
+        private final CaptureCallback mCallback;
+
+        private AndroidCaptureCallback(CaptureCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request,
+                long timestamp, long frameNumber) {
+            mCallback.onCaptureStarted(AndroidCameraCaptureSessionProxy.this, request, timestamp,
+                    frameNumber);
+        }
+
+        @Override
+        public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request,
+                CaptureResult partialResult) {
+            mCallback.onCaptureProgressed(AndroidCameraCaptureSessionProxy.this, request,
+                    partialResult);
+        }
+
+        @Override
+        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
+                TotalCaptureResult result) {
+            mCallback.onCaptureCompleted(AndroidCameraCaptureSessionProxy.this, request, result);
+        }
+
+        @Override
+        public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request,
+                CaptureFailure failure) {
+            mCallback.onCaptureFailed(AndroidCameraCaptureSessionProxy.this, request, failure);
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId,
+                long frameNumber) {
+            mCallback.onCaptureSequenceCompleted(AndroidCameraCaptureSessionProxy.this,
+                    sequenceId, frameNumber);
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+            mCallback.onCaptureSequenceAborted(AndroidCameraCaptureSessionProxy.this, sequenceId);
+        }
+    }
+
+    private final CameraCaptureSession mSession;
+
+    public AndroidCameraCaptureSessionProxy(CameraCaptureSession session) {
+        mSession = session;
+    }
+
+    @Override
+    public void abortCaptures() throws CameraAccessException {
+        mSession.abortCaptures();
+    }
+
+    @Override
+    public int capture(CaptureRequest request, CaptureCallback listener, Handler handler)
+            throws CameraAccessException {
+        return mSession.capture(request, new AndroidCaptureCallback(listener), handler);
+    }
+
+    @Override
+    public int captureBurst(List<CaptureRequest> requests, CaptureCallback listener, Handler handler)
+            throws CameraAccessException {
+        return mSession.captureBurst(requests, new AndroidCaptureCallback(listener), handler);
+    }
+
+    @Override
+    public void close() {
+        mSession.close();
+    }
+
+    @Override
+    public CameraDeviceProxy getDevice() {
+        return new CameraDeviceProxy(mSession.getDevice());
+    }
+
+    @Override
+    public int setRepeatingBurst(List<CaptureRequest> requests, CaptureCallback listener,
+            Handler handler) throws CameraAccessException {
+        return mSession.setRepeatingBurst(requests, new AndroidCaptureCallback(listener), handler);
+    }
+
+    @Override
+    public int setRepeatingRequest(CaptureRequest request, CaptureCallback listener, Handler handler)
+            throws CameraAccessException {
+        return mSession.setRepeatingRequest(request, new AndroidCaptureCallback(listener),
+                handler);
+    }
+
+    @Override
+    public void stopRepeating() throws CameraAccessException {
+        mSession.stopRepeating();
+    }
+}
diff --git a/src/com/android/camera/one/v2/camera2proxy/AndroidImageProxy.java b/src/com/android/camera/one/v2/camera2proxy/AndroidImageProxy.java
new file mode 100644
index 0000000..80838c0
--- /dev/null
+++ b/src/com/android/camera/one/v2/camera2proxy/AndroidImageProxy.java
@@ -0,0 +1,94 @@
+/*
+ * 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 com.android.camera.one.v2.camera2proxy;
+
+import android.graphics.Rect;
+
+/**
+ * An {@link ImageProxy} backed by an {@link android.media.Image}.
+ */
+public class AndroidImageProxy implements ImageProxy {
+    private final android.media.Image mImage;
+
+    public AndroidImageProxy(android.media.Image image) {
+        mImage = image;
+    }
+
+    /**
+     * @see {@link android.media.Image#getCropRect}
+     */
+    @Override
+    public Rect getCropRect() {
+        return mImage.getCropRect();
+    }
+
+    /**
+     * @see {@link android.media.Image#setCropRect}
+     */
+    @Override
+    public void setCropRect(Rect cropRect) {
+        mImage.setCropRect(cropRect);
+    }
+
+    /**
+     * @see {@link android.media.Image#getFormat}
+     */
+    @Override
+    public int getFormat() {
+        return mImage.getFormat();
+    }
+
+    /**
+     * @see {@link android.media.Image#getHeight}
+     */
+    @Override
+    public int getHeight() {
+        return mImage.getHeight();
+    }
+
+    /**
+     * @see {@link android.media.Image#getPlanes}
+     */
+    @Override
+    public android.media.Image.Plane[] getPlanes() {
+        return mImage.getPlanes();
+    }
+
+    /**
+     * @see {@link android.media.Image#getTimestamp}
+     */
+    @Override
+    public long getTimestamp() {
+        return mImage.getTimestamp();
+    }
+
+    /**
+     * @see {@link android.media.Image#getWidth}
+     */
+    @Override
+    public int getWidth() {
+        return mImage.getWidth();
+    }
+
+    /**
+     * @see {@link android.media.Image#close}
+     */
+    @Override
+    public void close() {
+        mImage.close();
+    }
+}
diff --git a/src/com/android/camera/one/v2/components/ImageSaver.java b/src/com/android/camera/one/v2/camera2proxy/CameraCaptureSessionClosedException.java
similarity index 66%
copy from src/com/android/camera/one/v2/components/ImageSaver.java
copy to src/com/android/camera/one/v2/camera2proxy/CameraCaptureSessionClosedException.java
index 7138029..0afab3a 100644
--- a/src/com/android/camera/one/v2/components/ImageSaver.java
+++ b/src/com/android/camera/one/v2/camera2proxy/CameraCaptureSessionClosedException.java
@@ -14,16 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.camera.one.v2.components;
-
-import com.android.camera.one.v2.camera2proxy.ImageProxy;
+package com.android.camera.one.v2.camera2proxy;
 
 /**
- * Interface for an image-saving object.
+ * A checked-exception to replace the {@link IllegalStateException} thrown by
+ * camera2 API calls when the associated
+ * {@link android.hardware.camera2.CameraCaptureSession} is closed.
  */
-public interface ImageSaver {
-    /**
-     * Implementations should save the image to disk and close it.
-     */
-    public void saveAndCloseImage(ImageProxy image);
+public class CameraCaptureSessionClosedException extends Exception {
 }
diff --git a/src/com/android/camera/one/v2/camera2proxy/CameraCaptureSessionProxy.java b/src/com/android/camera/one/v2/camera2proxy/CameraCaptureSessionProxy.java
new file mode 100644
index 0000000..423391e
--- /dev/null
+++ b/src/com/android/camera/one/v2/camera2proxy/CameraCaptureSessionProxy.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.android.camera.one.v2.camera2proxy;
+
+import java.util.List;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Handler;
+
+import com.android.camera.async.SafeCloseable;
+
+/**
+ * Interface for {@link android.hardware.camera2.CameraCaptureSession}.
+ * <p>
+ * Note that this also enables translation of IllegalStateException (an
+ * unchecked exception) resulting from the underlying session being closed into
+ * a checked exception, forcing callers to explicitly handle this edge case.
+ * </p>
+ */
+public interface CameraCaptureSessionProxy extends SafeCloseable {
+    public interface CaptureCallback {
+        public void onCaptureCompleted(CameraCaptureSessionProxy session, CaptureRequest request,
+                TotalCaptureResult result);
+
+        public void onCaptureFailed(CameraCaptureSessionProxy session, CaptureRequest request,
+                CaptureFailure failure);
+
+        public void onCaptureProgressed(CameraCaptureSessionProxy session, CaptureRequest request,
+                CaptureResult partialResult);
+
+        public void onCaptureSequenceAborted(CameraCaptureSessionProxy session, int sequenceId);
+
+        public void onCaptureSequenceCompleted(CameraCaptureSessionProxy session, int sequenceId,
+                long frameNumber);
+
+        public void onCaptureStarted(CameraCaptureSessionProxy session, CaptureRequest request,
+                long timestamp, long frameNumber);
+    }
+
+    public interface StateCallback {
+        public void onActive(CameraCaptureSessionProxy session);
+
+        public void onClosed(CameraCaptureSessionProxy session);
+
+        public void onConfigureFailed(CameraCaptureSessionProxy session);
+
+        public void onConfigured(CameraCaptureSessionProxy session);
+
+        public void onReady(CameraCaptureSessionProxy session);
+    }
+
+    public void abortCaptures() throws CameraAccessException;
+
+    public int capture(CaptureRequest request, CaptureCallback listener, Handler handler)
+            throws CameraAccessException, CameraCaptureSessionClosedException;
+
+    public int captureBurst(List<CaptureRequest> requests, CaptureCallback listener, Handler
+            handler) throws CameraAccessException, CameraCaptureSessionClosedException;
+
+    @Override
+    public void close();
+
+    public CameraDeviceProxy getDevice();
+
+    public int setRepeatingBurst(List<CaptureRequest> requests, CaptureCallback listener, Handler
+            handler) throws CameraAccessException, CameraCaptureSessionClosedException;
+
+    public int setRepeatingRequest(CaptureRequest request, CaptureCallback listener, Handler
+            handler) throws CameraAccessException, CameraCaptureSessionClosedException;
+
+    public void stopRepeating() throws CameraAccessException, CameraCaptureSessionClosedException;
+}
diff --git a/src/com/android/camera/one/v2/camera2proxy/CameraDeviceProxy.java b/src/com/android/camera/one/v2/camera2proxy/CameraDeviceProxy.java
new file mode 100644
index 0000000..916e205
--- /dev/null
+++ b/src/com/android/camera/one/v2/camera2proxy/CameraDeviceProxy.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.camera.one.v2.camera2proxy;
+
+import java.util.List;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.os.Handler;
+import android.view.Surface;
+
+public class CameraDeviceProxy {
+    private static class AndroidCaptureSessionStateCallback extends
+            CameraCaptureSession.StateCallback {
+        private final CameraCaptureSessionProxy.StateCallback mStateCallback;
+
+        private AndroidCaptureSessionStateCallback(
+                CameraCaptureSessionProxy.StateCallback stateCallback) {
+            mStateCallback = stateCallback;
+        }
+
+        public void onConfigured(CameraCaptureSession session) {
+            mStateCallback.onConfigured(new AndroidCameraCaptureSessionProxy(session));
+        }
+
+        public void onConfigureFailed(CameraCaptureSession session) {
+            mStateCallback.onConfigureFailed(new AndroidCameraCaptureSessionProxy(session));
+        }
+
+        public void onReady(CameraCaptureSession session) {
+            mStateCallback.onReady(new AndroidCameraCaptureSessionProxy(session));
+        }
+
+        public void onActive(CameraCaptureSession session) {
+            mStateCallback.onActive(new AndroidCameraCaptureSessionProxy(session));
+        }
+
+        public void onClosed(CameraCaptureSession session) {
+            mStateCallback.onClosed(new AndroidCameraCaptureSessionProxy(session));
+        }
+    }
+
+    private final CameraDevice mCameraDevice;
+
+    public CameraDeviceProxy(CameraDevice cameraDevice) {
+        mCameraDevice = cameraDevice;
+    }
+
+    public String getId() {
+        return mCameraDevice.getId();
+    }
+
+    public void createCaptureSession(List<Surface> list,
+            CameraCaptureSessionProxy.StateCallback stateCallback, Handler handler)
+            throws CameraAccessException {
+        mCameraDevice.createCaptureSession(list, new AndroidCaptureSessionStateCallback(
+                stateCallback), handler);
+    }
+
+    public CaptureRequest.Builder createCaptureRequest(int i) throws CameraAccessException {
+        return mCameraDevice.createCaptureRequest(i);
+    }
+
+    public void close() {
+        mCameraDevice.close();
+    }
+}
diff --git a/src/com/android/camera/one/v2/camera2proxy/ForwardingImageProxy.java b/src/com/android/camera/one/v2/camera2proxy/ForwardingImageProxy.java
new file mode 100644
index 0000000..ba90083
--- /dev/null
+++ b/src/com/android/camera/one/v2/camera2proxy/ForwardingImageProxy.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.camera.one.v2.camera2proxy;
+
+import android.graphics.Rect;
+
+import com.android.camera.async.SafeCloseable;
+
+/**
+ * Forwards all {@link ImageProxy} methods.
+ */
+public class ForwardingImageProxy implements ImageProxy {
+    private final ImageProxy mImpl;
+
+    public ForwardingImageProxy(ImageProxy proxy) {
+        mImpl = proxy;
+    }
+
+    /**
+     * @see {@link android.media.Image#getCropRect}
+     */
+    public Rect getCropRect() {
+        return mImpl.getCropRect();
+    }
+
+    /**
+     * @see {@link android.media.Image#setCropRect}
+     */
+    public void setCropRect(Rect cropRect) {
+        mImpl.setCropRect(cropRect);
+    }
+
+    /**
+     * @see {@link android.media.Image#getFormat}
+     */
+    public int getFormat() {
+        return mImpl.getFormat();
+    }
+
+    /**
+     * @see {@link android.media.Image#getHeight}
+     */
+    public int getHeight() {
+        return mImpl.getHeight();
+    }
+
+    /**
+     * @see {@link android.media.Image#getPlanes}
+     */
+    public android.media.Image.Plane[] getPlanes() {
+        return mImpl.getPlanes();
+    }
+
+    /**
+     * @see {@link android.media.Image#getTimestamp}
+     */
+    public long getTimestamp() {
+        return mImpl.getTimestamp();
+    }
+
+    /**
+     * @see {@link android.media.Image#getWidth}
+     */
+    public int getWidth() {
+        return mImpl.getWidth();
+    }
+
+    /**
+     * @see {@link android.media.Image#close}
+     */
+    @Override
+    public void close() {
+        mImpl.close();
+    }
+}
diff --git a/src/com/android/camera/one/v2/camera2proxy/ImageProxy.java b/src/com/android/camera/one/v2/camera2proxy/ImageProxy.java
index ce90bcd..6c79061 100644
--- a/src/com/android/camera/one/v2/camera2proxy/ImageProxy.java
+++ b/src/com/android/camera/one/v2/camera2proxy/ImageProxy.java
@@ -23,147 +23,45 @@
 /**
  * Wraps {@link android.media.Image} with a mockable interface.
  */
-public class ImageProxy implements SafeCloseable {
-    private final ImageProxy mImpl;
-
-    public ImageProxy(ImageProxy proxy) {
-        mImpl = proxy;
-    }
-
-    public ImageProxy(android.media.Image image) {
-        mImpl = new Impl(image);
-    }
-
-    private ImageProxy() {
-        mImpl = null;
-    }
-
+public interface ImageProxy extends SafeCloseable {
     /**
      * @see {@link android.media.Image#getCropRect}
      */
-    public Rect getCropRect() {
-        return mImpl.getCropRect();
-    }
+    public Rect getCropRect();
 
     /**
      * @see {@link android.media.Image#setCropRect}
      */
-    public void setCropRect(Rect cropRect) {
-        mImpl.setCropRect(cropRect);
-    }
+    public void setCropRect(Rect cropRect);
 
     /**
      * @see {@link android.media.Image#getFormat}
      */
-    public int getFormat() {
-        return mImpl.getFormat();
-    }
+    public int getFormat();
 
     /**
      * @see {@link android.media.Image#getHeight}
      */
-    public int getHeight() {
-        return mImpl.getHeight();
-    }
+    public int getHeight();
 
     /**
      * @see {@link android.media.Image#getPlanes}
      */
-    public android.media.Image.Plane[] getPlanes() {
-        return mImpl.getPlanes();
-    }
+    public android.media.Image.Plane[] getPlanes();
 
     /**
      * @see {@link android.media.Image#getTimestamp}
      */
-    public long getTimestamp() {
-        return mImpl.getTimestamp();
-    }
+    public long getTimestamp();
 
     /**
      * @see {@link android.media.Image#getWidth}
      */
-    public int getWidth() {
-        return mImpl.getWidth();
-    }
+    public int getWidth();
 
     /**
      * @see {@link android.media.Image#close}
      */
     @Override
-    public void close() {
-        mImpl.close();
-    }
-
-    private static class Impl extends ImageProxy {
-        private final android.media.Image mImage;
-
-        public Impl(android.media.Image image) {
-            mImage = image;
-        }
-
-        /**
-         * @see {@link android.media.Image#getCropRect}
-         */
-        @Override
-        public Rect getCropRect() {
-            return mImage.getCropRect();
-        }
-
-        /**
-         * @see {@link android.media.Image#setCropRect}
-         */
-        @Override
-        public void setCropRect(Rect cropRect) {
-            mImage.setCropRect(cropRect);
-        }
-
-        /**
-         * @see {@link android.media.Image#getFormat}
-         */
-        @Override
-        public int getFormat() {
-            return mImage.getFormat();
-        }
-
-        /**
-         * @see {@link android.media.Image#getHeight}
-         */
-        @Override
-        public int getHeight() {
-            return mImage.getHeight();
-        }
-
-        /**
-         * @see {@link android.media.Image#getPlanes}
-         */
-        @Override
-        public android.media.Image.Plane[] getPlanes() {
-            return mImage.getPlanes();
-        }
-
-        /**
-         * @see {@link android.media.Image#getTimestamp}
-         */
-        @Override
-        public long getTimestamp() {
-            return mImage.getTimestamp();
-        }
-
-        /**
-         * @see {@link android.media.Image#getWidth}
-         */
-        @Override
-        public int getWidth() {
-            return mImage.getWidth();
-        }
-
-        /**
-         * @see {@link android.media.Image#close}
-         */
-        @Override
-        public void close() {
-            mImage.close();
-        }
-    }
+    public void close();
 }
diff --git a/src/com/android/camera/one/v2/commands/AFScanHoldReset.java b/src/com/android/camera/one/v2/commands/AFScanHoldReset.java
new file mode 100644
index 0000000..0578945
--- /dev/null
+++ b/src/com/android/camera/one/v2/commands/AFScanHoldReset.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.android.camera.one.v2.commands;
+
+import android.graphics.PointF;
+import android.hardware.camera2.CameraAccessException;
+
+import com.android.camera.async.ResettingDelayedExecutor;
+import com.android.camera.async.Updatable;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+import com.android.camera.one.v2.common.MeteringParameters;
+
+/**
+ * Performs a full Auto Focus scan, holds for a period of time, and then resets
+ * the preview.
+ */
+public class AFScanHoldReset implements CameraCommand {
+    private final CameraCommand mAFScanCommand;
+    private final ResettingDelayedExecutor mDelayedExecutor;
+    private final Runnable mPreviewRunnable;
+    private final Updatable<MeteringParameters> mMeteringParametersUpdatable;
+
+    public AFScanHoldReset(CameraCommand afScanCommand, ResettingDelayedExecutor delayedExecutor,
+            Runnable previewRunnable, Updatable<MeteringParameters> meteringParametersUpdatable) {
+        mAFScanCommand = afScanCommand;
+        mDelayedExecutor = delayedExecutor;
+        mPreviewRunnable = previewRunnable;
+        mMeteringParametersUpdatable = meteringParametersUpdatable;
+    }
+
+    @Override
+    public void run() throws CameraAccessException, InterruptedException,
+            CameraCaptureSessionClosedException {
+        mAFScanCommand.run();
+        mDelayedExecutor.execute(new Runnable() {
+            public void run() {
+                // Reset metering regions and restart the preview
+                mMeteringParametersUpdatable.update(new MeteringParameters(new PointF(), new
+                        PointF(), MeteringParameters.Mode.GLOBAL));
+                mPreviewRunnable.run();
+            }
+        });
+    }
+}
diff --git a/src/com/android/camera/one/v2/components/CameraCommand.java b/src/com/android/camera/one/v2/commands/CameraCommand.java
similarity index 69%
rename from src/com/android/camera/one/v2/components/CameraCommand.java
rename to src/com/android/camera/one/v2/commands/CameraCommand.java
index de13899..652b456 100644
--- a/src/com/android/camera/one/v2/components/CameraCommand.java
+++ b/src/com/android/camera/one/v2/commands/CameraCommand.java
@@ -13,14 +13,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.camera.one.v2.components;
+
+package com.android.camera.one.v2.commands;
 
 import android.hardware.camera2.CameraAccessException;
 
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+
 /**
  * A generic camera command which may be interrupted and may throw
  * {@link android.hardware.camera2.CameraAccessException}.
  */
 public interface CameraCommand {
-    public void run() throws InterruptedException, CameraAccessException;
+    /**
+     * @throws InterruptedException If interrupted while executing the command.
+     * @throws CameraAccessException If the camera is not available when
+     *             accessed by the command.
+     */
+    public void run() throws InterruptedException, CameraAccessException,
+            CameraCaptureSessionClosedException;
 }
diff --git a/src/com/android/camera/one/v2/commands/CameraCommandExecutor.java b/src/com/android/camera/one/v2/commands/CameraCommandExecutor.java
new file mode 100644
index 0000000..0358f1e
--- /dev/null
+++ b/src/com/android/camera/one/v2/commands/CameraCommandExecutor.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.camera.one.v2.commands;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
+
+import android.hardware.camera2.CameraAccessException;
+
+import com.android.camera.async.SafeCloseable;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionProxy;
+
+/**
+ * Executes camera commands on a thread pool.
+ */
+public class CameraCommandExecutor implements SafeCloseable {
+    private class CommandRunnable implements Runnable {
+        private final CameraCommand mCommand;
+
+        public CommandRunnable(CameraCommand command) {
+            mCommand = command;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mCommand.run();
+            } catch (InterruptedException e) {
+                // If interrupted, just return.
+            } catch (CameraAccessException e) {
+                // If the camera was closed and the command failed, just return.
+            } catch (CameraCaptureSessionClosedException e) {
+                // If the session was closed and the command failed, just return.
+            }
+        }
+    }
+
+    private final ExecutorService mExecutor;
+
+    public CameraCommandExecutor(ExecutorService threadPoolExecutor) {
+        mExecutor = threadPoolExecutor;
+    }
+
+    public void execute(CameraCommand command) {
+        try {
+            mExecutor.submit(new CommandRunnable(command));
+        } catch (RejectedExecutionException e) {
+            // If the executor is shut down, the command will not be executed.
+            // So, we can ignore this exception.
+        }
+    }
+
+    @Override
+    public void close() {
+        mExecutor.shutdownNow();
+    }
+}
diff --git a/src/com/android/camera/one/v2/commands/FlashBasedPhotoCommand.java b/src/com/android/camera/one/v2/commands/FlashBasedPhotoCommand.java
new file mode 100644
index 0000000..268e760
--- /dev/null
+++ b/src/com/android/camera/one/v2/commands/FlashBasedPhotoCommand.java
@@ -0,0 +1,66 @@
+/*
+ * 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 com.android.camera.one.v2.commands;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CaptureResult;
+
+import com.android.camera.async.Pollable;
+import com.android.camera.one.OneCamera.PhotoCaptureParameters;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+
+/**
+ * Combines a flash-enabled command and a non-flash command into a single
+ * command which fires depending on the current flash setting and AE metadata.
+ */
+public class FlashBasedPhotoCommand implements CameraCommand {
+    private final CameraCommand mFlashPhotoCommand;
+    private final CameraCommand mNoFlashPhotoCommand;
+    private final Pollable<Integer> mAEState;
+    private final PhotoCaptureParameters.Flash mFlashState;
+
+    public FlashBasedPhotoCommand(
+            CameraCommand flashPhotoCommand,
+            CameraCommand noFlashPhotoCommand,
+            Pollable<Integer> aeState,
+            PhotoCaptureParameters.Flash flashState) {
+        mFlashPhotoCommand = flashPhotoCommand;
+        mNoFlashPhotoCommand = noFlashPhotoCommand;
+        mAEState = aeState;
+        mFlashState = flashState;
+    }
+
+    @Override
+    public void run() throws InterruptedException, CameraAccessException,
+            CameraCaptureSessionClosedException {
+        boolean needsFlash = false;
+        if (mFlashState == PhotoCaptureParameters.Flash.ON) {
+            needsFlash = true;
+        } else {
+            Integer mostRecentAEState = mAEState.get(CaptureResult.CONTROL_AE_STATE_INACTIVE);
+            if (mostRecentAEState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) {
+                needsFlash = true;
+            }
+        }
+
+        if (needsFlash) {
+            mFlashPhotoCommand.run();
+        } else {
+            mNoFlashPhotoCommand.run();
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/commands/FullAFScanCommand.java b/src/com/android/camera/one/v2/commands/FullAFScanCommand.java
new file mode 100644
index 0000000..76ffbf9
--- /dev/null
+++ b/src/com/android/camera/one/v2/commands/FullAFScanCommand.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 com.android.camera.one.v2.commands;
+
+import java.util.Arrays;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CaptureRequest;
+
+import com.android.camera.async.UpdatableCountDownLatch;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionProxy;
+import com.android.camera.one.v2.core.FrameServer;
+import com.android.camera.one.v2.core.RequestBuilder;
+import com.android.camera.one.v2.core.TriggeredAFScanStateResponseListener;
+
+/**
+ * Performs a full auto focus scan.
+ */
+public class FullAFScanCommand implements CameraCommand {
+    private final FrameServer mFrameServer;
+    private final RequestBuilder.Factory mBuilderFactory;
+    private final int mTemplateType;
+
+    /**
+     * @param frameServer Used for sending requests to the camera.
+     * @param builder Used for building requests.
+     * @param templateType See
+     *            {@link android.hardware.camera2.CameraDevice#createCaptureRequest}
+     */
+    public FullAFScanCommand(FrameServer frameServer, RequestBuilder.Factory builder, int
+            templateType) {
+        mFrameServer = frameServer;
+        mBuilderFactory = builder;
+        mTemplateType = templateType;
+    }
+
+    /**
+     * Performs an auto-focus scan, blocking until the scan starts, runs, and
+     * completes.
+     */
+    public void run() throws InterruptedException, CameraAccessException,
+            CameraCaptureSessionClosedException {
+        FrameServer.Session session = mFrameServer.tryCreateExclusiveSession();
+        if (session == null) {
+            // If there are already other commands interacting with the
+            // FrameServer, don't wait to run the AF command, instead just
+            // abort.
+            return;
+        }
+        try {
+            UpdatableCountDownLatch scanDoneLatch = new UpdatableCountDownLatch(1);
+            TriggeredAFScanStateResponseListener afScanStateListener = new TriggeredAFScanStateResponseListener(
+                    scanDoneLatch);
+
+            // Build a request to send a repeating AF_IDLE
+            RequestBuilder idleBuilder = mBuilderFactory.create(mTemplateType);
+            idleBuilder.addResponseListener(afScanStateListener);
+            idleBuilder.setParam(CaptureRequest.CONTROL_MODE, CaptureRequest
+                    .CONTROL_MODE_AUTO);
+            idleBuilder.setParam(CaptureRequest.CONTROL_AF_MODE, CaptureRequest
+                    .CONTROL_AF_MODE_AUTO);
+            idleBuilder.setParam(CaptureRequest.CONTROL_AF_TRIGGER,
+                    CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
+
+            // Build a request to send a single AF_TRIGGER
+            RequestBuilder triggerBuilder = mBuilderFactory.create(mTemplateType);
+            triggerBuilder.addResponseListener(afScanStateListener);
+            triggerBuilder.setParam(CaptureRequest.CONTROL_MODE, CaptureRequest
+                    .CONTROL_MODE_AUTO);
+            triggerBuilder.setParam(CaptureRequest.CONTROL_AF_MODE, CaptureRequest
+                    .CONTROL_AF_MODE_AUTO);
+            triggerBuilder.setParam(CaptureRequest.CONTROL_AF_TRIGGER,
+                    CaptureRequest.CONTROL_AF_TRIGGER_START);
+
+            session.submitRequest(Arrays.asList(idleBuilder.build()),
+                    FrameServer.RequestType.REPEATING);
+            session.submitRequest(Arrays.asList(triggerBuilder.build()),
+                    FrameServer.RequestType.NON_REPEATING);
+
+            // Block until the scan is done.
+            // TODO If the HAL never transitions out of scanning mode, this will
+            // block forever (or until interrupted because the app is paused).
+            // So, maybe use a generous timeout and log as HAL errors.
+            scanDoneLatch.await();
+        } finally {
+            session.close();
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/commands/LoggingCameraCommand.java b/src/com/android/camera/one/v2/commands/LoggingCameraCommand.java
new file mode 100644
index 0000000..48125dc
--- /dev/null
+++ b/src/com/android/camera/one/v2/commands/LoggingCameraCommand.java
@@ -0,0 +1,50 @@
+/*
+ * 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 com.android.camera.one.v2.commands;
+
+import android.hardware.camera2.CameraAccessException;
+
+import com.android.camera.debug.Log;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+
+/**
+ * Wraps a {@link CameraCommand} with logging.
+ */
+public class LoggingCameraCommand implements CameraCommand {
+    private static final Log.Tag TAG = new Log.Tag("CameraCommand");
+    private final CameraCommand mCommand;
+    private final String mName;
+
+    public LoggingCameraCommand(CameraCommand command, String name) {
+        mCommand = command;
+        mName = name;
+    }
+
+    @Override
+    public void run() throws InterruptedException, CameraAccessException,
+            CameraCaptureSessionClosedException {
+        Log.v(TAG, String.format("Executing Command: %s: START", mName));
+        try {
+            mCommand.run();
+        } catch (Exception e) {
+            Log.e(TAG, String.format("Executing Command: %s: Exception: ", mName));
+            e.printStackTrace();
+            throw e;
+        }
+        Log.v(TAG, String.format("Executing Command: %s: END", mName));
+    }
+}
diff --git a/src/com/android/camera/one/v2/components/PreviewCommand.java b/src/com/android/camera/one/v2/commands/PreviewCommand.java
similarity index 75%
rename from src/com/android/camera/one/v2/components/PreviewCommand.java
rename to src/com/android/camera/one/v2/commands/PreviewCommand.java
index 392d217..83f185a 100644
--- a/src/com/android/camera/one/v2/components/PreviewCommand.java
+++ b/src/com/android/camera/one/v2/commands/PreviewCommand.java
@@ -14,17 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.camera.one.v2.components;
+package com.android.camera.one.v2.commands;
 
 import java.util.Arrays;
 
 import android.hardware.camera2.CameraAccessException;
 
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
 import com.android.camera.one.v2.core.FrameServer;
 import com.android.camera.one.v2.core.RequestBuilder;
 
 /**
- * Sends preview requests to a {@link FrameServer}.
+ * Sends repeating preview requests to a {@link FrameServer}.
  */
 public class PreviewCommand implements CameraCommand {
     private final FrameServer mFrameServer;
@@ -32,8 +33,8 @@
     private final int mRequestType;
 
     /**
-     * Constructs a Preview. Note that it is the {@link RequestBuilder.Factory}
-     * 's responsibility to attach the relevant
+     * Constructs a Preview. Note that it is the responsiblity of the
+     * {@link RequestBuilder.Factory} to attach the relevant
      * {@link com.android.camera.one.v2.core.CaptureStream}s, such as the
      * viewfinder surface.
      */
@@ -44,13 +45,12 @@
         mRequestType = requestType;
     }
 
-    public void run() throws InterruptedException, CameraAccessException {
-        FrameServer.Session session = mFrameServer.createSession();
-        try {
+    public void run() throws InterruptedException, CameraAccessException,
+            CameraCaptureSessionClosedException {
+        try (FrameServer.Session session = mFrameServer.createSession()) {
             RequestBuilder photoRequest = mBuilderFactory.create(mRequestType);
-            session.submitRequest(Arrays.asList(photoRequest.build()), true);
-        } finally {
-            session.close();
+            session.submitRequest(Arrays.asList(photoRequest.build()),
+                    FrameServer.RequestType.REPEATING);
         }
     }
 }
diff --git a/src/com/android/camera/one/v2/commands/RunnableCameraCommand.java b/src/com/android/camera/one/v2/commands/RunnableCameraCommand.java
new file mode 100644
index 0000000..902bf07
--- /dev/null
+++ b/src/com/android/camera/one/v2/commands/RunnableCameraCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.android.camera.one.v2.commands;
+
+/**
+ * Provides a convenient {@link Runnable} for executing a {@link CameraCommand}
+ * on a predetermined {@link CameraCommandExecutor}.
+ */
+public class RunnableCameraCommand implements Runnable {
+    private final CameraCommandExecutor mExecutor;
+    private final CameraCommand mCommand;
+
+    public RunnableCameraCommand(CameraCommandExecutor executor, CameraCommand command) {
+        mExecutor = executor;
+        mCommand = command;
+    }
+
+    @Override
+    public void run() {
+        mExecutor.execute(mCommand);
+    }
+}
diff --git a/src/com/android/camera/one/v2/components/StaticPictureCommand.java b/src/com/android/camera/one/v2/commands/StaticPictureCommand.java
similarity index 70%
rename from src/com/android/camera/one/v2/components/StaticPictureCommand.java
rename to src/com/android/camera/one/v2/commands/StaticPictureCommand.java
index b7e837c..547ba55 100644
--- a/src/com/android/camera/one/v2/components/StaticPictureCommand.java
+++ b/src/com/android/camera/one/v2/commands/StaticPictureCommand.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.camera.one.v2.components;
+package com.android.camera.one.v2.commands;
 
 import java.util.Arrays;
 
@@ -22,7 +22,10 @@
 import android.hardware.camera2.CameraDevice;
 
 import com.android.camera.async.BufferQueue;
+import com.android.camera.async.Updatable;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
 import com.android.camera.one.v2.camera2proxy.ImageProxy;
+import com.android.camera.one.v2.core.FrameExposureResponseListener;
 import com.android.camera.one.v2.core.FrameServer;
 import com.android.camera.one.v2.core.RequestBuilder;
 import com.android.camera.one.v2.sharedimagereader.SharedImageReader;
@@ -34,42 +37,43 @@
     private final FrameServer mFrameServer;
     private final RequestBuilder.Factory mBuilderFactory;
     private final SharedImageReader mImageReader;
-    private final ImageSaver mImageSaver;
+    private final Updatable<ImageProxy> mImageSaver;
+    private final Updatable<Void> mImageExposureUpdatable;
 
     public StaticPictureCommand(FrameServer frameServer, RequestBuilder.Factory builder,
-            SharedImageReader imageReader, ImageSaver imageSaver) {
+            SharedImageReader imageReader, Updatable<ImageProxy> imageSaver,
+            Updatable<Void> imageExposureUpdatable) {
         mFrameServer = frameServer;
         mBuilderFactory = builder;
         mImageReader = imageReader;
         mImageSaver = imageSaver;
+        mImageExposureUpdatable = imageExposureUpdatable;
     }
 
     /**
      * Sends a request to take a picture and blocks until it completes.
      */
     public void run() throws
-            InterruptedException, CameraAccessException {
-        FrameServer.Session session = mFrameServer.createSession();
-        try {
+            InterruptedException, CameraAccessException, CameraCaptureSessionClosedException {
+        try (FrameServer.Session session = mFrameServer.createSession()) {
             RequestBuilder photoRequest = mBuilderFactory.create(CameraDevice
                     .TEMPLATE_STILL_CAPTURE);
 
             try (SharedImageReader.ImageCaptureBufferQueue imageStream = mImageReader
                     .createStream(1)) {
                 photoRequest.addStream(imageStream);
-                // TODO Add a {@link ResponseListener} to notify the caller of
-                // when the frame is exposed.
-                session.submitRequest(Arrays.asList(photoRequest.build()), false);
+                photoRequest.addResponseListener(new FrameExposureResponseListener(
+                        mImageExposureUpdatable));
+                session.submitRequest(Arrays.asList(photoRequest.build()),
+                        FrameServer.RequestType.NON_REPEATING);
 
                 ImageProxy image = imageStream.getNext();
-                mImageSaver.saveAndCloseImage(image);
+                mImageSaver.update(image);
             } catch (BufferQueue.BufferQueueClosedException e) {
                 // If we get here, the request was submitted, but the image
                 // never arrived.
                 // TODO Log failure and notify the caller
             }
-        } finally {
-            session.close();
         }
     }
 }
diff --git a/src/com/android/camera/one/v2/common/CaptureSessionCreator.java b/src/com/android/camera/one/v2/common/CaptureSessionCreator.java
new file mode 100644
index 0000000..88446ea
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/CaptureSessionCreator.java
@@ -0,0 +1,103 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import java.util.List;
+
+import android.hardware.camera2.CameraAccessException;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Surface;
+
+import com.android.camera.async.FutureResult;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionProxy;
+import com.android.camera.one.v2.camera2proxy.CameraDeviceProxy;
+import com.android.camera.util.ScopedFactory;
+
+/**
+ * Asynchronously creates capture sessions.
+ */
+public class CaptureSessionCreator implements Runnable {
+    private final CameraDeviceProxy mDevice;
+    private final Handler mCameraHandler;
+    private final List<Surface> mSurfaces;
+    private final FutureResult<CameraCaptureSessionProxy> mSessionFuture;
+    private final ScopedFactory<CameraCaptureSessionProxy, Runnable> mCaptureSessionScopeEntrance;
+
+    /**
+     * @param device The device on which to create the capture session.
+     * @param cameraHandler The handler on which to process capture session
+     *            state callbacks.
+     * @param surfaces The surfaces to configure the session with.
+     * @param sessionFuture The {@link com.android.camera.async.FutureResult} in
+     *            which to place the asynchronously-created.
+     * @param captureSessionScopeEntrance A factory to produce a runnable to
+     *            execute if/when the CameraCaptureSession is available.
+     */
+    public CaptureSessionCreator(CameraDeviceProxy device, Handler cameraHandler,
+            List<Surface> surfaces, FutureResult<CameraCaptureSessionProxy> sessionFuture,
+            ScopedFactory<CameraCaptureSessionProxy, Runnable> captureSessionScopeEntrance) {
+        mDevice = device;
+        mCameraHandler = cameraHandler;
+        mSurfaces = surfaces;
+        mSessionFuture = sessionFuture;
+        mCaptureSessionScopeEntrance = captureSessionScopeEntrance;
+    }
+
+    @Override
+    public void run() {
+        try {
+            mDevice.createCaptureSession(mSurfaces, new CameraCaptureSessionProxy.StateCallback() {
+                @Override
+                public void onActive(CameraCaptureSessionProxy session) {
+                    // Ignore.
+                }
+
+                @Override
+                public void onConfigureFailed(CameraCaptureSessionProxy session) {
+                    mSessionFuture.setCancelled();
+                    session.close();
+                }
+
+                @Override
+                public void onConfigured(CameraCaptureSessionProxy session) {
+                    boolean valueSet = mSessionFuture.setValue(session);
+                    if (valueSet) {
+                        mCaptureSessionScopeEntrance.get(session).run();
+                    } else {
+                        // If the future was already marked with cancellation or
+                        // an exception, close the session.
+                        session.close();
+                    }
+                }
+
+                @Override
+                public void onReady(CameraCaptureSessionProxy session) {
+                    // Ignore.
+                }
+
+                @Override
+                public void onClosed(CameraCaptureSessionProxy session) {
+                    mSessionFuture.setCancelled();
+                    session.close();
+                }
+            }, mCameraHandler);
+        } catch (CameraAccessException e) {
+            mSessionFuture.setException(e);
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/DeferredManualAutoFocus.java b/src/com/android/camera/one/v2/common/DeferredManualAutoFocus.java
new file mode 100644
index 0000000..30ce112
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/DeferredManualAutoFocus.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * A
+ * {@link com.android.camera.one.v2.common.GenericOneCameraImpl.ManualAutoFocus}
+ * on which {@link #triggerFocusAndMeterAtPoint} may be called even if the
+ * underlying camera is not yet ready.
+ */
+public class DeferredManualAutoFocus implements GenericOneCameraImpl.ManualAutoFocus {
+    private final Future<GenericOneCameraImpl.ManualAutoFocus> mManualAutoFocusFuture;
+
+    public DeferredManualAutoFocus(Future<GenericOneCameraImpl.ManualAutoFocus> manualAutoFocusFuture) {
+        mManualAutoFocusFuture = manualAutoFocusFuture;
+    }
+
+    @Override
+    public void triggerFocusAndMeterAtPoint(float nx, float ny) {
+        if (mManualAutoFocusFuture.isDone()) {
+            try {
+                GenericOneCameraImpl.ManualAutoFocus af = mManualAutoFocusFuture.get();
+                af.triggerFocusAndMeterAtPoint(nx, ny);
+            } catch (InterruptedException | ExecutionException | CancellationException e) {
+                // If the {@link Future} is not ready, do nothing.
+                return;
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/DeferredPictureTaker.java b/src/com/android/camera/one/v2/common/DeferredPictureTaker.java
new file mode 100644
index 0000000..40aa903
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/DeferredPictureTaker.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import com.android.camera.one.OneCamera;
+import com.android.camera.session.CaptureSession;
+
+/**
+ * A {@link com.android.camera.one.v2.common.GenericOneCameraImpl.PictureTaker}
+ * on which {@link #takePicture} may be called even if the underlying camera is
+ * not yet ready.
+ */
+public class DeferredPictureTaker implements GenericOneCameraImpl.PictureTaker {
+    private final Future<GenericOneCameraImpl.PictureTaker> mPictureTakerFuture;
+
+    public DeferredPictureTaker(Future<GenericOneCameraImpl.PictureTaker> pictureTakerFuture) {
+        mPictureTakerFuture = pictureTakerFuture;
+    }
+
+    public void takePicture(OneCamera.PhotoCaptureParameters params, CaptureSession session) {
+        if (mPictureTakerFuture.isDone()) {
+            try {
+                GenericOneCameraImpl.PictureTaker taker = mPictureTakerFuture.get();
+                taker.takePicture(params, session);
+            } catch (InterruptedException | ExecutionException | CancellationException e) {
+                return;
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/FullSizeAspectRatioProvider.java b/src/com/android/camera/one/v2/common/FullSizeAspectRatioProvider.java
new file mode 100644
index 0000000..29e4914
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/FullSizeAspectRatioProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CameraCharacteristics;
+
+/**
+ * Provides the full-size aspect ratio of the camera with the given characteristics.
+ */
+public class FullSizeAspectRatioProvider {
+    private final CameraCharacteristics mCharacteristics;
+
+    public FullSizeAspectRatioProvider(CameraCharacteristics characteristics) {
+        mCharacteristics = characteristics;
+    }
+
+    /**
+     * Calculate the aspect ratio of the full size capture on this device.
+     *
+     * @return The aspect ratio, in terms of width/height of the full capture
+     *         size.
+     */
+    public float get() {
+        Rect activeArraySize = mCharacteristics.get(
+                CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        return ((float) (activeArraySize.width())) / activeArraySize.height();
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/GenericOneCameraImpl.java b/src/com/android/camera/one/v2/common/GenericOneCameraImpl.java
new file mode 100644
index 0000000..a19fd28
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/GenericOneCameraImpl.java
@@ -0,0 +1,193 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import android.content.Context;
+import android.view.Surface;
+
+import com.android.camera.async.Listenable;
+import com.android.camera.async.SafeCloseable;
+import com.android.camera.async.Updatable;
+import com.android.camera.one.OneCamera;
+import com.android.camera.one.v2.AutoFocusHelper;
+import com.android.camera.session.CaptureSession;
+import com.android.camera.util.Callback;
+import com.android.camera.util.ScopedFactory;
+import com.android.camera.util.Size;
+
+/**
+ * A generic, composable {@link OneCamera}.
+ * <p>
+ * Note: This implementation assumes that these four methods are invoked in
+ * sequences matching the following regex:
+ * <p>
+ * startPreview (takePicture | triggerFocusAndMeterAtPoint)* close
+ * <p>
+ * All other methods may be called at any time.
+ */
+public class GenericOneCameraImpl implements OneCamera {
+    public interface ManualAutoFocus {
+        /**
+         * @See {@link OneCamera#triggerFocusAndMeterAtPoint}
+         */
+        void triggerFocusAndMeterAtPoint(float nx, float ny);
+    }
+
+    public interface PictureTaker {
+        /**
+         * @See {@link OneCamera#takePicture}
+         */
+        public void takePicture(OneCamera.PhotoCaptureParameters params, CaptureSession session);
+    }
+
+    private final Set<SafeCloseable> mCloseListeners;
+    private final PictureTaker mPictureTaker;
+    private final ManualAutoFocus mManualAutoFocus;
+    private final Listenable<Integer> mAFStateListenable;
+    private final Listenable<Boolean> mReadyStateListenable;
+    private final float mMaxZoom;
+    private final Updatable<Float> mZoom;
+    private final Size[] mSupportedPreviewSizes;
+    private final float mFullSizeAspectRatio;
+    private final Facing mDirection;
+    private final PreviewSizeSelector mPreviewSizeSelector;
+    private final Listenable<Boolean> mPreviewStartSuccessListenable;
+    private final ScopedFactory<Surface, Runnable> mPreviewScopeEntrance;
+
+    public GenericOneCameraImpl(Set<SafeCloseable> closeListeners, PictureTaker pictureTaker,
+            ManualAutoFocus manualAutoFocus, Listenable<Integer> afStateProvider,
+            Listenable<Boolean> readyStateListenable, float maxZoom, Updatable<Float> zoom,
+            Size[] supportedPreviewSizes, float fullSizeAspectRatio, Facing direction,
+            PreviewSizeSelector previewSizeSelector,
+            Listenable<Boolean> previewStartSuccessListenable,
+            ScopedFactory<Surface, Runnable> previewScopeEntrance) {
+        mPreviewStartSuccessListenable = previewStartSuccessListenable;
+        mPreviewScopeEntrance = previewScopeEntrance;
+        mCloseListeners = new HashSet<>(closeListeners);
+        mMaxZoom = maxZoom;
+        mSupportedPreviewSizes = supportedPreviewSizes;
+        mFullSizeAspectRatio = fullSizeAspectRatio;
+        mDirection = direction;
+        mPreviewSizeSelector = previewSizeSelector;
+        mPictureTaker = pictureTaker;
+        mManualAutoFocus = manualAutoFocus;
+        mAFStateListenable = afStateProvider;
+        mReadyStateListenable = readyStateListenable;
+        mZoom = zoom;
+    }
+
+    @Override
+    public void triggerFocusAndMeterAtPoint(float nx, float ny) {
+        mManualAutoFocus.triggerFocusAndMeterAtPoint(nx, ny);
+    }
+
+    @Override
+    public void takePicture(PhotoCaptureParameters params, CaptureSession session) {
+        mPictureTaker.takePicture(params, session);
+    }
+
+    @Override
+    public void startBurst(BurstParameters params, CaptureSession session) {
+        // TODO delete from OneCamera interface
+    }
+
+    @Override
+    public void stopBurst() {
+        // TODO delete from OneCamera interface
+    }
+
+    @Override
+    public void setFocusStateListener(final FocusStateListener listener) {
+        mAFStateListenable.setCallback(new Callback<Integer>() {
+            @Override
+            public void onCallback(Integer afState) {
+                // TODO delete frameNumber from FocusStateListener callback. It
+                // is optional and never actually used.
+                long frameNumber = -1;
+                listener.onFocusStatusUpdate(AutoFocusHelper.stateFromCamera2State(afState),
+                        frameNumber);
+            }
+        });
+    }
+
+    @Override
+    public void setReadyStateChangedListener(final ReadyStateChangedListener listener) {
+        mReadyStateListenable.setCallback(new Callback<Boolean>() {
+            @Override
+            public void onCallback(Boolean result) {
+                listener.onReadyStateChanged(result);
+            }
+        });
+    }
+
+    @Override
+    public void startPreview(Surface surface, final CaptureReadyCallback listener) {
+        mPreviewStartSuccessListenable.setCallback(new Callback<Boolean>() {
+            @Override
+            public void onCallback(Boolean success) {
+                if (success) {
+                    listener.onReadyForCapture();
+                } else {
+                    listener.onSetupFailed();
+                }
+            }
+        });
+
+        mPreviewScopeEntrance.get(surface).run();
+    }
+
+    @Override
+    public void close(CloseCallback closeCallback) {
+        // TODO Remove CloseCallback from the interface. It is always null.
+        for (SafeCloseable listener : mCloseListeners) {
+            listener.close();
+        }
+    }
+
+    @Override
+    public Size[] getSupportedPreviewSizes() {
+        return mSupportedPreviewSizes;
+    }
+
+    @Override
+    public float getFullSizeAspectRatio() {
+        return mFullSizeAspectRatio;
+    }
+
+    @Override
+    public Facing getDirection() {
+        return mDirection;
+    }
+
+    @Override
+    public float getMaxZoom() {
+        return mMaxZoom;
+    }
+
+    @Override
+    public void setZoom(float zoom) {
+        mZoom.update(zoom);
+    }
+
+    @Override
+    public Size pickPreviewSize(Size pictureSize, Context context) {
+        return mPreviewSizeSelector.pickPreviewSize(pictureSize, context);
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/ManualAutoFocusImpl.java b/src/com/android/camera/one/v2/common/ManualAutoFocusImpl.java
new file mode 100644
index 0000000..9b2c52c
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/ManualAutoFocusImpl.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.graphics.PointF;
+
+import com.android.camera.async.Updatable;
+
+/**
+ * A ManualAutoFocus implementation which updates metering parameters and runs
+ * an AF Scan.
+ */
+public class ManualAutoFocusImpl implements GenericOneCameraImpl.ManualAutoFocus {
+    private final Updatable<MeteringParameters> mMeteringParameters;
+    private final Runnable mAFScanRunnable;
+
+    public ManualAutoFocusImpl(Updatable<MeteringParameters> meteringParameters,
+            Runnable afScanRunnable) {
+        mMeteringParameters = meteringParameters;
+        mAFScanRunnable = afScanRunnable;
+    }
+
+    @Override
+    public void triggerFocusAndMeterAtPoint(float nx, float ny) {
+        PointF point = new PointF(nx, ny);
+        mMeteringParameters.update(new MeteringParameters(point, point, MeteringParameters.Mode
+                .POINT));
+        mAFScanRunnable.run();
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/MeteringParameters.java b/src/com/android/camera/one/v2/common/MeteringParameters.java
new file mode 100644
index 0000000..ffd26e5
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/MeteringParameters.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.graphics.PointF;
+
+public class MeteringParameters {
+    public static enum Mode {
+        POINT,
+        GLOBAL
+    };
+
+    private final PointF mAFPoint;
+    private final PointF mAEPoint;
+    private final Mode mMode;
+
+    public MeteringParameters(PointF afPoint, PointF aePoint, Mode mode) {
+        mAFPoint = afPoint;
+        mAEPoint = aePoint;
+        mMode = mode;
+    }
+
+    public PointF getAFPoint() {
+        return mAFPoint;
+    }
+
+    public PointF getAEPoint() {
+        return mAEPoint;
+    }
+
+    public Mode getMode() {
+        return mMode;
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/PictureCallbackAdaptor.java b/src/com/android/camera/one/v2/common/PictureCallbackAdaptor.java
new file mode 100644
index 0000000..03a89d2
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/PictureCallbackAdaptor.java
@@ -0,0 +1,108 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import java.util.concurrent.Executor;
+
+import android.net.Uri;
+
+import com.android.camera.async.ConcurrentState;
+import com.android.camera.async.Updatable;
+import com.android.camera.one.OneCamera;
+import com.android.camera.session.CaptureSession;
+import com.android.camera.util.Callback;
+
+/**
+ * Provides {@link Updatable}s linked to the given
+ * {@link OneCamera.PictureCallback}.
+ */
+public class PictureCallbackAdaptor {
+    private final OneCamera.PictureCallback mPictureCallback;
+    private final Executor mMainExecutor;
+
+    public PictureCallbackAdaptor(OneCamera.PictureCallback pictureCallback,
+            Executor mainExecutor) {
+        mPictureCallback = pictureCallback;
+        mMainExecutor = mainExecutor;
+    }
+
+    public Updatable<Void> provideQuickExposeUpdatable() {
+        ConcurrentState<Void> exposeState = new ConcurrentState<>();
+        exposeState.addCallback(new Callback<Long>() {
+            @Override
+            public void onCallback(Long timestamp) {
+                mPictureCallback.onQuickExpose();
+            }
+        }, mMainExecutor);
+        return exposeState;
+    }
+
+    public Updatable<byte[]> provideThumbnailUpdatable() {
+        ConcurrentState<byte[]> thumbnailState = new ConcurrentState<>();
+        thumbnailState.addCallback(new Callback<byte[]>() {
+            @Override
+            public void onCallback(byte[] jpegData) {
+                mPictureCallback.onThumbnailResult(jpegData);
+            }
+        }, mMainExecutor);
+        return thumbnailState;
+    }
+
+    public Updatable<CaptureSession> providePictureTakenUpdatable() {
+        ConcurrentState<CaptureSession> state = new ConcurrentState<>();
+        state.addCallback(new Callback<CaptureSession>() {
+            @Override
+            public void onCallback(CaptureSession session) {
+                mPictureCallback.onPictureTaken(session);
+            }
+        }, mMainExecutor);
+        return state;
+    }
+
+    public Updatable<Uri> providePictureSavedUpdatable() {
+        ConcurrentState<Uri> state = new ConcurrentState<>();
+        state.addCallback(new Callback<Uri>() {
+            @Override
+            public void onCallback(Uri uri) {
+                mPictureCallback.onPictureSaved(uri);
+            }
+        }, mMainExecutor);
+        return state;
+    }
+
+    public Updatable<Void> providePictureTakingFailedUpdatable() {
+        ConcurrentState<Void> state = new ConcurrentState<>();
+        state.addCallback(new Callback<Void>() {
+            @Override
+            public void onCallback(Void v) {
+                mPictureCallback.onPictureTakingFailed();
+            }
+        }, mMainExecutor);
+        return state;
+    }
+
+    public Updatable<Float> providePictureTakingProgressUpdatable() {
+        ConcurrentState<Float> state = new ConcurrentState<>();
+        state.addCallback(new Callback<Float>() {
+            @Override
+            public void onCallback(Float progress) {
+                mPictureCallback.onTakePictureProgress(progress);
+            }
+        }, mMainExecutor);
+        return state;
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/PollableAEMode.java b/src/com/android/camera/one/v2/common/PollableAEMode.java
new file mode 100644
index 0000000..fe9e068
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/PollableAEMode.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.hardware.camera2.CaptureRequest;
+
+import com.android.camera.async.Pollable;
+import com.android.camera.one.OneCamera;
+
+/**
+ * Enables polling for the current AE Mode to use based on the current flash
+ * state.
+ */
+public class PollableAEMode implements Pollable<Integer> {
+    private final Pollable<OneCamera.PhotoCaptureParameters.Flash> mFlashPollable;
+
+    public PollableAEMode(Pollable<OneCamera.PhotoCaptureParameters.Flash> flashPollable) {
+        mFlashPollable = flashPollable;
+    }
+
+    @Override
+    public Integer get(Integer defaultValue) {
+        try {
+            return get();
+        } catch (NoValueSetException e) {
+            return defaultValue;
+        }
+    }
+
+    @Override
+    public Integer get() throws NoValueSetException {
+        switch (mFlashPollable.get()) {
+            case AUTO:
+                return CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH;
+            case ON:
+                return CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+            case OFF:
+                return CaptureRequest.CONTROL_AE_MODE_ON;
+            default:
+                return CaptureRequest.CONTROL_AE_MODE_ON;
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/PollableAERegion.java b/src/com/android/camera/one/v2/common/PollableAERegion.java
new file mode 100644
index 0000000..93f221d
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/PollableAERegion.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 com.android.camera.one.v2.common;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.camera2.params.MeteringRectangle;
+
+import com.android.camera.app.OrientationManager;
+import com.android.camera.async.Pollable;
+import com.android.camera.one.v2.AutoFocusHelper;
+
+/**
+ * Enables polling for the current AE metering rectangles based on the current
+ * metering parameters and crop region.
+ */
+public class PollableAERegion implements Pollable<MeteringRectangle[]> {
+    private final Pollable<MeteringParameters> mMeteringParameters;
+    private final Pollable<Rect> mCropRegion;
+    private final OrientationManager.DeviceOrientation mSensorOrientation;
+
+    public PollableAERegion(Pollable<MeteringParameters> meteringParameters,
+            Pollable<Rect> cropRegion, OrientationManager.DeviceOrientation sensorOrientation) {
+        mMeteringParameters = meteringParameters;
+        mCropRegion = cropRegion;
+        mSensorOrientation = sensorOrientation;
+    }
+
+    @Override
+    public MeteringRectangle[] get(MeteringRectangle[] defaultValue) {
+        try {
+            return get();
+        } catch (NoValueSetException e) {
+            return defaultValue;
+        }
+    }
+
+    @Override
+    public MeteringRectangle[] get() throws NoValueSetException {
+        MeteringParameters parameters = mMeteringParameters.get();
+        if (parameters.getMode() == MeteringParameters.Mode.POINT) {
+            Rect cropRegion = mCropRegion.get();
+            PointF point = parameters.getAEPoint();
+            return AutoFocusHelper.aeRegionsForNormalizedCoord(point.x, point.y, cropRegion,
+                    mSensorOrientation.getDegrees());
+        } else {
+            return AutoFocusHelper.getZeroWeightRegion();
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/PollableAFRegion.java b/src/com/android/camera/one/v2/common/PollableAFRegion.java
new file mode 100644
index 0000000..71759d3
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/PollableAFRegion.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.camera2.params.MeteringRectangle;
+
+import com.android.camera.app.OrientationManager;
+import com.android.camera.async.Pollable;
+import com.android.camera.one.v2.AutoFocusHelper;
+
+/**
+ * Enables polling for the current AF metering rectangles based on the current
+ * metering parameters and crop region.
+ */
+public class PollableAFRegion implements Pollable<MeteringRectangle[]> {
+    private final Pollable<MeteringParameters> mMeteringParameters;
+    private final Pollable<Rect> mCropRegion;
+    private final OrientationManager.DeviceOrientation mSensorOrientation;
+
+    public PollableAFRegion(Pollable<MeteringParameters> meteringParameters,
+                            Pollable<Rect> cropRegion,
+                            OrientationManager.DeviceOrientation sensorOrientation) {
+        mMeteringParameters = meteringParameters;
+        mCropRegion = cropRegion;
+        mSensorOrientation = sensorOrientation;
+    }
+
+    @Override
+    public MeteringRectangle[] get(MeteringRectangle[] defaultValue) {
+        try {
+            return get();
+        } catch (NoValueSetException e) {
+            return defaultValue;
+        }
+    }
+
+    @Override
+    public MeteringRectangle[] get() throws NoValueSetException {
+        MeteringParameters parameters = mMeteringParameters.get();
+        if (parameters.getMode() == MeteringParameters.Mode.POINT) {
+            Rect cropRegion = mCropRegion.get();
+            PointF point = parameters.getAEPoint();
+            return AutoFocusHelper.afRegionsForNormalizedCoord(point.x, point.y, cropRegion,
+                    mSensorOrientation.getDegrees());
+        } else {
+            return AutoFocusHelper.getZeroWeightRegion();
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/PollableZoomedCropRegion.java b/src/com/android/camera/one/v2/common/PollableZoomedCropRegion.java
new file mode 100644
index 0000000..9e78262
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/PollableZoomedCropRegion.java
@@ -0,0 +1,56 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CameraCharacteristics;
+
+import com.android.camera.async.Pollable;
+import com.android.camera.one.v2.AutoFocusHelper;
+
+/**
+ * Enables polling for the current crop region based on the current zoom.
+ */
+public class PollableZoomedCropRegion implements Pollable<Rect> {
+    private final CameraCharacteristics mCharacteristics;
+    private final Pollable<Float> mZoom;
+
+    public PollableZoomedCropRegion(CameraCharacteristics characteristics, Pollable<Float> zoom) {
+        mCharacteristics = characteristics;
+        mZoom = zoom;
+    }
+
+    @Override
+    public Rect get(Rect defaultValue) {
+        try {
+            return get();
+        } catch (NoValueSetException e) {
+            return defaultValue;
+        }
+    }
+
+    @Override
+    public Rect get() throws NoValueSetException {
+        float zoom = mZoom.get();
+        Rect sensor = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        int xCenter = sensor.width() / 2;
+        int yCenter = sensor.height() / 2;
+        int xDelta = (int) (0.5f * sensor.width() / zoom);
+        int yDelta = (int) (0.5f * sensor.height() / zoom);
+        return new Rect(xCenter - xDelta, yCenter - yDelta, xCenter + xDelta, yCenter + yDelta);
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/PreviewSizeSelector.java b/src/com/android/camera/one/v2/common/PreviewSizeSelector.java
new file mode 100644
index 0000000..89cc3a1
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/PreviewSizeSelector.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+
+import com.android.camera.CaptureModuleUtil;
+import com.android.camera.util.Size;
+
+/**
+ * Picks a preview size.
+ */
+public class PreviewSizeSelector {
+    private final int mImageFormat;
+    private final Size[] mSupportedPreviewSizes;
+
+    public PreviewSizeSelector(int imageFormat, Size[] supportedPreviewSizes) {
+        mImageFormat = imageFormat;
+        mSupportedPreviewSizes = supportedPreviewSizes;
+    }
+
+    public Size pickPreviewSize(Size pictureSize, Context context) {
+        if (pictureSize == null) {
+            // TODO The default should be selected by the caller, and
+            // pictureSize should never be null.
+            pictureSize = getDefaultPictureSize();
+        }
+        float pictureAspectRatio = pictureSize.getWidth() / (float) pictureSize.getHeight();
+
+        // Since devices only have one raw resolution we need to be more
+        // flexible for selecting a matching preview resolution.
+        Double aspectRatioTolerance = mImageFormat == ImageFormat.RAW_SENSOR ? 10d : null;
+        Size size = CaptureModuleUtil.getOptimalPreviewSize(context, mSupportedPreviewSizes,
+                pictureAspectRatio, aspectRatioTolerance);
+        return size;
+    }
+
+    /**
+     * @return The largest supported picture size.
+     */
+    private Size getDefaultPictureSize() {
+        Size[] supportedSizes = mSupportedPreviewSizes;
+
+        // Find the largest supported size.
+        Size largestSupportedSize = supportedSizes[0];
+        long largestSupportedSizePixels =
+                largestSupportedSize.getWidth() * largestSupportedSize.getHeight();
+        for (int i = 1; i < supportedSizes.length; i++) {
+            long numPixels = supportedSizes[i].getWidth() * supportedSizes[i].getHeight();
+            if (numPixels > largestSupportedSizePixels) {
+                largestSupportedSize = supportedSizes[i];
+                largestSupportedSizePixels = numPixels;
+            }
+        }
+        return new Size(largestSupportedSize.getWidth(), largestSupportedSize.getHeight());
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/SensorOrientationProvider.java b/src/com/android/camera/one/v2/common/SensorOrientationProvider.java
new file mode 100644
index 0000000..890aad6
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/SensorOrientationProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import com.android.camera.app.OrientationManager;
+
+public class SensorOrientationProvider {
+    private final CameraCharacteristics mCameraCharacteristics;
+
+    public SensorOrientationProvider(CameraCharacteristics cameraCharacteristics) {
+        mCameraCharacteristics = cameraCharacteristics;
+    }
+
+    public OrientationManager.DeviceOrientation getSensorOrientation() {
+        switch (mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)) {
+            case 0:
+                return OrientationManager.DeviceOrientation.CLOCKWISE_0;
+            case 90:
+                return OrientationManager.DeviceOrientation.CLOCKWISE_90;
+            case 180:
+                return OrientationManager.DeviceOrientation.CLOCKWISE_180;
+            case 270:
+                return OrientationManager.DeviceOrientation.CLOCKWISE_270;
+            default:
+                // Per API documentation, this case should never execute.
+                return OrientationManager.DeviceOrientation.CLOCKWISE_0;
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/common/SupportedPreviewSizeProvider.java b/src/com/android/camera/one/v2/common/SupportedPreviewSizeProvider.java
new file mode 100644
index 0000000..78e9d8c
--- /dev/null
+++ b/src/com/android/camera/one/v2/common/SupportedPreviewSizeProvider.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.android.camera.one.v2.common;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.params.StreamConfigurationMap;
+
+import com.android.camera.util.Size;
+
+public class SupportedPreviewSizeProvider {
+    private final CameraCharacteristics mCharacteristics;
+
+    public SupportedPreviewSizeProvider(CameraCharacteristics characteristics) {
+        mCharacteristics = characteristics;
+    }
+
+    public Size[] getSupportedPreviewSizes() {
+        StreamConfigurationMap config = mCharacteristics
+                .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+        return Size.convert(config.getOutputSizes(SurfaceTexture.class));
+    }
+}
diff --git a/src/com/android/camera/one/v2/components/AutoFocusMonitor.java b/src/com/android/camera/one/v2/components/AutoFocusMonitor.java
deleted file mode 100644
index 3b47e05..0000000
--- a/src/com/android/camera/one/v2/components/AutoFocusMonitor.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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 com.android.camera.one.v2.components;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-import android.hardware.camera2.CaptureFailure;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-
-import com.android.camera.one.v2.core.ResponseListener;
-
-/**
- * A {@link com.android.camera.one.v2.core.ResponseListener} which monitors the auto-focus state.
- * TODO Replace listener+executor pattern with generic event listener
- */
-public class AutoFocusMonitor implements ResponseListener {
-    private final Executor mListenerExecutor;
-    private final List<Listener> mListeners;
-    private volatile int mLatestAFState;
-    private long mLatestUpdateTimestamp;
-
-    /**
-     * @param listeners The list of listeners to notify of changes in auto focus state.
-     * @param listenerExecutor The executor on which to invoke the listeners.
-     */
-    public AutoFocusMonitor(List<Listener> listeners, Executor listenerExecutor) {
-        mListenerExecutor = listenerExecutor;
-        mListeners = new ArrayList<>(listeners);
-        mLatestAFState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
-        mLatestUpdateTimestamp = -1;
-    }
-
-    /**
-     * @return The most recently-observed auto-focus state. One of
-     *         {@link CaptureResult}.CONTROL_AF_STATE_*
-     */
-    public int getLatestAFState() {
-        return mLatestAFState;
-    }
-
-    @Override
-    public void onStarted(long timestamp) {
-
-    }
-
-    @Override
-    public void onProgressed(long timestamp, CaptureResult partialResult) {
-        if (timestamp > mLatestUpdateTimestamp) {
-            final Integer newValue = partialResult.get(CaptureResult.CONTROL_AF_STATE);
-            if (newValue != null && newValue != mLatestAFState) {
-                mLatestAFState = newValue;
-                mLatestUpdateTimestamp = timestamp;
-                for (final Listener listener : mListeners) {
-                    mListenerExecutor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            listener.onAutoFocusStateChange(newValue.intValue());
-                        }
-                    });
-                }
-            }
-        }
-    }
-
-    @Override
-    public void onCompleted(long timestamp, TotalCaptureResult result) {
-
-    }
-
-    @Override
-    public void onFailed(CaptureFailure failure) {
-
-    }
-
-    public static interface Listener {
-        /**
-         * @param newAFState One of {@link CaptureResult}.CONTROL_AF_STATE_*
-         */
-        public void onAutoFocusStateChange(int newAFState);
-    }
-}
diff --git a/src/com/android/camera/one/v2/components/FullAFScanCommand.java b/src/com/android/camera/one/v2/components/FullAFScanCommand.java
deleted file mode 100644
index 20f2477..0000000
--- a/src/com/android/camera/one/v2/components/FullAFScanCommand.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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 com.android.camera.one.v2.components;
-
-import java.util.Arrays;
-
-import android.hardware.camera2.CameraAccessException;
-import android.hardware.camera2.CaptureRequest;
-import android.hardware.camera2.CaptureResult;
-
-import com.android.camera.async.BufferQueue;
-import com.android.camera.one.v2.core.FrameServer;
-import com.android.camera.one.v2.core.MetadataChangeResponseListener;
-import com.android.camera.one.v2.core.RequestBuilder;
-
-/**
- * Performs a full auto focus scan.
- */
-public class FullAFScanCommand implements CameraCommand {
-    private final FrameServer mFrameServer;
-    private final RequestBuilder.Factory mBuilderFactory;
-    private final int mTemplateType;
-
-    /**
-     * @param frameServer Used for sending requests to the camera.
-     * @param builder Used for building requests.
-     * @param templateType See
-     *            {@link android.hardware.camera2.CameraDevice#createCaptureRequest}
-     *            .
-     */
-    public FullAFScanCommand(FrameServer frameServer, RequestBuilder.Factory builder, int
-            templateType) {
-        mFrameServer = frameServer;
-        mBuilderFactory = builder;
-        mTemplateType = templateType;
-    }
-
-    /**
-     * Performs an auto-focus scan, blocking until the scan starts, runs, and
-     * completes.
-     */
-    public void run() throws InterruptedException, CameraAccessException {
-        FrameServer.Session session = mFrameServer.createExclusiveSession();
-        try {
-            // Build a request to send a repeating AF_IDLE
-            RequestBuilder idleBuilder = mBuilderFactory.create(mTemplateType);
-            // Listen to AF state changes resulting from this repeating AF_IDLE
-            // request
-            MetadataChangeResponseListener<Integer> mFocusStateChangeListener = new
-                    MetadataChangeResponseListener<>(CaptureResult.CONTROL_AF_STATE);
-            idleBuilder.addResponseListener(mFocusStateChangeListener);
-
-            // Build a request to send a single AF_TRIGGER
-            RequestBuilder triggerBuilder = mBuilderFactory.create(mTemplateType);
-            triggerBuilder.setParam(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
-            triggerBuilder.setParam(CaptureRequest.CONTROL_AF_MODE, CaptureRequest
-                    .CONTROL_AF_MODE_CONTINUOUS_PICTURE);
-            triggerBuilder.setParam(CaptureRequest.CONTROL_AF_TRIGGER,
-                    CaptureRequest.CONTROL_AF_TRIGGER_START);
-
-            // Request a stream of changes to the AF-state
-            try (BufferQueue<Integer> afStateBufferQueue =
-                    mFocusStateChangeListener.getValueStream()) {
-                // Submit the repeating request first.
-                session.submitRequest(Arrays.asList(idleBuilder.build()), true);
-                session.submitRequest(Arrays.asList(triggerBuilder.build()), false);
-
-                while (true) {
-                    try {
-                        // FIXME The current MetadataChangeResponseListener
-                        // doesn't actually close the stream, so this may block
-                        // forever.
-                        // TODO Using a timeout here would solve this problem.
-                        Integer newAFState = afStateBufferQueue.getNext();
-                        if (newAFState == CaptureResult.CONTROL_AF_STATE_INACTIVE ||
-                                newAFState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
-                                newAFState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
-                            return;
-                        }
-                    } catch (BufferQueue.BufferQueueClosedException e) {
-                        // No more CaptureResults are being provided for the
-                        // repeating request from {@link idleBuilder}. If we get
-                        // here, it's because capture has been aborted, or a
-                        // framework error (since we have an exclusive session,
-                        // nothing else should have submitted repeating requests
-                        // which would end this one). Either way, we should just
-                        // return and release control of the session.
-                        return;
-                    }
-                }
-            }
-        } finally {
-            session.close();
-        }
-    }
-}
diff --git a/src/com/android/camera/one/v2/core/DecoratingRequestBuilderBuilder.java b/src/com/android/camera/one/v2/core/DecoratingRequestBuilderBuilder.java
new file mode 100644
index 0000000..3399268
--- /dev/null
+++ b/src/com/android/camera/one/v2/core/DecoratingRequestBuilderBuilder.java
@@ -0,0 +1,100 @@
+/*
+ * 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 com.android.camera.one.v2.core;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CaptureRequest;
+
+import com.android.camera.async.ConstantPollable;
+import com.android.camera.async.Pollable;
+
+/**
+ * A {@link RequestBuilder.Factory} which allows modifying each
+ * {@link RequestBuilder} that is created. <br>
+ * TODO Write Tests
+ */
+public class DecoratingRequestBuilderBuilder implements RequestBuilder.Factory {
+    private static class Parameter<T> {
+        private final CaptureRequest.Key<T> key;
+        private final Pollable<T> value;
+
+        private Parameter(CaptureRequest.Key<T> key, Pollable<T> value) {
+            this.key = key;
+            this.value = value;
+        }
+
+        public void addToBuilder(RequestBuilder builder) {
+            try {
+                builder.setParam(key, value.get());
+            } catch (Pollable.NoValueSetException e) {
+                // If there is no value to set, do nothing.
+            }
+        }
+    }
+
+    private final RequestBuilder.Factory mRequestBuilderFactory;
+    private final Set<ResponseListener> mResponseListeners;
+    private final List<Parameter<?>> mParameters;
+    private final List<CaptureStream> mCaptureStreams;
+
+    public DecoratingRequestBuilderBuilder(RequestBuilder.Factory requestBuilderFactory) {
+        mRequestBuilderFactory = requestBuilderFactory;
+        mResponseListeners = new HashSet<>();
+        mParameters = new ArrayList<>();
+        mCaptureStreams = new ArrayList<>();
+    }
+
+    public <T> DecoratingRequestBuilderBuilder withParam(CaptureRequest.Key<T> key, T value) {
+        return withParam(key, new ConstantPollable<T>(value));
+    }
+
+    public <T> DecoratingRequestBuilderBuilder withParam(CaptureRequest.Key<T> key,
+            Pollable<T> value) {
+        mParameters.add(new Parameter<T>(key, value));
+        return this;
+    }
+
+    public DecoratingRequestBuilderBuilder withResponseListener(ResponseListener listener) {
+        mResponseListeners.add(listener);
+        return this;
+    }
+
+    public DecoratingRequestBuilderBuilder withStream(CaptureStream stream) {
+        mCaptureStreams.add(stream);
+        return this;
+    }
+
+    @Override
+    public RequestBuilder create(int templateType) throws CameraAccessException {
+        RequestBuilder builder = mRequestBuilderFactory.create(templateType);
+        for (Parameter param : mParameters) {
+            param.addToBuilder(builder);
+        }
+        for (ResponseListener listener : mResponseListeners) {
+            builder.addResponseListener(listener);
+        }
+        for (CaptureStream stream : mCaptureStreams) {
+            builder.addStream(stream);
+        }
+        return builder;
+    }
+}
diff --git a/src/com/android/camera/one/v2/core/DecoratingRequestBuilderFactory.java b/src/com/android/camera/one/v2/core/DecoratingRequestBuilderFactory.java
deleted file mode 100644
index e49fe86..0000000
--- a/src/com/android/camera/one/v2/core/DecoratingRequestBuilderFactory.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 com.android.camera.one.v2.core;
-
-import java.util.List;
-
-import android.hardware.camera2.CameraAccessException;
-
-/**
- * A {@link RequestBuilder.Factory} which allows modifying each
- * {@link RequestBuilder} that is created.
- */
-public class DecoratingRequestBuilderFactory implements RequestBuilder.Factory {
-    private final RequestBuilder.Factory mRequestBuilderFactory;
-    private final List<RequestBuilderDecorator> mDecorators;
-
-    public DecoratingRequestBuilderFactory(RequestBuilder.Factory requestBuilderFactory,
-            List<RequestBuilderDecorator> decorators) {
-        mRequestBuilderFactory = requestBuilderFactory;
-        mDecorators = decorators;
-    }
-
-    @Override
-    public RequestBuilder create(int templateType) throws CameraAccessException {
-        RequestBuilder builder = mRequestBuilderFactory.create(templateType);
-        for (RequestBuilderDecorator decorator : mDecorators) {
-            decorator.decorateRequest(builder);
-        }
-        return builder;
-    }
-
-    public static interface RequestBuilderDecorator {
-        public void decorateRequest(RequestBuilder builder);
-    }
-}
diff --git a/src/com/android/camera/one/v2/core/FrameExposureResponseListener.java b/src/com/android/camera/one/v2/core/FrameExposureResponseListener.java
new file mode 100644
index 0000000..8d09129
--- /dev/null
+++ b/src/com/android/camera/one/v2/core/FrameExposureResponseListener.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.android.camera.one.v2.core;
+
+import com.android.camera.async.Updatable;
+
+public class FrameExposureResponseListener extends ResponseListener {
+    private final Updatable<Void> mExposureUpdatable;
+
+    public FrameExposureResponseListener(Updatable<Void> exposureUpdatable) {
+        mExposureUpdatable = exposureUpdatable;
+    }
+
+    @Override
+    public void onStarted(long timestamp) {
+        mExposureUpdatable.update(null);
+    }
+}
diff --git a/src/com/android/camera/one/v2/core/FrameServer.java b/src/com/android/camera/one/v2/core/FrameServer.java
index 7bc7131..fd24d5b 100644
--- a/src/com/android/camera/one/v2/core/FrameServer.java
+++ b/src/com/android/camera/one/v2/core/FrameServer.java
@@ -21,6 +21,8 @@
 
 import android.hardware.camera2.CameraAccessException;
 
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+
 /**
  * Provides thread-safe concurrent access to a camera.
  * <p>
@@ -37,6 +39,14 @@
     public static class SessionClosedException extends RuntimeException {
     }
 
+    public static enum RequestType {
+        REPEATING, NON_REPEATING;
+
+        public boolean isRepeating() {
+            return equals(REPEATING);
+        }
+    }
+
     /**
      * A Session enables submitting multiple Requests for frames.
      */
@@ -61,8 +71,9 @@
          * @throws java.lang.InterruptedException if interrupted before the
          *             request is be submitted.
          */
-        public synchronized void submitRequest(List<Request> burstRequests, boolean repeating)
-                throws CameraAccessException, InterruptedException {
+        public synchronized void submitRequest(List<Request> burstRequests, RequestType type)
+                throws CameraAccessException, InterruptedException,
+                CameraCaptureSessionClosedException {
             try {
                 if (mClosed) {
                     throw new SessionClosedException();
@@ -74,7 +85,7 @@
                     mCameraLock.acquire();
                 }
                 try {
-                    mCaptureSession.submitRequest(burstRequests, repeating);
+                    mCaptureSession.submitRequest(burstRequests, type.isRepeating());
                 } finally {
                     if (!mExclusive) {
                         mCameraLock.release();
diff --git a/src/com/android/camera/one/v2/core/FrameworkFailureResponseListener.java b/src/com/android/camera/one/v2/core/FrameworkFailureResponseListener.java
index 59b243a..bacea2a 100644
--- a/src/com/android/camera/one/v2/core/FrameworkFailureResponseListener.java
+++ b/src/com/android/camera/one/v2/core/FrameworkFailureResponseListener.java
@@ -18,43 +18,24 @@
 
 import android.hardware.camera2.CaptureFailure;
 import android.hardware.camera2.CaptureRequest;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
 
-import com.android.camera.async.ConcurrentBufferQueue;
-import com.android.camera.async.BufferQueue;
+import com.android.camera.async.Updatable;
 
 /**
- * A {@link ResponseListener} which provides a
- * stream of any CaptureRequests which triggered framework errors.
+ * A {@link ResponseListener} which provides a stream of any CaptureRequests
+ * which triggered framework errors.
  */
-public class FrameworkFailureResponseListener implements ResponseListener {
-    private final ConcurrentBufferQueue<CaptureRequest> mFailureStream;
+public class FrameworkFailureResponseListener extends ResponseListener {
+    private final Updatable<CaptureRequest> mFailureStream;
 
-    public FrameworkFailureResponseListener() {
-        mFailureStream = new ConcurrentBufferQueue<>();
-    }
-
-    public BufferQueue getFailureStream() {
-        return mFailureStream;
-    }
-
-    @Override
-    public void onStarted(long timestamp) {
-    }
-
-    @Override
-    public void onProgressed(long timestamp, CaptureResult partialResult) {
-    }
-
-    @Override
-    public void onCompleted(long timestamp, TotalCaptureResult result) {
+    public FrameworkFailureResponseListener(Updatable<CaptureRequest> failureStream) {
+        mFailureStream = failureStream;
     }
 
     @Override
     public void onFailed(CaptureFailure failure) {
         if (failure.getReason() == CaptureFailure.REASON_ERROR) {
-            mFailureStream.append(failure.getRequest());
+            mFailureStream.update(failure.getRequest());
         }
     }
 }
diff --git a/src/com/android/camera/one/v2/core/MetadataChangeResponseListener.java b/src/com/android/camera/one/v2/core/MetadataChangeResponseListener.java
deleted file mode 100644
index 9c2606d..0000000
--- a/src/com/android/camera/one/v2/core/MetadataChangeResponseListener.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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 com.android.camera.one.v2.core;
-
-import android.hardware.camera2.CaptureFailure;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-
-import com.android.camera.async.ConcurrentBufferQueue;
-import com.android.camera.async.BufferQueue;
-
-/**
- * A {@link ResponseListener} which listens for changes to a particular metadata
- * key and serializes changes in the value to a blocking queue.
- */
-public class MetadataChangeResponseListener<V> implements ResponseListener {
-    private final ConcurrentBufferQueue<V> mNewValues;
-    private final CaptureResult.Key<V> mKey;
-
-    private long mMostRecentTimestamp = -1;
-    private V mMostRecentValue = null;
-
-    /**
-     * @param key The key associated with the value for which to listen to
-     *            changes.
-     */
-    public MetadataChangeResponseListener(CaptureResult.Key<V> key) {
-        mNewValues = new ConcurrentBufferQueue<>();
-        mKey = key;
-    }
-
-    public BufferQueue<V> getValueStream() {
-        return mNewValues;
-    }
-
-    /**
-     * @return The most recent value, or null if no value has been set yet.
-     */
-    public V getMostRecentValue() {
-        return mMostRecentValue;
-    }
-
-    @Override
-    public void onStarted(long timestamp) {
-    }
-
-    @Override
-    public void onProgressed(long timestamp, CaptureResult partialResult) {
-        if (timestamp > mMostRecentTimestamp) {
-            V newValue = partialResult.get(mKey);
-            if (newValue != null) {
-                if (mMostRecentValue != newValue) {
-                    mNewValues.append(newValue);
-                }
-                mMostRecentValue = newValue;
-                mMostRecentTimestamp = timestamp;
-            }
-        }
-    }
-
-    @Override
-    public void onCompleted(long timestamp, TotalCaptureResult result) {
-    }
-
-    @Override
-    public void onFailed(CaptureFailure failure) {
-    }
-}
diff --git a/src/com/android/camera/one/v2/core/MetadataResponseListener.java b/src/com/android/camera/one/v2/core/MetadataResponseListener.java
new file mode 100644
index 0000000..0b25ef7
--- /dev/null
+++ b/src/com/android/camera/one/v2/core/MetadataResponseListener.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.android.camera.one.v2.core;
+
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+
+import com.android.camera.async.Updatable;
+
+/**
+ * A {@link ResponseListener} which listens for a particular metadata key.
+ */
+public class MetadataResponseListener<V> extends ResponseListener {
+    private final Updatable<V> mUpdatable;
+    private final CaptureResult.Key<V> mKey;
+
+    private long mMostRecentTimestamp = -1;
+    private V mMostRecentValue = null;
+
+    /**
+     * @param key The key associated with the value for which to listen to
+     *            changes.
+     */
+    public MetadataResponseListener(CaptureResult.Key<V> key, Updatable<V> updatable) {
+        mKey = key;
+        mUpdatable = updatable;
+    }
+
+    @Override
+    public void onProgressed(CaptureResult partialResult) {
+        V newValue = partialResult.get(mKey);
+        if (newValue != null) {
+            mUpdatable.update(newValue);
+        }
+    }
+
+    @Override
+    public void onCompleted(TotalCaptureResult totalCaptureResult) {
+        V newValue = totalCaptureResult.get(mKey);
+        if (newValue != null) {
+            mUpdatable.update(newValue);
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/core/RefCountedImageProxy.java b/src/com/android/camera/one/v2/core/RefCountedImageProxy.java
index 9e5b2f1..80a7795 100644
--- a/src/com/android/camera/one/v2/core/RefCountedImageProxy.java
+++ b/src/com/android/camera/one/v2/core/RefCountedImageProxy.java
@@ -17,13 +17,14 @@
 package com.android.camera.one.v2.core;
 
 import com.android.camera.async.RefCountBase;
+import com.android.camera.one.v2.camera2proxy.ForwardingImageProxy;
 import com.android.camera.one.v2.camera2proxy.ImageProxy;
 
 /**
  * Wraps ImageProxy with reference counting, starting with a fixed number of
  * references.
  */
-public class RefCountedImageProxy extends ImageProxy {
+public class RefCountedImageProxy extends ForwardingImageProxy {
     private final RefCountBase<ImageProxy> mRefCount;
 
     /**
diff --git a/src/com/android/camera/one/v2/core/RequestBuilder.java b/src/com/android/camera/one/v2/core/RequestBuilder.java
index e56c83e..338fa01 100644
--- a/src/com/android/camera/one/v2/core/RequestBuilder.java
+++ b/src/com/android/camera/one/v2/core/RequestBuilder.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.camera.one.v2.core;
 
 import java.util.ArrayList;
@@ -26,6 +27,7 @@
 import android.view.Surface;
 
 import com.android.camera.async.BufferQueue;
+import com.android.camera.async.ConcurrentBufferQueue;
 import com.android.camera.one.v2.camera2proxy.CaptureRequestBuilderProxy;
 
 /**
@@ -50,8 +52,8 @@
         private final CaptureRequestBuilderProxy mBuilderProxy;
 
         private UnregisteredStreamProvider(CaptureStream captureStream,
-                                           BufferQueue<Long> timestampQueue,
-                                           CaptureRequestBuilderProxy builderProxy) {
+                BufferQueue<Long> timestampQueue,
+                CaptureRequestBuilderProxy builderProxy) {
             mCaptureStream = captureStream;
             mTimestampQueue = timestampQueue;
             mAllocated = new AtomicBoolean(false);
@@ -69,12 +71,17 @@
     }
 
     private static class RequestImpl implements Request {
+        private static interface Allocation {
+            public void allocate() throws InterruptedException;
+
+            public void abort();
+        }
         private final CaptureRequestBuilderProxy mCaptureRequestBuilder;
         private final List<Allocation> mAllocations;
         private final ResponseListener mResponseListener;
 
         public RequestImpl(CaptureRequestBuilderProxy builder, List<Allocation> allocations,
-                           ResponseListener responseListener) {
+                ResponseListener responseListener) {
             mCaptureRequestBuilder = builder;
             mAllocations = allocations;
             mResponseListener = responseListener;
@@ -99,12 +106,6 @@
                 allocation.abort();
             }
         }
-
-        private static interface Allocation {
-            public void allocate() throws InterruptedException;
-
-            public void abort();
-        }
     }
 
     private final CaptureRequestBuilderProxy mBuilder;
@@ -137,6 +138,7 @@
 
     /**
      * Sets the given key-value pair.
+     *
      * @see {@link CaptureRequest.Builder#set}.
      */
     public <T> void setParam(CaptureRequest.Key<T> key, T value) {
@@ -153,10 +155,12 @@
      * @param captureStream
      */
     public void addStream(CaptureStream captureStream) {
-        TimestampResponseListener timestampResponseListener = new TimestampResponseListener();
+        ConcurrentBufferQueue<Long> timestamps = new ConcurrentBufferQueue<>();
+        TimestampResponseListener timestampResponseListener = new TimestampResponseListener(
+                timestamps);
 
         mAllocations.add(new UnregisteredStreamProvider(captureStream,
-                timestampResponseListener.getTimestamps(), mBuilder));
+                timestamps, mBuilder));
 
         mResponseListeners.add(timestampResponseListener);
     }
diff --git a/src/com/android/camera/one/v2/core/ResponseListener.java b/src/com/android/camera/one/v2/core/ResponseListener.java
index 20ce850..a1034b6 100644
--- a/src/com/android/camera/one/v2/core/ResponseListener.java
+++ b/src/com/android/camera/one/v2/core/ResponseListener.java
@@ -24,7 +24,7 @@
  * Like {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback},
  * but for events related to single requests.
  */
-public interface ResponseListener {
+public abstract class ResponseListener {
     /**
      * Note that this is typically invoked on the camera thread and at high
      * frequency, so implementations must execute quickly and not make
@@ -32,7 +32,8 @@
      *
      * @See {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback#onCaptureStarted}
      */
-    public void onStarted(long timestamp);
+    public void onStarted(long timestamp) {
+    }
 
     /**
      * Note that this is typically invoked on the camera thread and at high
@@ -41,7 +42,8 @@
      *
      * @See {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback#onCaptureProgressed}
      */
-    public void onProgressed(long timestamp, CaptureResult partialResult);
+    public void onProgressed(CaptureResult partialResult) {
+    }
 
     /**
      * Note that this is typically invoked on the camera thread and at high
@@ -50,7 +52,8 @@
      *
      * @See {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback#onCaptureCompleted}
      */
-    public void onCompleted(long timestamp, TotalCaptureResult result);
+    public void onCompleted(TotalCaptureResult result) {
+    }
 
     /**
      * Note that this is typically invoked on the camera thread and at high
@@ -59,5 +62,6 @@
      *
      * @See {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback#onCaptureFailed}
      */
-    public void onFailed(CaptureFailure failure);
+    public void onFailed(CaptureFailure failure) {
+    }
 }
diff --git a/src/com/android/camera/one/v2/core/ResponseListenerBroadcaster.java b/src/com/android/camera/one/v2/core/ResponseListenerBroadcaster.java
index baca7ec..5acc62b 100644
--- a/src/com/android/camera/one/v2/core/ResponseListenerBroadcaster.java
+++ b/src/com/android/camera/one/v2/core/ResponseListenerBroadcaster.java
@@ -27,7 +27,7 @@
  * Combines multiple {@link ResponseListener}s into a single one which
  * dispatches to all listeners for each callback.
  */
-public class ResponseListenerBroadcaster implements ResponseListener {
+public class ResponseListenerBroadcaster extends ResponseListener {
     private final List<ResponseListener> mListeners;
 
     public ResponseListenerBroadcaster(List<ResponseListener> listeners) {
@@ -42,16 +42,16 @@
     }
 
     @Override
-    public void onProgressed(long timestamp, CaptureResult partialResult) {
+    public void onProgressed(CaptureResult partialResult) {
         for (ResponseListener listener : mListeners) {
-            listener.onProgressed(timestamp, partialResult);
+            listener.onProgressed(partialResult);
         }
     }
 
     @Override
-    public void onCompleted(long timestamp, TotalCaptureResult result) {
+    public void onCompleted(TotalCaptureResult result) {
         for (ResponseListener listener : mListeners) {
-            listener.onCompleted(timestamp, result);
+            listener.onCompleted(result);
         }
     }
 
diff --git a/src/com/android/camera/one/v2/core/SingleCloseImageProxy.java b/src/com/android/camera/one/v2/core/SingleCloseImageProxy.java
index af968e7..7ac9f7f 100644
--- a/src/com/android/camera/one/v2/core/SingleCloseImageProxy.java
+++ b/src/com/android/camera/one/v2/core/SingleCloseImageProxy.java
@@ -19,12 +19,13 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import com.android.camera.one.v2.camera2proxy.ForwardingImageProxy;
 import com.android.camera.one.v2.camera2proxy.ImageProxy;
 
 /**
  * Wraps {@link ImageProxy} to filter out multiple calls to {@link #close}.
  */
-public class SingleCloseImageProxy extends ImageProxy {
+public class SingleCloseImageProxy extends ForwardingImageProxy {
     private final AtomicBoolean mClosed;
 
     /**
diff --git a/src/com/android/camera/one/v2/core/TagDispatchCaptureSession.java b/src/com/android/camera/one/v2/core/TagDispatchCaptureSession.java
index c568f96..36d4783 100644
--- a/src/com/android/camera/one/v2/core/TagDispatchCaptureSession.java
+++ b/src/com/android/camera/one/v2/core/TagDispatchCaptureSession.java
@@ -22,13 +22,14 @@
 import java.util.Map;
 
 import android.hardware.camera2.CameraAccessException;
-import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CaptureFailure;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
 import android.os.Handler;
 
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
+import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionProxy;
 import com.android.camera.one.v2.camera2proxy.CaptureRequestBuilderProxy;
 
 /**
@@ -36,9 +37,11 @@
  * {@link Request}s and dispatches to the appropriate {@link ResponseListener}
  * on a per-request basis, instead of for every {@link CaptureRequest} submitted
  * at the same time.
+ * <p/>
+ * TODO Write Tests
  */
 public class TagDispatchCaptureSession {
-    private static class CaptureCallback extends CameraCaptureSession.CaptureCallback {
+    private static class CaptureCallback implements CameraCaptureSessionProxy.CaptureCallback {
         private final Map<Object, ResponseListener> mListeners;
 
         /**
@@ -50,52 +53,51 @@
         }
 
         @Override
-        public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request,
+        public void onCaptureStarted(CameraCaptureSessionProxy session, CaptureRequest request,
                 long timestamp, long frameNumber) {
             Object tag = request.getTag();
             mListeners.get(tag).onStarted(timestamp);
         }
 
         @Override
-        public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request,
+        public void onCaptureProgressed(CameraCaptureSessionProxy session, CaptureRequest request,
                 CaptureResult partialResult) {
             Object tag = request.getTag();
-            long timestamp = partialResult.get(CaptureResult.SENSOR_TIMESTAMP);
-            mListeners.get(tag).onProgressed(timestamp, partialResult);
+            mListeners.get(tag).onProgressed(partialResult);
         }
 
         @Override
-        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
+        public void onCaptureCompleted(CameraCaptureSessionProxy session, CaptureRequest request,
                 TotalCaptureResult result) {
             Object tag = request.getTag();
-            long timestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
-            mListeners.get(tag).onCompleted(timestamp, result);
+            mListeners.get(tag).onCompleted(result);
         }
 
         @Override
-        public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request,
+        public void onCaptureFailed(CameraCaptureSessionProxy session, CaptureRequest request,
                 CaptureFailure failure) {
             Object tag = request.getTag();
             mListeners.get(tag).onFailed(failure);
         }
 
         @Override
-        public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+        public void onCaptureSequenceAborted(CameraCaptureSessionProxy session, int sequenceId) {
             // Ignored
         }
 
         @Override
-        public void onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId,
+        public void onCaptureSequenceCompleted(CameraCaptureSessionProxy session, int sequenceId,
                 long frameNumber) {
             // Ignored
         }
     }
 
-    private final CameraCaptureSession mCaptureSession;
+    private final CameraCaptureSessionProxy mCaptureSession;
     private final Handler mCameraHandler;
     private long mTagCounter;
 
-    public TagDispatchCaptureSession(CameraCaptureSession captureSession, Handler cameraHandler) {
+    public TagDispatchCaptureSession(CameraCaptureSessionProxy captureSession, Handler
+            cameraHandler) {
         mCaptureSession = captureSession;
         mCameraHandler = cameraHandler;
         mTagCounter = 0;
@@ -109,7 +111,7 @@
 
     /**
      * Submits the given burst request to the underlying
-     * {@link CameraCaptureSession}.
+     * {@link CameraCaptureSessionProxy}.
      * <p/>
      * Note that the Tag associated with the {@link CaptureRequest} from each
      * {@link Request} will be overwritten.
@@ -125,7 +127,7 @@
      *             resources necessary for each {@link Request}.
      */
     public void submitRequest(List<Request> burstRequests, boolean repeating) throws
-            CameraAccessException, InterruptedException {
+            CameraAccessException, InterruptedException, CameraCaptureSessionClosedException {
         try {
             Map<Object, ResponseListener> tagListenerMap = new HashMap<Object, ResponseListener>();
             List<CaptureRequest> captureRequests = new ArrayList<>(burstRequests.size());
diff --git a/src/com/android/camera/one/v2/core/TimestampResponseListener.java b/src/com/android/camera/one/v2/core/TimestampResponseListener.java
index a9c5683..039ed73 100644
--- a/src/com/android/camera/one/v2/core/TimestampResponseListener.java
+++ b/src/com/android/camera/one/v2/core/TimestampResponseListener.java
@@ -22,35 +22,20 @@
 
 import com.android.camera.async.ConcurrentBufferQueue;
 import com.android.camera.async.BufferQueue;
+import com.android.camera.async.Updatable;
 
 /**
  * A {@link ResponseListener} which provides a stream of timestamps.
  */
-public class TimestampResponseListener implements ResponseListener {
-    private final ConcurrentBufferQueue<Long> mTimestamps;
+public class TimestampResponseListener extends ResponseListener {
+    private final Updatable<Long> mTimestamps;
 
-    public TimestampResponseListener() {
-        mTimestamps = new ConcurrentBufferQueue<>();
-    }
-
-    public BufferQueue<Long> getTimestamps() {
-        return mTimestamps;
+    public TimestampResponseListener(Updatable<Long> timestamps) {
+        mTimestamps = timestamps;
     }
 
     @Override
     public void onStarted(long timestamp) {
-        mTimestamps.append(timestamp);
-    }
-
-    @Override
-    public void onProgressed(long timestamp, CaptureResult partialResult) {
-    }
-
-    @Override
-    public void onCompleted(long timestamp, TotalCaptureResult result) {
-    }
-
-    @Override
-    public void onFailed(CaptureFailure failure) {
+        mTimestamps.update(timestamp);
     }
 }
diff --git a/src/com/android/camera/one/v2/core/TotalCaptureResultResponseListener.java b/src/com/android/camera/one/v2/core/TotalCaptureResultResponseListener.java
index f6be60d..46b1c38 100644
--- a/src/com/android/camera/one/v2/core/TotalCaptureResultResponseListener.java
+++ b/src/com/android/camera/one/v2/core/TotalCaptureResultResponseListener.java
@@ -22,36 +22,21 @@
 
 import com.android.camera.async.ConcurrentBufferQueue;
 import com.android.camera.async.BufferQueue;
+import com.android.camera.async.Updatable;
 
 /**
  * A {@link ResponseListener} which provides a stream of
  * {@link TotalCaptureResult}s.
  */
-public class TotalCaptureResultResponseListener implements ResponseListener {
-    private final ConcurrentBufferQueue<TotalCaptureResult> mResults;
+public class TotalCaptureResultResponseListener extends ResponseListener {
+    private final Updatable<TotalCaptureResult> mResults;
 
-    public TotalCaptureResultResponseListener() {
-        mResults = new ConcurrentBufferQueue<>();
-    }
-
-    public BufferQueue<TotalCaptureResult> getResult() {
-        return mResults;
+    public TotalCaptureResultResponseListener(Updatable<TotalCaptureResult> results) {
+        mResults = results;
     }
 
     @Override
-    public void onStarted(long timestamp) {
-    }
-
-    @Override
-    public void onProgressed(long timestamp, CaptureResult partialResult) {
-    }
-
-    @Override
-    public void onCompleted(long timestamp, TotalCaptureResult result) {
-        mResults.append(result);
-    }
-
-    @Override
-    public void onFailed(CaptureFailure failure) {
+    public void onCompleted(TotalCaptureResult result) {
+        mResults.update(result);
     }
 }
diff --git a/src/com/android/camera/one/v2/core/TriggeredAFScanStateResponseListener.java b/src/com/android/camera/one/v2/core/TriggeredAFScanStateResponseListener.java
new file mode 100644
index 0000000..f65238f
--- /dev/null
+++ b/src/com/android/camera/one/v2/core/TriggeredAFScanStateResponseListener.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.android.camera.one.v2.core;
+
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+
+import com.android.camera.async.Updatable;
+
+/**
+ * Maintains the current state of auto-focus scans resulting from explicit
+ * trigger requests. This maintains the subset of the finite state machine of
+ * {@link android.hardware.camera2.CaptureResult#CONTROL_AF_STATE} which relates
+ * to AF_TRIGGER.
+ */
+public class TriggeredAFScanStateResponseListener extends ResponseListener {
+    private final Updatable<Void> mScanCompleteUpdatable;
+    private boolean mTriggered;
+    // If mCurrentState is SCANNING, then this is the frame number of the
+    // trigger start.
+    private long mTriggerFrameNumber;
+
+    /**
+     * @param scanCompleteUpdatable The {@link Updatable} to be notified when an
+     *            AF scan has been completed.
+     */
+    public TriggeredAFScanStateResponseListener(Updatable<Void> scanCompleteUpdatable) {
+        mScanCompleteUpdatable = scanCompleteUpdatable;
+        mTriggered = false;
+    }
+
+    @Override
+    public void onProgressed(CaptureResult result) {
+        processUpdate(
+                result.getRequest().get(CaptureRequest.CONTROL_AF_TRIGGER),
+                result.get(CaptureResult.CONTROL_AF_STATE),
+                result.getFrameNumber());
+    }
+
+    @Override
+    public void onCompleted(TotalCaptureResult result) {
+        processUpdate(
+                result.getRequest().get(CaptureRequest.CONTROL_AF_TRIGGER),
+                result.get(CaptureResult.CONTROL_AF_STATE),
+                result.getFrameNumber());
+    }
+
+    private void processUpdate(Integer afTrigger, Integer afState, long frameNumber) {
+        if (!mTriggered) {
+            if (afTrigger != null) {
+                if (afTrigger == CaptureRequest.CONTROL_AF_TRIGGER_START) {
+                    mTriggered = true;
+                    mTriggerFrameNumber = frameNumber;
+                }
+            }
+        } else {
+            // Only process results in-order. That is, only transition from
+            // SCANNING to IDLE if a result for a frame *after* the trigger
+            // indicates that the AF system has stopped scanning.
+            if (frameNumber > mTriggerFrameNumber) {
+                if (afState != null) {
+                    if (afState == CaptureResult.CONTROL_AF_STATE_INACTIVE ||
+                            afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
+                            afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
+                        mTriggered = false;
+                        mScanCompleteUpdatable.update(null);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/one/v2/sharedimagereader/BoundedImageBufferQueue.java b/src/com/android/camera/one/v2/sharedimagereader/BoundedImageBufferQueue.java
index ce1e893..ca090cc 100644
--- a/src/com/android/camera/one/v2/sharedimagereader/BoundedImageBufferQueue.java
+++ b/src/com/android/camera/one/v2/sharedimagereader/BoundedImageBufferQueue.java
@@ -23,6 +23,7 @@
 import com.android.camera.async.BoundedBufferQueue;
 import com.android.camera.async.ConcurrentBufferQueue;
 import com.android.camera.async.BufferQueueController;
+import com.android.camera.one.v2.camera2proxy.ForwardingImageProxy;
 import com.android.camera.one.v2.camera2proxy.ImageProxy;
 
 /**
@@ -66,7 +67,7 @@
      * An {@link ImageProxy} which overrides close() to release the logical
      * ticket associated with the image.
      */
-    private class TicketReleasingImageProxy extends ImageProxy {
+    private class TicketReleasingImageProxy extends ForwardingImageProxy {
         private final AtomicBoolean mClosed;
 
         public TicketReleasingImageProxy(ImageProxy image) {
@@ -187,7 +188,7 @@
     }
 
     @Override
-    public void append(ImageProxy image) {
+    public void update(ImageProxy image) {
         synchronized (mLock) {
             if (mClosed) {
                 image.close();
@@ -200,7 +201,7 @@
             }
 
             mTicketsAvailable--;
-            mImageSequence.append(new TicketReleasingImageProxy(image));
+            mImageSequence.update(new TicketReleasingImageProxy(image));
         }
     }
 
diff --git a/src/com/android/camera/one/v2/sharedimagereader/ImageDistributor.java b/src/com/android/camera/one/v2/sharedimagereader/ImageDistributor.java
index 6ce1908..01ba3f9 100644
--- a/src/com/android/camera/one/v2/sharedimagereader/ImageDistributor.java
+++ b/src/com/android/camera/one/v2/sharedimagereader/ImageDistributor.java
@@ -109,7 +109,6 @@
             while (mGlobalTimestampBufferQueue.getNext() <= timestamp) {
             }
         } catch (InterruptedException e) {
-            e.printStackTrace();
             image.close();
             return;
         } catch (BufferQueue.BufferQueueClosedException e) {
@@ -151,7 +150,7 @@
             // before the underlying reference count is decremented, regardless
             // of how many times it is closed from each stream.
             ImageProxy singleCloseImage = new SingleCloseImageProxy(sharedImage);
-            outputStream.append(singleCloseImage);
+            outputStream.update(singleCloseImage);
         }
     }
 
diff --git a/src/com/android/camera/one/v2/sharedimagereader/ImageDistributorOnImageAvailableListener.java b/src/com/android/camera/one/v2/sharedimagereader/ImageDistributorOnImageAvailableListener.java
new file mode 100644
index 0000000..0d39967
--- /dev/null
+++ b/src/com/android/camera/one/v2/sharedimagereader/ImageDistributorOnImageAvailableListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.android.camera.one.v2.sharedimagereader;
+
+import android.media.ImageReader;
+
+import com.android.camera.one.v2.camera2proxy.AndroidImageProxy;
+
+/**
+ * Connects an {@link ImageReader} to an {@link ImageDistributor} with a new
+ * thread to handle image availability callbacks.
+ */
+public class ImageDistributorOnImageAvailableListener implements
+        ImageReader.OnImageAvailableListener {
+    private final ImageDistributor mImageDistributor;
+
+    public ImageDistributorOnImageAvailableListener(ImageDistributor imageDistributor) {
+        mImageDistributor = imageDistributor;
+    }
+
+    @Override
+    public void onImageAvailable(ImageReader imageReader) {
+        mImageDistributor.distributeImage(new AndroidImageProxy(imageReader.acquireNextImage()));
+    }
+}
diff --git a/src/com/android/camera/one/v2/sharedimagereader/SharedImageReader.java b/src/com/android/camera/one/v2/sharedimagereader/SharedImageReader.java
index fe340d4..0249790 100644
--- a/src/com/android/camera/one/v2/sharedimagereader/SharedImageReader.java
+++ b/src/com/android/camera/one/v2/sharedimagereader/SharedImageReader.java
@@ -24,6 +24,7 @@
 import android.media.ImageReader;
 import android.view.Surface;
 
+import com.android.camera.async.BlockingCloseable;
 import com.android.camera.async.RefCountedBufferQueueController;
 import com.android.camera.async.BufferQueue;
 import com.android.camera.one.v2.camera2proxy.ImageProxy;
@@ -33,7 +34,7 @@
  * Enables the creation of multiple logical image streams, each with their own
  * guaranteed capacity, over a single ImageReader.
  */
-public class SharedImageReader implements AutoCloseable {
+public class SharedImageReader implements BlockingCloseable {
     public class ImageCaptureBufferQueue implements CaptureStream, BufferQueue<ImageProxy> {
         private final int mCapacity;
         private final BoundedImageBufferQueue mImageStream;
diff --git a/src/com/android/camera/one/v2/components/ImageSaver.java b/src/com/android/camera/util/ScopedFactory.java
similarity index 66%
rename from src/com/android/camera/one/v2/components/ImageSaver.java
rename to src/com/android/camera/util/ScopedFactory.java
index 7138029..a113f75 100644
--- a/src/com/android/camera/one/v2/components/ImageSaver.java
+++ b/src/com/android/camera/util/ScopedFactory.java
@@ -13,17 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package com.android.camera.one.v2.components;
-
-import com.android.camera.one.v2.camera2proxy.ImageProxy;
+package com.android.camera.util;
 
 /**
- * Interface for an image-saving object.
+ * A factory which returns a TResult when TScope is available.
  */
-public interface ImageSaver {
-    /**
-     * Implementations should save the image to disk and close it.
-     */
-    public void saveAndCloseImage(ImageProxy image);
+public interface ScopedFactory<TScope, TResult> {
+    public TResult get(TScope scope);
 }