Camera: Add support for new camera zoom API

- Add NDK API spec for the new zoom API
The new zoom API combines optical and digital zoom, and supports both
zoom-out and zoom-in with more precision.
- Add new NDK API to specify separate zoom ratio ranges for different
bokeh modes.
- Add ZoomRatioMapper in camera service to convert between
control.zoomRation to and from scaler.cropRegion.

Test: Camera CTS/ITS/CtsVerifier/ZoomRatioTest
Bug: 130025314
Change-Id: I4c7d867f840b5720bc73bb0485e8a9a93d2276b5
diff --git a/camera/ndk/impl/ACameraMetadata.cpp b/camera/ndk/impl/ACameraMetadata.cpp
index 7c41b5e..e15e1a0 100644
--- a/camera/ndk/impl/ACameraMetadata.cpp
+++ b/camera/ndk/impl/ACameraMetadata.cpp
@@ -480,6 +480,7 @@
         case ACAMERA_CONTROL_POST_RAW_SENSITIVITY_BOOST:
         case ACAMERA_CONTROL_ENABLE_ZSL:
         case ACAMERA_CONTROL_BOKEH_MODE:
+        case ACAMERA_CONTROL_ZOOM_RATIO:
         case ACAMERA_EDGE_MODE:
         case ACAMERA_FLASH_MODE:
         case ACAMERA_HOT_PIXEL_MODE:
diff --git a/camera/ndk/include/camera/NdkCameraMetadataTags.h b/camera/ndk/include/camera/NdkCameraMetadataTags.h
index 0652cbb..b4a907f 100644
--- a/camera/ndk/include/camera/NdkCameraMetadataTags.h
+++ b/camera/ndk/include/camera/NdkCameraMetadataTags.h
@@ -518,11 +518,22 @@
      * region and output only the intersection rectangle as the metering region in the result
      * metadata.  If the region is entirely outside the crop region, it will be ignored and
      * not reported in the result metadata.</p>
+     * <p>Starting from API level 30, the coordinate system of activeArraySize or
+     * preCorrectionActiveArraySize is used to represent post-zoomRatio field of view, not
+     * pre-zoom field of view. This means that the same aeRegions values at different
+     * ACAMERA_CONTROL_ZOOM_RATIO represent different parts of the scene. The aeRegions
+     * coordinates are relative to the activeArray/preCorrectionActiveArray representing the
+     * zoomed field of view. If ACAMERA_CONTROL_ZOOM_RATIO is set to 1.0 (default), the same
+     * aeRegions at different ACAMERA_SCALER_CROP_REGION still represent the same parts of the
+     * scene as they do before. See ACAMERA_CONTROL_ZOOM_RATIO for details. Whether to use
+     * activeArraySize or preCorrectionActiveArraySize still depends on distortion correction
+     * mode.</p>
      * <p>The data representation is <code>int[5 * area_count]</code>.
      * Every five elements represent a metering region of <code>(xmin, ymin, xmax, ymax, weight)</code>.
      * The rectangle is defined to be inclusive on xmin and ymin, but exclusive on xmax and
      * ymax.</p>
      *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
      * @see ACAMERA_DISTORTION_CORRECTION_MODE
      * @see ACAMERA_SCALER_CROP_REGION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
@@ -698,11 +709,22 @@
      * region and output only the intersection rectangle as the metering region in the result
      * metadata. If the region is entirely outside the crop region, it will be ignored and
      * not reported in the result metadata.</p>
+     * <p>Starting from API level 30, the coordinate system of activeArraySize or
+     * preCorrectionActiveArraySize is used to represent post-zoomRatio field of view, not
+     * pre-zoom field of view. This means that the same afRegions values at different
+     * ACAMERA_CONTROL_ZOOM_RATIO represent different parts of the scene. The afRegions
+     * coordinates are relative to the activeArray/preCorrectionActiveArray representing the
+     * zoomed field of view. If ACAMERA_CONTROL_ZOOM_RATIO is set to 1.0 (default), the same
+     * afRegions at different ACAMERA_SCALER_CROP_REGION still represent the same parts of the
+     * scene as they do before. See ACAMERA_CONTROL_ZOOM_RATIO for details. Whether to use
+     * activeArraySize or preCorrectionActiveArraySize still depends on distortion correction
+     * mode.</p>
      * <p>The data representation is <code>int[5 * area_count]</code>.
      * Every five elements represent a metering region of <code>(xmin, ymin, xmax, ymax, weight)</code>.
      * The rectangle is defined to be inclusive on xmin and ymin, but exclusive on xmax and
      * ymax.</p>
      *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
      * @see ACAMERA_DISTORTION_CORRECTION_MODE
      * @see ACAMERA_SCALER_CROP_REGION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
@@ -873,11 +895,22 @@
      * region and output only the intersection rectangle as the metering region in the result
      * metadata.  If the region is entirely outside the crop region, it will be ignored and
      * not reported in the result metadata.</p>
+     * <p>Starting from API level 30, the coordinate system of activeArraySize or
+     * preCorrectionActiveArraySize is used to represent post-zoomRatio field of view, not
+     * pre-zoom field of view. This means that the same awbRegions values at different
+     * ACAMERA_CONTROL_ZOOM_RATIO represent different parts of the scene. The awbRegions
+     * coordinates are relative to the activeArray/preCorrectionActiveArray representing the
+     * zoomed field of view. If ACAMERA_CONTROL_ZOOM_RATIO is set to 1.0 (default), the same
+     * awbRegions at different ACAMERA_SCALER_CROP_REGION still represent the same parts of
+     * the scene as they do before. See ACAMERA_CONTROL_ZOOM_RATIO for details. Whether to use
+     * activeArraySize or preCorrectionActiveArraySize still depends on distortion correction
+     * mode.</p>
      * <p>The data representation is <code>int[5 * area_count]</code>.
      * Every five elements represent a metering region of <code>(xmin, ymin, xmax, ymax, weight)</code>.
      * The rectangle is defined to be inclusive on xmin and ymin, but exclusive on xmax and
      * ymax.</p>
      *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
      * @see ACAMERA_DISTORTION_CORRECTION_MODE
      * @see ACAMERA_SCALER_CROP_REGION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
@@ -1735,8 +1768,10 @@
     ACAMERA_CONTROL_AF_SCENE_CHANGE =                           // byte (acamera_metadata_enum_android_control_af_scene_change_t)
             ACAMERA_CONTROL_START + 42,
     /**
-     * <p>The list of bokeh modes that are supported by this camera device, and each bokeh mode's
-     * maximum streaming (non-stall) size with bokeh effect.</p>
+     * <p>The list of bokeh modes for ACAMERA_CONTROL_BOKEH_MODE that are supported by this camera
+     * device, and each bokeh mode's maximum streaming (non-stall) size with bokeh effect.</p>
+     *
+     * @see ACAMERA_CONTROL_BOKEH_MODE
      *
      * <p>Type: int32[3*n]</p>
      *
@@ -1761,9 +1796,31 @@
      * application configures a stream with larger dimension, the stream may not have bokeh
      * effect applied.</p>
      */
-    ACAMERA_CONTROL_AVAILABLE_BOKEH_CAPABILITIES =              // int32[3*n]
+    ACAMERA_CONTROL_AVAILABLE_BOKEH_MAX_SIZES =                 // int32[3*n]
             ACAMERA_CONTROL_START + 43,
     /**
+     * <p>The ranges of supported zoom ratio for non-OFF ACAMERA_CONTROL_BOKEH_MODE.</p>
+     *
+     * @see ACAMERA_CONTROL_BOKEH_MODE
+     *
+     * <p>Type: float[2*n]</p>
+     *
+     * <p>This tag may appear in:
+     * <ul>
+     *   <li>ACameraMetadata from ACameraManager_getCameraCharacteristics</li>
+     * </ul></p>
+     *
+     * <p>When bokeh mode is enabled, the camera device may have limited range of zoom ratios
+     * compared to when bokeh mode is disabled. This tag lists the zoom ratio ranges for all
+     * supported non-OFF bokeh modes, in the same order as in
+     * ACAMERA_CONTROL_AVAILABLE_BOKEH_CAPABILITIES.</p>
+     * <p>Range [1.0, 1.0] means that no zoom (optical or digital) is supported.</p>
+     *
+     * @see ACAMERA_CONTROL_AVAILABLE_BOKEH_CAPABILITIES
+     */
+    ACAMERA_CONTROL_AVAILABLE_BOKEH_ZOOM_RATIO_RANGES =         // float[2*n]
+            ACAMERA_CONTROL_START + 44,
+    /**
      * <p>Whether bokeh mode is enabled for a particular capture request.</p>
      *
      * <p>Type: byte (acamera_metadata_enum_android_control_bokeh_mode_t)</p>
@@ -1804,7 +1861,71 @@
      * bokeh mode is off.</p>
      */
     ACAMERA_CONTROL_BOKEH_MODE =                                // byte (acamera_metadata_enum_android_control_bokeh_mode_t)
-            ACAMERA_CONTROL_START + 44,
+            ACAMERA_CONTROL_START + 45,
+    /**
+     * <p>Minimum and maximum zoom ratios supported by this camera device.</p>
+     *
+     * <p>Type: float[2]</p>
+     *
+     * <p>This tag may appear in:
+     * <ul>
+     *   <li>ACameraMetadata from ACameraManager_getCameraCharacteristics</li>
+     * </ul></p>
+     *
+     * <p>If the camera device supports zoom-out from 1x zoom, minZoom will be less than 1.0, and
+     * setting android.control.zoomRation to values less than 1.0 increases the camera's field
+     * of view.</p>
+     */
+    ACAMERA_CONTROL_ZOOM_RATIO_RANGE =                          // float[2]
+            ACAMERA_CONTROL_START + 46,
+    /**
+     * <p>The desired zoom ratio</p>
+     *
+     * <p>Type: float</p>
+     *
+     * <p>This tag may appear in:
+     * <ul>
+     *   <li>ACameraMetadata from ACameraCaptureSession_captureCallback_result callbacks</li>
+     *   <li>ACaptureRequest</li>
+     * </ul></p>
+     *
+     * <p>Instead of using ACAMERA_SCALER_CROP_REGION with dual purposes of crop and zoom, the
+     * application can now choose to use this tag to specify the desired zoom level. The
+     * ACAMERA_SCALER_CROP_REGION can still be used to specify the horizontal or vertical
+     * crop to achieve aspect ratios different than the native camera sensor.</p>
+     * <p>By using this control, the application gains a simpler way to control zoom, which can
+     * be a combination of optical and digital zoom. More specifically, for a logical
+     * multi-camera with more than one focal length, using a floating point zoom ratio offers
+     * more zoom precision when a telephoto lens is used, as well as allowing zoom ratio of
+     * less than 1.0 to zoom out to a wide field of view.</p>
+     * <p>Note that the coordinate system of cropRegion, AE/AWB/AF regions, and faces now changes
+     * to the effective after-zoom field-of-view represented by rectangle of (0, 0,
+     * activeArrayWidth, activeArrayHeight).</p>
+     * <p>For example, if ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE is 4032*3024, and the preview stream
+     * is configured to the same 4:3 aspect ratio, the application can achieve 2.0x zoom in
+     * one of two ways:</p>
+     * <ul>
+     * <li>zoomRatio = 2.0, scaler.cropRegion = (0, 0, 4032, 3024)</li>
+     * <li>zoomRatio = 1.0 (default), scaler.cropRegion = (1008, 756, 3024, 2268)</li>
+     * </ul>
+     * <p>If the application intends to set aeRegions to be top-left quarter of the preview
+     * field-of-view, the ACAMERA_CONTROL_AE_REGIONS should be set to (0, 0, 2016, 1512) with
+     * zoomRatio set to 2.0. Alternatively, the application can set aeRegions to the equivalent
+     * region of (1008, 756, 2016, 1512) for zoomRatio of 1.0. If the application doesn't
+     * explicitly set ACAMERA_CONTROL_ZOOM_RATIO, its value defaults to 1.0.</p>
+     * <p>This coordinate system change isn't applicable to RAW capture and its related metadata
+     * such as intrinsicCalibration and lensShadingMap.</p>
+     * <p>One limitation of controlling zoom using zoomRatio is that the ACAMERA_SCALER_CROP_REGION
+     * must only be used for letterboxing or pillarboxing of the sensor active array, and no
+     * FREEFORM cropping can be used with ACAMERA_CONTROL_ZOOM_RATIO other than 1.0.</p>
+     *
+     * @see ACAMERA_CONTROL_AE_REGIONS
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
+     * @see ACAMERA_SCALER_CROP_REGION
+     * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
+     */
+    ACAMERA_CONTROL_ZOOM_RATIO =                                // float
+            ACAMERA_CONTROL_START + 47,
     ACAMERA_CONTROL_END,
 
     /**
@@ -3203,8 +3324,17 @@
      * for rounding and other hardware requirements; the final
      * crop region used will be included in the output capture
      * result.</p>
+     * <p>Starting from API level 30, it's strongly recommended to use ACAMERA_CONTROL_ZOOM_RATIO
+     * to take advantage of better support for zoom with logical multi-camera. The benefits
+     * include better precision with optical-digital zoom combination, and ability to do
+     * zoom-out from 1.0x. When using ACAMERA_CONTROL_ZOOM_RATIO for zoom, the crop region in
+     * the capture request must be either letterboxing or pillarboxing (but not both). The
+     * coordinate system is post-zoom, meaning that the activeArraySize or
+     * preCorrectionActiveArraySize covers the camera device's field of view "after" zoom.
+     * See ACAMERA_CONTROL_ZOOM_RATIO for details.</p>
      * <p>The data representation is int[4], which maps to (left, top, width, height).</p>
      *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
      * @see ACAMERA_DISTORTION_CORRECTION_MODE
      * @see ACAMERA_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
@@ -3232,6 +3362,12 @@
      * <p>Crop regions that have a width or height that is smaller
      * than this ratio allows will be rounded up to the minimum
      * allowed size by the camera device.</p>
+     * <p>Starting from API level 30, when using ACAMERA_CONTROL_ZOOM_RATIO to zoom in or out,
+     * the application must use ACAMERA_CONTROL_ZOOM_RATIO_RANGE to query both the minimum and
+     * maximum zoom ratio.</p>
+     *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
+     * @see ACAMERA_CONTROL_ZOOM_RATIO_RANGE
      */
     ACAMERA_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM =                 // float
             ACAMERA_SCALER_START + 4,
@@ -3406,8 +3542,24 @@
      * <p>Camera devices that support FREEFORM cropping will support any crop region that
      * is inside of the active array. The camera device will apply the same crop region and
      * return the final used crop region in capture result metadata ACAMERA_SCALER_CROP_REGION.</p>
+     * <p>Starting from API level 30,</p>
+     * <ul>
+     * <li>If the camera device supports FREEFORM cropping, in order to do FREEFORM cropping, the
+     * application must set ACAMERA_CONTROL_ZOOM_RATIO to 1.0, and use ACAMERA_SCALER_CROP_REGION
+     * for zoom.</li>
+     * <li>To do CENTER_ONLY zoom, the application has below 2 options:<ol>
+     * <li>Set ACAMERA_CONTROL_ZOOM_RATIO to 1.0; adjust zoom by ACAMERA_SCALER_CROP_REGION.</li>
+     * <li>Adjust zoom by ACAMERA_CONTROL_ZOOM_RATIO; use ACAMERA_SCALER_CROP_REGION to crop
+     * the field of view vertically (letterboxing) or horizontally (pillarboxing), but not
+     * windowboxing.</li>
+     * </ol>
+     * </li>
+     * <li>Setting ACAMERA_CONTROL_ZOOM_RATIO to values different than 1.0 and
+     * ACAMERA_SCALER_CROP_REGION to be windowboxing at the same time is undefined behavior.</li>
+     * </ul>
      * <p>LEGACY capability devices will only support CENTER_ONLY cropping.</p>
      *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
      * @see ACAMERA_SCALER_CROP_REGION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
      */
@@ -4707,9 +4859,20 @@
      * When the distortion correction mode is not OFF, the coordinate system follows
      * ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE, with
      * <code>(0, 0)</code> being the top-left pixel of the active array.</p>
-     * <p>Only available if ACAMERA_STATISTICS_FACE_DETECT_MODE == FULL</p>
+     * <p>Only available if ACAMERA_STATISTICS_FACE_DETECT_MODE == FULL.</p>
+     * <p>Starting from API level 30, the coordinate system of activeArraySize or
+     * preCorrectionActiveArraySize is used to represent post-zoomRatio field of view, not
+     * pre-zoomRatio field of view. This means that if the relative position of faces and
+     * the camera device doesn't change, when zooming in by increasing
+     * ACAMERA_CONTROL_ZOOM_RATIO, the face landmarks move farther away from the center of the
+     * activeArray or preCorrectionActiveArray. If ACAMERA_CONTROL_ZOOM_RATIO is set to 1.0
+     * (default), the face landmarks coordinates won't change as ACAMERA_SCALER_CROP_REGION
+     * changes. See ACAMERA_CONTROL_ZOOM_RATIO for details. Whether to use activeArraySize or
+     * preCorrectionActiveArraySize still depends on distortion correction mode.</p>
      *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
      * @see ACAMERA_DISTORTION_CORRECTION_MODE
+     * @see ACAMERA_SCALER_CROP_REGION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
      * @see ACAMERA_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
      * @see ACAMERA_STATISTICS_FACE_DETECT_MODE
@@ -4738,10 +4901,21 @@
      * When the distortion correction mode is not OFF, the coordinate system follows
      * ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE, with
      * <code>(0, 0)</code> being the top-left pixel of the active array.</p>
-     * <p>Only available if ACAMERA_STATISTICS_FACE_DETECT_MODE != OFF
-     * The data representation is <code>int[4]</code>, which maps to <code>(left, top, right, bottom)</code>.</p>
+     * <p>Only available if ACAMERA_STATISTICS_FACE_DETECT_MODE != OFF.</p>
+     * <p>Starting from API level 30, the coordinate system of activeArraySize or
+     * preCorrectionActiveArraySize is used to represent post-zoomRatio field of view, not
+     * pre-zoomRatio field of view. This means that if the relative position of faces and
+     * the camera device doesn't change, when zooming in by increasing
+     * ACAMERA_CONTROL_ZOOM_RATIO, the face rectangles grow larger and move farther away from
+     * the center of the activeArray or preCorrectionActiveArray. If ACAMERA_CONTROL_ZOOM_RATIO
+     * is set to 1.0 (default), the face rectangles won't change as ACAMERA_SCALER_CROP_REGION
+     * changes. See ACAMERA_CONTROL_ZOOM_RATIO for details. Whether to use activeArraySize or
+     * preCorrectionActiveArraySize still depends on distortion correction mode.</p>
+     * <p>The data representation is <code>int[4]</code>, which maps to <code>(left, top, right, bottom)</code>.</p>
      *
+     * @see ACAMERA_CONTROL_ZOOM_RATIO
      * @see ACAMERA_DISTORTION_CORRECTION_MODE
+     * @see ACAMERA_SCALER_CROP_REGION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
      * @see ACAMERA_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
      * @see ACAMERA_STATISTICS_FACE_DETECT_MODE
diff --git a/services/camera/libcameraservice/Android.bp b/services/camera/libcameraservice/Android.bp
index 9cc15cd..6052a06 100644
--- a/services/camera/libcameraservice/Android.bp
+++ b/services/camera/libcameraservice/Android.bp
@@ -56,7 +56,9 @@
         "device3/StatusTracker.cpp",
         "device3/Camera3BufferManager.cpp",
         "device3/Camera3StreamSplitter.cpp",
+        "device3/CoordinateMapper.cpp",
         "device3/DistortionMapper.cpp",
+        "device3/ZoomRatioMapper.cpp",
         "gui/RingBufferConsumer.cpp",
         "utils/CameraThreadState.cpp",
         "hidl/AidlCameraDeviceCallbacks.cpp",
diff --git a/services/camera/libcameraservice/common/CameraProviderManager.cpp b/services/camera/libcameraservice/common/CameraProviderManager.cpp
index 7ed4c3d..23f7884 100644
--- a/services/camera/libcameraservice/common/CameraProviderManager.cpp
+++ b/services/camera/libcameraservice/common/CameraProviderManager.cpp
@@ -40,6 +40,7 @@
 #include <utils/Trace.h>
 
 #include "api2/HeicCompositeStream.h"
+#include "device3/ZoomRatioMapper.h"
 
 namespace android {
 
@@ -232,6 +233,15 @@
     return deviceInfo->hasFlashUnit();
 }
 
+bool CameraProviderManager::supportNativeZoomRatio(const std::string &id) const {
+    std::lock_guard<std::mutex> lock(mInterfaceMutex);
+
+    auto deviceInfo = findDeviceInfoLocked(id);
+    if (deviceInfo == nullptr) return false;
+
+    return deviceInfo->supportNativeZoomRatio();
+}
+
 status_t CameraProviderManager::getResourceCost(const std::string &id,
         CameraResourceCost* cost) const {
     std::lock_guard<std::mutex> lock(mInterfaceMutex);
@@ -2035,6 +2045,13 @@
                 __FUNCTION__, strerror(-res), res);
     }
 
+    res = camera3::ZoomRatioMapper::overrideZoomRatioTags(
+            &mCameraCharacteristics, &mSupportNativeZoomRatio);
+    if (OK != res) {
+        ALOGE("%s: Unable to override zoomRatio related tags: %s (%d)",
+                __FUNCTION__, strerror(-res), res);
+    }
+
     camera_metadata_entry flashAvailable =
             mCameraCharacteristics.find(ANDROID_FLASH_INFO_AVAILABLE);
     if (flashAvailable.count == 1 &&
diff --git a/services/camera/libcameraservice/common/CameraProviderManager.h b/services/camera/libcameraservice/common/CameraProviderManager.h
index 2ef1f6f..651b8a1 100644
--- a/services/camera/libcameraservice/common/CameraProviderManager.h
+++ b/services/camera/libcameraservice/common/CameraProviderManager.h
@@ -187,6 +187,11 @@
     bool hasFlashUnit(const std::string &id) const;
 
     /**
+     * Return true if the camera device has native zoom ratio support.
+     */
+    bool supportNativeZoomRatio(const std::string &id) const;
+
+    /**
      * Return the resource cost of this camera device
      */
     status_t getResourceCost(const std::string &id,
@@ -416,6 +421,7 @@
             sp<ProviderInfo> mParentProvider;
 
             bool hasFlashUnit() const { return mHasFlashUnit; }
+            bool supportNativeZoomRatio() const { return mSupportNativeZoomRatio; }
             virtual status_t setTorchMode(bool enabled) = 0;
             virtual status_t getCameraInfo(hardware::CameraInfo *info) const = 0;
             virtual bool isAPI1Compatible() const = 0;
@@ -449,10 +455,11 @@
                     mIsLogicalCamera(false), mResourceCost(resourceCost),
                     mStatus(hardware::camera::common::V1_0::CameraDeviceStatus::PRESENT),
                     mParentProvider(parentProvider), mHasFlashUnit(false),
-                    mPublicCameraIds(publicCameraIds) {}
+                    mSupportNativeZoomRatio(false), mPublicCameraIds(publicCameraIds) {}
             virtual ~DeviceInfo();
         protected:
-            bool mHasFlashUnit;
+            bool mHasFlashUnit; // const after constructor
+            bool mSupportNativeZoomRatio; // const after constructor
             const std::vector<std::string>& mPublicCameraIds;
 
             template<class InterfaceT>
diff --git a/services/camera/libcameraservice/device3/Camera3Device.cpp b/services/camera/libcameraservice/device3/Camera3Device.cpp
index 4e5c8d6..1c5281d 100644
--- a/services/camera/libcameraservice/device3/Camera3Device.cpp
+++ b/services/camera/libcameraservice/device3/Camera3Device.cpp
@@ -133,6 +133,7 @@
         session->close();
         return res;
     }
+    mSupportNativeZoomRatio = manager->supportNativeZoomRatio(mId.string());
 
     std::vector<std::string> physicalCameraIds;
     bool isLogical = manager->isLogicalCamera(mId.string(), &physicalCameraIds);
@@ -147,8 +148,11 @@
                 return res;
             }
 
-            if (DistortionMapper::isDistortionSupported(mPhysicalDeviceInfoMap[physicalId])) {
-                mDistortionMappers[physicalId].setupStaticInfo(mPhysicalDeviceInfoMap[physicalId]);
+            bool usePrecorrectArray =
+                    DistortionMapper::isDistortionSupported(mPhysicalDeviceInfoMap[physicalId]);
+            if (usePrecorrectArray) {
+                res = mDistortionMappers[physicalId].setupStaticInfo(
+                        mPhysicalDeviceInfoMap[physicalId]);
                 if (res != OK) {
                     SET_ERR_L("Unable to read camera %s's calibration fields for distortion "
                             "correction", physicalId.c_str());
@@ -156,6 +160,15 @@
                     return res;
                 }
             }
+
+            res = mZoomRatioMappers[physicalId].initZoomRatioTags(
+                    &mPhysicalDeviceInfoMap[physicalId],
+                    mSupportNativeZoomRatio, usePrecorrectArray);
+            if (res != OK) {
+                SET_ERR_L("Failed to initialize camera %s's zoomRatio tags: %s (%d)",
+                        physicalId.c_str(), strerror(-res), res);
+                return res;
+            }
         }
     }
 
@@ -331,13 +344,23 @@
         }
     }
 
-    if (DistortionMapper::isDistortionSupported(mDeviceInfo)) {
+    bool usePrecorrectArray = DistortionMapper::isDistortionSupported(mDeviceInfo);
+    if (usePrecorrectArray) {
         res = mDistortionMappers[mId.c_str()].setupStaticInfo(mDeviceInfo);
         if (res != OK) {
             SET_ERR_L("Unable to read necessary calibration fields for distortion correction");
             return res;
         }
     }
+
+    res = mZoomRatioMappers[mId.c_str()].initZoomRatioTags(&mDeviceInfo,
+            mSupportNativeZoomRatio, usePrecorrectArray);
+    if (res != OK) {
+        SET_ERR_L("Failed to initialize zoomRatio tags: %s (%d)",
+                strerror(-res), res);
+        return res;
+    }
+
     return OK;
 }
 
@@ -2124,6 +2147,15 @@
         set_camera_metadata_vendor_id(rawRequest, mVendorTagId);
         mRequestTemplateCache[templateId].acquire(rawRequest);
 
+        // Override the template request with zoomRatioMapper
+        res = mZoomRatioMappers[mId.c_str()].initZoomRatioInTemplate(
+                &mRequestTemplateCache[templateId]);
+        if (res != OK) {
+            CLOGE("Failed to update zoom ratio for template %d: %s (%d)",
+                    templateId, strerror(-res), res);
+            return res;
+        }
+
         *request = mRequestTemplateCache[templateId];
         mLastTemplateId = templateId;
     }
@@ -3146,14 +3178,15 @@
         int32_t numBuffers, CaptureResultExtras resultExtras, bool hasInput,
         bool hasAppCallback, nsecs_t maxExpectedDuration,
         std::set<String8>& physicalCameraIds, bool isStillCapture,
-        bool isZslCapture, const SurfaceMap& outputSurfaces) {
+        bool isZslCapture, const std::set<std::string>& cameraIdsWithZoom,
+        const SurfaceMap& outputSurfaces) {
     ATRACE_CALL();
     Mutex::Autolock l(mInFlightLock);
 
     ssize_t res;
     res = mInFlightMap.add(frameNumber, InFlightRequest(numBuffers, resultExtras, hasInput,
             hasAppCallback, maxExpectedDuration, physicalCameraIds, isStillCapture, isZslCapture,
-            outputSurfaces));
+            cameraIdsWithZoom, outputSurfaces));
     if (res < 0) return res;
 
     if (mInFlightMap.size() == 1) {
@@ -3504,7 +3537,7 @@
         CaptureResultExtras &resultExtras,
         CameraMetadata &collectedPartialResult,
         uint32_t frameNumber,
-        bool reprocess, bool zslStillCapture,
+        bool reprocess, bool zslStillCapture, const std::set<std::string>& cameraIdsWithZoom,
         const std::vector<PhysicalCaptureResultInfo>& physicalMetadatas) {
     ATRACE_CALL();
     if (pendingMetadata.isEmpty())
@@ -3573,19 +3606,39 @@
             mDistortionMappers[mId.c_str()].correctCaptureResult(&captureResult.mMetadata);
     if (res != OK) {
         SET_ERR("Unable to correct capture result metadata for frame %d: %s (%d)",
-                frameNumber, strerror(res), res);
+                frameNumber, strerror(-res), res);
         return;
     }
+
+    // Fix up result metadata to account for zoom ratio availabilities between
+    // HAL and app.
+    bool zoomRatioIs1 = cameraIdsWithZoom.find(mId.c_str()) == cameraIdsWithZoom.end();
+    res = mZoomRatioMappers[mId.c_str()].updateCaptureResult(
+            &captureResult.mMetadata, zoomRatioIs1);
+    if (res != OK) {
+        SET_ERR("Failed to update capture result zoom ratio metadata for frame %d: %s (%d)",
+                frameNumber, strerror(-res), res);
+        return;
+    }
+
     for (auto& physicalMetadata : captureResult.mPhysicalMetadatas) {
         String8 cameraId8(physicalMetadata.mPhysicalCameraId);
-        if (mDistortionMappers.find(cameraId8.c_str()) == mDistortionMappers.end()) {
-            continue;
+        if (mDistortionMappers.find(cameraId8.c_str()) != mDistortionMappers.end()) {
+            res = mDistortionMappers[cameraId8.c_str()].correctCaptureResult(
+                    &physicalMetadata.mPhysicalCameraMetadata);
+            if (res != OK) {
+                SET_ERR("Unable to correct physical capture result metadata for frame %d: %s (%d)",
+                        frameNumber, strerror(-res), res);
+                return;
+            }
         }
-        res = mDistortionMappers[cameraId8.c_str()].correctCaptureResult(
-                &physicalMetadata.mPhysicalCameraMetadata);
+
+        zoomRatioIs1 = cameraIdsWithZoom.find(cameraId8.c_str()) == cameraIdsWithZoom.end();
+        res = mZoomRatioMappers[cameraId8.c_str()].updateCaptureResult(
+                &physicalMetadata.mPhysicalCameraMetadata, zoomRatioIs1);
         if (res != OK) {
-            SET_ERR("Unable to correct physical capture result metadata for frame %d: %s (%d)",
-                    frameNumber, strerror(res), res);
+            SET_ERR("Failed to update camera %s's physical zoom ratio metadata for "
+                    "frame %d: %s(%d)", cameraId8.c_str(), frameNumber, strerror(-res), res);
             return;
         }
     }
@@ -3790,7 +3843,7 @@
                 sendCaptureResult(metadata, request.resultExtras,
                     collectedPartialResult, frameNumber,
                     hasInputBufferInRequest, request.zslCapture && request.stillCapture,
-                    request.physicalMetadatas);
+                    request.cameraIdsWithZoom, request.physicalMetadatas);
             }
         }
 
@@ -4008,7 +4061,7 @@
                 sendCaptureResult(r.pendingMetadata, r.resultExtras,
                     r.collectedPartialResult, msg.frame_number,
                     r.hasInputBuffer, r.zslCapture && r.stillCapture,
-                    r.physicalMetadatas);
+                    r.cameraIdsWithZoom, r.physicalMetadatas);
             }
             bool timestampIncreasing = !(r.zslCapture || r.hasInputBuffer);
             returnOutputBuffers(r.pendingOutputBuffers.array(),
@@ -5610,6 +5663,7 @@
                 // request in a batch as new
                 !(batchedRequest && i > 0);
         if (newRequest) {
+            std::set<std::string> cameraIdsWithZoom;
             /**
              * HAL workaround:
              * Insert a dummy trigger ID if a trigger is set but no trigger ID is
@@ -5642,6 +5696,28 @@
                             return INVALID_OPERATION;
                         }
                     }
+
+                    for (it = captureRequest->mSettingsList.begin();
+                            it != captureRequest->mSettingsList.end(); it++) {
+                        if (parent->mZoomRatioMappers.find(it->cameraId) ==
+                                parent->mZoomRatioMappers.end()) {
+                            continue;
+                        }
+
+                        camera_metadata_entry_t e = it->metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+                        if (e.count > 0 && e.data.f[0] != 1.0f) {
+                            cameraIdsWithZoom.insert(it->cameraId);
+                        }
+
+                        res = parent->mZoomRatioMappers[it->cameraId].updateCaptureRequest(
+                            &(it->metadata));
+                        if (res != OK) {
+                            SET_ERR("RequestThread: Unable to correct capture requests "
+                                    "for zoom ratio for request %d: %s (%d)",
+                                    halRequest->frame_number, strerror(-res), res);
+                            return INVALID_OPERATION;
+                        }
+                    }
                 }
             }
 
@@ -5652,6 +5728,7 @@
             captureRequest->mSettingsList.begin()->metadata.sort();
             halRequest->settings = captureRequest->mSettingsList.begin()->metadata.getAndLock();
             mPrevRequest = captureRequest;
+            mPrevCameraIdsWithZoom = cameraIdsWithZoom;
             ALOGVV("%s: Request settings are NEW", __FUNCTION__);
 
             IF_ALOGV() {
@@ -5839,7 +5916,7 @@
                 /*hasInput*/halRequest->input_buffer != NULL,
                 hasCallback,
                 calculateMaxExpectedDuration(halRequest->settings),
-                requestedPhysicalCameras, isStillCapture, isZslCapture,
+                requestedPhysicalCameras, isStillCapture, isZslCapture, mPrevCameraIdsWithZoom,
                 (mUseHalBufManager) ? uniqueSurfaceIdMap :
                                       SurfaceMap{});
         ALOGVV("%s: registered in flight requestId = %" PRId32 ", frameNumber = %" PRId64
diff --git a/services/camera/libcameraservice/device3/Camera3Device.h b/services/camera/libcameraservice/device3/Camera3Device.h
index eabc44d..5faabd1 100644
--- a/services/camera/libcameraservice/device3/Camera3Device.h
+++ b/services/camera/libcameraservice/device3/Camera3Device.h
@@ -46,6 +46,7 @@
 #include "device3/StatusTracker.h"
 #include "device3/Camera3BufferManager.h"
 #include "device3/DistortionMapper.h"
+#include "device3/ZoomRatioMapper.h"
 #include "utils/TagMonitor.h"
 #include "utils/LatencyHistogram.h"
 #include <camera_metadata_hidden.h>
@@ -442,6 +443,7 @@
     sp<HalInterface> mInterface;
 
     CameraMetadata             mDeviceInfo;
+    bool                       mSupportNativeZoomRatio;
     std::unordered_map<std::string, CameraMetadata> mPhysicalDeviceInfoMap;
 
     CameraMetadata             mRequestTemplateCache[CAMERA3_TEMPLATE_COUNT];
@@ -980,6 +982,7 @@
 
         sp<CaptureRequest> mPrevRequest;
         int32_t            mPrevTriggers;
+        std::set<std::string> mPrevCameraIdsWithZoom;
 
         uint32_t           mFrameNumber;
 
@@ -1078,6 +1081,9 @@
         // Indicates a ZSL capture request
         bool zslCapture;
 
+        // Requested camera ids (both logical and physical) with zoomRatio != 1.0f
+        std::set<std::string> cameraIdsWithZoom;
+
         // What shared surfaces an output should go to
         SurfaceMap outputSurfaces;
 
@@ -1099,7 +1105,7 @@
         InFlightRequest(int numBuffers, CaptureResultExtras extras, bool hasInput,
                 bool hasAppCallback, nsecs_t maxDuration,
                 const std::set<String8>& physicalCameraIdSet, bool isStillCapture,
-                bool isZslCapture,
+                bool isZslCapture, const std::set<std::string>& idsWithZoom,
                 const SurfaceMap& outSurfaces = SurfaceMap{}) :
                 shutterTimestamp(0),
                 sensorTimestamp(0),
@@ -1114,6 +1120,7 @@
                 physicalCameraIds(physicalCameraIdSet),
                 stillCapture(isStillCapture),
                 zslCapture(isZslCapture),
+                cameraIdsWithZoom(idsWithZoom),
                 outputSurfaces(outSurfaces) {
         }
     };
@@ -1131,7 +1138,7 @@
     status_t registerInFlight(uint32_t frameNumber,
             int32_t numBuffers, CaptureResultExtras resultExtras, bool hasInput,
             bool callback, nsecs_t maxExpectedDuration, std::set<String8>& physicalCameraIds,
-            bool isStillCapture, bool isZslCapture,
+            bool isStillCapture, bool isZslCapture, const std::set<std::string>& cameraIdsWithZoom,
             const SurfaceMap& outputSurfaces);
 
     /**
@@ -1256,6 +1263,7 @@
             CaptureResultExtras &resultExtras,
             CameraMetadata &collectedPartialResult, uint32_t frameNumber,
             bool reprocess, bool zslStillCapture,
+            const std::set<std::string>& cameraIdsWithZoom,
             const std::vector<PhysicalCaptureResultInfo>& physicalMetadatas);
 
     bool isLastFullResult(const InFlightRequest& inFlightRequest);
@@ -1288,6 +1296,11 @@
     // logical camera and its physical subcameras.
     std::unordered_map<std::string, camera3::DistortionMapper> mDistortionMappers;
 
+    /**
+     * Zoom ratio mapper support
+     */
+    std::unordered_map<std::string, camera3::ZoomRatioMapper> mZoomRatioMappers;
+
     // Debug tracker for metadata tag value changes
     // - Enabled with the -m <taglist> option to dumpsys, such as
     //   dumpsys -m android.control.aeState,android.control.aeMode
diff --git a/services/camera/libcameraservice/device3/CoordinateMapper.cpp b/services/camera/libcameraservice/device3/CoordinateMapper.cpp
new file mode 100644
index 0000000..d62f397
--- /dev/null
+++ b/services/camera/libcameraservice/device3/CoordinateMapper.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#include <system/camera_metadata_tags.h>
+
+#include "device3/CoordinateMapper.h"
+
+namespace android {
+
+namespace camera3 {
+
+/**
+ * Metadata keys to correct when adjusting coordinates for distortion correction
+ */
+
+// Both capture request and result
+constexpr std::array<uint32_t, 3> CoordinateMapper::kMeteringRegionsToCorrect = {
+    ANDROID_CONTROL_AF_REGIONS,
+    ANDROID_CONTROL_AE_REGIONS,
+    ANDROID_CONTROL_AWB_REGIONS
+};
+
+// Both capture request and result
+constexpr std::array<uint32_t, 1> CoordinateMapper::kRectsToCorrect = {
+    ANDROID_SCALER_CROP_REGION,
+};
+
+// Only for capture result
+constexpr std::array<uint32_t, 2> CoordinateMapper::kResultPointsToCorrectNoClamp = {
+    ANDROID_STATISTICS_FACE_RECTANGLES, // Says rectangles, is really points
+    ANDROID_STATISTICS_FACE_LANDMARKS,
+};
+
+} // namespace camera3
+
+} // namespace android
diff --git a/services/camera/libcameraservice/device3/CoordinateMapper.h b/services/camera/libcameraservice/device3/CoordinateMapper.h
new file mode 100644
index 0000000..5164856
--- /dev/null
+++ b/services/camera/libcameraservice/device3/CoordinateMapper.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#ifndef ANDROID_SERVERS_COORDINATEMAPPER_H
+#define ANDROID_SERVERS_COORDINATEMAPPER_H
+
+#include <array>
+
+namespace android {
+
+namespace camera3 {
+
+class CoordinateMapper {
+    // Right now only stores metadata tags containing 2D coordinates
+    // to be corrected.
+protected:
+    // Metadata key lists to correct
+
+    // Both capture request and result
+    static const std::array<uint32_t, 3> kMeteringRegionsToCorrect;
+
+    // Both capture request and result
+    static const std::array<uint32_t, 1> kRectsToCorrect;
+
+    // Only for capture results; don't clamp
+    static const std::array<uint32_t, 2> kResultPointsToCorrectNoClamp;
+}; // class CoordinateMapper
+
+} // namespace camera3
+
+} // namespace android
+
+#endif
diff --git a/services/camera/libcameraservice/device3/DistortionMapper.cpp b/services/camera/libcameraservice/device3/DistortionMapper.cpp
index ae7af8e..8132225 100644
--- a/services/camera/libcameraservice/device3/DistortionMapper.cpp
+++ b/services/camera/libcameraservice/device3/DistortionMapper.cpp
@@ -27,41 +27,14 @@
 
 namespace camera3 {
 
-/**
- * Metadata keys to correct when adjusting coordinates for distortion correction
- */
-
-// Both capture request and result
-constexpr std::array<uint32_t, 3> DistortionMapper::kMeteringRegionsToCorrect = {
-    ANDROID_CONTROL_AF_REGIONS,
-    ANDROID_CONTROL_AE_REGIONS,
-    ANDROID_CONTROL_AWB_REGIONS
-};
-
-// Only capture request
-constexpr std::array<uint32_t, 1> DistortionMapper::kRequestRectsToCorrect = {
-    ANDROID_SCALER_CROP_REGION,
-};
-
-// Only for capture result
-constexpr std::array<uint32_t, 1> DistortionMapper::kResultRectsToCorrect = {
-    ANDROID_SCALER_CROP_REGION,
-};
-
-// Only for capture result
-constexpr std::array<uint32_t, 2> DistortionMapper::kResultPointsToCorrectNoClamp = {
-    ANDROID_STATISTICS_FACE_RECTANGLES, // Says rectangles, is really points
-    ANDROID_STATISTICS_FACE_LANDMARKS,
-};
-
 
 DistortionMapper::DistortionMapper() : mValidMapping(false), mValidGrids(false) {
 }
 
-bool DistortionMapper::isDistortionSupported(const CameraMetadata &result) {
+bool DistortionMapper::isDistortionSupported(const CameraMetadata &deviceInfo) {
     bool isDistortionCorrectionSupported = false;
     camera_metadata_ro_entry_t distortionCorrectionModes =
-            result.find(ANDROID_DISTORTION_CORRECTION_AVAILABLE_MODES);
+            deviceInfo.find(ANDROID_DISTORTION_CORRECTION_AVAILABLE_MODES);
     for (size_t i = 0; i < distortionCorrectionModes.count; i++) {
         if (distortionCorrectionModes.data.u8[i] !=
                 ANDROID_DISTORTION_CORRECTION_MODE_OFF) {
@@ -124,7 +97,7 @@
                 if (res != OK) return res;
             }
         }
-        for (auto rect : kRequestRectsToCorrect) {
+        for (auto rect : kRectsToCorrect) {
             e = request->find(rect);
             res = mapCorrectedRectToRaw(e.data.i32, e.count / 4, /*clamp*/true);
             if (res != OK) return res;
@@ -160,7 +133,7 @@
                 if (res != OK) return res;
             }
         }
-        for (auto rect : kResultRectsToCorrect) {
+        for (auto rect : kRectsToCorrect) {
             e = result->find(rect);
             res = mapRawRectToCorrected(e.data.i32, e.count / 4, /*clamp*/true);
             if (res != OK) return res;
@@ -390,7 +363,6 @@
     return OK;
 }
 
-
 status_t DistortionMapper::mapCorrectedRectToRaw(int32_t *rects, int rectCount, bool clamp,
         bool simple) const {
     if (!mValidMapping) return INVALID_OPERATION;
diff --git a/services/camera/libcameraservice/device3/DistortionMapper.h b/services/camera/libcameraservice/device3/DistortionMapper.h
index 4c0a1a6..a255003 100644
--- a/services/camera/libcameraservice/device3/DistortionMapper.h
+++ b/services/camera/libcameraservice/device3/DistortionMapper.h
@@ -22,6 +22,7 @@
 #include <mutex>
 
 #include "camera/CameraMetadata.h"
+#include "device3/CoordinateMapper.h"
 
 namespace android {
 
@@ -31,7 +32,7 @@
  * Utilities to transform between raw (distorted) and warped (corrected) coordinate systems
  * for cameras that support geometric distortion
  */
-class DistortionMapper {
+class DistortionMapper : private CoordinateMapper {
   public:
     DistortionMapper();
 
@@ -150,20 +151,6 @@
     // Fuzziness for float inequality tests
     constexpr static float kFloatFuzz = 1e-4;
 
-    // Metadata key lists to correct
-
-    // Both capture request and result
-    static const std::array<uint32_t, 3> kMeteringRegionsToCorrect;
-
-    // Only capture request
-    static const std::array<uint32_t, 1> kRequestRectsToCorrect;
-
-    // Only capture result
-    static const std::array<uint32_t, 1> kResultRectsToCorrect;
-
-    // Only for capture results; don't clamp
-    static const std::array<uint32_t, 2> kResultPointsToCorrectNoClamp;
-
     // Single implementation for various mapCorrectedToRaw methods
     template<typename T>
     status_t mapCorrectedToRawImpl(T* coordPairs, int coordCount, bool clamp, bool simple) const;
diff --git a/services/camera/libcameraservice/device3/ZoomRatioMapper.cpp b/services/camera/libcameraservice/device3/ZoomRatioMapper.cpp
new file mode 100644
index 0000000..7718819
--- /dev/null
+++ b/services/camera/libcameraservice/device3/ZoomRatioMapper.cpp
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#define LOG_TAG "Camera3-ZoomRatioMapper"
+//#define LOG_NDEBUG 0
+
+#include <algorithm>
+
+#include "device3/ZoomRatioMapper.h"
+
+namespace android {
+
+namespace camera3 {
+
+ZoomRatioMapper::ZoomRatioMapper() : mHalSupportsZoomRatio(false) {
+}
+
+status_t ZoomRatioMapper::initZoomRatioInTemplate(CameraMetadata *request) {
+    camera_metadata_entry_t entry;
+    entry = request->find(ANDROID_CONTROL_ZOOM_RATIO);
+    float defaultZoomRatio = 1.0f;
+    if (entry.count == 0) {
+        return request->update(ANDROID_CONTROL_ZOOM_RATIO, &defaultZoomRatio, 1);
+    }
+    return OK;
+}
+
+status_t ZoomRatioMapper::overrideZoomRatioTags(
+        CameraMetadata* deviceInfo, bool* supportNativeZoomRatio) {
+    if (deviceInfo == nullptr || supportNativeZoomRatio == nullptr) {
+        return BAD_VALUE;
+    }
+
+    camera_metadata_entry_t entry;
+    entry = deviceInfo->find(ANDROID_CONTROL_ZOOM_RATIO_RANGE);
+    if (entry.count != 2 && entry.count != 0) return BAD_VALUE;
+
+    // Hal has zoom ratio support
+    if (entry.count == 2) {
+        *supportNativeZoomRatio = true;
+        return OK;
+    }
+
+    // Hal has no zoom ratio support
+    *supportNativeZoomRatio = false;
+
+    entry = deviceInfo->find(ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
+    if (entry.count != 1) {
+        ALOGI("%s: Camera device doesn't support SCALER_AVAILABLE_MAX_DIGITAL_ZOOM key!",
+                __FUNCTION__);
+        return OK;
+    }
+
+    float zoomRange[] = {1.0f, entry.data.f[0]};
+    status_t res = deviceInfo->update(ANDROID_CONTROL_ZOOM_RATIO_RANGE, zoomRange, 2);
+    if (res != OK) {
+        ALOGE("%s: Failed to update CONTROL_ZOOM_RATIO_RANGE key: %s (%d)",
+                __FUNCTION__, strerror(-res), res);
+        return res;
+    }
+
+    std::vector<int32_t> requestKeys;
+    entry = deviceInfo->find(ANDROID_REQUEST_AVAILABLE_REQUEST_KEYS);
+    if (entry.count > 0) {
+        requestKeys.insert(requestKeys.end(), entry.data.i32, entry.data.i32 + entry.count);
+    }
+    requestKeys.push_back(ANDROID_CONTROL_ZOOM_RATIO);
+    res = deviceInfo->update(ANDROID_REQUEST_AVAILABLE_REQUEST_KEYS,
+            requestKeys.data(), requestKeys.size());
+    if (res != OK) {
+        ALOGE("%s: Failed to update REQUEST_AVAILABLE_REQUEST_KEYS: %s (%d)",
+                __FUNCTION__, strerror(-res), res);
+        return res;
+    }
+
+    std::vector<int32_t> resultKeys;
+    entry = deviceInfo->find(ANDROID_REQUEST_AVAILABLE_RESULT_KEYS);
+    if (entry.count > 0) {
+        resultKeys.insert(resultKeys.end(), entry.data.i32, entry.data.i32 + entry.count);
+    }
+    resultKeys.push_back(ANDROID_CONTROL_ZOOM_RATIO);
+    res = deviceInfo->update(ANDROID_REQUEST_AVAILABLE_RESULT_KEYS,
+            resultKeys.data(), resultKeys.size());
+    if (res != OK) {
+        ALOGE("%s: Failed to update REQUEST_AVAILABLE_RESULT_KEYS: %s (%d)",
+                __FUNCTION__, strerror(-res), res);
+        return res;
+    }
+
+    std::vector<int32_t> charKeys;
+    entry = deviceInfo->find(ANDROID_REQUEST_AVAILABLE_CHARACTERISTICS_KEYS);
+    if (entry.count > 0) {
+        charKeys.insert(charKeys.end(), entry.data.i32, entry.data.i32 + entry.count);
+    }
+    charKeys.push_back(ANDROID_CONTROL_ZOOM_RATIO_RANGE);
+    res = deviceInfo->update(ANDROID_REQUEST_AVAILABLE_CHARACTERISTICS_KEYS,
+            charKeys.data(), charKeys.size());
+    if (res != OK) {
+        ALOGE("%s: Failed to update REQUEST_AVAILABLE_CHARACTERISTICS_KEYS: %s (%d)",
+                __FUNCTION__, strerror(-res), res);
+        return res;
+    }
+
+    return OK;
+}
+
+status_t ZoomRatioMapper::initZoomRatioTags(const CameraMetadata* deviceInfo,
+        bool supportNativeZoomRatio, bool usePrecorrectArray) {
+    std::lock_guard<std::mutex> lock(mMutex);
+
+    camera_metadata_ro_entry_t entry;
+
+    entry = deviceInfo->find(ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE);
+    if (entry.count != 4) return BAD_VALUE;
+    int32_t arrayW = entry.data.i32[2];
+    int32_t arrayH = entry.data.i32[3];
+
+    entry = deviceInfo->find(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+    if (entry.count != 4) return BAD_VALUE;
+    int32_t activeW = entry.data.i32[2];
+    int32_t activeH = entry.data.i32[3];
+
+    if (usePrecorrectArray) {
+        mArrayWidth = arrayW;
+        mArrayHeight = arrayH;
+    } else {
+        mArrayWidth = activeW;
+        mArrayHeight = activeH;
+    }
+    mHalSupportsZoomRatio = supportNativeZoomRatio;
+
+    ALOGV("%s: array size: %d x %d, mHalSupportsZoomRatio %d",
+            __FUNCTION__, mArrayWidth, mArrayHeight, mHalSupportsZoomRatio);
+    return OK;
+}
+
+status_t ZoomRatioMapper::updateCaptureRequest(CameraMetadata* request) {
+    std::lock_guard<std::mutex> lock(mMutex);
+    status_t res = OK;
+    bool zoomRatioIs1 = true;
+    camera_metadata_entry_t entry;
+
+    entry = request->find(ANDROID_CONTROL_ZOOM_RATIO);
+    if (entry.count == 1 && entry.data.f[0] != 1.0f) {
+        zoomRatioIs1 = false;
+    }
+
+    if (mHalSupportsZoomRatio && zoomRatioIs1) {
+        res = separateZoomFromCropLocked(request, false/*isResult*/);
+    } else if (!mHalSupportsZoomRatio && !zoomRatioIs1) {
+        res = combineZoomAndCropLocked(request, false/*isResult*/);
+    }
+
+    // If CONTROL_ZOOM_RATIO is in request, but HAL doesn't support
+    // CONTROL_ZOOM_RATIO, remove it from the request.
+    if (!mHalSupportsZoomRatio && entry.count == 1) {
+        request->erase(ANDROID_CONTROL_ZOOM_RATIO);
+    }
+
+    return res;
+}
+
+status_t ZoomRatioMapper::updateCaptureResult(CameraMetadata* result, bool requestedZoomRatioIs1) {
+    std::lock_guard<std::mutex> lock(mMutex);
+    status_t res = OK;
+
+    if (mHalSupportsZoomRatio && requestedZoomRatioIs1) {
+        res = combineZoomAndCropLocked(result, true/*isResult*/);
+    } else if (!mHalSupportsZoomRatio && !requestedZoomRatioIs1) {
+        res = separateZoomFromCropLocked(result, true/*isResult*/);
+    } else {
+        camera_metadata_entry_t entry = result->find(ANDROID_CONTROL_ZOOM_RATIO);
+        if (entry.count == 0) {
+            float zoomRatio1x = 1.0f;
+            result->update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio1x, 1);
+        }
+    }
+
+    return res;
+}
+
+float ZoomRatioMapper::deriveZoomRatio(const CameraMetadata* metadata) {
+    float zoomRatio = 1.0;
+
+    camera_metadata_ro_entry_t entry;
+    entry = metadata->find(ANDROID_SCALER_CROP_REGION);
+    if (entry.count != 4) return zoomRatio;
+
+    // Center of the preCorrection/active size
+    float arrayCenterX = mArrayWidth / 2.0;
+    float arrayCenterY = mArrayHeight / 2.0;
+
+    // Re-map crop region to coordinate system centered to (arrayCenterX,
+    // arrayCenterY).
+    float cropRegionLeft = arrayCenterX - entry.data.i32[0] ;
+    float cropRegionTop = arrayCenterY - entry.data.i32[1];
+    float cropRegionRight = entry.data.i32[0] + entry.data.i32[2] - arrayCenterX;
+    float cropRegionBottom = entry.data.i32[1] + entry.data.i32[3] - arrayCenterY;
+
+    // Calculate the scaling factor for left, top, bottom, right
+    float zoomRatioLeft = std::max(mArrayWidth / (2 * cropRegionLeft), 1.0f);
+    float zoomRatioTop = std::max(mArrayHeight / (2 * cropRegionTop), 1.0f);
+    float zoomRatioRight = std::max(mArrayWidth / (2 * cropRegionRight), 1.0f);
+    float zoomRatioBottom = std::max(mArrayHeight / (2 * cropRegionBottom), 1.0f);
+
+    // Use minimum scaling factor to handle letterboxing or pillarboxing
+    zoomRatio = std::min(std::min(zoomRatioLeft, zoomRatioRight),
+            std::min(zoomRatioTop, zoomRatioBottom));
+
+    ALOGV("%s: derived zoomRatio is %f", __FUNCTION__, zoomRatio);
+    return zoomRatio;
+}
+
+status_t ZoomRatioMapper::separateZoomFromCropLocked(CameraMetadata* metadata, bool isResult) {
+    status_t res;
+    float zoomRatio = deriveZoomRatio(metadata);
+
+    // Update zoomRatio metadata tag
+    res = metadata->update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio, 1);
+    if (res != OK) {
+        ALOGE("%s: Failed to update ANDROID_CONTROL_ZOOM_RATIO: %s(%d)",
+                __FUNCTION__, strerror(-res), res);
+        return res;
+    }
+
+    // Scale regions using zoomRatio
+    camera_metadata_entry_t entry;
+    for (auto region : kMeteringRegionsToCorrect) {
+        entry = metadata->find(region);
+        for (size_t j = 0; j < entry.count; j += 5) {
+            int32_t weight = entry.data.i32[j + 4];
+            if (weight == 0) {
+                continue;
+            }
+            // Top-left is inclusively clamped
+            scaleCoordinates(entry.data.i32 + j, 1, zoomRatio, ClampInclusive);
+            // Bottom-right is exclusively clamped
+            scaleCoordinates(entry.data.i32 + j + 2, 1, zoomRatio, ClampExclusive);
+        }
+    }
+
+    for (auto rect : kRectsToCorrect) {
+        entry = metadata->find(rect);
+        scaleRects(entry.data.i32, entry.count / 4, zoomRatio);
+    }
+
+    if (isResult) {
+        for (auto pts : kResultPointsToCorrectNoClamp) {
+            entry = metadata->find(pts);
+            scaleCoordinates(entry.data.i32, entry.count / 2, zoomRatio, ClampOff);
+        }
+    }
+
+    return OK;
+}
+
+status_t ZoomRatioMapper::combineZoomAndCropLocked(CameraMetadata* metadata, bool isResult) {
+    float zoomRatio = 1.0f;
+    camera_metadata_entry_t entry;
+    entry = metadata->find(ANDROID_CONTROL_ZOOM_RATIO);
+    if (entry.count == 1) {
+        zoomRatio = entry.data.f[0];
+    }
+
+    // Unscale regions with zoomRatio
+    status_t res;
+    for (auto region : kMeteringRegionsToCorrect) {
+        entry = metadata->find(region);
+        for (size_t j = 0; j < entry.count; j += 5) {
+            int32_t weight = entry.data.i32[j + 4];
+            if (weight == 0) {
+                continue;
+            }
+            // Top-left is inclusively clamped
+            scaleCoordinates(entry.data.i32 + j, 1, 1.0 / zoomRatio, ClampInclusive);
+            // Bottom-right is exclusively clamped
+            scaleCoordinates(entry.data.i32 + j + 2, 1, 1.0 / zoomRatio, ClampExclusive);
+        }
+    }
+    for (auto rect : kRectsToCorrect) {
+        entry = metadata->find(rect);
+        scaleRects(entry.data.i32, entry.count / 4, 1.0 / zoomRatio);
+    }
+    if (isResult) {
+        for (auto pts : kResultPointsToCorrectNoClamp) {
+            entry = metadata->find(pts);
+            scaleCoordinates(entry.data.i32, entry.count / 2, 1.0 / zoomRatio, ClampOff);
+        }
+    }
+
+    zoomRatio = 1.0;
+    res = metadata->update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio, 1);
+    if (res != OK) {
+        return res;
+    }
+
+    return OK;
+}
+
+void ZoomRatioMapper::scaleCoordinates(int32_t* coordPairs, int coordCount,
+        float scaleRatio, ClampMode clamp) {
+    for (int i = 0; i < coordCount * 2; i += 2) {
+        float x = coordPairs[i];
+        float y = coordPairs[i + 1];
+        float xCentered = x - mArrayWidth / 2;
+        float yCentered = y - mArrayHeight / 2;
+        float scaledX = xCentered * scaleRatio;
+        float scaledY = yCentered * scaleRatio;
+        scaledX += mArrayWidth / 2;
+        scaledY += mArrayHeight / 2;
+        // Clamp to within activeArray/preCorrectionActiveArray
+        coordPairs[i] = static_cast<int32_t>(scaledX);
+        coordPairs[i+1] = static_cast<int32_t>(scaledY);
+        if (clamp != ClampOff) {
+            int32_t right, bottom;
+            if (clamp == ClampInclusive) {
+                right = mArrayWidth - 1;
+                bottom = mArrayHeight - 1;
+            } else {
+                right = mArrayWidth;
+                bottom = mArrayHeight;
+            }
+            coordPairs[i] =
+                    std::min(right, std::max(0, coordPairs[i]));
+            coordPairs[i+1] =
+                    std::min(bottom, std::max(0, coordPairs[i+1]));
+        }
+        ALOGV("%s: coordinates: %d, %d", __FUNCTION__, coordPairs[i], coordPairs[i+1]);
+    }
+}
+
+void ZoomRatioMapper::scaleRects(int32_t* rects, int rectCount,
+        float scaleRatio) {
+    for (int i = 0; i < rectCount * 4; i += 4) {
+        // Map from (l, t, width, height) to (l, t, r, b).
+        // [l, t] is inclusive, and [r, b] is exclusive.
+        int32_t coords[4] = {
+            rects[i],
+            rects[i + 1],
+            rects[i] + rects[i + 2],
+            rects[i + 1] + rects[i + 3]
+        };
+
+        // top-left
+        scaleCoordinates(coords, 1, scaleRatio, ClampInclusive);
+        // bottom-right
+        scaleCoordinates(coords+2, 1, scaleRatio, ClampExclusive);
+
+        // Map back to (l, t, width, height)
+        rects[i] = coords[0];
+        rects[i + 1] = coords[1];
+        rects[i + 2] = coords[2] - coords[0];
+        rects[i + 3] = coords[3] - coords[1];
+    }
+}
+
+} // namespace camera3
+
+} // namespace android
diff --git a/services/camera/libcameraservice/device3/ZoomRatioMapper.h b/services/camera/libcameraservice/device3/ZoomRatioMapper.h
new file mode 100644
index 0000000..38efbfd
--- /dev/null
+++ b/services/camera/libcameraservice/device3/ZoomRatioMapper.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#ifndef ANDROID_SERVERS_ZOOMRATIOMAPPER_H
+#define ANDROID_SERVERS_ZOOMRATIOMAPPER_H
+
+#include <utils/Errors.h>
+#include <array>
+#include <mutex>
+
+#include "camera/CameraMetadata.h"
+#include "device3/CoordinateMapper.h"
+
+namespace android {
+
+namespace camera3 {
+
+/**
+ * Utilities to convert between the new zoomRatio and existing cropRegion
+ * metadata tags. Note that this class does conversions in 2 scenarios:
+ * - HAL supports zoomRatio and the application uses cropRegion, or
+ * - HAL doesn't support zoomRatio, but the application uses zoomRatio
+ */
+class ZoomRatioMapper : private CoordinateMapper {
+  public:
+    ZoomRatioMapper();
+    ZoomRatioMapper(const ZoomRatioMapper& other) :
+            mHalSupportsZoomRatio(other.mHalSupportsZoomRatio),
+            mArrayWidth(other.mArrayWidth), mArrayHeight(other.mArrayHeight) {}
+
+    /**
+     * Initialize request template with valid zoomRatio if necessary.
+     */
+    static status_t initZoomRatioInTemplate(CameraMetadata *request);
+
+    /**
+     * Override zoomRatio related tags in the static metadata.
+     */
+    static status_t overrideZoomRatioTags(
+            CameraMetadata* deviceInfo, bool* supportNativeZoomRatio);
+
+    /**
+     * Initialize zoom ratio mapper with static metadata.
+     *
+     * Note:
+     * This function may modify the static metadata with zoomRatio related
+     * tags.
+     */
+    status_t initZoomRatioTags(const CameraMetadata *deviceInfo,
+            bool supportNativeZoomRatio, bool usePrecorrectArray);
+
+    /**
+     * Update capture request to handle both cropRegion and zoomRatio.
+     */
+    status_t updateCaptureRequest(CameraMetadata *request);
+
+    /**
+     * Update capture result to handle both cropRegion and zoomRatio.
+     */
+    status_t updateCaptureResult(CameraMetadata *request, bool requestedZoomRatioIs1);
+
+  public: // Visible for testing. Do not use concurently.
+    enum ClampMode {
+        ClampOff,
+        ClampInclusive,
+        ClampExclusive,
+    };
+
+    void scaleCoordinates(int32_t* coordPairs, int coordCount,
+            float scaleRatio, ClampMode clamp);
+
+  private:
+    bool mHalSupportsZoomRatio;
+
+    // active array / pre-correction array dimension
+    int32_t mArrayWidth, mArrayHeight;
+
+    mutable std::mutex mMutex;
+
+    float deriveZoomRatio(const CameraMetadata* metadata);
+
+    void scaleRects(int32_t* rects, int rectCount, float scaleRatio);
+
+    status_t separateZoomFromCropLocked(CameraMetadata* metadata, bool isResult);
+    status_t combineZoomAndCropLocked(CameraMetadata* metadata, bool isResult);
+};
+
+} // namespace camera3
+
+} // namespace android
+
+#endif
diff --git a/services/camera/libcameraservice/tests/ZoomRatioTest.cpp b/services/camera/libcameraservice/tests/ZoomRatioTest.cpp
new file mode 100644
index 0000000..1bfa03f
--- /dev/null
+++ b/services/camera/libcameraservice/tests/ZoomRatioTest.cpp
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#define LOG_NDEBUG 0
+#define LOG_TAG "ZoomRatioMapperTest"
+
+#include <gtest/gtest.h>
+#include <utils/Errors.h>
+
+#include "../device3/ZoomRatioMapper.h"
+
+using namespace std;
+using namespace android;
+using namespace android::camera3;
+
+constexpr int kMaxAllowedPixelError = 1;
+constexpr float kMaxAllowedRatioError = 0.1;
+
+constexpr int32_t testActiveArraySize[] = {100, 100, 1024, 768};
+constexpr int32_t testPreCorrActiveArraySize[] = {90, 90, 1044, 788};
+
+constexpr int32_t testDefaultCropSize[][4] = {
+      {0, 0, 1024, 768},   // active array default crop
+      {0, 0, 1044, 788},   // preCorrection active array default crop
+};
+
+constexpr int32_t test2xCropRegion[][4] = {
+      {256, 192, 512, 384}, // active array 2x zoom crop
+      {261, 197, 522, 394}, // preCorrection active array default crop
+};
+
+constexpr int32_t testLetterBoxSize[][4] = {
+      {0, 96, 1024, 576}, // active array 2x zoom crop
+      {0, 106, 1024, 576}, // preCorrection active array default crop
+};
+
+status_t setupTestMapper(ZoomRatioMapper *m, float maxDigitalZoom,
+        const int32_t activeArray[4], const int32_t preCorrectArray[4],
+        bool hasZoomRatioRange, float zoomRatioRange[2],
+        bool usePreCorrectArray) {
+    CameraMetadata deviceInfo;
+
+    deviceInfo.update(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE, activeArray, 4);
+    deviceInfo.update(ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE, preCorrectArray, 4);
+    deviceInfo.update(ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM, &maxDigitalZoom, 1);
+    if (hasZoomRatioRange) {
+        deviceInfo.update(ANDROID_CONTROL_ZOOM_RATIO_RANGE, zoomRatioRange, 2);
+    }
+
+    bool supportNativeZoomRatio;
+    status_t res = ZoomRatioMapper::overrideZoomRatioTags(&deviceInfo, &supportNativeZoomRatio);
+    if (res != OK) {
+        return res;
+    }
+
+    return m->initZoomRatioTags(&deviceInfo, hasZoomRatioRange, usePreCorrectArray);
+}
+
+TEST(ZoomRatioTest, Initialization) {
+    CameraMetadata deviceInfo;
+    status_t res;
+    camera_metadata_entry_t entry;
+
+    deviceInfo.update(ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE,
+            testPreCorrActiveArraySize, 4);
+    deviceInfo.update(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE, testActiveArraySize, 4);
+
+    // Test initialization from devices not supporting zoomRange
+    float maxDigitalZoom = 4.0f;
+    ZoomRatioMapper mapperNoZoomRange;
+    deviceInfo.update(ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM, &maxDigitalZoom, 1);
+    bool supportNativeZoomRatio;
+    res = ZoomRatioMapper::overrideZoomRatioTags(&deviceInfo, &supportNativeZoomRatio);
+    ASSERT_EQ(res, OK);
+    ASSERT_EQ(supportNativeZoomRatio, false);
+    res = mapperNoZoomRange.initZoomRatioTags(&deviceInfo,
+            supportNativeZoomRatio, true/*usePreCorrectArray*/);
+    ASSERT_EQ(res, OK);
+    res = mapperNoZoomRange.initZoomRatioTags(&deviceInfo,
+            supportNativeZoomRatio, false/*usePreCorrectArray*/);
+    ASSERT_EQ(res, OK);
+
+    entry = deviceInfo.find(ANDROID_CONTROL_ZOOM_RATIO_RANGE);
+    ASSERT_EQ(entry.count, 2U);
+    ASSERT_EQ(entry.data.f[0], 1.0);
+    ASSERT_EQ(entry.data.f[1], maxDigitalZoom);
+
+    entry = deviceInfo.find(ANDROID_REQUEST_AVAILABLE_CHARACTERISTICS_KEYS);
+    ASSERT_GT(entry.count, 0U);
+    ASSERT_NE(std::find(entry.data.i32, entry.data.i32 + entry.count,
+            ANDROID_CONTROL_ZOOM_RATIO_RANGE), entry.data.i32 + entry.count);
+
+    entry = deviceInfo.find(ANDROID_REQUEST_AVAILABLE_REQUEST_KEYS);
+    ASSERT_GT(entry.count, 0U);
+    ASSERT_NE(std::find(entry.data.i32, entry.data.i32 + entry.count,
+            ANDROID_CONTROL_ZOOM_RATIO), entry.data.i32 + entry.count);
+
+    entry = deviceInfo.find(ANDROID_REQUEST_AVAILABLE_RESULT_KEYS);
+    ASSERT_GT(entry.count, 0U);
+    ASSERT_NE(std::find(entry.data.i32, entry.data.i32 + entry.count,
+            ANDROID_CONTROL_ZOOM_RATIO), entry.data.i32 + entry.count);
+
+    // Test initialization from devices supporting zoomRange
+    float ratioRange[2] = {0.2f, maxDigitalZoom};
+    deviceInfo.update(ANDROID_CONTROL_ZOOM_RATIO_RANGE, ratioRange, 2);
+    res = ZoomRatioMapper::overrideZoomRatioTags(&deviceInfo, &supportNativeZoomRatio);
+    ASSERT_EQ(res, OK);
+    ASSERT_EQ(supportNativeZoomRatio, true);
+    ZoomRatioMapper mapperWithZoomRange;
+    res = mapperWithZoomRange.initZoomRatioTags(&deviceInfo,
+            supportNativeZoomRatio, true/*usePreCorrectArray*/);
+    ASSERT_EQ(res, OK);
+    res = mapperWithZoomRange.initZoomRatioTags(&deviceInfo,
+            supportNativeZoomRatio, false/*usePreCorrectArray*/);
+    ASSERT_EQ(res, OK);
+
+    entry = deviceInfo.find(ANDROID_CONTROL_ZOOM_RATIO_RANGE);
+    ASSERT_EQ(entry.count, 2U);
+    ASSERT_EQ(entry.data.f[0], ratioRange[0]);
+    ASSERT_EQ(entry.data.f[1], ratioRange[1]);
+
+    // Test default zoom ratio in template
+    CameraMetadata requestTemplate;
+    res = ZoomRatioMapper::initZoomRatioInTemplate(&requestTemplate);
+    ASSERT_EQ(res, OK);
+    entry = requestTemplate.find(ANDROID_CONTROL_ZOOM_RATIO);
+    ASSERT_EQ(entry.count, 1U);
+    ASSERT_EQ(entry.data.f[0], 1.0f);
+
+    float customRatio = 0.5f;
+    res = requestTemplate.update(ANDROID_CONTROL_ZOOM_RATIO, &customRatio, 1);
+    ASSERT_EQ(res, OK);
+    res = ZoomRatioMapper::initZoomRatioInTemplate(&requestTemplate);
+    ASSERT_EQ(res, OK);
+    entry = requestTemplate.find(ANDROID_CONTROL_ZOOM_RATIO);
+    ASSERT_EQ(entry.count, 1U);
+    ASSERT_EQ(entry.data.f[0], customRatio);
+}
+
+void subScaleCoordinatesTest(bool usePreCorrectArray) {
+    ZoomRatioMapper mapper;
+    float maxDigitalZoom = 4.0f;
+    float zoomRatioRange[2];
+    ASSERT_EQ(OK, setupTestMapper(&mapper, maxDigitalZoom,
+            testActiveArraySize, testPreCorrActiveArraySize,
+            false/*hasZoomRatioRange*/, zoomRatioRange,
+            usePreCorrectArray));
+
+    size_t index = 0;
+    int32_t width = testActiveArraySize[2];
+    int32_t height = testActiveArraySize[3];
+    if (usePreCorrectArray) {
+        index = 1;
+        width = testPreCorrActiveArraySize[2];
+        height = testPreCorrActiveArraySize[3];
+    }
+
+    std::array<int32_t, 16> originalCoords = {
+            0, 0, // top-left
+            width, 0, // top-right
+            0, height, // bottom-left
+            width, height, // bottom-right
+            width / 2, height / 2, // center
+            width / 4, height / 4, // top-left after 2x
+            width / 3, height * 2 / 3, // bottom-left after 3x zoom
+            width * 7 / 8, height / 2, // middle-right after 1.33x zoom
+    };
+
+    // Verify 1.0x zoom doesn't change the coordinates
+    auto coords = originalCoords;
+    mapper.scaleCoordinates(coords.data(), coords.size()/2, 1.0f, ZoomRatioMapper::ClampOff);
+    for (size_t i = 0; i < coords.size(); i++) {
+        EXPECT_EQ(coords[i], originalCoords[i]);
+    }
+
+    // Verify 2.0x zoom work as expected (no clamping)
+    std::array<float, 16> expected2xCoords = {
+            - width / 2.0f, - height / 2.0f,// top-left
+            width * 3 / 2.0f, - height / 2.0f, // top-right
+            - width / 2.0f, height * 3 / 2.0f, // bottom-left
+            width * 3 / 2.0f, height * 3 / 2.0f, // bottom-right
+            width / 2.0f, height / 2.0f, // center
+            0, 0, // top-left after 2x
+            width / 6.0f, height - height / 6.0f, // bottom-left after 3x zoom
+            width + width / 4.0f, height / 2.0f, // middle-right after 1.33x zoom
+    };
+    coords = originalCoords;
+    mapper.scaleCoordinates(coords.data(), coords.size()/2, 2.0f, ZoomRatioMapper::ClampOff);
+    for (size_t i = 0; i < coords.size(); i++) {
+        EXPECT_LE(std::abs(coords[i] - expected2xCoords[i]), kMaxAllowedPixelError);
+    }
+
+    // Verify 2.0x zoom work as expected (with inclusive clamping)
+    std::array<float, 16> expected2xCoordsClampedInc = {
+            0, 0, // top-left
+            static_cast<float>(width) - 1, 0, // top-right
+            0, static_cast<float>(height) - 1, // bottom-left
+            static_cast<float>(width) - 1, static_cast<float>(height) - 1, // bottom-right
+            width / 2.0f, height / 2.0f, // center
+            0, 0, // top-left after 2x
+            width / 6.0f, height - height / 6.0f , // bottom-left after 3x zoom
+            static_cast<float>(width) - 1,  height / 2.0f, // middle-right after 1.33x zoom
+    };
+    coords = originalCoords;
+    mapper.scaleCoordinates(coords.data(), coords.size()/2, 2.0f, ZoomRatioMapper::ClampInclusive);
+    for (size_t i = 0; i < coords.size(); i++) {
+        EXPECT_LE(std::abs(coords[i] - expected2xCoordsClampedInc[i]), kMaxAllowedPixelError);
+    }
+
+    // Verify 2.0x zoom work as expected (with exclusive clamping)
+    std::array<float, 16> expected2xCoordsClampedExc = {
+            0, 0, // top-left
+            static_cast<float>(width), 0, // top-right
+            0, static_cast<float>(height), // bottom-left
+            static_cast<float>(width), static_cast<float>(height), // bottom-right
+            width / 2.0f, height / 2.0f, // center
+            0, 0, // top-left after 2x
+            width / 6.0f, height - height / 6.0f , // bottom-left after 3x zoom
+            static_cast<float>(width),  height / 2.0f, // middle-right after 1.33x zoom
+    };
+    coords = originalCoords;
+    mapper.scaleCoordinates(coords.data(), coords.size()/2, 2.0f, ZoomRatioMapper::ClampExclusive);
+    for (size_t i = 0; i < coords.size(); i++) {
+        EXPECT_LE(std::abs(coords[i] - expected2xCoordsClampedExc[i]), kMaxAllowedPixelError);
+    }
+
+    // Verify 0.33x zoom work as expected
+    std::array<float, 16> expectedZoomOutCoords = {
+            width / 3.0f, height / 3.0f, // top-left
+            width * 2 / 3.0f, height / 3.0f, // top-right
+            width / 3.0f, height * 2 / 3.0f, // bottom-left
+            width * 2 / 3.0f, height * 2 / 3.0f, // bottom-right
+            width / 2.0f, height / 2.0f, // center
+            width * 5 / 12.0f, height * 5 / 12.0f, // top-left after 2x
+            width * 4 / 9.0f, height * 5 / 9.0f, // bottom-left after 3x zoom-in
+            width * 5 / 8.0f, height / 2.0f, // middle-right after 1.33x zoom-in
+    };
+    coords = originalCoords;
+    mapper.scaleCoordinates(coords.data(), coords.size()/2, 1.0f/3, ZoomRatioMapper::ClampOff);
+    for (size_t i = 0; i < coords.size(); i++) {
+        EXPECT_LE(std::abs(coords[i] - expectedZoomOutCoords[i]), kMaxAllowedPixelError);
+    }
+}
+
+TEST(ZoomRatioTest, scaleCoordinatesTest) {
+    subScaleCoordinatesTest(false/*usePreCorrectArray*/);
+    subScaleCoordinatesTest(true/*usePreCorrectArray*/);
+}
+
+void subCropOverMaxDigitalZoomTest(bool usePreCorrectArray) {
+    status_t res;
+    ZoomRatioMapper mapper;
+    float noZoomRatioRange[2];
+    res = setupTestMapper(&mapper, 4.0/*maxDigitalZoom*/,
+            testActiveArraySize, testPreCorrActiveArraySize,
+            false/*hasZoomRatioRange*/, noZoomRatioRange,
+            usePreCorrectArray);
+    ASSERT_EQ(res, OK);
+
+    CameraMetadata metadata;
+    camera_metadata_entry_t entry;
+
+    size_t index = usePreCorrectArray ? 1 : 0;
+    metadata.update(ANDROID_SCALER_CROP_REGION, testDefaultCropSize[index], 4);
+    res = mapper.updateCaptureRequest(&metadata);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i ++) {
+        EXPECT_EQ(entry.data.i32[i], testDefaultCropSize[index][i]);
+    }
+
+    metadata.update(ANDROID_SCALER_CROP_REGION, test2xCropRegion[index], 4);
+    res = mapper.updateCaptureResult(&metadata, true/*requestedZoomRatioIs1*/);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i ++) {
+        EXPECT_EQ(entry.data.i32[i], test2xCropRegion[index][i]);
+    }
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    ASSERT_TRUE(entry.count == 0 || (entry.count == 1 && entry.data.f[0] == 1.0f));
+}
+
+TEST(ZoomRatioTest, CropOverMaxDigitalZoomTest) {
+    subCropOverMaxDigitalZoomTest(false/*usePreCorrectArray*/);
+    subCropOverMaxDigitalZoomTest(true/*usePreCorrectArray*/);
+}
+
+void subCropOverZoomRangeTest(bool usePreCorrectArray) {
+    status_t res;
+    ZoomRatioMapper mapper;
+    float zoomRatioRange[2] = {0.5f, 4.0f};
+    res = setupTestMapper(&mapper, 4.0/*maxDigitalZoom*/,
+            testActiveArraySize, testPreCorrActiveArraySize,
+            true/*hasZoomRatioRange*/, zoomRatioRange,
+            usePreCorrectArray);
+    ASSERT_EQ(res, OK);
+
+    CameraMetadata metadata;
+    camera_metadata_entry_t entry;
+
+    size_t index = usePreCorrectArray ? 1 : 0;
+
+    // 2x zoom crop region, zoomRatio is 1.0f
+    metadata.update(ANDROID_SCALER_CROP_REGION, test2xCropRegion[index], 4);
+    res = mapper.updateCaptureRequest(&metadata);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i++) {
+        EXPECT_EQ(entry.data.i32[i], testDefaultCropSize[index][i]);
+    }
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    EXPECT_NEAR(entry.data.f[0], 2.0f, kMaxAllowedRatioError);
+
+    res = mapper.updateCaptureResult(&metadata, true/*requestedZoomRatioIs1*/);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    EXPECT_NEAR(entry.data.f[0], 1.0f, kMaxAllowedRatioError);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i++) {
+        EXPECT_EQ(entry.data.i32[i], test2xCropRegion[index][i]);
+    }
+
+    // Letter boxing crop region, zoomRatio is 1.0
+    float zoomRatio = 1.0f;
+    metadata.update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio, 1);
+    metadata.update(ANDROID_SCALER_CROP_REGION, testLetterBoxSize[index], 4);
+    res = mapper.updateCaptureRequest(&metadata);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i++) {
+        EXPECT_EQ(entry.data.i32[i], testLetterBoxSize[index][i]);
+    }
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    EXPECT_NEAR(entry.data.f[0], 1.0f, kMaxAllowedRatioError);
+
+    res = mapper.updateCaptureResult(&metadata, true/*requestedZoomRatioIs1*/);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i++) {
+        EXPECT_EQ(entry.data.i32[i], testLetterBoxSize[index][i]);
+    }
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    EXPECT_NEAR(entry.data.f[0], 1.0f, kMaxAllowedRatioError);
+}
+
+TEST(ZoomRatioTest, CropOverZoomRangeTest) {
+    subCropOverZoomRangeTest(false/*usePreCorrectArray*/);
+    subCropOverZoomRangeTest(true/*usePreCorrectArray*/);
+}
+
+void subZoomOverMaxDigitalZoomTest(bool usePreCorrectArray) {
+    status_t res;
+    ZoomRatioMapper mapper;
+    float noZoomRatioRange[2];
+    res = setupTestMapper(&mapper, 4.0/*maxDigitalZoom*/,
+            testActiveArraySize, testPreCorrActiveArraySize,
+            false/*hasZoomRatioRange*/, noZoomRatioRange,
+            usePreCorrectArray);
+    ASSERT_EQ(res, OK);
+
+    CameraMetadata metadata;
+    float zoomRatio = 3.0f;
+    camera_metadata_entry_t entry;
+
+    size_t index = usePreCorrectArray ? 1 : 0;
+
+    // Full active array crop, zoomRatio is 3.0f
+    metadata.update(ANDROID_SCALER_CROP_REGION, testDefaultCropSize[index], 4);
+    metadata.update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio, 1);
+    res = mapper.updateCaptureRequest(&metadata);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    std::array<float, 4> expectedCrop = {
+        testDefaultCropSize[index][2] / 3.0f, /*x*/
+        testDefaultCropSize[index][3] / 3.0f, /*y*/
+        testDefaultCropSize[index][2] / 3.0f, /*width*/
+        testDefaultCropSize[index][3] / 3.0f, /*height*/
+    };
+    for (int i = 0; i < 4; i++) {
+        EXPECT_LE(std::abs(entry.data.i32[i] - expectedCrop[i]), kMaxAllowedPixelError);
+    }
+
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    if (entry.count == 1) {
+        EXPECT_NEAR(entry.data.f[0], 1.0f, kMaxAllowedRatioError);
+    }
+}
+
+TEST(ZoomRatioTest, ZoomOverMaxDigitalZoomTest) {
+    subZoomOverMaxDigitalZoomTest(false/*usePreCorrectArray*/);
+    subZoomOverMaxDigitalZoomTest(true/*usePreCorrectArray*/);
+}
+
+void subZoomOverZoomRangeTest(bool usePreCorrectArray) {
+    status_t res;
+    ZoomRatioMapper mapper;
+    float zoomRatioRange[2] = {1.0f, 4.0f};
+    res = setupTestMapper(&mapper, 4.0/*maxDigitalZoom*/,
+            testActiveArraySize, testPreCorrActiveArraySize,
+            true/*hasZoomRatioRange*/, zoomRatioRange,
+            usePreCorrectArray);
+    ASSERT_EQ(res, OK);
+
+    CameraMetadata metadata;
+    float zoomRatio = 3.0f;
+    camera_metadata_entry_t entry;
+    size_t index = usePreCorrectArray ? 1 : 0;
+
+    // Full active array crop, zoomRatio is 3.0f
+    metadata.update(ANDROID_SCALER_CROP_REGION, testDefaultCropSize[index], 4);
+    metadata.update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio, 1);
+    res = mapper.updateCaptureRequest(&metadata);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i ++) {
+        EXPECT_EQ(entry.data.i32[i], testDefaultCropSize[index][i]);
+    }
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    ASSERT_EQ(entry.data.f[0], zoomRatio);
+
+    res = mapper.updateCaptureResult(&metadata, false/*requestedZoomRatioIs1*/);
+    ASSERT_EQ(res, OK);
+    entry = metadata.find(ANDROID_SCALER_CROP_REGION);
+    ASSERT_EQ(entry.count, 4U);
+    for (int i = 0; i < 4; i ++) {
+        EXPECT_EQ(entry.data.i32[i], testDefaultCropSize[index][i]);
+    }
+    entry = metadata.find(ANDROID_CONTROL_ZOOM_RATIO);
+    ASSERT_EQ(entry.data.f[0], zoomRatio);
+}
+
+TEST(ZoomRatioTest, ZoomOverZoomRangeTest) {
+    subZoomOverZoomRangeTest(false/*usePreCorrectArray*/);
+    subZoomOverZoomRangeTest(true/*usePreCorrectArray*/);
+}