1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ex.camera2.portability;
18
19import static android.hardware.camera2.CaptureRequest.*;
20
21import android.graphics.Matrix;
22import android.graphics.Rect;
23import android.graphics.RectF;
24import android.hardware.camera2.CameraAccessException;
25import android.hardware.camera2.CameraDevice;
26import android.hardware.camera2.params.MeteringRectangle;
27import android.location.Location;
28import android.util.Range;
29
30import com.android.ex.camera2.portability.CameraCapabilities.FlashMode;
31import com.android.ex.camera2.portability.CameraCapabilities.FocusMode;
32import com.android.ex.camera2.portability.CameraCapabilities.SceneMode;
33import com.android.ex.camera2.portability.CameraCapabilities.WhiteBalance;
34import com.android.ex.camera2.portability.debug.Log;
35import com.android.ex.camera2.utils.Camera2RequestSettingsSet;
36
37import java.util.List;
38import java.util.Objects;
39
40/**
41 * The subclass of {@link CameraSettings} for Android Camera 2 API.
42 */
43public class AndroidCamera2Settings extends CameraSettings {
44    private static final Log.Tag TAG = new Log.Tag("AndCam2Set");
45
46    private final Builder mTemplateSettings;
47    private final Camera2RequestSettingsSet mRequestSettings;
48    /** Sensor's active array bounds. */
49    private final Rect mActiveArray;
50    /** Crop rectangle for digital zoom (measured WRT the active array). */
51    private final Rect mCropRectangle;
52    /** Bounds of visible preview portion (measured WRT the active array). */
53    private Rect mVisiblePreviewRectangle;
54
55    /**
56     * Create a settings representation that answers queries of unspecified
57     * options in the same way as the provided template would.
58     *
59     * <p>The default settings provided by the given template are only ever used
60     * for reporting back to the client app (i.e. when it queries an option
61     * it didn't explicitly set first). {@link Camera2RequestSettingsSet}s
62     * generated by an instance of this class will have any settings not
63     * modified using one of that instance's mutators forced to default, so that
64     * their effective values when submitting a capture request will be those of
65     * the template that is provided to the camera framework at that time.</p>
66     *
67     * @param camera Device from which to draw default settings
68     *               (non-{@code null}).
69     * @param template Specific template to use for the defaults.
70     * @param activeArray Boundary coordinates of the sensor's active array
71     *                    (non-{@code null}).
72     * @param preview Dimensions of preview streams.
73     * @param photo Dimensions of captured images.
74     *
75     * @throws IllegalArgumentException If {@code camera} or {@code activeArray}
76     *                                  is {@code null}.
77     * @throws CameraAccessException Upon internal framework/driver failure.
78     */
79    public AndroidCamera2Settings(CameraDevice camera, int template, Rect activeArray,
80                                  Size preview, Size photo) throws CameraAccessException {
81        if (camera == null) {
82            throw new NullPointerException("camera must not be null");
83        }
84        if (activeArray == null) {
85            throw new NullPointerException("activeArray must not be null");
86        }
87
88        mTemplateSettings = camera.createCaptureRequest(template);
89        mRequestSettings = new Camera2RequestSettingsSet();
90        mActiveArray = activeArray;
91        mCropRectangle = new Rect(0, 0, activeArray.width(), activeArray.height());
92
93        mSizesLocked = false;
94
95        Range<Integer> previewFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE);
96        if (previewFpsRange != null) {
97            setPreviewFpsRange(previewFpsRange.getLower(), previewFpsRange.getUpper());
98        }
99        setPreviewSize(preview);
100        // TODO: mCurrentPreviewFormat
101        setPhotoSize(photo);
102        mJpegCompressQuality = queryTemplateDefaultOrMakeOneUp(JPEG_QUALITY, (byte) 0);
103        // TODO: mCurrentPhotoFormat
104        // NB: We're assuming that templates won't be zoomed in by default.
105        mCurrentZoomRatio = CameraCapabilities.ZOOM_RATIO_UNZOOMED;
106        // TODO: mCurrentZoomIndex
107        mExposureCompensationIndex =
108                queryTemplateDefaultOrMakeOneUp(CONTROL_AE_EXPOSURE_COMPENSATION, 0);
109
110        mCurrentFlashMode = flashModeFromRequest();
111        Integer currentFocusMode = mTemplateSettings.get(CONTROL_AF_MODE);
112        if (currentFocusMode != null) {
113            mCurrentFocusMode = AndroidCamera2Capabilities.focusModeFromInt(currentFocusMode);
114        }
115        Integer currentSceneMode = mTemplateSettings.get(CONTROL_SCENE_MODE);
116        if (currentSceneMode != null) {
117            mCurrentSceneMode = AndroidCamera2Capabilities.sceneModeFromInt(currentSceneMode);
118        }
119        Integer whiteBalance = mTemplateSettings.get(CONTROL_AWB_MODE);
120        if (whiteBalance != null) {
121            mWhiteBalance = AndroidCamera2Capabilities.whiteBalanceFromInt(whiteBalance);
122        }
123
124        mVideoStabilizationEnabled = queryTemplateDefaultOrMakeOneUp(
125                        CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_OFF) ==
126                CONTROL_VIDEO_STABILIZATION_MODE_ON;
127        mAutoExposureLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AE_LOCK, false);
128        mAutoWhiteBalanceLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AWB_LOCK, false);
129        // TODO: mRecordingHintEnabled
130        // TODO: mGpsData
131        android.util.Size exifThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE);
132        if (exifThumbnailSize != null) {
133            mExifThumbnailSize =
134                    new Size(exifThumbnailSize.getWidth(), exifThumbnailSize.getHeight());
135        }
136    }
137
138    public AndroidCamera2Settings(AndroidCamera2Settings other) {
139        super(other);
140        mTemplateSettings = other.mTemplateSettings;
141        mRequestSettings = new Camera2RequestSettingsSet(other.mRequestSettings);
142        mActiveArray = other.mActiveArray;
143        mCropRectangle = new Rect(other.mCropRectangle);
144    }
145
146    @Override
147    public CameraSettings copy() {
148        return new AndroidCamera2Settings(this);
149    }
150
151    private <T> T queryTemplateDefaultOrMakeOneUp(Key<T> key, T defaultDefault) {
152        T val = mTemplateSettings.get(key);
153        if (val != null) {
154            return val;
155        } else {
156            // Spoof the default so matchesTemplateDefault excludes this key from generated sets.
157            // This approach beats a simple sentinel because it provides basic boolean support.
158            mTemplateSettings.set(key, defaultDefault);
159            return defaultDefault;
160        }
161    }
162
163    private FlashMode flashModeFromRequest() {
164        Integer autoExposure = mTemplateSettings.get(CONTROL_AE_MODE);
165        if (autoExposure != null) {
166            switch (autoExposure) {
167                case CONTROL_AE_MODE_ON:
168                    return FlashMode.OFF;
169                case CONTROL_AE_MODE_ON_AUTO_FLASH:
170                    return FlashMode.AUTO;
171                case CONTROL_AE_MODE_ON_ALWAYS_FLASH: {
172                    if (mTemplateSettings.get(FLASH_MODE) == FLASH_MODE_TORCH) {
173                        return FlashMode.TORCH;
174                    } else {
175                        return FlashMode.ON;
176                    }
177                }
178                case CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE:
179                    return FlashMode.RED_EYE;
180            }
181        }
182        return null;
183    }
184
185    @Override
186    public void setZoomRatio(float ratio) {
187        super.setZoomRatio(ratio);
188
189        // Compute the crop rectangle to be passed to the framework
190        mCropRectangle.set(0, 0,
191                toIntConstrained(
192                        mActiveArray.width() / mCurrentZoomRatio, 0, mActiveArray.width()),
193                toIntConstrained(
194                        mActiveArray.height() / mCurrentZoomRatio, 0, mActiveArray.height()));
195        mCropRectangle.offsetTo((mActiveArray.width() - mCropRectangle.width()) / 2,
196                (mActiveArray.height() - mCropRectangle.height()) / 2);
197
198        // Compute the effective crop rectangle to be used for computing focus/metering coordinates
199        mVisiblePreviewRectangle =
200                effectiveCropRectFromRequested(mCropRectangle, mCurrentPreviewSize);
201    }
202
203    private boolean matchesTemplateDefault(Key<?> setting) {
204        if (setting == CONTROL_AE_REGIONS) {
205            return mMeteringAreas.size() == 0;
206        } else if (setting == CONTROL_AF_REGIONS) {
207            return mFocusAreas.size() == 0;
208        } else if (setting == CONTROL_AE_TARGET_FPS_RANGE) {
209            Range<Integer> defaultFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE);
210            return (mPreviewFpsRangeMin == 0 && mPreviewFpsRangeMax == 0) ||
211                    (defaultFpsRange != null && mPreviewFpsRangeMin == defaultFpsRange.getLower() &&
212                            mPreviewFpsRangeMax == defaultFpsRange.getUpper());
213        } else if (setting == JPEG_QUALITY) {
214            return Objects.equals(mJpegCompressQuality,
215                    mTemplateSettings.get(JPEG_QUALITY));
216        } else if (setting == CONTROL_AE_EXPOSURE_COMPENSATION) {
217            return Objects.equals(mExposureCompensationIndex,
218                    mTemplateSettings.get(CONTROL_AE_EXPOSURE_COMPENSATION));
219        } else if (setting == CONTROL_VIDEO_STABILIZATION_MODE) {
220            Integer videoStabilization = mTemplateSettings.get(CONTROL_VIDEO_STABILIZATION_MODE);
221            return (videoStabilization != null &&
222                    (mVideoStabilizationEnabled && videoStabilization ==
223                            CONTROL_VIDEO_STABILIZATION_MODE_ON) ||
224                    (!mVideoStabilizationEnabled && videoStabilization ==
225                            CONTROL_VIDEO_STABILIZATION_MODE_OFF));
226        } else if (setting == CONTROL_AE_LOCK) {
227            return Objects.equals(mAutoExposureLocked, mTemplateSettings.get(CONTROL_AE_LOCK));
228        } else if (setting == CONTROL_AWB_LOCK) {
229            return Objects.equals(mAutoWhiteBalanceLocked, mTemplateSettings.get(CONTROL_AWB_LOCK));
230        } else if (setting == JPEG_THUMBNAIL_SIZE) {
231            if (mExifThumbnailSize == null) {
232                // It doesn't matter if this is true or false since setting this
233                // to null in the request settings will use the default anyway.
234                return false;
235            }
236            android.util.Size defaultThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE);
237            return (mExifThumbnailSize.width() == 0 && mExifThumbnailSize.height() == 0) ||
238                    (defaultThumbnailSize != null &&
239                            mExifThumbnailSize.width() == defaultThumbnailSize.getWidth() &&
240                            mExifThumbnailSize.height() == defaultThumbnailSize.getHeight());
241        }
242        Log.w(TAG, "Settings implementation checked default of unhandled option key");
243        // Since this class isn't equipped to handle it, claim it matches the default to prevent
244        // updateRequestSettingOrForceToDefault from going with the user-provided preference
245        return true;
246    }
247
248    private <T> void updateRequestSettingOrForceToDefault(Key<T> setting, T possibleChoice) {
249        mRequestSettings.set(setting, matchesTemplateDefault(setting) ? null : possibleChoice);
250    }
251
252    public Camera2RequestSettingsSet getRequestSettings() {
253        updateRequestSettingOrForceToDefault(CONTROL_AE_REGIONS,
254                legacyAreasToMeteringRectangles(mMeteringAreas));
255        updateRequestSettingOrForceToDefault(CONTROL_AF_REGIONS,
256                legacyAreasToMeteringRectangles(mFocusAreas));
257        updateRequestSettingOrForceToDefault(CONTROL_AE_TARGET_FPS_RANGE,
258                new Range(mPreviewFpsRangeMin, mPreviewFpsRangeMax));
259        // TODO: mCurrentPreviewFormat
260        updateRequestSettingOrForceToDefault(JPEG_QUALITY, mJpegCompressQuality);
261        // TODO: mCurrentPhotoFormat
262        mRequestSettings.set(SCALER_CROP_REGION, mCropRectangle);
263        // TODO: mCurrentZoomIndex
264        updateRequestSettingOrForceToDefault(CONTROL_AE_EXPOSURE_COMPENSATION,
265                mExposureCompensationIndex);
266        updateRequestFlashMode();
267        updateRequestFocusMode();
268        updateRequestSceneMode();
269        updateRequestWhiteBalance();
270        updateRequestSettingOrForceToDefault(CONTROL_VIDEO_STABILIZATION_MODE,
271                mVideoStabilizationEnabled ?
272                        CONTROL_VIDEO_STABILIZATION_MODE_ON : CONTROL_VIDEO_STABILIZATION_MODE_OFF);
273        // OIS shouldn't be on if software video stabilization is.
274        mRequestSettings.set(LENS_OPTICAL_STABILIZATION_MODE,
275                mVideoStabilizationEnabled ? LENS_OPTICAL_STABILIZATION_MODE_OFF :
276                        null);
277        updateRequestSettingOrForceToDefault(CONTROL_AE_LOCK, mAutoExposureLocked);
278        updateRequestSettingOrForceToDefault(CONTROL_AWB_LOCK, mAutoWhiteBalanceLocked);
279        // TODO: mRecordingHintEnabled
280        updateRequestGpsData();
281        if (mExifThumbnailSize != null) {
282            updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE,
283                    new android.util.Size(
284                            mExifThumbnailSize.width(), mExifThumbnailSize.height()));
285        } else {
286            updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE, null);
287        }
288
289        return mRequestSettings;
290    }
291
292    private MeteringRectangle[] legacyAreasToMeteringRectangles(
293            List<android.hardware.Camera.Area> reference) {
294        MeteringRectangle[] transformed = null;
295        if (reference.size() > 0) {
296            transformed = new MeteringRectangle[reference.size()];
297            for (int index = 0; index < reference.size(); ++index) {
298                android.hardware.Camera.Area source = reference.get(index);
299                Rect rectangle = source.rect;
300
301                // Old API coordinates were [-1000,1000]; new ones are [0,ACTIVE_ARRAY_SIZE).
302                // We're also going from preview image--relative to sensor active array--relative.
303                double oldLeft = (rectangle.left + 1000) / 2000.0;
304                double oldTop = (rectangle.top + 1000) / 2000.0;
305                double oldRight = (rectangle.right + 1000) / 2000.0;
306                double oldBottom = (rectangle.bottom + 1000) / 2000.0;
307                int left = mCropRectangle.left + toIntConstrained(
308                        mCropRectangle.width() * oldLeft, 0, mCropRectangle.width() - 1);
309                int top = mCropRectangle.top + toIntConstrained(
310                        mCropRectangle.height() * oldTop, 0, mCropRectangle.height() - 1);
311                int right = mCropRectangle.left + toIntConstrained(
312                        mCropRectangle.width() * oldRight, 0, mCropRectangle.width() - 1);
313                int bottom = mCropRectangle.top + toIntConstrained(
314                        mCropRectangle.height() * oldBottom, 0, mCropRectangle.height() - 1);
315                transformed[index] = new MeteringRectangle(left, top, right - left, bottom - top,
316                        source.weight);
317            }
318        }
319        return transformed;
320    }
321
322    private int toIntConstrained(double original, int min, int max) {
323        original = Math.max(original, min);
324        original = Math.min(original, max);
325        return (int) original;
326    }
327
328    private void updateRequestFlashMode() {
329        Integer aeMode = null;
330        Integer flashMode = null;
331        if (mCurrentFlashMode != null) {
332            switch (mCurrentFlashMode) {
333                case AUTO: {
334                    aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH;
335                    break;
336                }
337                case OFF: {
338                    aeMode = CONTROL_AE_MODE_ON;
339                    flashMode = FLASH_MODE_OFF;
340                    break;
341                }
342                case ON: {
343                    aeMode = CONTROL_AE_MODE_ON_ALWAYS_FLASH;
344                    flashMode = FLASH_MODE_SINGLE;
345                    break;
346                }
347                case TORCH: {
348                    flashMode = FLASH_MODE_TORCH;
349                    break;
350                }
351                case RED_EYE: {
352                    aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE;
353                    break;
354                }
355                default: {
356                    Log.w(TAG, "Unable to convert to API 2 flash mode: " + mCurrentFlashMode);
357                    break;
358                }
359            }
360        }
361        mRequestSettings.set(CONTROL_AE_MODE, aeMode);
362        mRequestSettings.set(FLASH_MODE, flashMode);
363    }
364
365    private void updateRequestFocusMode() {
366        Integer mode = null;
367        if (mCurrentFocusMode != null) {
368            switch (mCurrentFocusMode) {
369                case AUTO: {
370                    mode = CONTROL_AF_MODE_AUTO;
371                    break;
372                }
373                case CONTINUOUS_PICTURE: {
374                    mode = CONTROL_AF_MODE_CONTINUOUS_PICTURE;
375                    break;
376                }
377                case CONTINUOUS_VIDEO: {
378                    mode = CONTROL_AF_MODE_CONTINUOUS_VIDEO;
379                    break;
380                }
381                case EXTENDED_DOF: {
382                    mode = CONTROL_AF_MODE_EDOF;
383                    break;
384                }
385                case FIXED: {
386                    mode = CONTROL_AF_MODE_OFF;
387                    break;
388                }
389                // TODO: We cannot support INFINITY
390                case MACRO: {
391                    mode = CONTROL_AF_MODE_MACRO;
392                    break;
393                }
394                default: {
395                    Log.w(TAG, "Unable to convert to API 2 focus mode: " + mCurrentFocusMode);
396                    break;
397                }
398            }
399        }
400        mRequestSettings.set(CONTROL_AF_MODE, mode);
401    }
402
403    private void updateRequestSceneMode() {
404        Integer mode = null;
405        if (mCurrentSceneMode != null) {
406            switch (mCurrentSceneMode) {
407                case AUTO: {
408                    mode = CONTROL_SCENE_MODE_DISABLED;
409                    break;
410                }
411                case ACTION: {
412                    mode = CONTROL_SCENE_MODE_ACTION;
413                    break;
414                }
415                case BARCODE: {
416                    mode = CONTROL_SCENE_MODE_BARCODE;
417                    break;
418                }
419                case BEACH: {
420                    mode = CONTROL_SCENE_MODE_BEACH;
421                    break;
422                }
423                case CANDLELIGHT: {
424                    mode = CONTROL_SCENE_MODE_CANDLELIGHT;
425                    break;
426                }
427                case FIREWORKS: {
428                    mode = CONTROL_SCENE_MODE_FIREWORKS;
429                    break;
430                }
431                case HDR: {
432                    mode = CONTROL_SCENE_MODE_HDR;
433                    break;
434                }
435                case LANDSCAPE: {
436                    mode = CONTROL_SCENE_MODE_LANDSCAPE;
437                    break;
438                }
439                case NIGHT: {
440                    mode = CONTROL_SCENE_MODE_NIGHT;
441                    break;
442                }
443                // TODO: We cannot support NIGHT_PORTRAIT
444                case PARTY: {
445                    mode = CONTROL_SCENE_MODE_PARTY;
446                    break;
447                }
448                case PORTRAIT: {
449                    mode = CONTROL_SCENE_MODE_PORTRAIT;
450                    break;
451                }
452                case SNOW: {
453                    mode = CONTROL_SCENE_MODE_SNOW;
454                    break;
455                }
456                case SPORTS: {
457                    mode = CONTROL_SCENE_MODE_SPORTS;
458                    break;
459                }
460                case STEADYPHOTO: {
461                    mode = CONTROL_SCENE_MODE_STEADYPHOTO;
462                    break;
463                }
464                case SUNSET: {
465                    mode = CONTROL_SCENE_MODE_SUNSET;
466                    break;
467                }
468                case THEATRE: {
469                    mode = CONTROL_SCENE_MODE_THEATRE;
470                    break;
471                }
472                default: {
473                    Log.w(TAG, "Unable to convert to API 2 scene mode: " + mCurrentSceneMode);
474                    break;
475                }
476            }
477        }
478        mRequestSettings.set(CONTROL_SCENE_MODE, mode);
479    }
480
481    private void updateRequestWhiteBalance() {
482        Integer mode = null;
483        if (mWhiteBalance != null) {
484            switch (mWhiteBalance) {
485                case AUTO: {
486                    mode = CONTROL_AWB_MODE_AUTO;
487                    break;
488                }
489                case CLOUDY_DAYLIGHT: {
490                    mode = CONTROL_AWB_MODE_CLOUDY_DAYLIGHT;
491                    break;
492                }
493                case DAYLIGHT: {
494                    mode = CONTROL_AWB_MODE_DAYLIGHT;
495                    break;
496                }
497                case FLUORESCENT: {
498                    mode = CONTROL_AWB_MODE_FLUORESCENT;
499                    break;
500                }
501                case INCANDESCENT: {
502                    mode = CONTROL_AWB_MODE_INCANDESCENT;
503                    break;
504                }
505                case SHADE: {
506                    mode = CONTROL_AWB_MODE_SHADE;
507                    break;
508                }
509                case TWILIGHT: {
510                    mode = CONTROL_AWB_MODE_TWILIGHT;
511                    break;
512                }
513                case WARM_FLUORESCENT: {
514                    mode = CONTROL_AWB_MODE_WARM_FLUORESCENT;
515                    break;
516                }
517                default: {
518                    Log.w(TAG, "Unable to convert to API 2 white balance: " + mWhiteBalance);
519                    break;
520                }
521            }
522        }
523        mRequestSettings.set(CONTROL_AWB_MODE, mode);
524    }
525
526    private void updateRequestGpsData() {
527        if (mGpsData == null || mGpsData.processingMethod == null) {
528            // It's a hack since we always use GPS time stamp but does
529            // not use other fields sometimes. Setting processing
530            // method to null means the other fields should not be used.
531            mRequestSettings.set(JPEG_GPS_LOCATION, null);
532        } else {
533            Location location = new Location(mGpsData.processingMethod);
534            location.setTime(mGpsData.timeStamp);
535            location.setAltitude(mGpsData.altitude);
536            location.setLatitude(mGpsData.latitude);
537            location.setLongitude(mGpsData.longitude);
538            mRequestSettings.set(JPEG_GPS_LOCATION, location);
539        }
540    }
541
542    /**
543     * Calculate the effective crop rectangle for this preview viewport;
544     * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions
545     * without skewing.
546     *
547     * <p>Assumes the zoom level of the provided desired crop rectangle.</p>
548     *
549     * @param requestedCrop Desired crop rectangle, in active array space.
550     * @param previewSize Size of the preview buffer render target, in pixels (not in sensor space).
551     * @return A rectangle that serves as the preview stream's effective crop region (unzoomed), in
552     *          sensor space.
553     *
554     * @throws NullPointerException
555     *          If any of the args were {@code null}.
556     */
557    private static Rect effectiveCropRectFromRequested(Rect requestedCrop, Size previewSize) {
558        float aspectRatioArray = requestedCrop.width() * 1.0f / requestedCrop.height();
559        float aspectRatioPreview = previewSize.width() * 1.0f / previewSize.height();
560
561        float cropHeight, cropWidth;
562        if (aspectRatioPreview < aspectRatioArray) {
563            // The new width must be smaller than the height, so scale the width by AR
564            cropHeight = requestedCrop.height();
565            cropWidth = cropHeight * aspectRatioPreview;
566        } else {
567            // The new height must be smaller (or equal) than the width, so scale the height by AR
568            cropWidth = requestedCrop.width();
569            cropHeight = cropWidth / aspectRatioPreview;
570        }
571
572        Matrix translateMatrix = new Matrix();
573        RectF cropRect = new RectF(/*left*/0, /*top*/0, cropWidth, cropHeight);
574
575        // Now center the crop rectangle so its center is in the center of the active array
576        translateMatrix.setTranslate(requestedCrop.exactCenterX(), requestedCrop.exactCenterY());
577        translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY());
578
579        translateMatrix.mapRect(/*inout*/cropRect);
580
581        // Round the rect corners towards the nearest integer values
582        Rect result = new Rect();
583        cropRect.roundOut(result);
584        return result;
585    }
586}
587