merge in lmp-release history after reset to 47497bee8a3550af79f9a32c9362d7ee78364a71
diff --git a/camera2/portability/portability.mk b/camera2/portability/portability.mk
index a86c511..2ecc1df 100644
--- a/camera2/portability/portability.mk
+++ b/camera2/portability/portability.mk
@@ -19,5 +19,6 @@
 LOCAL_MODULE_TAGS := optional
 LOCAL_SDK_VERSION := current
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2-utils
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
index 74e76a3..65047f1 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
@@ -18,6 +18,7 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
+import android.graphics.ImageFormat;
 import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraAccessException;
@@ -30,6 +31,8 @@
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
 import android.hardware.camera2.params.MeteringRectangle;
+import android.media.Image;
+import android.media.ImageReader;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -38,7 +41,9 @@
 import android.view.Surface;
 
 import com.android.ex.camera2.portability.debug.Log;
+import com.android.ex.camera2.utils.Camera2RequestSettingsSet;
 
+import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -148,6 +153,10 @@
         return mDispatchThread;
     }
 
+    private static abstract class CaptureAvailableListener
+            extends CameraCaptureSession.CaptureListener
+            implements ImageReader.OnImageAvailableListener {};
+
     private class Camera2Handler extends HistoryHandler {
         // Caller-provided when leaving CAMERA_UNOPENED state:
         private CameraOpenCallback mOpenCallback;
@@ -157,8 +166,9 @@
         // Available in CAMERA_UNCONFIGURED state and above:
         private CameraDevice mCamera;
         private AndroidCamera2ProxyImpl mCameraProxy;
-        private CaptureRequest.Builder mPersistentRequestBuilder;
+        private Camera2RequestSettingsSet mPersistentSettings;
         private Rect mActiveArray;
+        private boolean mLegacyDevice;
 
         // Available in CAMERA_CONFIGURED state and above:
         private Size mPreviewSize;
@@ -168,6 +178,7 @@
         private SurfaceTexture mPreviewTexture;
         private Surface mPreviewSurface;
         private CameraCaptureSession mSession;
+        private ImageReader mCaptureReader;
 
         // Available from the beginning of PREVIEW_ACTIVE until the first preview frame arrives:
         private CameraStartPreviewCallback mOneshotPreviewingCallback;
@@ -175,6 +186,9 @@
         // Available in FOCUS_LOCKED between AF trigger receipt and whenever the lens stops moving:
         private CameraAFCallback mOneshotAfCallback;
 
+        // Available when taking picture between AE trigger receipt and autoexposure convergence
+        private CaptureAvailableListener mOneshotCaptureCallback;
+
         // Available whenever setAutoFocusMoveCallback() was last invoked with a non-null argument:
         private CameraAFMoveCallback mPassiveAfCallback;
 
@@ -226,13 +240,17 @@
                             mCamera = null;
                         }
                         mCameraProxy = null;
-                        mPersistentRequestBuilder = null;
+                        mPersistentSettings = null;
                         mActiveArray = null;
                         if (mPreviewSurface != null) {
                             mPreviewSurface.release();
                             mPreviewSurface = null;
                         }
                         mPreviewTexture = null;
+                        if (mCaptureReader != null) {
+                            mCaptureReader.close();
+                            mCaptureReader = null;
+                        }
                         mPreviewSize = null;
                         mPhotoSize = null;
                         mCameraIndex = 0;
@@ -265,8 +283,10 @@
                         mOneshotPreviewingCallback = (CameraStartPreviewCallback) msg.obj;
                         mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
                         try {
-                        mSession.setRepeatingRequest(mPersistentRequestBuilder.build(),
-                                /*listener*/mCameraFocusStateListener, /*handler*/this);
+                            mSession.setRepeatingRequest(
+                                    mPersistentSettings.createRequest(mCamera,
+                                            CameraDevice.TEMPLATE_PREVIEW, mPreviewSurface),
+                                    /*listener*/mCameraFocusStateListener, /*handler*/this);
                         } catch(CameraAccessException ex) {
                             Log.w(TAG, "Unable to start preview", ex);
                             mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
@@ -319,7 +339,7 @@
                     }*/
 
                     case CameraActions.APPLY_SETTINGS: {
-                        CameraSettings settings = (CameraSettings) msg.obj;
+                        AndroidCamera2Settings settings = (AndroidCamera2Settings) msg.obj;
                         applyToRequest(settings);
                         break;
                     }
@@ -365,17 +385,19 @@
 
                         // Send a one-time capture to trigger the camera driver to lock focus.
                         mCameraState.setState(AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
-                        mPersistentRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+                        Camera2RequestSettingsSet trigger =
+                                new Camera2RequestSettingsSet(mPersistentSettings);
+                        trigger.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                 CaptureRequest.CONTROL_AF_TRIGGER_START);
                         try {
-                            mSession.capture(mPersistentRequestBuilder.build(),
+                            mSession.capture(
+                                    trigger.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW,
+                                            mPreviewSurface),
                                     /*listener*/deferredCallbackSetter, /*handler*/ this);
                         } catch(CameraAccessException ex) {
                             Log.e(TAG, "Unable to lock autofocus", ex);
                             mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
                         }
-                        mPersistentRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
-                                CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
                         break;
                     }
 
@@ -389,18 +411,20 @@
 
                         // Send a one-time capture to trigger the camera driver to resume scanning.
                         mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
-                        mPersistentRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+                        Camera2RequestSettingsSet cancel =
+                                new Camera2RequestSettingsSet(mPersistentSettings);
+                        cancel.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                 CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
                         try {
-                            mSession.capture(mPersistentRequestBuilder.build(),
+                            mSession.capture(
+                                    cancel.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW,
+                                            mPreviewSurface),
                                     /*listener*/null, /*handler*/this);
                         } catch(CameraAccessException ex) {
                             Log.e(TAG, "Unable to cancel autofocus", ex);
                             mCameraState.setState(
                                     AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
                         }
-                        mPersistentRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
-                                CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
                         break;
                     }
 
@@ -431,15 +455,78 @@
 
                     case CameraActions.ENABLE_SHUTTER_SOUND: {
                         break;
-                    }
+                    }*/
 
                     case CameraActions.SET_DISPLAY_ORIENTATION: {
+                        // TODO: Need to handle preview in addition to capture
+                        // Only set the JPEG capture orientation if requested to do so; otherwise,
+                        // capture in the sensor's physical orientation
+                        mPersistentSettings.set(CaptureRequest.JPEG_ORIENTATION, msg.arg2 > 0 ?
+                                mCameraProxy.getCharacteristics().getJpegOrientation(msg.arg1) : 0);
                         break;
                     }
 
                     case CameraActions.CAPTURE_PHOTO: {
+                        if (mCameraState.getState() <
+                                        AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) {
+                            Log.e(TAG, "Photos may only be taken when a preview is active");
+                            break;
+                        }
+                        if (mCameraState.getState() !=
+                                AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED) {
+                            Log.w(TAG, "Taking a (likely blurry) photo without the lens locked");
+                        }
+
+                        final CaptureAvailableListener listener =
+                                (CaptureAvailableListener) msg.obj;
+                        if (mLegacyDevice) {
+                            // Just snap the shot
+                            mCaptureReader.setOnImageAvailableListener(listener, /*handler*/this);
+                            try {
+                                mSession.capture(
+                                        mPersistentSettings.createRequest(mCamera,
+                                                CameraDevice.TEMPLATE_STILL_CAPTURE,
+                                                mCaptureReader.getSurface()),
+                                        listener, /*handler*/this);
+                            } catch (CameraAccessException ex) {
+                                Log.e(TAG, "Unable to initiate legacy capture", ex);
+                            }
+                        } else {
+                            // Not a legacy device, so we need to let AE converge before capturing
+                            CameraCaptureSession.CaptureListener deferredCallbackSetter =
+                                    new CameraCaptureSession.CaptureListener() {
+                                @Override
+                                public void onCaptureCompleted(CameraCaptureSession session,
+                                                               CaptureRequest request,
+                                                               TotalCaptureResult result) {
+                                    mOneshotCaptureCallback = listener;
+                                }
+
+                                @Override
+                                public void onCaptureFailed(CameraCaptureSession session,
+                                                            CaptureRequest request,
+                                                            CaptureFailure failure) {
+                                    Log.e(TAG, "Autoexposure and capture failed with reason " +
+                                            failure.getReason());
+                                    // TODO: Make an error callback?
+                                }};
+
+                            // Set a one-time capture to trigger the camera driver's autoexposure:
+                            Camera2RequestSettingsSet expose =
+                                    new Camera2RequestSettingsSet(mPersistentSettings);
+                            expose.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+                            try {
+                                mSession.capture(
+                                        expose.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW,
+                                                mPreviewSurface),
+                                        /*listener*/deferredCallbackSetter, /*handler*/this);
+                            } catch (CameraAccessException ex) {
+                                Log.e(TAG, "Unable to run autoexposure and perform capture", ex);
+                            }
+                        }
                         break;
-                    }*/
+                    }
 
                     default: {
                         // TODO: Rephrase once everything has been implemented
@@ -474,8 +561,13 @@
         }
 
         public CameraSettings buildSettings(AndroidCamera2Capabilities caps) {
-            return new AndroidCamera2Settings(caps, mPersistentRequestBuilder, mPreviewSize,
-                    mPhotoSize);
+            try {
+                return new AndroidCamera2Settings(mCamera, CameraDevice.TEMPLATE_PREVIEW,
+                        mActiveArray, mPreviewSize, mPhotoSize);
+            } catch (CameraAccessException ex) {
+                Log.e(TAG, "Unable to query camera device to build settings representation");
+                return null;
+            }
         }
 
         /**
@@ -487,34 +579,19 @@
          *
          * @param settings The new/updated settings
          */
-        // TODO: Finish implementation to add support for all settings
-        private void applyToRequest(CameraSettings settings) {
+        private void applyToRequest(AndroidCamera2Settings settings) {
             // TODO: If invoked when in PREVIEW_READY state, a new preview size will not take effect
-            AndroidCamera2Capabilities.IntegralStringifier intifier =
-                    mCameraProxy.getSpecializedCapabilities().getIntegralStringifier();
+
+            mPersistentSettings.union(settings.getRequestSettings());
             mPreviewSize = settings.getCurrentPreviewSize();
             mPhotoSize = settings.getCurrentPhotoSize();
-            mPersistentRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
-                    intifier.intify(settings.getCurrentFocusMode()));
-
-            mPersistentRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS,
-                    legacyAreasToMeteringRectangles(settings.getFocusAreas()));
-            mPersistentRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS,
-                    legacyAreasToMeteringRectangles(settings.getMeteringAreas()));
-
-            if (settings.getCurrentFlashMode() != CameraCapabilities.FlashMode.NO_FLASH) {
-                mPersistentRequestBuilder.set(CaptureRequest.FLASH_MODE,
-                        intifier.intify(settings.getCurrentFlashMode()));
-            }
-            if (settings.getCurrentSceneMode() != CameraCapabilities.SceneMode.NO_SCENE_MODE) {
-                mPersistentRequestBuilder.set(CaptureRequest.CONTROL_SCENE_MODE,
-                        intifier.intify(settings.getCurrentSceneMode()));
-            }
 
             if (mCameraState.getState() >= AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) {
                 // If we're already previewing, reflect most settings immediately
                 try {
-                    mSession.setRepeatingRequest(mPersistentRequestBuilder.build(),
+                    mSession.setRepeatingRequest(
+                            mPersistentSettings.createRequest(mCamera,
+                                    CameraDevice.TEMPLATE_PREVIEW, mPreviewSurface),
                             /*listener*/mCameraFocusStateListener, /*handler*/this);
                 } catch (CameraAccessException ex) {
                     Log.e(TAG, "Failed to apply updated request settings", ex);
@@ -525,46 +602,6 @@
             }
         }
 
-        private MeteringRectangle[] legacyAreasToMeteringRectangles(
-                List<android.hardware.Camera.Area> reference) {
-            MeteringRectangle[] transformed = null;
-            if (reference.size() > 0) {
-
-                transformed = new MeteringRectangle[reference.size()];
-                for (int index = 0; index < reference.size(); ++index) {
-                    android.hardware.Camera.Area source = reference.get(index);
-                    Rect rectangle = source.rect;
-
-                    // Old API coordinates were [-1000,1000]; new ones are [0,ACTIVE_ARRAY_SIZE).
-                    double oldLeft = (rectangle.left + 1000) / 2000.0;
-                    double oldTop = (rectangle.top + 1000) / 2000.0;
-                    double oldRight = (rectangle.right + 1000) / 2000.0;
-                    double oldBottom = (rectangle.bottom + 1000) / 2000.0;
-                    int left = toIntConstrained(
-                            mActiveArray.width() * oldLeft + mActiveArray.left,
-                            0, mActiveArray.width() - 1);
-                    int top = toIntConstrained(
-                            mActiveArray.height() * oldTop + mActiveArray.top,
-                            0, mActiveArray.height() - 1);
-                    int right = toIntConstrained(
-                            mActiveArray.width() * oldRight + mActiveArray.left,
-                            0, mActiveArray.width() - 1);
-                    int bottom = toIntConstrained(
-                            mActiveArray.height() * oldBottom + mActiveArray.top,
-                            0, mActiveArray.height() - 1);
-                    transformed[index] = new MeteringRectangle(left, top,
-                            right - left, bottom - top, source.weight);
-                }
-            }
-            return transformed;
-        }
-
-        private int toIntConstrained(double original, int min, int max) {
-            original = Math.max(original, min);
-            original = Math.min(original, max);
-            return (int) original;
-        }
-
         private void setPreviewTexture(SurfaceTexture surfaceTexture) {
             // TODO: Must be called after providing a .*Settings populated with sizes
             // TODO: We don't technically offer a selection of sizes tailored to SurfaceTextures!
@@ -589,14 +626,19 @@
             surfaceTexture.setDefaultBufferSize(mPreviewSize.width(), mPreviewSize.height());
 
             if (mPreviewSurface != null) {
-                mPersistentRequestBuilder.removeTarget(mPreviewSurface);
                 mPreviewSurface.release();
             }
             mPreviewSurface = new Surface(surfaceTexture);
-            mPersistentRequestBuilder.addTarget(mPreviewSurface);
+
+            if (mCaptureReader != null) {
+                mCaptureReader.close();
+            }
+            mCaptureReader = ImageReader.newInstance(
+                    mPhotoSize.width(), mPhotoSize.height(), ImageFormat.JPEG, 1);
 
             try {
-                mCamera.createCaptureSession(Arrays.asList(mPreviewSurface),
+                mCamera.createCaptureSession(
+                        Arrays.asList(mPreviewSurface, mCaptureReader.getSurface()),
                         mCameraPreviewStateListener, this);
             } catch (CameraAccessException ex) {
                 Log.e(TAG, "Failed to create camera capture session", ex);
@@ -625,10 +667,12 @@
                                 mCameraManager.getCameraCharacteristics(mCameraId);
                         mCameraProxy = new AndroidCamera2ProxyImpl(mCameraIndex, mCamera,
                                     getCameraDeviceInfo().getCharacteristics(mCameraIndex), props);
-                        mPersistentRequestBuilder =
-                                camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+                        mPersistentSettings = new Camera2RequestSettingsSet();
                         mActiveArray =
                                 props.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+                        mLegacyDevice =
+                                props.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
+                                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
                         mCameraState.setState(AndroidCamera2StateHolder.CAMERA_UNCONFIGURED);
                         mOpenCallback.onCameraOpened(mCameraProxy);
                     } catch (CameraAccessException ex) {
@@ -723,6 +767,37 @@
                         }
                     }
                 }
+
+                Integer aeStateMaybe = result.get(CaptureResult.CONTROL_AE_STATE);
+                if (aeStateMaybe != null) {
+                    int aeState = aeStateMaybe;
+
+                    switch (aeState) {
+                        case CaptureResult.CONTROL_AE_STATE_CONVERGED:
+                        case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:
+                        case CaptureResult.CONTROL_AE_STATE_LOCKED: {
+                            if (mOneshotCaptureCallback != null) {
+                                // A call to takePicture() was just made, and autoexposure converged
+                                // so it's time to initiate the capture!
+                                mCaptureReader.setOnImageAvailableListener(mOneshotCaptureCallback,
+                                        /*handler*/Camera2Handler.this);
+                                try {
+                                    mSession.capture(
+                                            mPersistentSettings.createRequest(mCamera,
+                                                    CameraDevice.TEMPLATE_STILL_CAPTURE,
+                                                    mCaptureReader.getSurface()),
+                                            /*listener*/mOneshotCaptureCallback,
+                                            /*handler*/Camera2Handler.this);
+                                } catch (CameraAccessException ex) {
+                                    Log.e(TAG, "Unable to initiate capture", ex);
+                                } finally {
+                                    mOneshotCaptureCallback = null;
+                                }
+                            }
+                            break;
+                        }
+                    }
+                }
             }
 
             @Override
@@ -836,15 +911,50 @@
 
         // TODO: Implement
         @Override
-        public void takePicture(Handler handler,
-                                CameraShutterCallback shutter,
+        public void takePicture(final Handler handler,
+                                final CameraShutterCallback shutter,
                                 CameraPictureCallback raw,
                                 CameraPictureCallback postview,
-                                CameraPictureCallback jpeg) {}
+                                final CameraPictureCallback jpeg) {
+            // TODO: We never call raw or postview
+            final CaptureAvailableListener picListener =
+                    new CaptureAvailableListener() {
+                @Override
+                public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request,
+                                             long timestamp) {
+                    if (shutter != null) {
+                        handler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                shutter.onShutter(AndroidCamera2ProxyImpl.this);
+                            }});
+                    }
+                }
 
-        // TODO: Remove this method override once we handle the message
-        @Override
-        public void setDisplayOrientation(int degrees) {}
+                @Override
+                public void onImageAvailable(ImageReader reader) {
+                    try (Image image = reader.acquireNextImage()) {
+                        if (jpeg != null) {
+                            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
+                            final byte[] pixels = new byte[buffer.remaining()];
+                            buffer.get(pixels);
+                            handler.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    jpeg.onPictureTaken(pixels, AndroidCamera2ProxyImpl.this);
+                                }});
+                        }
+                    }
+                }};
+            mDispatchThread.runJob(new Runnable() {
+                @Override
+                public void run() {
+                    mCameraState.waitForStates(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE |
+                            AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
+                    mCameraHandler.obtainMessage(CameraActions.CAPTURE_PHOTO, picListener)
+                            .sendToTarget();
+                }});
+        }
 
         // TODO: Implement
         @Override
@@ -882,6 +992,15 @@
 
         @Override
         public boolean applySettings(CameraSettings settings) {
+            if (settings == null) {
+                Log.w(TAG, "null parameters in applySettings()");
+                return false;
+            }
+            if (!(settings instanceof AndroidCamera2Settings)) {
+                Log.e(TAG, "Provided settings not compatible with the backing framework API");
+                return false;
+            }
+
             return applySettingsHelper(settings, AndroidCamera2StateHolder.CAMERA_UNCONFIGURED |
                     AndroidCamera2StateHolder.CAMERA_CONFIGURED |
                     AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
@@ -995,7 +1114,7 @@
             return mFirstFrontCameraId;
         }
 
-        private static class AndroidCharacteristics2 implements Characteristics {
+        private static class AndroidCharacteristics2 extends Characteristics {
             private CameraCharacteristics mCameraInfo;
 
             AndroidCharacteristics2(CameraCharacteristics cameraInfo) {
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java
index b81e098..4bdbe64 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java
@@ -38,11 +38,8 @@
 public class AndroidCamera2Capabilities extends CameraCapabilities {
     private static Log.Tag TAG = new Log.Tag("AndCam2Capabs");
 
-    private IntegralStringifier mIntStringifier;
-
     AndroidCamera2Capabilities(CameraCharacteristics p) {
-        super(new IntegralStringifier());
-        mIntStringifier = (IntegralStringifier) getStringifier();
+        super(new Stringifier());
 
         StreamConfigurationMap s = p.get(SCALER_STREAM_CONFIGURATION_MAP);
 
@@ -99,18 +96,18 @@
         if (mMaxNumOfMeteringArea > 0) {
             mSupportedFeatures.add(Feature.METERING_AREA);
         }
-    }
 
-    public IntegralStringifier getIntegralStringifier() {
-        return mIntStringifier;
+        // TODO: Detect other features
     }
 
     private void buildSceneModes(CameraCharacteristics p) {
-        for (int scene : p.get(CONTROL_AVAILABLE_SCENE_MODES)) {
-            SceneMode equiv = mIntStringifier.sceneModeFromInt(scene);
-            if (equiv != SceneMode.NO_SCENE_MODE) {
-                // equiv isn't a default generated because we couldn't handle this mode, so add it
-                mSupportedSceneModes.add(equiv);
+        int[] scenes = p.get(CONTROL_AVAILABLE_SCENE_MODES);
+        if (scenes != null) {
+            for (int scene : scenes) {
+                SceneMode equiv = sceneModeFromInt(scene);
+                if (equiv != null) {
+                    mSupportedSceneModes.add(equiv);
+                }
             }
         }
     }
@@ -118,275 +115,145 @@
     private void buildFlashModes(CameraCharacteristics p) {
         mSupportedFlashModes.add(FlashMode.OFF);
         if (p.get(FLASH_INFO_AVAILABLE)) {
+            mSupportedFlashModes.add(FlashMode.AUTO);
             mSupportedFlashModes.add(FlashMode.ON);
             mSupportedFlashModes.add(FlashMode.TORCH);
-            // TODO: New modes aren't represented here
+            for (int expose : p.get(CONTROL_AE_AVAILABLE_MODES)) {
+                if (expose == CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE) {
+                    mSupportedFlashModes.add(FlashMode.RED_EYE);
+                }
+            }
         }
     }
 
     private void buildFocusModes(CameraCharacteristics p) {
-        for (int focus : p.get(CONTROL_AF_AVAILABLE_MODES)) {
-            FocusMode equiv = mIntStringifier.focusModeFromInt(focus);
-            if (equiv != FocusMode.AUTO || focus == CONTROL_AF_MODE_AUTO) {
-                // equiv isn't a default generated because we couldn't handle this mode, so add it
-                mSupportedFocusModes.add(equiv);
+        int[] focuses = p.get(CONTROL_AF_AVAILABLE_MODES);
+        if (focuses != null) {
+            for (int focus : focuses) {
+                FocusMode equiv = focusModeFromInt(focus);
+                if (equiv != null) {
+                    mSupportedFocusModes.add(equiv);
+                }
             }
         }
     }
 
     private void buildWhiteBalances(CameraCharacteristics p) {
-        for (int bal : p.get(CONTROL_AWB_AVAILABLE_MODES)) {
-            WhiteBalance equiv = mIntStringifier.whiteBalanceFromInt(bal);
-            if (equiv != WhiteBalance.AUTO || bal == CONTROL_AWB_MODE_AUTO) {
-                // equiv isn't a default generated because we couldn't handle this mode, so add it
-                mSupportedWhiteBalances.add(equiv);
+        int[] bals = p.get(CONTROL_AWB_AVAILABLE_MODES);
+        if (bals != null) {
+            for (int bal : bals) {
+                WhiteBalance equiv = whiteBalanceFromInt(bal);
+                if (equiv != null) {
+                    mSupportedWhiteBalances.add(equiv);
+                }
             }
         }
     }
 
-    public static class IntegralStringifier extends Stringifier {
-        /**
-         * Converts the focus mode to API-related integer representation.
-         *
-         * @param fm The focus mode to convert.
-         * @return The corresponding {@code int} used by the camera framework
-         *         API, or {@link CONTROL_AF_MODE_AUTO} if that fails.
-         */
-        public int intify(FocusMode fm) {
-            switch (fm) {
-                case AUTO:
-                    return CONTROL_AF_MODE_AUTO;
-                case CONTINUOUS_PICTURE:
-                    return CONTROL_AF_MODE_CONTINUOUS_PICTURE;
-                case CONTINUOUS_VIDEO:
-                    return CONTROL_AF_MODE_CONTINUOUS_VIDEO;
-                case EXTENDED_DOF:
-                    return CONTROL_AF_MODE_EDOF;
-                case FIXED:
-                    return CONTROL_AF_MODE_OFF;
-                case MACRO:
-                    return CONTROL_AF_MODE_MACRO;
-                // TODO: New modes aren't represented here
-            }
-            return CONTROL_AF_MODE_AUTO;
+    /**
+     * Converts the API-related integer representation of the focus mode to the
+     * abstract representation.
+     *
+     * @param fm The integral representation.
+     * @return The mode represented by the input integer, or {@code null} if it
+     *         cannot be converted.
+     */
+    public static FocusMode focusModeFromInt(int fm) {
+        switch (fm) {
+            case CONTROL_AF_MODE_AUTO:
+                return FocusMode.AUTO;
+            case CONTROL_AF_MODE_CONTINUOUS_PICTURE:
+                return FocusMode.CONTINUOUS_PICTURE;
+            case CONTROL_AF_MODE_CONTINUOUS_VIDEO:
+                return FocusMode.CONTINUOUS_VIDEO;
+            case CONTROL_AF_MODE_EDOF:
+                return FocusMode.EXTENDED_DOF;
+            case CONTROL_AF_MODE_OFF:
+                return FocusMode.FIXED;
+            // TODO: We cannot support INFINITY
+            case CONTROL_AF_MODE_MACRO:
+                return FocusMode.MACRO;
         }
+        Log.w(TAG, "Unable to convert from API 2 focus mode: " + fm);
+        return null;
+    }
 
-        /**
-         * Converts the API-related integer representation of the focus mode to
-         * the abstract representation.
-         *
-         * @param val The integral representation.
-         * @return The mode represented by the input integer, or the focus mode
-         *         with the lowest ordinal if it cannot be converted.
-         */
-        public FocusMode focusModeFromInt(int fm) {
-            switch (fm) {
-                case CONTROL_AF_MODE_AUTO:
-                    return FocusMode.AUTO;
-                case CONTROL_AF_MODE_CONTINUOUS_PICTURE:
-                    return FocusMode.CONTINUOUS_PICTURE;
-                case CONTROL_AF_MODE_CONTINUOUS_VIDEO:
-                    return FocusMode.CONTINUOUS_VIDEO;
-                case CONTROL_AF_MODE_EDOF:
-                    return FocusMode.EXTENDED_DOF;
-                case CONTROL_AF_MODE_OFF:
-                    return FocusMode.FIXED;
-                case CONTROL_AF_MODE_MACRO:
-                    return FocusMode.MACRO;
-                // TODO: New modes aren't represented here
-            }
-            return FocusMode.values()[0];
+    /**
+     * Converts the API-related integer representation of the scene mode to the
+     * abstract representation.
+     *
+     * @param sm The integral representation.
+     * @return The mode represented by the input integer, or {@code null} if it
+     *         cannot be converted.
+     */
+    public static SceneMode sceneModeFromInt(int sm) {
+        switch (sm) {
+            case CONTROL_SCENE_MODE_DISABLED:
+                return SceneMode.AUTO;
+            case CONTROL_SCENE_MODE_ACTION:
+                return SceneMode.ACTION;
+            case CONTROL_SCENE_MODE_BARCODE:
+                return SceneMode.BARCODE;
+            case CONTROL_SCENE_MODE_BEACH:
+                return SceneMode.BEACH;
+            case CONTROL_SCENE_MODE_CANDLELIGHT:
+                return SceneMode.CANDLELIGHT;
+            case CONTROL_SCENE_MODE_FIREWORKS:
+                return SceneMode.FIREWORKS;
+            // TODO: We cannot support HDR
+            case CONTROL_SCENE_MODE_LANDSCAPE:
+                return SceneMode.LANDSCAPE;
+            case CONTROL_SCENE_MODE_NIGHT:
+                return SceneMode.NIGHT;
+            // TODO: We cannot support NIGHT_PORTRAIT
+            case CONTROL_SCENE_MODE_PARTY:
+                return SceneMode.PARTY;
+            case CONTROL_SCENE_MODE_PORTRAIT:
+                return SceneMode.PORTRAIT;
+            case CONTROL_SCENE_MODE_SNOW:
+                return SceneMode.SNOW;
+            case CONTROL_SCENE_MODE_SPORTS:
+                return SceneMode.SPORTS;
+            case CONTROL_SCENE_MODE_STEADYPHOTO:
+                return SceneMode.STEADYPHOTO;
+            case CONTROL_SCENE_MODE_SUNSET:
+                return SceneMode.SUNSET;
+            case CONTROL_SCENE_MODE_THEATRE:
+                return SceneMode.THEATRE;
+            // TODO: We cannot expose FACE_PRIORITY, or HIGH_SPEED_VIDEO
         }
+        Log.w(TAG, "Unable to convert from API 2 scene mode: " + sm);
+        return null;
+    }
 
-        /**
-         * Converts the flash mode to API-related integer representation.
-         *
-         * @param fm The flash mode to convert.
-         * @return The corresponding {@code int} used by the camera framework
-         *         API, or {@link CONTROL_AF_MODE_AUTO} if that fails.
-         */
-        public int intify(FlashMode flm) {
-            switch (flm) {
-                case OFF:
-                    return FLASH_MODE_OFF;
-                case ON:
-                    return FLASH_MODE_SINGLE;
-                case TORCH:
-                    return FLASH_MODE_TORCH;
-                // TODO: New modes aren't represented here
-            }
-            return FLASH_MODE_OFF;
+    /**
+     * Converts the API-related integer representation of the white balance to
+     * the abstract representation.
+     *
+     * @param wb The integral representation.
+     * @return The balance represented by the input integer, or {@code null} if
+     *         it cannot be converted.
+     */
+    public static WhiteBalance whiteBalanceFromInt(int wb) {
+        switch (wb) {
+            case CONTROL_AWB_MODE_AUTO:
+                return WhiteBalance.AUTO;
+            case CONTROL_AWB_MODE_CLOUDY_DAYLIGHT:
+                return WhiteBalance.CLOUDY_DAYLIGHT;
+            case CONTROL_AWB_MODE_DAYLIGHT:
+                return WhiteBalance.DAYLIGHT;
+            case CONTROL_AWB_MODE_FLUORESCENT:
+                return WhiteBalance.FLUORESCENT;
+            case CONTROL_AWB_MODE_INCANDESCENT:
+                return WhiteBalance.INCANDESCENT;
+            case CONTROL_AWB_MODE_SHADE:
+                return WhiteBalance.SHADE;
+            case CONTROL_AWB_MODE_TWILIGHT:
+                return WhiteBalance.TWILIGHT;
+            case CONTROL_AWB_MODE_WARM_FLUORESCENT:
+                return WhiteBalance.WARM_FLUORESCENT;
         }
-
-        /**
-         * Converts the API-related integer representation of the flash mode to
-         * the abstract representation.
-         *
-         * @param flm The integral representation.
-         * @return The mode represented by the input integer, or the flash mode
-         *         with the lowest ordinal if it cannot be converted.
-         */
-        public FlashMode flashModeFromInt(int flm) {
-            switch (flm) {
-                case FLASH_MODE_OFF:
-                    return FlashMode.OFF;
-                case FLASH_MODE_SINGLE:
-                    return FlashMode.ON;
-                case FLASH_MODE_TORCH:
-                    return FlashMode.TORCH;
-                // TODO: New modes aren't represented here
-            }
-            return FlashMode.values()[0];
-        }
-
-        /**
-         * Converts the scene mode to API-related integer representation.
-         *
-         * @param fm The scene mode to convert.
-         * @return The corresponding {@code int} used by the camera framework
-         *         API, or {@link CONTROL_SCENE_MODE_DISABLED} if that fails.
-         */
-        public int intify(SceneMode sm) {
-            switch (sm) {
-                case AUTO:
-                    return CONTROL_SCENE_MODE_DISABLED;
-                case ACTION:
-                    return CONTROL_SCENE_MODE_ACTION;
-                case BARCODE:
-                    return CONTROL_SCENE_MODE_BARCODE;
-                case BEACH:
-                    return CONTROL_SCENE_MODE_BEACH;
-                case CANDLELIGHT:
-                    return CONTROL_SCENE_MODE_CANDLELIGHT;
-                case FIREWORKS:
-                    return CONTROL_SCENE_MODE_FIREWORKS;
-                case LANDSCAPE:
-                    return CONTROL_SCENE_MODE_LANDSCAPE;
-                case NIGHT:
-                    return CONTROL_SCENE_MODE_NIGHT;
-                case PARTY:
-                    return CONTROL_SCENE_MODE_PARTY;
-                case PORTRAIT:
-                    return CONTROL_SCENE_MODE_PORTRAIT;
-                case SNOW:
-                    return CONTROL_SCENE_MODE_SNOW;
-                case SPORTS:
-                    return CONTROL_SCENE_MODE_SPORTS;
-                case STEADYPHOTO:
-                    return CONTROL_SCENE_MODE_STEADYPHOTO;
-                case SUNSET:
-                    return CONTROL_SCENE_MODE_SUNSET;
-                case THEATRE:
-                    return CONTROL_SCENE_MODE_THEATRE;
-                // TODO: New modes aren't represented here
-            }
-            return CONTROL_SCENE_MODE_DISABLED;
-        }
-
-        /**
-         * Converts the API-related integer representation of the scene mode to
-         * the abstract representation.
-         *
-         * @param sm The integral representation.
-         * @return The mode represented by the input integer, or the scene mode
-         *         with the lowest ordinal if it cannot be converted.
-         */
-        public SceneMode sceneModeFromInt(int sm) {
-            switch (sm) {
-                case CONTROL_SCENE_MODE_DISABLED:
-                    return SceneMode.AUTO;
-                case CONTROL_SCENE_MODE_ACTION:
-                    return SceneMode.ACTION;
-                case CONTROL_SCENE_MODE_BARCODE:
-                    return SceneMode.BARCODE;
-                case CONTROL_SCENE_MODE_BEACH:
-                    return SceneMode.BEACH;
-                case CONTROL_SCENE_MODE_CANDLELIGHT:
-                    return SceneMode.CANDLELIGHT;
-                case CONTROL_SCENE_MODE_FIREWORKS:
-                    return SceneMode.FIREWORKS;
-                case CONTROL_SCENE_MODE_LANDSCAPE:
-                    return SceneMode.LANDSCAPE;
-                case CONTROL_SCENE_MODE_NIGHT:
-                    return SceneMode.NIGHT;
-                case CONTROL_SCENE_MODE_PARTY:
-                    return SceneMode.PARTY;
-                case CONTROL_SCENE_MODE_PORTRAIT:
-                    return SceneMode.PORTRAIT;
-                case CONTROL_SCENE_MODE_SNOW:
-                    return SceneMode.SNOW;
-                case CONTROL_SCENE_MODE_SPORTS:
-                    return SceneMode.SPORTS;
-                case CONTROL_SCENE_MODE_STEADYPHOTO:
-                    return SceneMode.STEADYPHOTO;
-                case CONTROL_SCENE_MODE_SUNSET:
-                    return SceneMode.SUNSET;
-                case CONTROL_SCENE_MODE_THEATRE:
-                    return SceneMode.THEATRE;
-                // TODO: New modes aren't represented here
-            }
-            return SceneMode.values()[0];
-        }
-
-        /**
-         * Converts the white balance to API-related integer representation.
-         *
-         * @param fm The white balance to convert.
-         * @return The corresponding {@code int} used by the camera framework
-         *         API, or {@link CONTROL_SCENE_MODE_DISABLED} if that fails.
-         */
-        public int intify(WhiteBalance wb) {
-            switch (wb) {
-                case AUTO:
-                    return CONTROL_AWB_MODE_AUTO;
-                case CLOUDY_DAYLIGHT:
-                    return CONTROL_AWB_MODE_CLOUDY_DAYLIGHT;
-                case DAYLIGHT:
-                    return CONTROL_AWB_MODE_DAYLIGHT;
-                case FLUORESCENT:
-                    return CONTROL_AWB_MODE_FLUORESCENT;
-                case INCANDESCENT:
-                    return CONTROL_AWB_MODE_INCANDESCENT;
-                case SHADE:
-                    return CONTROL_AWB_MODE_SHADE;
-                case TWILIGHT:
-                    return CONTROL_AWB_MODE_TWILIGHT;
-                case WARM_FLUORESCENT:
-                    return CONTROL_AWB_MODE_WARM_FLUORESCENT;
-                // TODO: New modes aren't represented here
-            }
-            return CONTROL_AWB_MODE_AUTO;
-        }
-
-        /**
-         * Converts the API-related integer representation of the white balance
-         * to the abstract representation.
-         *
-         * @param wb The integral representation.
-         * @return The balance represented by the input integer, or the white
-         *         balance with the lowest ordinal if it cannot be converted.
-         */
-        public WhiteBalance whiteBalanceFromInt(int wb) {
-            switch (wb) {
-                case CONTROL_AWB_MODE_AUTO:
-                    return WhiteBalance.AUTO;
-                case CONTROL_AWB_MODE_CLOUDY_DAYLIGHT:
-                    return WhiteBalance.CLOUDY_DAYLIGHT;
-                case CONTROL_AWB_MODE_DAYLIGHT:
-                    return WhiteBalance.DAYLIGHT;
-                case CONTROL_AWB_MODE_FLUORESCENT:
-                    return WhiteBalance.FLUORESCENT;
-                case CONTROL_AWB_MODE_INCANDESCENT:
-                    return WhiteBalance.INCANDESCENT;
-                case CONTROL_AWB_MODE_SHADE:
-                    return WhiteBalance.SHADE;
-                case CONTROL_AWB_MODE_TWILIGHT:
-                    return WhiteBalance.TWILIGHT;
-                case CONTROL_AWB_MODE_WARM_FLUORESCENT:
-                    return WhiteBalance.WARM_FLUORESCENT;
-                // TODO: New modes aren't represented here
-            }
-            return WhiteBalance.values()[0];
-        }
+        Log.w(TAG, "Unable to convert from API 2 white balance: " + wb);
+        return null;
     }
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
index e21e177..288ded7 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
@@ -16,32 +16,449 @@
 
 package com.android.ex.camera2.portability;
 
-import android.hardware.camera2.CaptureRequest;
+import static android.hardware.camera2.CaptureRequest.*;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.util.Range;
+
+import com.android.ex.camera2.portability.CameraCapabilities.FlashMode;
+import com.android.ex.camera2.portability.CameraCapabilities.FocusMode;
+import com.android.ex.camera2.portability.CameraCapabilities.SceneMode;
+import com.android.ex.camera2.portability.CameraCapabilities.WhiteBalance;
+import com.android.ex.camera2.portability.debug.Log;
+import com.android.ex.camera2.utils.Camera2RequestSettingsSet;
+
+import java.util.List;
+import java.util.Objects;
 
 /**
  * The subclass of {@link CameraSettings} for Android Camera 2 API.
  */
 public class AndroidCamera2Settings extends CameraSettings {
-    // TODO: Implement more completely
-    public AndroidCamera2Settings(AndroidCamera2Capabilities capabilities,
-                                  CaptureRequest.Builder request,
-                                  Size preview, Size photo) {
-        // TODO: Support zoom
-        setZoomRatio(1.0f);
-        setZoomIndex(0);
+    private static final Log.Tag TAG = new Log.Tag("AndCam2Set");
 
-        // TODO: Set exposure compensation
+    private final Builder mTemplateSettings;
+    private final Rect mActiveArray;
+    private final Camera2RequestSettingsSet mRequestSettings;
 
-        AndroidCamera2Capabilities.IntegralStringifier stringifier =
-                capabilities.getIntegralStringifier();
-        setFocusMode(stringifier.focusModeFromInt(request.get(CaptureRequest.CONTROL_AF_MODE)));
-        setFlashMode(stringifier.flashModeFromInt(request.get(CaptureRequest.FLASH_MODE)));
-        setSceneMode(stringifier.sceneModeFromInt(request.get(CaptureRequest.CONTROL_SCENE_MODE)));
+    /**
+     * Create a settings representation that answers queries of unspecified
+     * options in the same way as the provided template would.
+     *
+     * <p>The default settings provided by the given template are only ever used
+     * for reporting back to the client app (i.e. when it queries an option
+     * it didn't explicitly set first). {@link Camera2RequestSettingsSet}s
+     * generated by an instance of this class will have any settings not
+     * modified using one of that instance's mutators forced to default, so that
+     * their effective values when submitting a capture request will be those of
+     * the template that is provided to the camera framework at that time.</p>
+     *
+     * @param camera Device from which to draw default settings.
+     * @param template Specific template to use for the defaults.
+     * @param activeArray Boundary coordinates of the sensor's active array.
+     * @param preview Dimensions of preview streams.
+     * @param photo Dimensions of captured images.
+     *
+     * @throws CameraAccessException Upon internal framework/driver failure.
+     */
+    public AndroidCamera2Settings(CameraDevice camera, int template, Rect activeArray,
+                                  Size preview, Size photo) throws CameraAccessException {
+        mTemplateSettings = camera.createCaptureRequest(template);
+        mActiveArray = activeArray;
+        mRequestSettings = new Camera2RequestSettingsSet();
 
-        // This is mutability-safe because those setters copy the Size objects
+        Range<Integer> previewFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE);
+        if (previewFpsRange != null) {
+            setPreviewFpsRange(previewFpsRange.getLower(), previewFpsRange.getUpper());
+        }
         setPreviewSize(preview);
+        // TODO: mCurrentPreviewFormat
         setPhotoSize(photo);
+        mJpegCompressQuality = queryTemplateDefaultOrMakeOneUp(JPEG_QUALITY, (byte) 0);
+        // TODO: mCurrentPhotoFormat
+        // TODO: mCurrentZoomRatio
+        mCurrentZoomRatio = 1.0f;
+        // TODO: mCurrentZoomIndex
+        mExposureCompensationIndex =
+                queryTemplateDefaultOrMakeOneUp(CONTROL_AE_EXPOSURE_COMPENSATION, 0);
+        mCurrentFlashMode = flashModeFromRequest();
+        mCurrentFocusMode = AndroidCamera2Capabilities.focusModeFromInt(
+                mTemplateSettings.get(CONTROL_AF_MODE));
+        mCurrentSceneMode = AndroidCamera2Capabilities.sceneModeFromInt(
+                mTemplateSettings.get(CONTROL_SCENE_MODE));
+        mWhiteBalance = AndroidCamera2Capabilities.whiteBalanceFromInt(
+                mTemplateSettings.get(CONTROL_AWB_MODE));
+        mVideoStabilizationEnabled = queryTemplateDefaultOrMakeOneUp(
+                        CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_OFF) ==
+                CONTROL_VIDEO_STABILIZATION_MODE_ON;
+        mAutoExposureLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AE_LOCK, false);
+        mAutoWhiteBalanceLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AWB_LOCK, false);
+        // TODO: mRecordingHintEnabled
+        // TODO: mGpsData
+        android.util.Size exifThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE);
+        if (exifThumbnailSize != null) {
+            mExifThumbnailSize =
+                    new Size(exifThumbnailSize.getWidth(), exifThumbnailSize.getHeight());
+        }
+    }
 
-        // TODO: Initialize formats, too
+    public AndroidCamera2Settings(AndroidCamera2Settings other) {
+        super(other);
+        mTemplateSettings = other.mTemplateSettings;
+        mActiveArray = other.mActiveArray;
+        mRequestSettings = new Camera2RequestSettingsSet(other.mRequestSettings);
+    }
+
+    @Override
+    public CameraSettings copy() {
+        return new AndroidCamera2Settings(this);
+    }
+
+    private <T> T queryTemplateDefaultOrMakeOneUp(Key<T> key, T defaultDefault) {
+        T val = mTemplateSettings.get(key);
+        if (val != null) {
+            return val;
+        } else {
+            // Spoof the default so matchesTemplateDefault excludes this key from generated sets.
+            // This approach beats a simple sentinel because it provides basic boolean support.
+            mTemplateSettings.set(key, defaultDefault);
+            return defaultDefault;
+        }
+    }
+
+    private FlashMode flashModeFromRequest() {
+        Integer autoExposure = mTemplateSettings.get(CONTROL_AE_MODE);
+        if (autoExposure != null) {
+            switch (autoExposure) {
+                case CONTROL_AE_MODE_ON:
+                    return FlashMode.OFF;
+                case CONTROL_AE_MODE_ON_AUTO_FLASH:
+                    return FlashMode.AUTO;
+                case CONTROL_AE_MODE_ON_ALWAYS_FLASH: {
+                    if (mTemplateSettings.get(FLASH_MODE) == FLASH_MODE_TORCH) {
+                        return FlashMode.TORCH;
+                    } else {
+                        return FlashMode.ON;
+                    }
+                }
+                case CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE:
+                    return FlashMode.RED_EYE;
+            }
+        }
+        return null;
+    }
+
+    private boolean matchesTemplateDefault(Key<?> setting) {
+        if (setting == CONTROL_AE_REGIONS) {
+            return mMeteringAreas.size() == 0;
+        } else if (setting == CONTROL_AF_REGIONS) {
+            return mFocusAreas.size() == 0;
+        } else if (setting == CONTROL_AE_TARGET_FPS_RANGE) {
+            Range defaultFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE);
+            return (mPreviewFpsRangeMin == 0 && mPreviewFpsRangeMax == 0) ||
+                    (defaultFpsRange != null && mPreviewFpsRangeMin == defaultFpsRange.getLower() &&
+                            mPreviewFpsRangeMax == defaultFpsRange.getUpper());
+        } else if (setting == JPEG_QUALITY) {
+            return Objects.equals(mJpegCompressQuality,
+                    mTemplateSettings.get(JPEG_QUALITY));
+        } else if (setting == CONTROL_AE_EXPOSURE_COMPENSATION) {
+            return Objects.equals(mExposureCompensationIndex,
+                    mTemplateSettings.get(CONTROL_AE_EXPOSURE_COMPENSATION));
+        } else if (setting == CONTROL_VIDEO_STABILIZATION_MODE) {
+            Integer videoStabilization = mTemplateSettings.get(CONTROL_VIDEO_STABILIZATION_MODE);
+            return (videoStabilization != null &&
+                    (mVideoStabilizationEnabled && videoStabilization ==
+                            CONTROL_VIDEO_STABILIZATION_MODE_ON) ||
+                    (!mVideoStabilizationEnabled && videoStabilization ==
+                            CONTROL_VIDEO_STABILIZATION_MODE_OFF));
+        } else if (setting == CONTROL_AE_LOCK) {
+            return Objects.equals(mAutoExposureLocked, mTemplateSettings.get(CONTROL_AE_LOCK));
+        } else if (setting == CONTROL_AWB_LOCK) {
+            return Objects.equals(mAutoWhiteBalanceLocked, mTemplateSettings.get(CONTROL_AWB_LOCK));
+        } else if (setting == JPEG_THUMBNAIL_SIZE) {
+            android.util.Size defaultThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE);
+            return (mExifThumbnailSize.width() == 0 && mExifThumbnailSize.height() == 0) ||
+                    (defaultThumbnailSize != null &&
+                            mExifThumbnailSize.width() == defaultThumbnailSize.getWidth() &&
+                            mExifThumbnailSize.height() == defaultThumbnailSize.getHeight());
+        }
+        Log.w(TAG, "Settings implementation checked default of unhandled option key");
+        // Since this class isn't equipped to handle it, claim it matches the default to prevent
+        // updateRequestSettingOrForceToDefault from going with the user-provided preference
+        return true;
+    }
+
+    private <T> void updateRequestSettingOrForceToDefault(Key<T> setting, T possibleChoice) {
+        mRequestSettings.set(setting, matchesTemplateDefault(setting) ? null : possibleChoice);
+    }
+
+    public Camera2RequestSettingsSet getRequestSettings() {
+        updateRequestSettingOrForceToDefault(CONTROL_AE_REGIONS,
+                legacyAreasToMeteringRectangles(mMeteringAreas));
+        updateRequestSettingOrForceToDefault(CONTROL_AF_REGIONS,
+                legacyAreasToMeteringRectangles(mFocusAreas));
+        updateRequestSettingOrForceToDefault(CONTROL_AE_TARGET_FPS_RANGE,
+                new Range(mPreviewFpsRangeMin, mPreviewFpsRangeMax));
+        // TODO: mCurrentPreviewFormat
+        updateRequestSettingOrForceToDefault(JPEG_QUALITY, mJpegCompressQuality);
+        // TODO: mCurrentPhotoFormat
+        // TODO: mCurrentZoomRatio
+        // TODO: mCurrentZoomIndex
+        updateRequestSettingOrForceToDefault(CONTROL_AE_EXPOSURE_COMPENSATION,
+                mExposureCompensationIndex);
+        updateRequestFlashMode();
+        updateRequestFocusMode();
+        updateRequestSceneMode();
+        updateRequestWhiteBalance();
+        updateRequestSettingOrForceToDefault(CONTROL_VIDEO_STABILIZATION_MODE,
+                mVideoStabilizationEnabled ?
+                        CONTROL_VIDEO_STABILIZATION_MODE_ON : CONTROL_VIDEO_STABILIZATION_MODE_OFF);
+        // OIS shouldn't be on if software video stabilization is.
+        mRequestSettings.set(LENS_OPTICAL_STABILIZATION_MODE,
+                mVideoStabilizationEnabled ? LENS_OPTICAL_STABILIZATION_MODE_OFF :
+                        null);
+        updateRequestSettingOrForceToDefault(CONTROL_AE_LOCK, mAutoExposureLocked);
+        updateRequestSettingOrForceToDefault(CONTROL_AWB_LOCK, mAutoWhiteBalanceLocked);
+        // TODO: mRecordingHintEnabled
+        // TODO: mGpsData
+        updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE,
+                new android.util.Size(
+                        mExifThumbnailSize.width(), mExifThumbnailSize.height()));
+
+        return mRequestSettings;
+    }
+
+    private MeteringRectangle[] legacyAreasToMeteringRectangles(
+            List<android.hardware.Camera.Area> reference) {
+        MeteringRectangle[] transformed = null;
+        if (reference.size() > 0) {
+
+            transformed = new MeteringRectangle[reference.size()];
+            for (int index = 0; index < reference.size(); ++index) {
+                android.hardware.Camera.Area source = reference.get(index);
+                Rect rectangle = source.rect;
+
+                // Old API coordinates were [-1000,1000]; new ones are [0,ACTIVE_ARRAY_SIZE).
+                double oldLeft = (rectangle.left + 1000) / 2000.0;
+                double oldTop = (rectangle.top + 1000) / 2000.0;
+                double oldRight = (rectangle.right + 1000) / 2000.0;
+                double oldBottom = (rectangle.bottom + 1000) / 2000.0;
+                int left = toIntConstrained( mActiveArray.width() * oldLeft + mActiveArray.left,
+                        0, mActiveArray.width() - 1);
+                int top = toIntConstrained( mActiveArray.height() * oldTop + mActiveArray.top,
+                        0, mActiveArray.height() - 1);
+                int right = toIntConstrained( mActiveArray.width() * oldRight + mActiveArray.left,
+                        0, mActiveArray.width() - 1);
+                int bottom = toIntConstrained( mActiveArray.height() * oldBottom + mActiveArray.top,
+                        0, mActiveArray.height() - 1);
+                transformed[index] = new MeteringRectangle(left, top, right - left, bottom - top,
+                        source.weight);
+            }
+        }
+        return transformed;
+    }
+
+    private int toIntConstrained(double original, int min, int max) {
+        original = Math.max(original, min);
+        original = Math.min(original, max);
+        return (int) original;
+    }
+
+    private void updateRequestFlashMode() {
+        Integer aeMode = null;
+        Integer flashMode = null;
+        if (mCurrentFlashMode != null) {
+            switch (mCurrentFlashMode) {
+                case AUTO: {
+                    aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH;
+                    break;
+                }
+                case OFF: {
+                    aeMode = CONTROL_AE_MODE_ON;
+                    flashMode = FLASH_MODE_OFF;
+                    break;
+                }
+                case ON: {
+                    aeMode = CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+                    flashMode = FLASH_MODE_SINGLE;
+                    break;
+                }
+                case TORCH: {
+                    flashMode = FLASH_MODE_TORCH;
+                    break;
+                }
+                case RED_EYE: {
+                    aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE;
+                    break;
+                }
+                default: {
+                    Log.w(TAG, "Unable to convert to API 2 flash mode: " + mCurrentFlashMode);
+                    break;
+                }
+            }
+        }
+        mRequestSettings.set(CONTROL_AE_MODE, aeMode);
+        mRequestSettings.set(FLASH_MODE, flashMode);
+    }
+
+    private void updateRequestFocusMode() {
+        Integer mode = null;
+        if (mCurrentFocusMode != null) {
+            switch (mCurrentFocusMode) {
+                case AUTO: {
+                    mode = CONTROL_AF_MODE_AUTO;
+                    break;
+                }
+                case CONTINUOUS_PICTURE: {
+                    mode = CONTROL_AF_MODE_CONTINUOUS_PICTURE;
+                    break;
+                }
+                case CONTINUOUS_VIDEO: {
+                    mode = CONTROL_AF_MODE_CONTINUOUS_VIDEO;
+                    break;
+                }
+                case EXTENDED_DOF: {
+                    mode = CONTROL_AF_MODE_EDOF;
+                    break;
+                }
+                case FIXED: {
+                    mode = CONTROL_AF_MODE_OFF;
+                    break;
+                }
+                // TODO: We cannot support INFINITY
+                case MACRO: {
+                    mode = CONTROL_AF_MODE_MACRO;
+                    break;
+                }
+                default: {
+                    Log.w(TAG, "Unable to convert to API 2 focus mode: " + mCurrentFocusMode);
+                    break;
+                }
+            }
+        }
+        mRequestSettings.set(CONTROL_AF_MODE, mode);
+    }
+
+    private void updateRequestSceneMode() {
+        Integer mode = null;
+        if (mCurrentSceneMode != null) {
+            switch (mCurrentSceneMode) {
+                case AUTO: {
+                    mode = CONTROL_SCENE_MODE_DISABLED;
+                    break;
+                }
+                case ACTION: {
+                    mode = CONTROL_SCENE_MODE_ACTION;
+                    break;
+                }
+                case BARCODE: {
+                    mode = CONTROL_SCENE_MODE_BARCODE;
+                    break;
+                }
+                case BEACH: {
+                    mode = CONTROL_SCENE_MODE_BEACH;
+                    break;
+                }
+                case CANDLELIGHT: {
+                    mode = CONTROL_SCENE_MODE_CANDLELIGHT;
+                    break;
+                }
+                case FIREWORKS: {
+                    mode = CONTROL_SCENE_MODE_FIREWORKS;
+                    break;
+                }
+                // TODO: We cannot support HDR
+                case LANDSCAPE: {
+                    mode = CONTROL_SCENE_MODE_LANDSCAPE;
+                    break;
+                }
+                case NIGHT: {
+                    mode = CONTROL_SCENE_MODE_NIGHT;
+                    break;
+                }
+                // TODO: We cannot support NIGHT_PORTRAIT
+                case PARTY: {
+                    mode = CONTROL_SCENE_MODE_PARTY;
+                    break;
+                }
+                case PORTRAIT: {
+                    mode = CONTROL_SCENE_MODE_PORTRAIT;
+                    break;
+                }
+                case SNOW: {
+                    mode = CONTROL_SCENE_MODE_SNOW;
+                    break;
+                }
+                case SPORTS: {
+                    mode = CONTROL_SCENE_MODE_SPORTS;
+                    break;
+                }
+                case STEADYPHOTO: {
+                    mode = CONTROL_SCENE_MODE_STEADYPHOTO;
+                    break;
+                }
+                case SUNSET: {
+                    mode = CONTROL_SCENE_MODE_SUNSET;
+                    break;
+                }
+                case THEATRE: {
+                    mode = CONTROL_SCENE_MODE_THEATRE;
+                    break;
+                }
+                default: {
+                    Log.w(TAG, "Unable to convert to API 2 scene mode: " + mCurrentSceneMode);
+                    break;
+                }
+            }
+        }
+        mRequestSettings.set(CONTROL_SCENE_MODE, mode);
+    }
+
+    private void updateRequestWhiteBalance() {
+        Integer mode = null;
+        if (mWhiteBalance != null) {
+            switch (mWhiteBalance) {
+                case AUTO: {
+                    mode = CONTROL_AWB_MODE_AUTO;
+                    break;
+                }
+                case CLOUDY_DAYLIGHT: {
+                    mode = CONTROL_AWB_MODE_CLOUDY_DAYLIGHT;
+                    break;
+                }
+                case DAYLIGHT: {
+                    mode = CONTROL_AWB_MODE_DAYLIGHT;
+                    break;
+                }
+                case FLUORESCENT: {
+                    mode = CONTROL_AWB_MODE_FLUORESCENT;
+                    break;
+                }
+                case INCANDESCENT: {
+                    mode = CONTROL_AWB_MODE_INCANDESCENT;
+                    break;
+                }
+                case SHADE: {
+                    mode = CONTROL_AWB_MODE_SHADE;
+                    break;
+                }
+                case TWILIGHT: {
+                    mode = CONTROL_AWB_MODE_TWILIGHT;
+                    break;
+                }
+                case WARM_FLUORESCENT: {
+                    mode = CONTROL_AWB_MODE_WARM_FLUORESCENT;
+                    break;
+                }
+                default: {
+                    Log.w(TAG, "Unable to convert to API 2 white balance: " + mWhiteBalance);
+                    break;
+                }
+            }
+        }
+        mRequestSettings.set(CONTROL_AWB_MODE, mode);
     }
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
index 9bbbb7a..95f0320 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
@@ -175,7 +175,7 @@
             return mFirstFrontCameraId;
         }
 
-        private static class AndroidCharacteristics implements Characteristics {
+        private static class AndroidCharacteristics extends Characteristics {
             private Camera.CameraInfo mCameraInfo;
 
             AndroidCharacteristics(Camera.CameraInfo cameraInfo) {
@@ -437,7 +437,14 @@
                     }
 
                     case CameraActions.SET_DISPLAY_ORIENTATION: {
-                        mCamera.setDisplayOrientation(msg.arg1);
+                        // Update preview orientation
+                        mCamera.setDisplayOrientation(
+                                mCharacteristics.getPreviewOrientation(msg.arg1));
+                        // Only set the JPEG capture orientation if requested to do so; otherwise,
+                        // capture in the sensor's physical orientation
+                        mParamsToSet.setRotation(
+                                msg.arg2 > 0 ? mCharacteristics.getJpegOrientation(msg.arg1) : 0);
+                        mCamera.setParameters(mParamsToSet);
                         break;
                     }
 
@@ -566,7 +573,6 @@
                 // Should use settings.getCurrentZoomRatio() instead here.
                 mParamsToSet.setZoom(settings.getCurrentZoomIndex());
             }
-            mParamsToSet.setRotation((int) settings.getCurrentPhotoRotationDegrees());
             mParamsToSet.setExposureCompensation(settings.getExposureCompensationIndex());
             if (mCapabilities.supports(CameraCapabilities.Feature.AUTO_EXPOSURE_LOCK)) {
                 mParamsToSet.setAutoExposureLock(settings.isAutoExposureLocked());
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java
index a4039bd..ceab7fe 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java
@@ -57,11 +57,19 @@
         }
         setRecordingHintEnabled(TRUE.equals(params.get(RECORDING_HINT)));
 
-        // Output: Photo size, compression quality, rotation.
-        setPhotoRotationDegrees(0f);
+        // Output: Photo size, compression quality
         setPhotoJpegCompressionQuality(params.getJpegQuality());
         Camera.Size paramPictureSize = params.getPictureSize();
         setPhotoSize(new Size(paramPictureSize.width, paramPictureSize.height));
         setPhotoFormat(params.getPictureFormat());
     }
+
+    public AndroidCameraSettings(AndroidCameraSettings other) {
+        super(other);
+    }
+
+    @Override
+    public CameraSettings copy() {
+        return new AndroidCameraSettings(this);
+    }
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
index d875cea..dd4f77c 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
@@ -618,16 +618,32 @@
                 CameraPictureCallback jpeg);
 
         /**
-         * Sets the display orientation for camera to adjust the preview orientation.
+         * Sets the display orientation for camera to adjust the preview and JPEG orientation.
          *
-         * @param degrees The rotation in degrees. Should be 0, 90, 180 or 270.
+         * @param degrees The counterclockwise rotation in degrees, relative to the device's natural
+         *                orientation. Should be 0, 90, 180 or 270.
          */
         public void setDisplayOrientation(final int degrees) {
+            setDisplayOrientation(degrees, true);
+        }
+
+        /**
+         * Sets the display orientation for camera to adjust the preview&mdash;and, optionally,
+         * JPEG&mdash;orientations.
+         * <p>If capture rotation is not requested, future captures will be returned in the sensor's
+         * physical rotation, which does not necessarily match the device's natural orientation.</p>
+         *
+         * @param degrees The counterclockwise rotation in degrees, relative to the device's natural
+         *                orientation. Should be 0, 90, 180 or 270.
+         * @param capture Whether to adjust the JPEG capture orientation as well as the preview one.
+         */
+        public void setDisplayOrientation(final int degrees, final boolean capture) {
             getDispatchThread().runJob(new Runnable() {
                 @Override
                 public void run() {
                     getCameraHandler()
-                            .obtainMessage(CameraActions.SET_DISPLAY_ORIENTATION, degrees, 0)
+                            .obtainMessage(CameraActions.SET_DISPLAY_ORIENTATION, degrees,
+                                    capture ? 1 : 0)
                             .sendToTarget();
                 }});
         }
@@ -713,7 +729,7 @@
          * @param statesToAwait Bitwise OR of the required camera states.
          * @return Whether the settings can be applied.
          */
-        protected boolean applySettingsHelper(final CameraSettings settings,
+        protected boolean applySettingsHelper(CameraSettings settings,
                                               final int statesToAwait) {
             if (settings == null) {
                 Log.v(TAG, "null parameters in applySettings()");
@@ -723,15 +739,14 @@
                 return false;
             }
 
-            final CameraSettings copyOfSettings = new CameraSettings(settings);
+            final CameraSettings copyOfSettings = settings.copy();
             getDispatchThread().runJob(new Runnable() {
                 @Override
                 public void run() {
                     getCameraState().waitForStates(statesToAwait);
                     getCameraHandler().obtainMessage(CameraActions.APPLY_SETTINGS, copyOfSettings)
                             .sendToTarget();
-                }
-            });
+                }});
             return true;
         }
 
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilities.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilities.java
index 1f2781b..6a4c72c 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilities.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilities.java
@@ -97,6 +97,7 @@
          * Focus is set at infinity.
          * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_INFINITY}.
          */
+        // TODO: Unsupported on API 2
         INFINITY,
         /**
          * Macro (close-up) focus mode.
@@ -182,6 +183,7 @@
          * Capture a scene using high dynamic range imaging techniques.
          * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_HDR}.
          */
+        // TODO: Unsupported on API 2
         HDR,
         /**
          * Take pictures on distant objects.
@@ -197,6 +199,7 @@
          * Take people pictures at night.
          * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_NIGHT_PORTRAIT}.
          */
+        // TODO: Unsupported on API 2
         NIGHT_PORTRAIT,
         /**
          * Take indoor low-light shot.
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java
index ada1f29..a657170 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java
@@ -1,7 +1,25 @@
+/*
+ * 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.ex.camera2.portability;
 
 import android.hardware.Camera;
 
+import com.android.ex.camera2.portability.debug.Log;
+
 /**
  * The device info for all attached cameras.
  */
@@ -35,27 +53,97 @@
     /**
      * Device characteristics for a single camera.
      */
-    public interface Characteristics {
+    public abstract class Characteristics {
+        private static final Log.Tag TAG = new Log.Tag("CamDvcInfChar");
+
         /**
          * @return Whether the camera faces the back of the device.
          */
-        boolean isFacingBack();
+        public abstract boolean isFacingBack();
 
         /**
          * @return Whether the camera faces the device's screen.
          */
-        boolean isFacingFront();
+        public abstract boolean isFacingFront();
 
         /**
          * @return The camera image orientation, or the clockwise rotation angle
          *         that must be applied to display it in its natural orientation
-         *         (in degrees, and always a multiple of 90).
+         *         (in degrees, always a multiple of 90, and between [90,270]).
          */
-        int getSensorOrientation();
+        public abstract int getSensorOrientation();
+
+        /**
+         * @param currentDisplayOrientation
+         *          The current display orientation, as measured clockwise from
+         *          the device's natural orientation (in degrees, always a
+         *          multiple of 90, and between 0 and 270, inclusive).
+         * @return
+         *          The relative preview image orientation, or the clockwise
+         *          rotation angle that must be applied to display preview
+         *          frames in the matching orientation, accounting for implicit
+         *          mirroring, if applicable (in degrees, always a multiple of
+         *          90, and between 0 and 270, inclusive).
+         */
+        public int getPreviewOrientation(int currentDisplayOrientation) {
+            // Drivers tend to mirror the image during front camera preview.
+            return getRelativeImageOrientation(currentDisplayOrientation, true);
+        }
+
+        /**
+         * @param currentDisplayOrientation
+         *          The current display orientation, as measured clockwise from
+         *          the device's natural orientation (in degrees, always a
+         *          multiple of 90, and between 0 and 270, inclusive).
+         * @return
+         *          The relative capture image orientation, or the clockwise
+         *          rotation angle that must be applied to display these frames
+         *          in the matching orientation (in degrees, always a multiple
+         *          of 90, and between 0 and 270, inclusive).
+         */
+        public int getJpegOrientation(int currentDisplayOrientation) {
+            // Don't mirror during capture!
+            return getRelativeImageOrientation(currentDisplayOrientation, false);
+        }
+
+        /**
+         * @param currentDisplayOrientaiton
+         *          {@link #getPreviewOrientation}, {@link #getJpegOrientation}
+         * @param compensateForMirroring
+         *          Whether to account for mirroring in the case of front-facing
+         *          cameras, which is necessary iff the OS/driver is
+         *          automatically reflecting the image.
+         * @return
+         *          {@link #getPreviewOrientation}, {@link #getJpegOrientation}
+         *
+         * @see android.hardware.Camera.setDisplayOrientation
+         */
+        protected int getRelativeImageOrientation(int currentDisplayOrientation,
+                                                  boolean compensateForMirroring) {
+            if (currentDisplayOrientation % 90 != 0) {
+                Log.e(TAG, "Provided display orientation is not divisible by 90");
+            }
+            if (currentDisplayOrientation < 0 || currentDisplayOrientation > 270) {
+                Log.e(TAG, "Provided display orientation is outside expected range");
+            }
+
+            int result = 0;
+            if (isFacingFront()) {
+                result = (getSensorOrientation() + currentDisplayOrientation) % 360;
+                if (compensateForMirroring) {
+                    result = (360 - result) % 360;
+                }
+            } else if (isFacingBack()) {
+                result = (getSensorOrientation() - currentDisplayOrientation + 360) % 360;
+            } else {
+                Log.e(TAG, "Camera is facing unhandled direction");
+            }
+            return result;
+        }
 
         /**
          * @return Whether the shutter sound can be disabled.
          */
-        boolean canDisableShutterSound();
+        public abstract boolean canDisableShutterSound();
     }
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
index 3948b2e..26d0f85 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
@@ -18,6 +18,8 @@
 
 import android.hardware.Camera;
 
+import com.android.ex.camera2.portability.debug.Log;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -26,7 +28,12 @@
 /**
  * A class which stores the camera settings.
  */
-public class CameraSettings {
+public abstract class CameraSettings {
+    private static final Log.Tag TAG = new Log.Tag("CamSet");
+
+    // Attempts to provide a value outside this range will be ignored.
+    private static final int MIN_JPEG_COMPRESSION_QUALITY = 1;
+    private static final int MAX_JPEG_COMPRESSION_QUALITY = 100;
 
     protected final Map<String, String> mGeneralSetting = new TreeMap<>();
     protected final List<Camera.Area> mMeteringAreas = new ArrayList<>();
@@ -37,11 +44,10 @@
     protected Size mCurrentPreviewSize;
     private int mCurrentPreviewFormat;
     protected Size mCurrentPhotoSize;
-    protected int mJpegCompressQuality;
+    protected byte mJpegCompressQuality;
     protected int mCurrentPhotoFormat;
     protected float mCurrentZoomRatio;
     protected int mCurrentZoomIndex;
-    protected float mPhotoRotationDegrees;
     protected int mExposureCompensationIndex;
     protected CameraCapabilities.FlashMode mCurrentFlashMode;
     protected CameraCapabilities.FocusMode mCurrentFocusMode;
@@ -96,7 +102,7 @@
      * @param src The source settings.
      * @return The copy of the source.
      */
-    public CameraSettings(CameraSettings src) {
+    protected CameraSettings(CameraSettings src) {
         mGeneralSetting.putAll(src.mGeneralSetting);
         mMeteringAreas.addAll(src.mMeteringAreas);
         mFocusAreas.addAll(src.mFocusAreas);
@@ -112,7 +118,6 @@
         mCurrentPhotoFormat = src.mCurrentPhotoFormat;
         mCurrentZoomRatio = src.mCurrentZoomRatio;
         mCurrentZoomIndex = src.mCurrentZoomIndex;
-        mPhotoRotationDegrees = src.mPhotoRotationDegrees;
         mExposureCompensationIndex = src.mExposureCompensationIndex;
         mCurrentFlashMode = src.mCurrentFlashMode;
         mCurrentFocusMode = src.mCurrentFocusMode;
@@ -126,6 +131,11 @@
         mExifThumbnailSize = src.mExifThumbnailSize;
     }
 
+    /**
+     * @return A copy of this object, as an instance of the implementing class.
+     */
+    public abstract CameraSettings copy();
+
     /** General setting **/
     @Deprecated
     public void setSetting(String key, String value) {
@@ -258,7 +268,12 @@
      * @param quality The quality for JPEG.
      */
     public void setPhotoJpegCompressionQuality(int quality) {
-        mJpegCompressQuality = quality;
+        if (quality < MIN_JPEG_COMPRESSION_QUALITY || quality > MAX_JPEG_COMPRESSION_QUALITY) {
+            Log.w(TAG, "Ignoring JPEG quality that falls outside the expected range");
+            return;
+        }
+        // This is safe because the positive numbers go up to 127.
+        mJpegCompressQuality = (byte) quality;
     }
 
     public int getPhotoJpegCompressionQuality() {
@@ -296,22 +311,15 @@
         mCurrentZoomIndex = index;
     }
 
-    /** Transformation **/
-
-    public void setPhotoRotationDegrees(float photoRotationDegrees) {
-        mPhotoRotationDegrees = photoRotationDegrees;
-    }
-
-    public float getCurrentPhotoRotationDegrees() {
-        return mPhotoRotationDegrees;
-    }
-
     /** Exposure **/
 
     public void setExposureCompensationIndex(int index) {
         mExposureCompensationIndex = index;
     }
 
+    /**
+     * @return The exposure compensation, with 0 meaning unadjusted.
+     */
     public int getExposureCompensationIndex() {
         return mExposureCompensationIndex;
     }
diff --git a/camera2/portability/tests/Android.mk b/camera2/portability/tests/Android.mk
index 5bb09ef..f0b24e0 100644
--- a/camera2/portability/tests/Android.mk
+++ b/camera2/portability/tests/Android.mk
@@ -18,7 +18,8 @@
 LOCAL_PACKAGE_NAME := android-ex-camera2-portability-tests
 LOCAL_MODULE_TAGS := tests
 LOCAL_SDK_VERSION := current
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2-portability android-support-test
+LOCAL_SRC_FILES := $(call all-java-files-under,src) $(call all-java-files-under,../../utils/tests)
+LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2-portability android-ex-camera2-utils \
+                               android-support-test mockito-target
 
 include $(BUILD_PACKAGE)
diff --git a/camera2/portability/tests/AndroidManifest.xml b/camera2/portability/tests/AndroidManifest.xml
index 2e6a38b..65cf709 100644
--- a/camera2/portability/tests/AndroidManifest.xml
+++ b/camera2/portability/tests/AndroidManifest.xml
@@ -19,8 +19,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.ex.camera2.portability.tests">
     <uses-permission android:name="android.permission.CAMERA" />
-    <application android:label="CameraToo">
-        <!--<uses-library android:name="android.test.runner" />-->
+    <application>
+        <uses-library android:name="android.test.runner" />
     </application>
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.ex.camera2.portability.tests" />
diff --git a/camera2/portability/tests/src/com/android/ex/camera2/portability/Camera2PortabilityTest.java b/camera2/portability/tests/src/com/android/ex/camera2/portability/Camera2PortabilityTest.java
index b421b98..034fac7 100644
--- a/camera2/portability/tests/src/com/android/ex/camera2/portability/Camera2PortabilityTest.java
+++ b/camera2/portability/tests/src/com/android/ex/camera2/portability/Camera2PortabilityTest.java
@@ -16,45 +16,49 @@
 
 package com.android.ex.camera2.portability;
 
-import static android.hardware.camera2.CameraCharacteristics.*;
+import static android.hardware.camera2.CaptureRequest.*;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
-import com.android.ex.camera2.portability.AndroidCamera2Capabilities.IntegralStringifier;
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CaptureRequest;
+import android.support.test.InjectContext;
+
 import com.android.ex.camera2.portability.CameraCapabilities.FlashMode;
 import com.android.ex.camera2.portability.CameraCapabilities.FocusMode;
 import com.android.ex.camera2.portability.CameraCapabilities.SceneMode;
 import com.android.ex.camera2.portability.CameraCapabilities.Stringifier;
 import com.android.ex.camera2.portability.CameraCapabilities.WhiteBalance;
+import com.android.ex.camera2.utils.Camera2DeviceTester;
 import org.junit.Test;
 
-import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.ParameterizedType;
 
-public class Camera2PortabilityTest {
-    private <E> void cameraCapabilitiesStringifierEach(Class<E> classy,
-                                                       Stringifier strfy,
-                                                       String call) throws Exception {
-        for(E val : (E[]) classy.getMethod("values").invoke(null)) {
-            String valString =
-                    (String) Stringifier.class.getMethod("stringify", classy).invoke(strfy, val);
-            assertEquals(val,
-                    Stringifier.class.getMethod(call, String.class).invoke(strfy, valString));
+public class Camera2PortabilityTest extends Camera2DeviceTester {
+    @Test
+    public void cameraCapabilitiesStringifier() {
+        Stringifier strfy = new Stringifier();
+        for(FocusMode val : FocusMode.values()) {
+            assertEquals(val, strfy.focusModeFromString(strfy.stringify(val)));
+        }
+        for(FlashMode val : FlashMode.values()) {
+            assertEquals(val, strfy.flashModeFromString(strfy.stringify(val)));
+        }
+        for(SceneMode val : SceneMode.values()) {
+            assertEquals(val, strfy.sceneModeFromString(strfy.stringify(val)));
+        }
+        for(WhiteBalance val : WhiteBalance.values()) {
+            assertEquals(val, strfy.whiteBalanceFromString(strfy.stringify(val)));
         }
     }
 
     @Test
-    public void cameraCapabilitiesStringifier() throws Exception {
-        Stringifier strfy = new Stringifier();
-        cameraCapabilitiesStringifierEach(FocusMode.class, strfy, "focusModeFromString");
-        cameraCapabilitiesStringifierEach(FlashMode.class, strfy, "flashModeFromString");
-        cameraCapabilitiesStringifierEach(SceneMode.class, strfy, "sceneModeFromString");
-        cameraCapabilitiesStringifierEach(WhiteBalance.class, strfy, "whiteBalanceFromString");
-    }
-
-    @Test
-    public void cameraCapabilitiesStringifierNull() throws Exception {
+    public void cameraCapabilitiesStringifierNull() {
         Stringifier strfy = new Stringifier();
         assertEquals(strfy.focusModeFromString(null), FocusMode.AUTO);
         assertEquals(strfy.flashModeFromString(null), FlashMode.NO_FLASH);
@@ -63,7 +67,7 @@
     }
 
     @Test
-    public void cameraCapabilitiesStringifierInvalid() throws Exception {
+    public void cameraCapabilitiesStringifierInvalid() {
         Stringifier strfy = new Stringifier();
         assertEquals(strfy.focusModeFromString("crap"), FocusMode.AUTO);
         assertEquals(strfy.flashModeFromString("crap"), FlashMode.NO_FLASH);
@@ -71,72 +75,93 @@
         assertEquals(strfy.whiteBalanceFromString("crap"), WhiteBalance.AUTO);
     }
 
-    private void cameraCapabilitiesIntifierEach(int apiVal,
-                                                IntegralStringifier intfy,
-                                                String call) throws Exception {
-        Method toCall = IntegralStringifier.class.getMethod(call, int.class);
-        Class<?> returnType = toCall.getReturnType();
-        Object returnVal = toCall.invoke(intfy, apiVal);
-        assertEquals(apiVal,
-                IntegralStringifier.class.getMethod("intify", returnType).invoke(intfy, returnVal));
+    private CameraCharacteristics buildFrameworkCharacteristics() throws CameraAccessException {
+        CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+        String id = manager.getCameraIdList()[0];
+        return manager.getCameraCharacteristics(id);
+    }
+
+    private void camera2SettingsCheckSingleOption(AndroidCamera2Settings setts,
+                                                      Key<?> apiKey, int apiVal) {
+        assertEquals(apiVal, setts.getRequestSettings().get(apiKey));
     }
 
     @Test
-    public void cameraCapabilitiesIntifier() throws Exception {
-        IntegralStringifier intstr = new IntegralStringifier();
+    public void camera2SettingsSetOptionsAndGetRequestSettings() throws CameraAccessException {
+        AndroidCamera2Settings set = new AndroidCamera2Settings(
+                mCamera, CameraDevice.TEMPLATE_PREVIEW, null, null, null);
 
         // Focus modes
-        cameraCapabilitiesIntifierEach(CONTROL_AF_MODE_AUTO, intstr, "focusModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AF_MODE_CONTINUOUS_PICTURE, intstr,
-                "focusModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AF_MODE_CONTINUOUS_VIDEO, intstr,
-                "focusModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AF_MODE_EDOF, intstr, "focusModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AF_MODE_OFF, intstr, "focusModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AF_MODE_MACRO, intstr, "focusModeFromInt");
-
-        // Flash modes
-        cameraCapabilitiesIntifierEach(FLASH_MODE_OFF, intstr, "flashModeFromInt");
-        cameraCapabilitiesIntifierEach(FLASH_MODE_SINGLE, intstr, "flashModeFromInt");
-        cameraCapabilitiesIntifierEach(FLASH_MODE_TORCH, intstr, "flashModeFromInt");
+        set.setFocusMode(FocusMode.AUTO);
+        camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_AUTO);
+        set.setFocusMode(FocusMode.CONTINUOUS_PICTURE);
+        camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+        set.setFocusMode(FocusMode.CONTINUOUS_VIDEO);
+        camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_CONTINUOUS_VIDEO);
+        set.setFocusMode(FocusMode.EXTENDED_DOF);
+        camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_EDOF);
+        set.setFocusMode(FocusMode.FIXED);
+        camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_OFF);
+        set.setFocusMode(FocusMode.MACRO);
+        camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_MACRO);
 
         // Scene modes
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_DISABLED, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_ACTION, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_BARCODE, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_BEACH, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_CANDLELIGHT, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_FIREWORKS, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_LANDSCAPE, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_NIGHT, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_PARTY, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_PORTRAIT, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_SNOW, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_SPORTS, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_STEADYPHOTO, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_SUNSET, intstr, "sceneModeFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_SCENE_MODE_THEATRE, intstr, "sceneModeFromInt");
+        set.setSceneMode(SceneMode.AUTO);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_DISABLED);
+        set.setSceneMode(SceneMode.ACTION);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_ACTION);
+        set.setSceneMode(SceneMode.BARCODE);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_BARCODE);
+        set.setSceneMode(SceneMode.BEACH);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_BEACH);
+        set.setSceneMode(SceneMode.CANDLELIGHT);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_CANDLELIGHT);
+        set.setSceneMode(SceneMode.FIREWORKS);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_FIREWORKS);
+        set.setSceneMode(SceneMode.LANDSCAPE);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_LANDSCAPE);
+        set.setSceneMode(SceneMode.NIGHT);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_NIGHT);
+        set.setSceneMode(SceneMode.PARTY);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_PARTY);
+        set.setSceneMode(SceneMode.PORTRAIT);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_PORTRAIT);
+        set.setSceneMode(SceneMode.SNOW);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_SNOW);
+        set.setSceneMode(SceneMode.SPORTS);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_SPORTS);
+        set.setSceneMode(SceneMode.STEADYPHOTO);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_STEADYPHOTO);
+        set.setSceneMode(SceneMode.SUNSET);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_SUNSET);
+        set.setSceneMode(SceneMode.THEATRE);
+        camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_THEATRE);
 
         // White balances
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_AUTO, intstr, "whiteBalanceFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_CLOUDY_DAYLIGHT, intstr,
-                "whiteBalanceFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_DAYLIGHT, intstr, "whiteBalanceFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_FLUORESCENT, intstr,
-                "whiteBalanceFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_INCANDESCENT, intstr,
-                "whiteBalanceFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_SHADE, intstr, "whiteBalanceFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_TWILIGHT, intstr, "whiteBalanceFromInt");
-        cameraCapabilitiesIntifierEach(CONTROL_AWB_MODE_WARM_FLUORESCENT, intstr,
-                "whiteBalanceFromInt");
+        set.setWhiteBalance(WhiteBalance.AUTO);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_AUTO);
+        set.setWhiteBalance(WhiteBalance.CLOUDY_DAYLIGHT);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_CLOUDY_DAYLIGHT);
+        set.setWhiteBalance(WhiteBalance.DAYLIGHT);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_DAYLIGHT);
+        set.setWhiteBalance(WhiteBalance.FLUORESCENT);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_FLUORESCENT);
+        set.setWhiteBalance(WhiteBalance.INCANDESCENT);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_INCANDESCENT);
+        set.setWhiteBalance(WhiteBalance.SHADE);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_SHADE);
+        set.setWhiteBalance(WhiteBalance.TWILIGHT);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_TWILIGHT);
+        set.setWhiteBalance(WhiteBalance.WARM_FLUORESCENT);
+        camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_WARM_FLUORESCENT);
     }
 
-    // TODO: Add a test checking whether stringification matches API representation
+    // TODO: Add a test checking whether stringification matches API 1 representation
 
     @Test
-    public void cameraCapabilitiesIntsMatchApi2Representations() throws Exception {
-        IntegralStringifier intstr = new IntegralStringifier();
+    public void camera2CapabilitiesFocusModeFromInt() throws CameraAccessException {
+        CameraCharacteristics chars = buildFrameworkCharacteristics();
+        AndroidCamera2Capabilities intstr = new AndroidCamera2Capabilities(chars);
 
         // Focus modes
         assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_AUTO), FocusMode.AUTO);
@@ -148,18 +173,12 @@
         assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_OFF), FocusMode.FIXED);
         assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_MACRO), FocusMode.MACRO);
 
-        // Flash modes
-        assertEquals(intstr.flashModeFromInt(FLASH_MODE_OFF), FlashMode.OFF);
-        assertEquals(intstr.flashModeFromInt(FLASH_MODE_SINGLE), FlashMode.ON);
-        assertEquals(intstr.flashModeFromInt(FLASH_MODE_TORCH), FlashMode.TORCH);
-
         // Scene modes
         assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_DISABLED), SceneMode.AUTO);
         assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_ACTION), SceneMode.ACTION);
         assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_BARCODE), SceneMode.BARCODE);
         assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_BEACH), SceneMode.BEACH);
-        assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_CANDLELIGHT),
-                SceneMode.CANDLELIGHT);
+        assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_CANDLELIGHT), SceneMode.CANDLELIGHT);
         assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_FIREWORKS), SceneMode.FIREWORKS);
         assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_LANDSCAPE), SceneMode.LANDSCAPE);
         assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_NIGHT), SceneMode.NIGHT);
diff --git a/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2DeviceTester.java b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2DeviceTester.java
new file mode 100644
index 0000000..4db6dfb
--- /dev/null
+++ b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2DeviceTester.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ex.camera2.utils;
+
+import android.content.Context;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.test.InjectContext;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+/**
+ * Subclasses of this have an {@code mCamera} instance variable representing the first camera.
+ */
+public class Camera2DeviceTester {
+    private static HandlerThread sThread;
+
+    private static Handler sHandler;
+
+    @BeforeClass
+    public static void setupBackgroundHandler() {
+        sThread = new HandlerThread("CameraFramework");
+        sThread.start();
+        sHandler = new Handler(sThread.getLooper());
+    }
+
+    @AfterClass
+    public static void teardownBackgroundHandler() throws Exception {
+        sThread.quitSafely();
+        sThread.join();
+    }
+
+    @InjectContext
+    public Context mContext;
+
+    private class DeviceCapturer extends CameraDevice.StateListener {
+        private CameraDevice mCamera;
+
+        public CameraDevice captureCameraDevice() throws Exception {
+            CameraManager manager =
+                    (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+            String id = manager.getCameraIdList()[0];
+            synchronized (this) {
+                manager.openCamera(id, this, sHandler);
+                wait();
+            }
+            return mCamera;
+        }
+
+        @Override
+        public synchronized void onOpened(CameraDevice camera) {
+            mCamera = camera;
+            notify();
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice camera) {}
+
+        @Override
+        public void onError(CameraDevice camera, int error) {}
+    }
+
+    protected CameraDevice mCamera;
+
+    @Before
+    public void obtainCameraCaptureRequestBuilderFactory() throws Exception {
+        mCamera = new DeviceCapturer().captureCameraDevice();
+    }
+
+    @After
+    public void releaseCameraCaptureRequestBuilderFactory() {
+        mCamera.close();
+    }
+}
diff --git a/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2UtilsTest.java b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2UtilsTest.java
index dd9566b..bb23e37 100644
--- a/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2UtilsTest.java
+++ b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2UtilsTest.java
@@ -23,24 +23,15 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.content.Context;
 import android.hardware.camera2.CameraCaptureSession.CaptureListener;
 import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CameraManager;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureRequest.Key;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.support.test.InjectContext;
 import android.view.Surface;
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
-public class Camera2UtilsTest {
+public class Camera2UtilsTest extends Camera2DeviceTester {
     private void captureListenerSplitterAllCallbacksReceived(CaptureListener splitter,
                                                              CaptureListener... terminals) {
         splitter.onCaptureCompleted(null, null, null);
@@ -152,65 +143,6 @@
         assertFalse(setUp.contains(CaptureRequest.CONTROL_AE_LOCK));
     }
 
-    private static HandlerThread sThread;
-
-    private static Handler sHandler;
-
-    @BeforeClass
-    public static void setupBackgroundHandler() {
-        sThread = new HandlerThread("CameraFramework");
-        sThread.start();
-        sHandler = new Handler(sThread.getLooper());
-    }
-
-    @AfterClass
-    public static void teardownBackgroundHandler() throws Exception {
-        sThread.quitSafely();
-        sThread.join();
-    }
-
-    @InjectContext
-    public Context mContext;
-
-    public class DeviceCapturer extends CameraDevice.StateListener {
-        private CameraDevice mCamera;
-
-        public CameraDevice captureCameraDevice() throws Exception {
-            CameraManager manager =
-                    (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
-            String id = manager.getCameraIdList()[0];
-            synchronized (this) {
-                manager.openCamera(id, this, sHandler);
-                wait();
-            }
-            return mCamera;
-        }
-
-        @Override
-        public synchronized void onOpened(CameraDevice camera) {
-            mCamera = camera;
-            notify();
-        }
-
-        @Override
-        public void onDisconnected(CameraDevice camera) {}
-
-        @Override
-        public void onError(CameraDevice camera, int error) {}
-    }
-
-    private CameraDevice mCamera;
-
-    @Before
-    public void obtainCameraCaptureRequestBuilderFactory() throws Exception {
-        mCamera = new DeviceCapturer().captureCameraDevice();
-    }
-
-    @After
-    public void releaseCameraCaptureRequestBuilderFactory() {
-        mCamera.close();
-    }
-
     @Test
     public void requestSettingsSetStartsWithoutChanges() {
         Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet();