Merge "camera2: Add HDR vendor tag for legacy mode." into lmp-dev
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 d139c62..913a575 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
@@ -198,6 +198,9 @@
         // Available whenever setAutoFocusMoveCallback() was last invoked with a non-null argument:
         private CameraAFMoveCallback mPassiveAfCallback;
 
+        // Gets reset on every state change
+        private int mCurrentAeState = CaptureResult.CONTROL_AE_STATE_INACTIVE;
+
         Camera2Handler(Looper looper) {
             super(looper);
         }
@@ -212,7 +215,7 @@
                         CameraOpenCallback openCallback = (CameraOpenCallback) msg.obj;
                         int cameraIndex = msg.arg1;
 
-                        if (mCameraState.getState() != AndroidCamera2StateHolder.CAMERA_UNOPENED) {
+                        if (mCameraState.getState() > AndroidCamera2StateHolder.CAMERA_UNOPENED) {
                             openCallback.onDeviceOpenedAlready(cameraIndex,
                                     generateHistoryString(cameraIndex));
                             break;
@@ -263,7 +266,7 @@
                         mPhotoSize = null;
                         mCameraIndex = 0;
                         mCameraId = null;
-                        mCameraState.setState(AndroidCamera2StateHolder.CAMERA_UNOPENED);
+                        changeState(AndroidCamera2StateHolder.CAMERA_UNOPENED);
                         break;
                     }
 
@@ -289,19 +292,23 @@
                         }
 
                         mOneshotPreviewingCallback = (CameraStartPreviewCallback) msg.obj;
-                        mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
+                        changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
                         try {
                             mSession.setRepeatingRequest(
                                     mPersistentSettings.createRequest(mCamera,
                                             CameraDevice.TEMPLATE_PREVIEW, mPreviewSurface),
-                                    /*listener*/mCameraFocusStateListener, /*handler*/this);
+                                    /*listener*/mCameraResultStateListener, /*handler*/this);
                         } catch(CameraAccessException ex) {
                             Log.w(TAG, "Unable to start preview", ex);
-                            mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
+                            changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
                         }
                         break;
                     }
 
+                    // FIXME: We need to tear down the CameraCaptureSession here
+                    // (and unlock the CameraSettings object from our
+                    // CameraProxy) so that the preview/photo sizes can be
+                    // changed again while no preview is running.
                     case CameraActions.STOP_PREVIEW: {
                         if (mCameraState.getState() <
                                         AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) {
@@ -310,7 +317,7 @@
                         }
 
                         mSession.stopRepeating();
-                        mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
+                        changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
                         break;
                     }
 
@@ -368,19 +375,39 @@
                         }
 
                         // The earliest we can reliably tell whether the autofocus has locked in
-                        // response to our latest request is when our one-time capture completes.
+                        // response to our latest request is when our one-time capture progresses.
                         // However, it will probably take longer than that, so once that happens,
                         // just start checking the repeating preview requests as they complete.
                         final CameraAFCallback callback = (CameraAFCallback) msg.obj;
                         CameraCaptureSession.CaptureListener deferredCallbackSetter =
                                 new CameraCaptureSession.CaptureListener() {
+                            private boolean mAlreadyDispatched = false;
+
+                            @Override
+                            public void onCaptureProgressed(CameraCaptureSession session,
+                                                            CaptureRequest request,
+                                                            CaptureResult result) {
+                                checkAfState(result);
+                            }
+
                             @Override
                             public void onCaptureCompleted(CameraCaptureSession session,
                                                            CaptureRequest request,
                                                            TotalCaptureResult result) {
-                                // Now our mCameraFocusStateListener will invoke the callback the
-                                // first time it finds the focus motor to be locked.
-                                mOneshotAfCallback = callback;
+                                checkAfState(result);
+                            }
+
+                            private void checkAfState(CaptureResult result) {
+                                if (result.get(CaptureResult.CONTROL_AF_STATE) != null &&
+                                        !mAlreadyDispatched) {
+                                    // Now our mCameraResultStateListener will invoke the callback
+                                    // the first time it finds the focus motor to be locked.
+                                    mAlreadyDispatched = true;
+                                    mOneshotAfCallback = callback;
+                                    // This is an optimization: check the AF state of this frame
+                                    // instead of simply waiting for the next.
+                                    mCameraResultStateListener.monitorControlStates(result);
+                                }
                             }
 
                             @Override
@@ -392,7 +419,7 @@
                             }};
 
                         // Send a one-time capture to trigger the camera driver to lock focus.
-                        mCameraState.setState(AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
+                        changeState(AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
                         Camera2RequestSettingsSet trigger =
                                 new Camera2RequestSettingsSet(mPersistentSettings);
                         trigger.set(CaptureRequest.CONTROL_AF_TRIGGER,
@@ -404,7 +431,7 @@
                                     /*listener*/deferredCallbackSetter, /*handler*/ this);
                         } catch(CameraAccessException ex) {
                             Log.e(TAG, "Unable to lock autofocus", ex);
-                            mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
+                            changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
                         }
                         break;
                     }
@@ -418,7 +445,7 @@
                         }
 
                         // Send a one-time capture to trigger the camera driver to resume scanning.
-                        mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
+                        changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE);
                         Camera2RequestSettingsSet cancel =
                                 new Camera2RequestSettingsSet(mPersistentSettings);
                         cancel.set(CaptureRequest.CONTROL_AF_TRIGGER,
@@ -430,8 +457,7 @@
                                     /*listener*/null, /*handler*/this);
                         } catch(CameraAccessException ex) {
                             Log.e(TAG, "Unable to cancel autofocus", ex);
-                            mCameraState.setState(
-                                    AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
+                            changeState(AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
                         }
                         break;
                     }
@@ -486,8 +512,21 @@
 
                         final CaptureAvailableListener listener =
                                 (CaptureAvailableListener) msg.obj;
-                        if (mLegacyDevice) {
-                            // Just snap the shot
+                        if (mLegacyDevice ||
+                                (mCurrentAeState == CaptureResult.CONTROL_AE_STATE_CONVERGED &&
+                                !mPersistentSettings.matches(CaptureRequest.CONTROL_AE_MODE,
+                                        CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH) &&
+                                !mPersistentSettings.matches(CaptureRequest.FLASH_MODE,
+                                        CaptureRequest.FLASH_MODE_SINGLE)))
+                                {
+                            // Legacy devices don't support the precapture state keys and instead
+                            // perform autoexposure convergence automatically upon capture.
+
+                            // On other devices, as long as it has already converged, it determined
+                            // that flash was not required, and we're not going to invalidate the
+                            // current exposure levels by forcing the force on, we can save
+                            // significant capture time by not forcing a recalculation.
+                            Log.i(TAG, "Skipping pre-capture autoexposure convergence");
                             mCaptureReader.setOnImageAvailableListener(listener, /*handler*/this);
                             try {
                                 mSession.capture(
@@ -496,17 +535,42 @@
                                                 mCaptureReader.getSurface()),
                                         listener, /*handler*/this);
                             } catch (CameraAccessException ex) {
-                                Log.e(TAG, "Unable to initiate legacy capture", ex);
+                                Log.e(TAG, "Unable to initiate immediate capture", ex);
                             }
                         } else {
-                            // Not a legacy device, so we need to let AE converge before capturing
+                            // We need to let AE converge before capturing. Once our one-time
+                            // trigger capture has made it into the pipeline, we'll start checking
+                            // for the completion of that convergence, capturing when that happens.
+                            Log.i(TAG, "Forcing pre-capture autoexposure convergence");
                             CameraCaptureSession.CaptureListener deferredCallbackSetter =
                                     new CameraCaptureSession.CaptureListener() {
+                                private boolean mAlreadyDispatched = false;
+
+                                @Override
+                                public void onCaptureProgressed(CameraCaptureSession session,
+                                                                CaptureRequest request,
+                                                                CaptureResult result) {
+                                    checkAeState(result);
+                                }
+
                                 @Override
                                 public void onCaptureCompleted(CameraCaptureSession session,
                                                                CaptureRequest request,
                                                                TotalCaptureResult result) {
-                                    mOneshotCaptureCallback = listener;
+                                    checkAeState(result);
+                                }
+
+                                private void checkAeState(CaptureResult result) {
+                                    if (result.get(CaptureResult.CONTROL_AE_STATE) != null &&
+                                            !mAlreadyDispatched) {
+                                        // Now our mCameraResultStateListener will invoke the
+                                        // callback once the autoexposure routine has converged.
+                                        mAlreadyDispatched = true;
+                                        mOneshotCaptureCallback = listener;
+                                        // This is an optimization: check the AE state of this frame
+                                        // instead of simply waiting for the next.
+                                        mCameraResultStateListener.monitorControlStates(result);
+                                    }
                                 }
 
                                 @Override
@@ -599,13 +663,13 @@
                     mSession.setRepeatingRequest(
                             mPersistentSettings.createRequest(mCamera,
                                     CameraDevice.TEMPLATE_PREVIEW, mPreviewSurface),
-                            /*listener*/mCameraFocusStateListener, /*handler*/this);
+                            /*listener*/mCameraResultStateListener, /*handler*/this);
                 } catch (CameraAccessException ex) {
                     Log.e(TAG, "Failed to apply updated request settings", ex);
                 }
             } else if (mCameraState.getState() < AndroidCamera2StateHolder.CAMERA_PREVIEW_READY) {
                 // If we're already ready to preview, this doesn't regress our state
-                mCameraState.setState(AndroidCamera2StateHolder.CAMERA_CONFIGURED);
+                changeState(AndroidCamera2StateHolder.CAMERA_CONFIGURED);
             }
         }
 
@@ -659,7 +723,17 @@
             } catch (CameraAccessException ex) {
                 Log.e(TAG, "Failed to close existing camera capture session", ex);
             }
-            mCameraState.setState(AndroidCamera2StateHolder.CAMERA_CONFIGURED);
+            changeState(AndroidCamera2StateHolder.CAMERA_CONFIGURED);
+        }
+
+        private void changeState(int newState) {
+            if (mCameraState.getState() != newState) {
+                mCameraState.setState(newState);
+                if (newState < AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) {
+                    mCurrentAeState = CaptureResult.CONTROL_AE_STATE_INACTIVE;
+                    mCameraResultStateListener.resetState();
+                }
+            }
         }
 
         // This listener monitors our connection to and disconnection from camera devices.
@@ -680,7 +754,7 @@
                         mLegacyDevice =
                                 props.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
                                         CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
-                        mCameraState.setState(AndroidCamera2StateHolder.CAMERA_UNCONFIGURED);
+                        changeState(AndroidCamera2StateHolder.CAMERA_UNCONFIGURED);
                         mOpenCallback.onCameraOpened(mCameraProxy);
                     } catch (CameraAccessException ex) {
                         mOpenCallback.onDeviceOpenFailure(mCameraIndex,
@@ -710,7 +784,7 @@
             @Override
             public void onConfigured(CameraCaptureSession session) {
                 mSession = session;
-                mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
+                changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
             }
 
             @Override
@@ -728,49 +802,76 @@
                 }
             }};
 
+        private abstract class CameraResultStateListener
+                extends CameraCaptureSession.CaptureListener {
+            public abstract void monitorControlStates(CaptureResult result);
+
+            public abstract void resetState();
+        }
+
         // This listener monitors requested captures and notifies any relevant callbacks.
-        private CameraCaptureSession.CaptureListener mCameraFocusStateListener =
-                new CameraCaptureSession.CaptureListener() {
+        private CameraResultStateListener mCameraResultStateListener =
+                new CameraResultStateListener() {
             private int mLastAfState = -1;
+            private long mLastAfFrameNumber = -1;
+            private long mLastAeFrameNumber = -1;
+
+            @Override
+            public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request,
+                                            CaptureResult result) {
+                monitorControlStates(result);
+            }
 
             @Override
             public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
                                            TotalCaptureResult result) {
+                monitorControlStates(result);
+            }
+
+            @Override
+            public void monitorControlStates(CaptureResult result) {
                 Integer afStateMaybe = result.get(CaptureResult.CONTROL_AF_STATE);
                 if (afStateMaybe != null) {
                     int afState = afStateMaybe;
-                    boolean afStateChanged = false;
-                    if (afState != mLastAfState) {
+                    // Since we handle both partial and total results for multiple frames here, we
+                    // might get the final callbacks for an earlier frame after receiving one or
+                    // more that correspond to the next one. To prevent our data from oscillating,
+                    // we never consider AF states that are older than the last one we've seen.
+                    if (result.getFrameNumber() > mLastAfFrameNumber) {
+                        boolean afStateChanged = afState != mLastAfState;
                         mLastAfState = afState;
-                        afStateChanged = true;
-                    }
+                        mLastAfFrameNumber = result.getFrameNumber();
 
-                    switch (afState) {
-                        case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN:
-                        case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED:
-                        case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED: {
-                            if (afStateChanged && mPassiveAfCallback != null) {
-                                // A CameraAFMoveCallback is attached. If we just started to scan,
-                                // the motor is moving; otherwise, it has settled.
-                                mPassiveAfCallback.onAutoFocusMoving(
-                                        afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                                        mCameraProxy);
+                        switch (afState) {
+                            case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN:
+                            case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED:
+                            case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED: {
+                                if (afStateChanged && mPassiveAfCallback != null) {
+                                    // A CameraAFMoveCallback is attached. If we just started to
+                                    // scan, the motor is moving; otherwise, it has settled.
+                                    mPassiveAfCallback.onAutoFocusMoving(
+                                            afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                                            mCameraProxy);
+                                }
+                                break;
                             }
-                            break;
-                        }
 
-                        case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:
-                        case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: {
-                            if (mOneshotAfCallback != null) {
-                                // A call to autoFocus() was just made to request a focus lock.
-                                // Notify the caller that the lens is now indefinitely fixed, and
-                                // report whether the image we're now stuck with is in focus.
-                                mOneshotAfCallback.onAutoFocus(
-                                        afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-                                        mCameraProxy);
-                                mOneshotAfCallback = null;
+                            case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:
+                            case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: {
+                                // This check must be made regardless of whether the focus state has
+                                // changed recently to avoid infinite waiting during autoFocus()
+                                // when the algorithm has already either converged or failed to.
+                                if (mOneshotAfCallback != null) {
+                                    // A call to autoFocus() was just made to request a focus lock.
+                                    // Notify the caller that the lens is now indefinitely fixed,
+                                    // and report whether the image we're stuck with is in focus.
+                                    mOneshotAfCallback.onAutoFocus(
+                                            afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                                            mCameraProxy);
+                                    mOneshotAfCallback = null;
+                                }
+                                break;
                             }
-                            break;
                         }
                     }
                 }
@@ -778,36 +879,55 @@
                 Integer aeStateMaybe = result.get(CaptureResult.CONTROL_AE_STATE);
                 if (aeStateMaybe != null) {
                     int aeState = aeStateMaybe;
+                    // Since we handle both partial and total results for multiple frames here, we
+                    // might get the final callbacks for an earlier frame after receiving one or
+                    // more that correspond to the next one. To prevent our data from oscillating,
+                    // we never consider AE states that are older than the last one we've seen.
+                    if (result.getFrameNumber() > mLastAeFrameNumber) {
+                        mCurrentAeState = aeStateMaybe;
+                        mLastAeFrameNumber = result.getFrameNumber();
 
-                    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()),
+                        switch (aeState) {
+                            case CaptureResult.CONTROL_AE_STATE_CONVERGED:
+                            case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:
+                            case CaptureResult.CONTROL_AE_STATE_LOCKED: {
+                                // This check must be made regardless of whether the exposure state
+                                // has changed recently to avoid infinite waiting during
+                                // takePicture() when the algorithm has already converged.
+                                if (mOneshotCaptureCallback != null) {
+                                    // A call to takePicture() was just made, and autoexposure
+                                    // converged so it's time to initiate the capture!
+                                    mCaptureReader.setOnImageAvailableListener(
                                             /*listener*/mOneshotCaptureCallback,
                                             /*handler*/Camera2Handler.this);
-                                } catch (CameraAccessException ex) {
-                                    Log.e(TAG, "Unable to initiate capture", ex);
-                                } finally {
-                                    mOneshotCaptureCallback = null;
+                                    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;
                             }
-                            break;
                         }
                     }
                 }
             }
 
             @Override
+            public void resetState() {
+                mLastAfState = -1;
+                mLastAfFrameNumber = -1;
+                mLastAeFrameNumber = -1;
+            }
+
+            @Override
             public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request,
                                         CaptureFailure failure) {
                 Log.e(TAG, "Capture attempt failed with reason " + failure.getReason());
@@ -819,6 +939,8 @@
         private final CameraDevice mCamera;
         private final CameraDeviceInfo.Characteristics mCharacteristics;
         private final AndroidCamera2Capabilities mCapabilities;
+        private CameraSettings mLastSettings;
+        private boolean mShutterSoundEnabled;
 
         public AndroidCamera2ProxyImpl(int cameraIndex, CameraDevice camera,
                 CameraDeviceInfo.Characteristics characteristics,
@@ -827,6 +949,8 @@
             mCamera = camera;
             mCharacteristics = characteristics;
             mCapabilities = new AndroidCamera2Capabilities(properties);
+            mLastSettings = null;
+            mShutterSoundEnabled = true;
         }
 
         // TODO: Implement
@@ -852,6 +976,26 @@
             return mCapabilities;
         }
 
+        // FIXME: Unlock the sizes in stopPreview(), as per the corresponding
+        // explanation on the STOP_PREVIEW case in the handler.
+        @Override
+        public void setPreviewTexture(SurfaceTexture surfaceTexture) {
+            // Once the Surface has been selected, we configure the session and
+            // are no longer able to change the sizes.
+            getSettings().setSizesLocked(true);
+            super.setPreviewTexture(surfaceTexture);
+        }
+
+        // FIXME: Unlock the sizes in stopPreview(), as per the corresponding
+        // explanation on the STOP_PREVIEW case in the handler.
+        @Override
+        public void setPreviewTextureSync(SurfaceTexture surfaceTexture) {
+            // Once the Surface has been selected, we configure the session and
+            // are no longer able to change the sizes.
+            getSettings().setSizesLocked(true);
+            super.setPreviewTexture(surfaceTexture);
+        }
+
         // TODO: Implement
         @Override
         public void setPreviewDataCallback(Handler handler, CameraPreviewDataCallback cb) {}
@@ -935,7 +1079,9 @@
                         handler.post(new Runnable() {
                             @Override
                             public void run() {
-                                mNoisemaker.play(MediaActionSound.SHUTTER_CLICK);
+                                if (mShutterSoundEnabled) {
+                                    mNoisemaker.play(MediaActionSound.SHUTTER_CLICK);
+                                }
                                 shutter.onShutter(AndroidCamera2ProxyImpl.this);
                             }});
                     }
@@ -959,8 +1105,9 @@
             mDispatchThread.runJob(new Runnable() {
                 @Override
                 public void run() {
-                    mCameraState.waitForStates(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE |
-                            AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
+                    // Wait until PREVIEW_ACTIVE or better
+                    mCameraState.waitForStates(
+                            ~(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE - 1));
                     mCameraHandler.obtainMessage(CameraActions.CAPTURE_PHOTO, picListener)
                             .sendToTarget();
                 }});
@@ -997,7 +1144,10 @@
 
         @Override
         public CameraSettings getSettings() {
-            return mCameraHandler.buildSettings(mCapabilities);
+            if (mLastSettings == null) {
+                mLastSettings = mCameraHandler.buildSettings(mCapabilities);
+            }
+            return mLastSettings;
         }
 
         @Override
@@ -1011,9 +1161,17 @@
                 return false;
             }
 
-            return applySettingsHelper(settings, AndroidCamera2StateHolder.CAMERA_UNCONFIGURED |
-                    AndroidCamera2StateHolder.CAMERA_CONFIGURED |
-                    AndroidCamera2StateHolder.CAMERA_PREVIEW_READY);
+            // Wait for any state that isn't OPENED
+            if (applySettingsHelper(settings, ~AndroidCamera2StateHolder.CAMERA_UNOPENED)) {
+                mLastSettings = settings;
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void enableShutterSound(boolean enable) {
+            mShutterSoundEnabled = enable;
         }
 
         // TODO: Implement
@@ -1040,19 +1198,22 @@
     private static class AndroidCamera2StateHolder extends CameraStateHolder {
         // Usage flow: openCamera() -> applySettings() -> setPreviewTexture() -> startPreview() ->
         //             autoFocus() -> takePicture()
+        // States are mutually exclusive, but must be separate bits so that they can be used with
+        // the StateHolder#waitForStates() and StateHolder#waitToAvoidStates() methods.
+        // Do not set the state to be a combination of these values!
         /* Camera states */
         /** No camera device is opened. */
-        public static final int CAMERA_UNOPENED = 1;
+        public static final int CAMERA_UNOPENED = 1 << 0;
         /** A camera is opened, but no settings have been provided. */
-        public static final int CAMERA_UNCONFIGURED = 2;
+        public static final int CAMERA_UNCONFIGURED = 1 << 1;
         /** The open camera has been configured by providing it with settings. */
-        public static final int CAMERA_CONFIGURED = 3;
+        public static final int CAMERA_CONFIGURED = 1 << 2;
         /** A capture session is ready to stream a preview, but still has no repeating request. */
-        public static final int CAMERA_PREVIEW_READY = 4;
+        public static final int CAMERA_PREVIEW_READY = 1 << 3;
         /** A preview is currently being streamed. */
-        public static final int CAMERA_PREVIEW_ACTIVE = 5;
+        public static final int CAMERA_PREVIEW_ACTIVE = 1 << 4;
         /** The lens is locked on a particular region. */
-        public static final int CAMERA_FOCUS_LOCKED = 6;
+        public static final int CAMERA_FOCUS_LOCKED = 1 << 5;
 
         public AndroidCamera2StateHolder() {
             this(CAMERA_UNOPENED);
@@ -1173,9 +1334,7 @@
 
             @Override
             public boolean canDisableShutterSound() {
-                // The new API doesn't support this operation, so don't encourage people to try it.
-                // TODO: What kind of assumptions have callers made about this result's meaning?
-                return false;
+                return true;
             }
 
             private static float[] convertRectToPoly(RectF rf) {
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 a132d08..d668f85 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
@@ -18,7 +18,9 @@
 
 import static android.hardware.camera2.CaptureRequest.*;
 
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.params.MeteringRectangle;
@@ -43,8 +45,12 @@
 
     private final Builder mTemplateSettings;
     private final Camera2RequestSettingsSet mRequestSettings;
+    /** Sensor's active array bounds. */
     private final Rect mActiveArray;
+    /** Crop rectangle for digital zoom (measured WRT the active array). */
     private final Rect mCropRectangle;
+    /** Bounds of visible preview portion (measured WRT the active array). */
+    private Rect mVisiblePreviewRectangle;
 
     /**
      * Create a settings representation that answers queries of unspecified
@@ -84,6 +90,8 @@
         mActiveArray = activeArray;
         mCropRectangle = new Rect(0, 0, activeArray.width(), activeArray.height());
 
+        mSizesLocked = false;
+
         Range<Integer> previewFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE);
         if (previewFpsRange != null) {
             setPreviewFpsRange(previewFpsRange.getLower(), previewFpsRange.getUpper());
@@ -177,6 +185,8 @@
     @Override
     public void setZoomRatio(float ratio) {
         super.setZoomRatio(ratio);
+
+        // Compute the crop rectangle to be passed to the framework
         mCropRectangle.set(0, 0,
                 toIntConstrained(
                         mActiveArray.width() / mCurrentZoomRatio, 0, mActiveArray.width()),
@@ -184,6 +194,10 @@
                         mActiveArray.height() / mCurrentZoomRatio, 0, mActiveArray.height()));
         mCropRectangle.offsetTo((mActiveArray.width() - mCropRectangle.width()) / 2,
                 (mActiveArray.height() - mCropRectangle.height()) / 2);
+
+        // Compute the effective crop rectangle to be used for computing focus/metering coordinates
+        mVisiblePreviewRectangle =
+                effectiveCropRectFromRequested(mCropRectangle, mCurrentPreviewSize);
     }
 
     private boolean matchesTemplateDefault(Key<?> setting) {
@@ -515,4 +529,49 @@
             mRequestSettings.set(JPEG_GPS_LOCATION, location);
         }
     }
+
+    /**
+     * Calculate the effective crop rectangle for this preview viewport;
+     * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions
+     * without skewing.
+     *
+     * <p>Assumes the zoom level of the provided desired crop rectangle.</p>
+     *
+     * @param requestedCrop Desired crop rectangle, in active array space.
+     * @param previewSize Size of the preview buffer render target, in pixels (not in sensor space).
+     * @return A rectangle that serves as the preview stream's effective crop region (unzoomed), in
+     *          sensor space.
+     *
+     * @throws NullPointerException
+     *          If any of the args were {@code null}.
+     */
+    private static Rect effectiveCropRectFromRequested(Rect requestedCrop, Size previewSize) {
+        float aspectRatioArray = requestedCrop.width() * 1.0f / requestedCrop.height();
+        float aspectRatioPreview = previewSize.width() * 1.0f / previewSize.height();
+
+        float cropHeight, cropWidth;
+        if (aspectRatioPreview < aspectRatioArray) {
+            // The new width must be smaller than the height, so scale the width by AR
+            cropHeight = requestedCrop.height();
+            cropWidth = cropHeight * aspectRatioPreview;
+        } else {
+            // The new height must be smaller (or equal) than the width, so scale the height by AR
+            cropWidth = requestedCrop.width();
+            cropHeight = cropWidth / aspectRatioPreview;
+        }
+
+        Matrix translateMatrix = new Matrix();
+        RectF cropRect = new RectF(/*left*/0, /*top*/0, cropWidth, cropHeight);
+
+        // Now center the crop rectangle so its center is in the center of the active array
+        translateMatrix.setTranslate(requestedCrop.exactCenterX(), requestedCrop.exactCenterY());
+        translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY());
+
+        translateMatrix.mapRect(/*inout*/cropRect);
+
+        // Round the rect corners towards the nearest integer values
+        Rect result = new Rect();
+        cropRect.roundOut(result);
+        return result;
+    }
 }
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 f8a2e38..358d5f6 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
@@ -400,6 +400,7 @@
                         break;
                     }
 
+                    // TODO: Lock the CameraSettings object's sizes
                     case CameraActions.SET_PREVIEW_TEXTURE_ASYNC: {
                         setPreviewTexture(msg.obj);
                         break;
@@ -424,6 +425,7 @@
                         break;
                     }
 
+                    // TODO: Unlock the CameraSettings object's sizes
                     case CameraActions.STOP_PREVIEW: {
                         mCamera.stopPreview();
                         break;
@@ -612,11 +614,15 @@
             if (mCapabilities.supports(CameraCapabilities.Feature.FOCUS_AREA)) {
                 if (settings.getFocusAreas().size() != 0) {
                     parameters.setFocusAreas(settings.getFocusAreas());
+                } else {
+                    parameters.setFocusAreas(null);
                 }
             }
             if (mCapabilities.supports(CameraCapabilities.Feature.METERING_AREA)) {
                 if (settings.getMeteringAreas().size() != 0) {
                     parameters.setMeteringAreas(settings.getMeteringAreas());
+                } else {
+                    parameters.setMeteringAreas(null);
                 }
             }
             if (settings.getCurrentFlashMode() != CameraCapabilities.FlashMode.NO_FLASH) {
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 f5421d6..ee69b54 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java
@@ -28,6 +28,8 @@
     public AndroidCameraSettings(CameraCapabilities capabilities, Camera.Parameters params) {
         CameraCapabilities.Stringifier stringifier = capabilities.getStringifier();
 
+        setSizesLocked(false);
+
         // Preview
         Camera.Size paramPreviewSize = params.getPreviewSize();
         setPreviewSize(new Size(paramPreviewSize.width, paramPreviewSize.height));
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 df94a41..b624b47 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
@@ -436,8 +436,24 @@
         /**
          * Sets the {@link android.graphics.SurfaceTexture} for preview.
          *
+         * <p>Note that, once this operation has been performed, it is no longer
+         * possible to change the preview or photo sizes in the
+         * {@link CameraSettings} instance for this camera, and the mutators for
+         * these fields are allowed to ignore all further invocations until the
+         * preview is stopped with {@link #stopPreview}.</p>
+         *
          * @param surfaceTexture The {@link SurfaceTexture} for preview.
+         *
+         * @see CameraSettings#setPhotoSize
+         * @see CameraSettings#setPreviewSize
          */
+        // XXX: Despite the above documentation about locking the sizes, the API
+        // 1 implementation doesn't currently enforce this at all, although the
+        // Camera class warns that preview sizes shouldn't be changed while a
+        // preview is running. Furthermore, the API 2 implementation doesn't yet
+        // unlock the sizes when stopPreview() is invoked (see related FIXME on
+        // the STOP_PREVIEW case in its handler; in the meantime, changing API 2
+        // sizes would require closing and reopening the camera.
         public void setPreviewTexture(final SurfaceTexture surfaceTexture) {
             getDispatchThread().runJob(new Runnable() {
                 @Override
@@ -452,7 +468,15 @@
          * Blocks until a {@link android.graphics.SurfaceTexture} has been set
          * for preview.
          *
+         * <p>Note that, once this operation has been performed, it is no longer
+         * possible to change the preview or photo sizes in the
+         * {@link CameraSettings} instance for this camera, and the mutators for
+         * these fields are allowed to ignore all further invocations.</p>
+         *
          * @param surfaceTexture The {@link SurfaceTexture} for preview.
+         *
+         * @see CameraSettings#setPhotoSize
+         * @see CameraSettings#setPreviewSize
          */
         public void setPreviewTextureSync(final SurfaceTexture surfaceTexture) {
             final WaitDoneBundle bundle = new WaitDoneBundle();
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 d0600e8..87e9adf 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
@@ -38,6 +38,7 @@
     protected final Map<String, String> mGeneralSetting = new TreeMap<>();
     protected final List<Camera.Area> mMeteringAreas = new ArrayList<>();
     protected final List<Camera.Area> mFocusAreas = new ArrayList<>();
+    protected boolean mSizesLocked;
     protected int mPreviewFpsRangeMin;
     protected int mPreviewFpsRangeMax;
     protected int mPreviewFrameRate;
@@ -116,6 +117,7 @@
         mGeneralSetting.putAll(src.mGeneralSetting);
         mMeteringAreas.addAll(src.mMeteringAreas);
         mFocusAreas.addAll(src.mFocusAreas);
+        mSizesLocked = src.mSizesLocked;
         mPreviewFpsRangeMin = src.mPreviewFpsRangeMin;
         mPreviewFpsRangeMax = src.mPreviewFpsRangeMax;
         mPreviewFrameRate = src.mPreviewFrameRate;
@@ -151,6 +153,19 @@
         mGeneralSetting.put(key, value);
     }
 
+    /**
+     * Changes whether classes outside this class are allowed to set the preview
+     * and photo capture sizes.
+     *
+     * @param locked Whether to prevent changes to these fields.
+     *
+     * @see #setPhotoSize
+     * @see #setPreviewSize
+     */
+    /*package*/ void setSizesLocked(boolean locked) {
+        mSizesLocked = locked;
+    }
+
     /**  Preview **/
 
     /**
@@ -212,9 +227,16 @@
 
     /**
      * @param previewSize The size to use for preview.
+     * @return Whether the operation was allowed (i.e. the sizes are unlocked).
      */
-    public void setPreviewSize(Size previewSize) {
+    public boolean setPreviewSize(Size previewSize) {
+        if (mSizesLocked) {
+            Log.w(TAG, "Attempt to change preview size while locked");
+            return false;
+        }
+
         mCurrentPreviewSize = new Size(previewSize);
+        return true;
     }
 
     /**
@@ -245,12 +267,17 @@
     }
 
     /**
-     * Sets the size for the photo.
-     *
-     * @param photoSize The photo size.
+     * @param photoSize The size to use for preview.
+     * @return Whether the operation was allowed (i.e. the sizes are unlocked).
      */
-    public void setPhotoSize(Size photoSize) {
+    public boolean setPhotoSize(Size photoSize) {
+        if (mSizesLocked) {
+            Log.w(TAG, "Attempt to change photo size while locked");
+            return false;
+        }
+
         mCurrentPhotoSize = new Size(photoSize);
+        return true;
     }
 
     /**
diff --git a/camera2/utils/src/com/android/ex/camera2/utils/Camera2RequestSettingsSet.java b/camera2/utils/src/com/android/ex/camera2/utils/Camera2RequestSettingsSet.java
index fac8f1a..6e1d825 100644
--- a/camera2/utils/src/com/android/ex/camera2/utils/Camera2RequestSettingsSet.java
+++ b/camera2/utils/src/com/android/ex/camera2/utils/Camera2RequestSettingsSet.java
@@ -146,7 +146,7 @@
      * to its default value or simply unset. While {@link #get} will return
      * {@code null} in both these cases, this method will return {@code true}
      * and {@code false}, respectively.</p>
-
+     *
      * @param key Which setting to look for.
      * @return Whether that setting has a value that will propagate with unions.
      *
@@ -160,8 +160,23 @@
     }
 
     /**
+     * Check whether the value of the specified setting matches the given one.
+     *
+     * <p>This method uses the {@code T} type's {@code equals} method, but is
+     * {@code null}-tolerant.</p>
+     *
+     * @param key Which of this class's settings to check.
+     * @param value Value to test for equality against.
+     * @return Whether they are the same.
+     */
+    public <T> boolean matches(Key<T> key, T value) {
+        return Objects.equals(get(key), value);
+    }
+
+    /**
      * Get this set of settings's revision identifier, which can be compared
      * against cached past values to determine whether it has been modified.
+     *
      * <p>Distinct revisions across the same object do not necessarily indicate
      * that the object's key/value pairs have changed at all, but the same
      * revision on the same object does imply that they've stayed the same.</p>
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 01eda8f..7847526 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,6 +23,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import android.graphics.Rect;
 import android.hardware.camera2.CameraCaptureSession.CaptureListener;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
@@ -234,6 +235,45 @@
         setUp.get(null);
     }
 
+    @Test
+    public void requestSettingsSetMatchesPrimitives() {
+        Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet();
+        assertTrue(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, null));
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, false));
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, true));
+
+        setUp.set(CaptureRequest.CONTROL_AE_LOCK, null);
+        assertTrue(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, null));
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, false));
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, true));
+
+        setUp.set(CaptureRequest.CONTROL_AE_LOCK, false);
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, null));
+        assertTrue(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, false));
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, true));
+
+        setUp.set(CaptureRequest.CONTROL_AE_LOCK, true);
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, null));
+        assertFalse(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, false));
+        assertTrue(setUp.matches(CaptureRequest.CONTROL_AE_LOCK, true));
+    }
+
+    @Test
+    public void requestSettingsSetMatchesReferences() {
+        Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet();
+        assertTrue(setUp.matches(CaptureRequest.SCALER_CROP_REGION, null));
+        assertFalse(setUp.matches(CaptureRequest.SCALER_CROP_REGION, new Rect(0, 0, 0, 0)));
+
+        setUp.set(CaptureRequest.SCALER_CROP_REGION, null);
+        assertTrue(setUp.matches(CaptureRequest.SCALER_CROP_REGION, null));
+        assertFalse(setUp.matches(CaptureRequest.SCALER_CROP_REGION, new Rect(0, 0, 0, 0)));
+
+        setUp.set(CaptureRequest.SCALER_CROP_REGION, new Rect(0, 0, 0, 0));
+        assertFalse(setUp.matches(CaptureRequest.SCALER_CROP_REGION, null));
+        assertTrue(setUp.matches(CaptureRequest.SCALER_CROP_REGION, new Rect(0, 0, 0, 0)));
+        assertFalse(setUp.matches(CaptureRequest.SCALER_CROP_REGION, new Rect(0, 0, 1, 1)));
+    }
+
     @Test(expected=NullPointerException.class)
     public void requestSettingsSetNullArgToCreateRequest0() throws Exception {
         Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet();