camera2: (LEGACY) Add face detection support and vstab modes

Also work-around CTS freezing forever in Camera#setPreviewTexture

* Set preview texture to null after unconfiguring (Camera#stopPreview)
* Remove all uses of recording hint

(Some devices disable face detection unconditionally when recording is not
using HDR mode; recording hints will be introduced after we add
capture intent support)

Change-Id: I2f0a80f1d580af214cb0f53abcbf33c7391790f1
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index dc71a06..febb015 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -67,6 +67,7 @@
 import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * Implementation of camera metadata marshal/unmarshal across Binder to
@@ -227,6 +228,7 @@
 
     private static final String CELLID_PROCESS = "CELLID";
     private static final String GPS_PROCESS = "GPS";
+    private static final int FACE_LANDMARK_SIZE = 6;
 
     private static String translateLocationProviderToProcess(final String provider) {
         if (provider == null) {
@@ -347,7 +349,7 @@
         // Check if key has been overridden to use a wrapper class on the java side.
         GetCommand g = sGetCommandMap.get(key);
         if (g != null) {
-            return (T) g.getValue(this, key);
+            return g.getValue(this, key);
         }
         return getBase(key);
     }
@@ -587,9 +589,71 @@
         return availableFormats;
     }
 
-    private Face[] getFaces() {
-        final int FACE_LANDMARK_SIZE = 6;
+    private boolean setFaces(Face[] faces) {
+        if (faces == null) {
+            return false;
+        }
 
+        int numFaces = faces.length;
+
+        // Detect if all faces are SIMPLE or not; count # of valid faces
+        boolean fullMode = true;
+        for (Face face : faces) {
+            if (face == null) {
+                numFaces--;
+                Log.w(TAG, "setFaces - null face detected, skipping");
+                continue;
+            }
+
+            if (face.getId() == Face.ID_UNSUPPORTED) {
+                fullMode = false;
+            }
+        }
+
+        Rect[] faceRectangles = new Rect[numFaces];
+        byte[] faceScores = new byte[numFaces];
+        int[] faceIds = null;
+        int[] faceLandmarks = null;
+
+        if (fullMode) {
+            faceIds = new int[numFaces];
+            faceLandmarks = new int[numFaces * FACE_LANDMARK_SIZE];
+        }
+
+        int i = 0;
+        for (Face face : faces) {
+            if (face == null) {
+                continue;
+            }
+
+            faceRectangles[i] = face.getBounds();
+            faceScores[i] = (byte)face.getScore();
+
+            if (fullMode) {
+                faceIds[i] = face.getId();
+
+                int j = 0;
+
+                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getLeftEyePosition().x;
+                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getLeftEyePosition().y;
+                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getRightEyePosition().x;
+                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getRightEyePosition().y;
+                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getMouthPosition().x;
+                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getMouthPosition().y;
+            }
+
+            i++;
+        }
+
+        set(CaptureResult.STATISTICS_FACE_RECTANGLES, faceRectangles);
+        set(CaptureResult.STATISTICS_FACE_IDS, faceIds);
+        set(CaptureResult.STATISTICS_FACE_LANDMARKS, faceLandmarks);
+        set(CaptureResult.STATISTICS_FACE_SCORES, faceScores);
+
+        return true;
+    }
+
+    private Face[] getFaces() {
         Integer faceDetectMode = get(CaptureResult.STATISTICS_FACE_DETECT_MODE);
         if (faceDetectMode == null) {
             Log.w(TAG, "Face detect mode metadata is null, assuming the mode is SIMPLE");
@@ -653,9 +717,12 @@
                 if (faceScores[i] <= Face.SCORE_MAX &&
                         faceScores[i] >= Face.SCORE_MIN &&
                         faceIds[i] >= 0) {
-                    Point leftEye = new Point(faceLandmarks[i*6], faceLandmarks[i*6+1]);
-                    Point rightEye = new Point(faceLandmarks[i*6+2], faceLandmarks[i*6+3]);
-                    Point mouth = new Point(faceLandmarks[i*6+4], faceLandmarks[i*6+5]);
+                    Point leftEye = new Point(faceLandmarks[i*FACE_LANDMARK_SIZE],
+                            faceLandmarks[i*FACE_LANDMARK_SIZE+1]);
+                    Point rightEye = new Point(faceLandmarks[i*FACE_LANDMARK_SIZE+2],
+                            faceLandmarks[i*FACE_LANDMARK_SIZE+3]);
+                    Point mouth = new Point(faceLandmarks[i*FACE_LANDMARK_SIZE+4],
+                            faceLandmarks[i*FACE_LANDMARK_SIZE+5]);
                     Face face = new Face(faceRectangles[i], faceScores[i], faceIds[i],
                             leftEye, rightEye, mouth);
                     faceList.add(face);
@@ -865,6 +932,13 @@
                 metadata.setFaceRectangles((Rect[]) value);
             }
         });
+        sSetCommandMap.put(CaptureResult.STATISTICS_FACES.getNativeKey(),
+                new SetCommand() {
+            @Override
+            public <T> void setValue(CameraMetadataNative metadata, T value) {
+                metadata.setFaces((Face[])value);
+            }
+        });
         sSetCommandMap.put(CaptureRequest.TONEMAP_CURVE.getNativeKey(), new SetCommand() {
             @Override
             public <T> void setValue(CameraMetadataNative metadata, T value) {
diff --git a/core/java/android/hardware/camera2/legacy/LegacyFaceDetectMapper.java b/core/java/android/hardware/camera2/legacy/LegacyFaceDetectMapper.java
new file mode 100644
index 0000000..1470b70
--- /dev/null
+++ b/core/java/android/hardware/camera2/legacy/LegacyFaceDetectMapper.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.camera2.legacy;
+
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.hardware.Camera.FaceDetectionListener;
+import android.hardware.camera2.impl.CameraMetadataNative;
+import android.hardware.camera2.legacy.ParameterUtils.ZoomData;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.params.Face;
+import android.hardware.camera2.utils.ListUtils;
+import android.hardware.camera2.utils.ParamsUtils;
+import android.util.Log;
+import android.util.Size;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static android.hardware.camera2.CaptureRequest.*;
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * Map legacy face detect callbacks into face detection results.
+ */
+@SuppressWarnings("deprecation")
+public class LegacyFaceDetectMapper {
+    private static String TAG = "LegacyFaceDetectMapper";
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+
+    private final Camera mCamera;
+    private final boolean mFaceDetectSupported;
+    private boolean mFaceDetectEnabled = false;
+
+    private final Object mLock = new Object();
+    private Camera.Face[] mFaces;
+    private Camera.Face[] mFacesPrev;
+    /**
+     * Instantiate a new face detect mapper.
+     *
+     * @param camera a non-{@code null} camera1 device
+     * @param characteristics a  non-{@code null} camera characteristics for that camera1
+     *
+     * @throws NullPointerException if any of the args were {@code null}
+     */
+    public LegacyFaceDetectMapper(Camera camera, CameraCharacteristics characteristics) {
+        mCamera = checkNotNull(camera, "camera must not be null");
+        checkNotNull(characteristics, "characteristics must not be null");
+
+        mFaceDetectSupported = ArrayUtils.contains(
+                characteristics.get(
+                        CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES),
+                STATISTICS_FACE_DETECT_MODE_SIMPLE);
+
+        if (!mFaceDetectSupported) {
+            return;
+        }
+
+       mCamera.setFaceDetectionListener(new FaceDetectionListener() {
+
+        @Override
+        public void onFaceDetection(Camera.Face[] faces, Camera camera) {
+            int lengthFaces = faces == null ? 0 : faces.length;
+            synchronized (mLock) {
+                if (mFaceDetectEnabled) {
+                    mFaces = faces;
+                } else if (lengthFaces > 0) {
+                    // stopFaceDetectMode could race against the requests, print a debug log
+                    Log.d(TAG,
+                            "onFaceDetection - Ignored some incoming faces since" +
+                            "face detection was disabled");
+                }
+            }
+
+            if (VERBOSE) {
+                Log.v(TAG, "onFaceDetection - read " + lengthFaces + " faces");
+            }
+        }
+       });
+    }
+
+    /**
+     * Process the face detect mode from the capture request into an api1 face detect toggle.
+     *
+     * <p>This method should be called after the parameters are {@link LegacyRequestMapper mapped}
+     * with the request.</p>
+     *
+     * <p>Callbacks are processed in the background, and the next call to {@link #mapResultTriggers}
+     * will have the latest faces detected as reflected by the camera1 callbacks.</p>
+     *
+     * <p>None of the arguments will be mutated.</p>
+     *
+     * @param captureRequest a non-{@code null} request
+     * @param parameters a non-{@code null} parameters corresponding to this request (read-only)
+     */
+    public void processFaceDetectMode(CaptureRequest captureRequest,
+            Camera.Parameters parameters) {
+        checkNotNull(captureRequest, "captureRequest must not be null");
+
+        /*
+         * statistics.faceDetectMode
+         */
+        int fdMode = ParamsUtils.getOrDefault(captureRequest, STATISTICS_FACE_DETECT_MODE,
+                STATISTICS_FACE_DETECT_MODE_OFF);
+
+        if (fdMode != STATISTICS_FACE_DETECT_MODE_OFF && !mFaceDetectSupported) {
+            Log.w(TAG,
+                    "processFaceDetectMode - Ignoring statistics.faceDetectMode; " +
+                    "face detection is not available");
+            return;
+        }
+
+        // Print some warnings out in case the values were wrong
+        switch (fdMode) {
+            case STATISTICS_FACE_DETECT_MODE_OFF:
+            case STATISTICS_FACE_DETECT_MODE_SIMPLE:
+                break;
+            case STATISTICS_FACE_DETECT_MODE_FULL:
+                Log.w(TAG,
+                        "processFaceDetectMode - statistics.faceDetectMode == FULL unsupported, " +
+                        "downgrading to SIMPLE");
+                break;
+            default:
+                Log.w(TAG, "processFaceDetectMode - ignoring unknown statistics.faceDetectMode = "
+                        + fdMode);
+                return;
+        }
+
+        boolean enableFaceDetect = fdMode != STATISTICS_FACE_DETECT_MODE_OFF;
+        synchronized (mLock) {
+            // Enable/disable face detection if it's changed since last time
+            if (enableFaceDetect != mFaceDetectEnabled) {
+                if (enableFaceDetect) {
+                    mCamera.startFaceDetection();
+
+                    if (VERBOSE) {
+                        Log.v(TAG, "processFaceDetectMode - start face detection");
+                    }
+                } else {
+                    mCamera.stopFaceDetection();
+
+                    if (VERBOSE) {
+                        Log.v(TAG, "processFaceDetectMode - stop face detection");
+                    }
+
+                    mFaces = null;
+                }
+
+                mFaceDetectEnabled = enableFaceDetect;
+            }
+        }
+    }
+
+    /**
+     * Update the {@code result} camera metadata map with the new value for the
+     * {@code statistics.faces} and {@code statistics.faceDetectMode}.
+     *
+     * <p>Face detect callbacks are processed in the background, and each call to
+     * {@link #mapResultFaces} will have the latest faces as reflected by the camera1 callbacks.</p>
+     *
+     * @param result a non-{@code null} result
+     * @param legacyRequest a non-{@code null} request (read-only)
+     */
+    public void mapResultFaces(CameraMetadataNative result, LegacyRequest legacyRequest) {
+        checkNotNull(result, "result must not be null");
+        checkNotNull(legacyRequest, "legacyRequest must not be null");
+
+        Camera.Face[] faces, previousFaces;
+        int fdMode;
+        synchronized (mLock) {
+            fdMode = mFaceDetectEnabled ?
+                            STATISTICS_FACE_DETECT_MODE_SIMPLE : STATISTICS_FACE_DETECT_MODE_OFF;
+
+            if (mFaceDetectEnabled) {
+                faces = mFaces;
+            } else {
+                faces = null;
+            }
+
+            previousFaces = mFacesPrev;
+            mFacesPrev = faces;
+        }
+
+        CameraCharacteristics characteristics = legacyRequest.characteristics;
+        CaptureRequest request = legacyRequest.captureRequest;
+        Size previewSize = legacyRequest.previewSize;
+        Camera.Parameters params = legacyRequest.parameters;
+
+        Rect activeArray = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        ZoomData zoomData = ParameterUtils.convertScalerCropRegion(activeArray,
+                request.get(CaptureRequest.SCALER_CROP_REGION), previewSize, params);
+
+        List<Face> convertedFaces = new ArrayList<>();
+        if (faces != null) {
+            for (Camera.Face face : faces) {
+                if (face != null) {
+                    convertedFaces.add(
+                            ParameterUtils.convertFaceFromLegacy(face, activeArray, zoomData));
+                } else {
+                    Log.w(TAG, "mapResultFaces - read NULL face from camera1 device");
+                }
+            }
+        }
+
+        if (VERBOSE && previousFaces != faces) { // Log only in verbose and IF the faces changed
+            Log.v(TAG, "mapResultFaces - changed to " + ListUtils.listToString(convertedFaces));
+        }
+
+        result.set(CaptureResult.STATISTICS_FACES, convertedFaces.toArray(new Face[0]));
+        result.set(CaptureResult.STATISTICS_FACE_DETECT_MODE, fdMode);
+    }
+}
diff --git a/core/java/android/hardware/camera2/legacy/LegacyFocusStateMapper.java b/core/java/android/hardware/camera2/legacy/LegacyFocusStateMapper.java
index e576b43..d0a3a3f 100644
--- a/core/java/android/hardware/camera2/legacy/LegacyFocusStateMapper.java
+++ b/core/java/android/hardware/camera2/legacy/LegacyFocusStateMapper.java
@@ -247,7 +247,8 @@
                 // No action necessary. The callbacks will handle transitions.
                 break;
             default:
-                Log.w(TAG, "mapTriggers - ignoring unknown control.afTrigger = " + afTrigger);
+                Log.w(TAG, "processRequestTriggers - ignoring unknown control.afTrigger = "
+                        + afTrigger);
         }
     }
 
diff --git a/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java b/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java
index 711edf4..b05508b 100644
--- a/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java
+++ b/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java
@@ -204,6 +204,11 @@
         mapSensor(m, p);
 
         /*
+         * statistics.*
+         */
+        mapStatistics(m, p);
+
+        /*
          * sync.*
          */
         mapSync(m, p);
@@ -487,6 +492,18 @@
 
     private static void mapControlOther(CameraMetadataNative m, Camera.Parameters p) {
         /*
+         * android.control.availableVideoStabilizationModes
+         */
+        {
+            int stabModes[] = p.isVideoStabilizationSupported() ?
+                    new int[] { CONTROL_VIDEO_STABILIZATION_MODE_OFF,
+                                CONTROL_VIDEO_STABILIZATION_MODE_ON } :
+                    new int[] { CONTROL_VIDEO_STABILIZATION_MODE_OFF };
+
+            m.set(CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES, stabModes);
+        }
+
+        /*
          * android.control.maxRegions
          */
         final int AE = 0, AWB = 1, AF = 2;
@@ -742,6 +759,31 @@
         m.set(SENSOR_INFO_PIXEL_ARRAY_SIZE, largestJpegSize);
     }
 
+    private static void mapStatistics(CameraMetadataNative m, Parameters p) {
+        /*
+         * statistics.info.availableFaceDetectModes
+         */
+        int[] fdModes;
+
+        if (p.getMaxNumDetectedFaces() > 0) {
+            fdModes = new int[] {
+                STATISTICS_FACE_DETECT_MODE_OFF,
+                STATISTICS_FACE_DETECT_MODE_SIMPLE
+                // FULL is never-listed, since we have no way to query it statically
+            };
+        } else {
+            fdModes = new int[] {
+                STATISTICS_FACE_DETECT_MODE_OFF
+            };
+        }
+        m.set(STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES, fdModes);
+
+        /*
+         * statistics.info.maxFaceCount
+         */
+        m.set(STATISTICS_INFO_MAX_FACE_COUNT, p.getMaxNumDetectedFaces());
+    }
+
     private static void mapSync(CameraMetadataNative m, Parameters p) {
         /*
          * sync.maxLatency
diff --git a/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java b/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java
index a6fe035..20f3fd2 100644
--- a/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java
+++ b/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java
@@ -150,10 +150,8 @@
             if (supported) {
                 params.setPreviewFpsRange(legacyFps[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
                         legacyFps[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
-                params.setRecordingHint(false);
             } else {
                 Log.w(TAG, "Unsupported FPS range set [" + legacyFps[0] + "," + legacyFps[1] + "]");
-                params.setRecordingHint(true);
             }
         }
 
@@ -248,6 +246,18 @@
          // TODO: Don't add control.awbLock to availableRequestKeys if it's not supported
         }
 
+        // control.videoStabilizationMode
+        {
+            Integer stabMode = getIfSupported(request, CONTROL_VIDEO_STABILIZATION_MODE,
+                    /*defaultValue*/CONTROL_VIDEO_STABILIZATION_MODE_OFF,
+                    params.isVideoStabilizationSupported(),
+                    /*allowedValue*/CONTROL_VIDEO_STABILIZATION_MODE_OFF);
+
+            if (stabMode != null) {
+                params.setVideoStabilization(stabMode == CONTROL_VIDEO_STABILIZATION_MODE_ON);
+            }
+        }
+
         // lens.focusDistance
         {
             boolean infinityFocusSupported =
diff --git a/core/java/android/hardware/camera2/legacy/LegacyResultMapper.java b/core/java/android/hardware/camera2/legacy/LegacyResultMapper.java
index 9eff943..a2487f4 100644
--- a/core/java/android/hardware/camera2/legacy/LegacyResultMapper.java
+++ b/core/java/android/hardware/camera2/legacy/LegacyResultMapper.java
@@ -35,6 +35,9 @@
 import java.util.List;
 
 import static com.android.internal.util.Preconditions.*;
+import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF;
+import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON;
+import static android.hardware.camera2.CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE;
 import static android.hardware.camera2.CaptureResult.*;
 
 /**
@@ -142,7 +145,6 @@
          */
         mapAwb(result, /*out*/params);
 
-
         /*
          * control.mode
          */
@@ -171,7 +173,6 @@
             }
         }
 
-
         /*
          * control.effectMode
          */
@@ -187,6 +188,15 @@
             }
         }
 
+        // control.videoStabilizationMode
+        {
+            int stabMode =
+                    (params.isVideoStabilizationSupported() && params.getVideoStabilization()) ?
+                        CONTROL_VIDEO_STABILIZATION_MODE_ON :
+                        CONTROL_VIDEO_STABILIZATION_MODE_OFF;
+            result.set(CONTROL_VIDEO_STABILIZATION_MODE, stabMode);
+        }
+
         /*
          * flash
          */
diff --git a/core/java/android/hardware/camera2/legacy/ParameterUtils.java b/core/java/android/hardware/camera2/legacy/ParameterUtils.java
index efd12f2..385f844 100644
--- a/core/java/android/hardware/camera2/legacy/ParameterUtils.java
+++ b/core/java/android/hardware/camera2/legacy/ParameterUtils.java
@@ -43,6 +43,7 @@
 /**
  * Various utilities for dealing with camera API1 parameters.
  */
+@SuppressWarnings("deprecation")
 public class ParameterUtils {
     /** Upper/left minimal point of a normalized rectangle */
     public static final int NORMALIZED_RECTANGLE_MIN = -1000;
@@ -164,19 +165,23 @@
          * <p>If the score is out of range of {@value Face#SCORE_MIN}, {@value Face#SCORE_MAX},
          * the score is clipped first and a warning is printed to logcat.</p>
          *
+         * <p>If the id is negative, the id is changed to 0 and a warning is printed to
+         * logcat.</p>
+         *
          * <p>All other parameters are passed-through as-is.</p>
          *
          * @return a new face with the optional features set
          */
         public Face toFace(
                 int id, Point leftEyePosition, Point rightEyePosition, Point mouthPosition) {
+            int idSafe = clipLower(id, /*lo*/0, rect, "id");
             int score = clip(weight,
                     Face.SCORE_MIN,
                     Face.SCORE_MAX,
                     rect,
                     "score");
 
-            return new Face(rect, score, id, leftEyePosition, rightEyePosition, mouthPosition);
+            return new Face(rect, score, idSafe, leftEyePosition, rightEyePosition, mouthPosition);
         }
 
         /**
@@ -861,6 +866,61 @@
                 /*usePreviewCrop*/true);
     }
 
+    /**
+     * Convert an api1 face into an active-array based api2 face.
+     *
+     * <p>Out-of-ranges scores and ids will be clipped to be within range (with a warning).</p>
+     *
+     * @param face a non-{@code null} api1 face
+     * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
+     * @param zoomData the calculated zoom data corresponding to this request
+     *
+     * @return a non-{@code null} api2 face
+     *
+     * @throws NullPointerException if the {@code face} was {@code null}
+     */
+    public static Face convertFaceFromLegacy(Camera.Face face, Rect activeArray,
+            ZoomData zoomData) {
+        checkNotNull(face, "face must not be null");
+
+        Face api2Face;
+
+        Camera.Area fakeArea = new Camera.Area(face.rect, /*weight*/1);
+
+        WeightedRectangle faceRect =
+                convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, fakeArea);
+
+        Point leftEye = face.leftEye, rightEye = face.rightEye, mouth = face.mouth;
+        if (leftEye != null && rightEye != null && mouth != null) {
+            leftEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
+                    leftEye, /*usePreviewCrop*/true);
+            rightEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
+                    leftEye, /*usePreviewCrop*/true);
+            mouth = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
+                    leftEye, /*usePreviewCrop*/true);
+
+            api2Face = faceRect.toFace(face.id, leftEye, rightEye, mouth);
+        } else {
+            api2Face = faceRect.toFace();
+        }
+
+        return api2Face;
+    }
+
+    private static Point convertCameraPointToActiveArrayPoint(
+            Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop) {
+        Rect pointedRect = new Rect(point.x, point.y, point.x, point.y);
+        Camera.Area pointedArea = new Area(pointedRect, /*weight*/1);
+
+        WeightedRectangle adjustedRect =
+                convertCameraAreaToActiveArrayRectangle(activeArray,
+                        zoomData, pointedArea, usePreviewCrop);
+
+        Point transformedPoint = new Point(adjustedRect.rect.left, adjustedRect.rect.top);
+
+        return transformedPoint;
+    }
+
     private static WeightedRectangle convertCameraAreaToActiveArrayRectangle(
             Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop) {
         Rect previewCrop = zoomData.previewCrop;
diff --git a/core/java/android/hardware/camera2/legacy/RequestThreadManager.java b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java
index c556c32..2533a28 100644
--- a/core/java/android/hardware/camera2/legacy/RequestThreadManager.java
+++ b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java
@@ -39,7 +39,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
 import static com.android.internal.util.Preconditions.*;
@@ -55,18 +54,23 @@
  * - An {@link CameraDeviceState} state machine that manages the callbacks for various operations.
  * </p>
  */
+@SuppressWarnings("deprecation")
 public class RequestThreadManager {
     private final String TAG;
     private final int mCameraId;
     private final RequestHandlerThread mRequestThread;
 
     private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG);
+    // For slightly more spammy messages that will get repeated every frame
+    private static final boolean VERBOSE =
+            Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.VERBOSE);
     private final Camera mCamera;
     private final CameraCharacteristics mCharacteristics;
 
     private final CameraDeviceState mDeviceState;
     private final CaptureCollector mCaptureCollector;
     private final LegacyFocusStateMapper mFocusStateMapper;
+    private final LegacyFaceDetectMapper mFaceDetectMapper;
 
     private static final int MSG_CONFIGURE_OUTPUTS = 1;
     private static final int MSG_SUBMIT_CAPTURE_REQUEST = 2;
@@ -219,6 +223,9 @@
             };
 
     private void stopPreview() {
+        if (VERBOSE) {
+            Log.v(TAG, "stopPreview - preview running? " + mPreviewRunning);
+        }
         if (mPreviewRunning) {
             mCamera.stopPreview();
             mPreviewRunning = false;
@@ -226,14 +233,18 @@
     }
 
     private void startPreview() {
+        if (VERBOSE) {
+            Log.v(TAG, "startPreview - preview running? " + mPreviewRunning);
+        }
         if (!mPreviewRunning) {
+            // XX: CameraClient:;startPreview is not getting called after a stop
             mCamera.startPreview();
             mPreviewRunning = true;
         }
     }
 
-    private void doJpegCapture(RequestHolder request) throws IOException {
-        if (DEBUG) Log.d(TAG, "doJpegCapture");
+    private void doJpegCapturePrepare(RequestHolder request) throws IOException {
+        if (DEBUG) Log.d(TAG, "doJpegCapturePrepare - preview running? " + mPreviewRunning);
 
         if (!mPreviewRunning) {
             if (DEBUG) Log.d(TAG, "doJpegCapture - create fake surface");
@@ -242,11 +253,20 @@
             mCamera.setPreviewTexture(mDummyTexture);
             startPreview();
         }
+    }
+
+    private void doJpegCapture(RequestHolder request) {
+        if (DEBUG) Log.d(TAG, "doJpegCapturePrepare");
+
         mCamera.takePicture(mJpegShutterCallback, /*raw*/null, mJpegCallback);
         mPreviewRunning = false;
     }
 
     private void doPreviewCapture(RequestHolder request) throws IOException {
+        if (VERBOSE) {
+            Log.v(TAG, "doPreviewCapture - preview running? " + mPreviewRunning);
+        }
+
         if (mPreviewRunning) {
             return; // Already running
         }
@@ -264,7 +284,20 @@
     }
 
     private void configureOutputs(Collection<Surface> outputs) throws IOException {
+        if (DEBUG) {
+            String outputsStr = outputs == null ? "null" : (outputs.size() + " surfaces");
+
+            Log.d(TAG, "configureOutputs with " + outputsStr);
+        }
+
         stopPreview();
+        /*
+         * Try to release the previous preview's surface texture earlier if we end up
+         * using a different one; this also reduces the likelihood of getting into a deadlock
+         * when disconnecting from the old previous texture at a later time.
+         */
+        mCamera.setPreviewTexture(/*surfaceTexture*/null);
+
         if (mGLThreadManager != null) {
             mGLThreadManager.waitUntilStarted();
             mGLThreadManager.ignoreNewFrames();
@@ -305,7 +338,6 @@
         }
         mParams.setPreviewFpsRange(bestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
                 bestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
-        mParams.setRecordingHint(true);
 
         if (mPreviewOutputs.size() > 0) {
             List<Size> outputSizes = new ArrayList<>(outputs.size());
@@ -613,10 +645,6 @@
                             }
                         }
 
-                        // Unconditionally process AF triggers, since they're non-idempotent
-                        // - must be done after setting the most-up-to-date AF mode
-                        mFocusStateMapper.processRequestTriggers(request, mParams);
-
                         try {
                             boolean success = mCaptureCollector.queueRequest(holder,
                                     mLastRequest, JPEG_FRAME_TIMEOUT, TimeUnit.MILLISECONDS);
@@ -624,6 +652,8 @@
                             if (!success) {
                                 Log.e(TAG, "Timed out while queueing capture request.");
                             }
+                            // Starting the preview needs to happen before enabling
+                            // face detection or auto focus
                             if (holder.hasPreviewTargets()) {
                                 doPreviewCapture(holder);
                             }
@@ -635,12 +665,33 @@
                                     Log.e(TAG, "Timed out waiting for prior requests to complete.");
                                 }
                                 mReceivedJpeg.close();
+                                doJpegCapturePrepare(holder);
+                                if (!mReceivedJpeg.block(JPEG_FRAME_TIMEOUT)) {
+                                    // TODO: report error to CameraDevice
+                                    Log.e(TAG, "Hit timeout for jpeg callback!");
+                                }
+                            }
+
+                            /*
+                             * Do all the actions that require a preview to have been started
+                             */
+
+                            // Toggle face detection on/off
+                            // - do this before AF to give AF a chance to use faces
+                            mFaceDetectMapper.processFaceDetectMode(request, /*in*/mParams);
+
+                            // Unconditionally process AF triggers, since they're non-idempotent
+                            // - must be done after setting the most-up-to-date AF mode
+                            mFocusStateMapper.processRequestTriggers(request, mParams);
+
+                            if (holder.hasJpegTargets()) {
                                 doJpegCapture(holder);
                                 if (!mReceivedJpeg.block(JPEG_FRAME_TIMEOUT)) {
                                     // TODO: report error to CameraDevice
                                     Log.e(TAG, "Hit timeout for jpeg callback!");
                                 }
                             }
+
                         } catch (IOException e) {
                             // TODO: report error to CameraDevice
                             throw new IOError(e);
@@ -677,6 +728,8 @@
                                 mLastRequest, timestampMutable.value);
                         // Update AF state
                         mFocusStateMapper.mapResultTriggers(result);
+                        // Update detected faces list
+                        mFaceDetectMapper.mapResultFaces(result, mLastRequest);
 
                         mDeviceState.setCaptureResult(holder, result);
                     }
@@ -731,6 +784,7 @@
         TAG = name;
         mDeviceState = checkNotNull(deviceState, "deviceState must not be null");
         mFocusStateMapper = new LegacyFocusStateMapper(mCamera);
+        mFaceDetectMapper = new LegacyFaceDetectMapper(mCamera, mCharacteristics);
         mCaptureCollector = new CaptureCollector(MAX_IN_FLIGHT_REQUESTS, mDeviceState);
         mRequestThread = new RequestHandlerThread(name, mRequestHandlerCb);
     }