You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
CameraView/camerakit/src/main/api16/com/flurgle/camerakit/Camera1.java

786 lines
28 KiB

package com.flurgle.camerakit;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_CONTINUOUS;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_OFF;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_TAP;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_TAP_WITH_MARKER;
import static com.flurgle.camerakit.CameraKit.Constants.SESSION_TYPE_PICTURE;
import static com.flurgle.camerakit.CameraKit.Constants.SESSION_TYPE_VIDEO;
@SuppressWarnings("deprecation")
class Camera1 extends CameraImpl {
private static final String TAG = Camera1.class.getSimpleName();
private static final int FOCUS_AREA_SIZE_DEFAULT = 200;
private static final int FOCUS_METERING_AREA_WEIGHT_DEFAULT = 1000;
private static final int DELAY_MILLIS_BEFORE_RESETTING_FOCUS = 3000;
private int mCameraId;
private Camera mCamera;
private ExtraProperties mExtraProperties;
private Size mPreviewSize;
private Size mCaptureSize;
private MediaRecorder mMediaRecorder;
private File mVideoFile;
private Camera.AutoFocusCallback mAutofocusCallback;
private int mDisplayOffset;
private int mDeviceOrientation;
private int mSensorOffset;
@Facing private int mFacing;
@Flash private int mFlash;
@Focus private int mFocus;
@ZoomMode private int mZoom;
@VideoQuality private int mVideoQuality;
@WhiteBalance private int mWhiteBalance;
@SessionType private int mSessionType;
private double mLatitude;
private double mLongitude;
private boolean mFocusOnTap;
private Handler mFocusHandler = new Handler();
private ConstantMapper.MapperImpl mMapper = new ConstantMapper.Mapper1();
private boolean mIsSetup = false;
private boolean mIsCapturingImage = false;
private boolean mIsCapturingVideo = false;
private final Object mLock = new Object();
Camera1(CameraView.CameraCallbacks callback, final PreviewImpl preview) {
super(callback, preview);
}
/**
* Preview surface is now available. If camera is open, set up.
*/
@Override
public void onSurfaceAvailable() {
Log.e(TAG, "onSurfaceAvailable, size is "+mPreview.getSurfaceSize());
if (shouldSetup()) setup();
}
/**
* Preview surface did change its size. Compute a new preview size.
* This requires stopping and restarting the preview.
*/
@Override
public void onSurfaceChanged() {
Log.e(TAG, "onSurfaceChanged, size is "+mPreview.getSurfaceSize());
if (mIsSetup) {
// Compute a new camera preview size.
Size newSize = computePreviewSize();
if (!newSize.equals(mPreviewSize)) {
mPreviewSize = newSize;
mCameraListener.onCameraPreviewSizeChanged();
synchronized (mLock) {
mCamera.stopPreview();
Camera.Parameters params = mCamera.getParameters();
params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mCamera.setParameters(params);
}
boolean invertPreviewSizes = shouldFlipSizes();
mPreview.setDesiredSize(
invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(),
invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight()
);
mCamera.startPreview();
}
}
}
private boolean shouldSetup() {
return isCameraOpened() && mPreview.isReady() && !mIsSetup;
}
// The act of binding an "open" camera to a "ready" preview.
// These can happen at different times but we want to end up here.
private void setup() {
try {
if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay(mPreview.getSurfaceHolder());
} else {
mCamera.setPreviewTexture(mPreview.getSurfaceTexture());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
boolean invertPreviewSizes = shouldFlipSizes(); // mDisplayOffset % 180 != 0;
mCaptureSize = computeCaptureSize();
mPreviewSize = computePreviewSize();
mCameraListener.onCameraPreviewSizeChanged();
mPreview.setDesiredSize(
invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(),
invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight()
);
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- not allowed during preview
mCamera.setParameters(params);
}
mCamera.startPreview();
mIsSetup = true;
}
@Override
void start() {
if (isCameraOpened()) stop();
if (collectCameraId()) {
mCamera = Camera.open(mCameraId);
mCameraListener.dispatchOnCameraOpened();
// Set parameters that might have been set before the camera was opened.
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
mergeFocus(params, mFocus);
mergeFlash(params, mFlash);
mergeLocation(params, mLatitude, mLongitude);
mergeWhiteBalance(params, mWhiteBalance);
params.setRecordingHint(mSessionType == SESSION_TYPE_VIDEO);
mCamera.setParameters(params);
}
// Try starting preview.
mCamera.setDisplayOrientation(computeSensorToDisplayOffset()); // <- not allowed during preview
if (shouldSetup()) setup();
collectExtraProperties();
}
}
@Override
void stop() {
mFocusHandler.removeCallbacksAndMessages(null);
if (isCameraOpened()) {
if (mIsCapturingVideo) endVideo();
mCamera.stopPreview();
mCamera.release();
mCameraListener.dispatchOnCameraClosed();
}
mCamera = null;
mPreviewSize = null;
mCaptureSize = null;
mIsSetup = false;
}
private boolean collectCameraId() {
int internalFacing = mMapper.mapFacing(mFacing);
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == internalFacing) {
mSensorOffset = cameraInfo.orientation;
mCameraId = i;
return true;
}
}
return false;
}
@Override
void onDisplayOffset(int displayOrientation) {
// I doubt this will ever change.
this.mDisplayOffset = displayOrientation;
}
@Override
void onDeviceOrientation(int deviceOrientation) {
this.mDeviceOrientation = deviceOrientation;
}
@Override
void setSessionType(@SessionType int sessionType) {
if (sessionType != mSessionType) {
mSessionType = sessionType;
if (isCameraOpened()) {
start();
}
}
}
@Override
void setLocation(double latitude, double longitude) {
double oldLat = mLatitude;
double oldLong = mLongitude;
mLatitude = latitude;
mLongitude = longitude;
if (isCameraOpened()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeLocation(params, oldLat, oldLong)) mCamera.setParameters(params);
}
}
}
private boolean mergeLocation(Camera.Parameters params, double oldLatitude, double oldLongitude) {
if (mLatitude != 0 && mLongitude != 0) {
params.setGpsLatitude(mLatitude);
params.setGpsLongitude(mLongitude);
params.setGpsTimestamp(System.currentTimeMillis());
params.setGpsProcessingMethod("Unknown");
}
return true;
}
@Override
void setFacing(@Facing int facing) {
if (facing != mFacing) {
mFacing = facing;
if (collectCameraId() && isCameraOpened()) {
start();
}
}
}
@Override
void setWhiteBalance(@WhiteBalance int whiteBalance) {
int old = mWhiteBalance;
mWhiteBalance = whiteBalance;
if (isCameraOpened()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeWhiteBalance(params, old)) mCamera.setParameters(params);
}
}
}
private boolean mergeWhiteBalance(Camera.Parameters params, @WhiteBalance int oldWhiteBalance) {
List<String> supported = params.getSupportedWhiteBalance();
String internal = mMapper.mapWhiteBalance(mWhiteBalance);
if (supported != null && supported.contains(internal)) {
params.setWhiteBalance(internal);
return true;
}
mWhiteBalance = oldWhiteBalance;
return false;
}
@Override
void setFlash(@Flash int flash) {
int old = mFlash;
mFlash = flash;
if (isCameraOpened()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeFlash(params, old)) mCamera.setParameters(params);
}
}
}
private boolean mergeFlash(Camera.Parameters params, @Flash int oldFlash) {
List<String> flashes = params.getSupportedFlashModes();
String internalFlash = mMapper.mapFlash(mFlash);
if (flashes != null && flashes.contains(internalFlash)) {
params.setFlashMode(internalFlash);
return true;
}
mFlash = oldFlash;
return false;
}
@Override
void setFocus(@Focus int focus) {
int old = mFocus;
mFocus = focus;
if (isCameraOpened()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeFocus(params, old)) mCamera.setParameters(params);
}
}
mFocusOnTap = mFocus == FOCUS_TAP || mFocus == FOCUS_TAP_WITH_MARKER;
}
private boolean mergeFocus(Camera.Parameters params, @Focus int oldFocus) {
final List<String> modes = params.getSupportedFocusModes();
switch (mFocus) {
case FOCUS_CONTINUOUS:
case FOCUS_TAP:
case FOCUS_TAP_WITH_MARKER:
if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
return true;
}
mFocus = oldFocus;
return false;
case FOCUS_OFF:
if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
return true;
} else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
return true;
} else if (modes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
return true;
}
}
return false;
}
@Override
void setZoomMode(@ZoomMode int zoom) {
this.mZoom = zoom;
}
@Override
void setVideoQuality(int videoQuality) {
this.mVideoQuality = videoQuality;
// TODO: restore preview size if needed.
}
@Override
void captureImage() {
if (mIsCapturingImage) return;
if (!isCameraOpened()) return;
if (mSessionType == SESSION_TYPE_VIDEO && mIsCapturingVideo) {
Camera.Parameters parameters = mCamera.getParameters();
if (!parameters.isVideoSnapshotSupported()) return;
}
// Set boolean to wait for image callback
mIsCapturingImage = true;
final int exifRotation = computeExifRotation();
final boolean exifFlip = computeExifFlip();
final int sensorToDisplay = computeSensorToDisplayOffset();
synchronized (mLock) {
Camera.Parameters parameters = mCamera.getParameters();
Log.e(TAG, "Setting exif rotation to "+exifRotation);
parameters.setRotation(exifRotation);
mCamera.setParameters(parameters);
}
// Is the final picture (decoded respecting EXIF) consistent with CameraView orientation?
// We must consider exifOrientation to bring back the picture in the sensor world.
// Then use sensorToDisplay to move to the display world, where CameraView lives.
final boolean consistentWithView = (exifRotation + sensorToDisplay + 180) % 180 == 0;
mCamera.takePicture(null, null, null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
mIsCapturingImage = false;
camera.startPreview(); // This is needed, read somewhere in the docs.
mCameraListener.processImage(data, consistentWithView, exifFlip);
}
});
}
@Override
void captureSnapshot() {
if (!isCameraOpened()) return;
if (mIsCapturingImage) return;
// This won't work while capturing a video.
// Switch to captureImage.
if (mIsCapturingVideo) {
captureImage();
return;
}
mIsCapturingImage = true;
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
// Got to rotate the preview frame, since byte[] data here does not include
// EXIF tags automatically set by camera. So either we add EXIF, or we rotate.
// Adding EXIF to a byte array, unfortunately, is hard.
Camera.Parameters params = mCamera.getParameters();
final int sensorToDevice = computeExifRotation();
final int sensorToDisplay = computeSensorToDisplayOffset();
final boolean exifFlip = computeExifFlip();
final boolean flip = sensorToDevice % 180 != 0;
final int preWidth = mPreviewSize.getWidth();
final int preHeight = mPreviewSize.getHeight();
final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight;
final int format = params.getPreviewFormat();
new Thread(new Runnable() {
@Override
public void run() {
final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0;
byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice);
YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null);
mCameraListener.processSnapshot(yuv, consistentWithView, exifFlip);
mIsCapturingImage = false;
}
}).start();
}
});
}
@Override
Size getCaptureSize() {
return mCaptureSize;
}
@Override
Size getPreviewSize() {
return mPreviewSize;
}
@Override
boolean shouldFlipSizes() {
return mSensorOffset % 180 != 0;
}
@Override
boolean isCameraOpened() {
return mCamera != null;
}
@Nullable
@Override
ExtraProperties getExtraProperties() {
return mExtraProperties;
}
// Internal:
/**
* Returns how much should the sensor image be rotated before being shown.
* It is meant to be fed to Camera.setDisplayOrientation().
*/
private int computeSensorToDisplayOffset() {
if (mFacing == CameraKit.Constants.FACING_FRONT) {
// or: (360 - ((mSensorOffset + mDisplayOffset) % 360)) % 360;
return ((mSensorOffset - mDisplayOffset) + 360 + 180) % 360;
} else {
return (mSensorOffset - mDisplayOffset + 360) % 360;
}
}
/**
* Returns the orientation to be set as a exif tag. This is already managed by
* the camera APIs as long as you call {@link Camera.Parameters#setRotation(int)}.
* This ignores flipping for facing camera.
*/
private int computeExifRotation() {
if (mFacing == CameraKit.Constants.FACING_FRONT) {
return (mSensorOffset - mDeviceOrientation + 360) % 360;
} else {
return (mSensorOffset + mDeviceOrientation) % 360;
}
}
/**
* Whether the exif tag should include a 'flip' operation.
*/
private boolean computeExifFlip() {
return mFacing == CameraKit.Constants.FACING_FRONT;
}
/**
* This is called either on cameraView.start(), or when the underlying surface changes.
* It is possible that in the first call the preview surface has not already computed its
* dimensions.
* But when it does, the {@link PreviewImpl.SurfaceCallback} should be called,
* and this should be refreshed.
*/
private Size computeCaptureSize() {
Camera.Parameters params = mCamera.getParameters();
if (mSessionType == SESSION_TYPE_PICTURE) {
// Choose the max size.
List<Size> captureSizes = sizesFromList(params.getSupportedPictureSizes());
return Collections.max(captureSizes);
} else {
// Choose according to developer choice in setVideoQuality.
// The Camcorder internally checks for cameraParameters.getSupportedVideoSizes() etc.
// So its output is our output.
CamcorderProfile camcorderProfile = getCamcorderProfile(mVideoQuality);
return new Size(camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight);
}
}
private Size computePreviewSize() {
Camera.Parameters params = mCamera.getParameters();
List<Size> previewSizes = sizesFromList(params.getSupportedPreviewSizes());
return computePreviewSize(previewSizes, mCaptureSize, mPreview.getSurfaceSize());
}
private void collectExtraProperties() {
Camera.Parameters params = mCamera.getParameters();
mExtraProperties = new ExtraProperties(params.getVerticalViewAngle(),
params.getHorizontalViewAngle());
}
// -----------------
// Video recording stuff.
@Override
void startVideo(@NonNull File videoFile) {
mVideoFile = videoFile;
if (mIsCapturingVideo) return;
if (!isCameraOpened()) return;
Camera.Parameters params = mCamera.getParameters();
params.setVideoStabilization(false);
if (mSessionType == SESSION_TYPE_VIDEO) {
mIsCapturingVideo = true;
initMediaRecorder();
try {
mMediaRecorder.prepare();
} catch (Exception e) {
e.printStackTrace();
mVideoFile = null;
endVideo();
return;
}
mMediaRecorder.start();
} else {
throw new IllegalStateException("Can't record video while session type is picture");
}
}
@Override
void endVideo() {
if (mIsCapturingVideo) {
mIsCapturingVideo = false;
mMediaRecorder.stop();
mMediaRecorder.release();
mMediaRecorder = null;
if (mVideoFile != null) {
mCameraListener.dispatchOnVideoTaken(mVideoFile);
mVideoFile = null;
}
}
}
private void initMediaRecorder() {
mMediaRecorder = new MediaRecorder();
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
mMediaRecorder.setProfile(getCamcorderProfile(mVideoQuality));
mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath());
mMediaRecorder.setOrientationHint(computeSensorToDisplayOffset()); // TODO is this correct? Should we use exif orientation? Maybe not.
// Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface());
}
@NonNull
private CamcorderProfile getCamcorderProfile(@VideoQuality int videoQuality) {
switch (videoQuality) {
case CameraKit.Constants.VIDEO_QUALITY_HIGHEST:
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_HIGH);
case CameraKit.Constants.VIDEO_QUALITY_2160P:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_2160P);
}
// Don't break.
case CameraKit.Constants.VIDEO_QUALITY_1080P:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_1080P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_1080P);
}
// Don't break.
case CameraKit.Constants.VIDEO_QUALITY_720P:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_720P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_720P);
}
// Don't break.
case CameraKit.Constants.VIDEO_QUALITY_480P:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_480P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_480P);
}
// Don't break.
case CameraKit.Constants.VIDEO_QUALITY_QVGA:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_QVGA)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_QVGA);
}
// Don't break.
case CameraKit.Constants.VIDEO_QUALITY_LOWEST:
default:
// Fallback to lowest.
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_LOW);
}
}
// -----------------
// Tap to focus stuff.
@Override
void onTouchEvent(MotionEvent event) {
if (!mFocusOnTap) return;
if (mCamera == null) return;
if (event.getAction() != MotionEvent.ACTION_UP) return;
Rect rect = calculateFocusArea(event.getX(), event.getY());
List<Camera.Area> meteringAreas = new ArrayList<>();
meteringAreas.add(new Camera.Area(rect, getFocusMeteringAreaWeight()));
synchronized (mLock) {
Camera.Parameters parameters = mCamera.getParameters();
boolean autofocusSupported = parameters.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_AUTO);
if (autofocusSupported) {
if (parameters.getMaxNumFocusAreas() > 0) parameters.setFocusAreas(meteringAreas);
if (parameters.getMaxNumMeteringAreas() > 0)
parameters.setMeteringAreas(meteringAreas);
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
mCamera.setParameters(parameters);
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
resetFocus(success, camera);
}
});
}
}
}
private int getFocusAreaSize() {
return FOCUS_AREA_SIZE_DEFAULT;
}
private int getFocusMeteringAreaWeight() {
return FOCUS_METERING_AREA_WEIGHT_DEFAULT;
}
private void resetFocus(final boolean success, final Camera camera) {
mFocusHandler.removeCallbacksAndMessages(null);
mFocusHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (camera != null) {
camera.cancelAutoFocus();
synchronized (mLock) {
Camera.Parameters params = camera.getParameters();
if (!params.getFocusMode().equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
params.setFocusAreas(null);
params.setMeteringAreas(null);
mCamera.setParameters(params);
}
}
if (mAutofocusCallback != null) mAutofocusCallback.onAutoFocus(success, camera);
}
}
}, DELAY_MILLIS_BEFORE_RESETTING_FOCUS);
}
private Rect calculateFocusArea(float x, float y) {
int buffer = getFocusAreaSize() / 2;
int centerX = calculateCenter(x, mPreview.getView().getWidth(), buffer);
int centerY = calculateCenter(y, mPreview.getView().getHeight(), buffer);
return new Rect(
centerX - buffer,
centerY - buffer,
centerX + buffer,
centerY + buffer
);
}
private static int calculateCenter(float coord, int dimen, int buffer) {
int normalized = (int) ((coord / dimen) * 2000 - 1000);
if (Math.abs(normalized) + buffer > 1000) {
if (normalized > 0) {
return 1000 - buffer;
} else {
return -1000 + buffer;
}
} else {
return normalized;
}
}
// -----------------
// Size static stuff.
/**
* Returns a list of {@link Size} out of Camera.Sizes.
*/
@Nullable
private static List<Size> sizesFromList(List<Camera.Size> sizes) {
if (sizes == null) return null;
List<Size> result = new ArrayList<>(sizes.size());
for (Camera.Size size : sizes) {
result.add(new Size(size.width, size.height));
}
return result;
}
/**
* Policy here is to return a size that is big enough to fit the surface size,
* and possibly consistent with the capture size.
* @param sizes list of possible sizes
* @param captureSize size representing desired aspect ratio
* @param surfaceSize size representing the current surface size. We'd like the returned value to be bigger.
* @return chosen size
*/
private static Size computePreviewSize(List<Size> sizes, Size captureSize, Size surfaceSize) {
if (sizes == null) return null;
List<Size> consistent = new ArrayList<>(5);
List<Size> bigEnoughAndConsistent = new ArrayList<>(5);
final AspectRatio targetRatio = AspectRatio.of(captureSize.getWidth(), captureSize.getHeight());
final Size targetSize = surfaceSize;
for (Size size : sizes) {
AspectRatio ratio = AspectRatio.of(size.getWidth(), size.getHeight());
if (ratio.equals(targetRatio)) {
consistent.add(size);
if (size.getHeight() >= targetSize.getHeight() && size.getWidth() >= targetSize.getWidth()) {
bigEnoughAndConsistent.add(size);
}
}
}
if (bigEnoughAndConsistent.size() > 0) return Collections.min(bigEnoughAndConsistent);
if (consistent.size() > 0) return Collections.max(consistent);
return Collections.max(sizes);
}
}