/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dialer.callcomposer.camera;

import android.content.Context;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import com.android.dialer.callcomposer.camera.camerafocus.FocusOverlayManager;
import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Class which manages interactions with the camera, but does not do any UI. This class is designed
 * to be a singleton to ensure there is one component managing the camera and releasing the native
 * resources. In order to acquire a camera, a caller must:
 *
 * <ul>
 *   <li>Call selectCamera to select front or back camera
 *   <li>Call setSurface to control where the preview is shown
 *   <li>Call openCamera to request the camera start preview
 * </ul>
 *
 * Callers should call onPause and onResume to ensure that the camera is release while the activity
 * is not active. This class is not thread safe. It should only be called from one thread (the UI
 * thread or test thread)
 */
public class CameraManager implements FocusOverlayManager.Listener {
  /** Callbacks for the camera manager listener */
  public interface CameraManagerListener {
    void onCameraError(int errorCode, Exception e);

    void onCameraChanged();
  }

  /** Callback when taking image or video */
  public interface MediaCallback {
    int MEDIA_CAMERA_CHANGED = 1;
    int MEDIA_NO_DATA = 2;

    void onMediaReady(Uri uriToMedia, String contentType, int width, int height);

    void onMediaFailed(Exception exception);

    void onMediaInfo(int what);
  }

  // Error codes
  private static final int ERROR_OPENING_CAMERA = 1;
  private static final int ERROR_SHOWING_PREVIEW = 2;
  private static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 3;
  private static final int ERROR_TAKING_PICTURE = 4;

  private static final int NO_CAMERA_SELECTED = -1;

  private static final Camera.ShutterCallback NOOP_SHUTTER_CALLBACK =
      new Camera.ShutterCallback() {
        @Override
        public void onShutter() {
          // Do nothing
        }
      };

  private static CameraManager instance;

  /** The CameraInfo for the currently selected camera */
  private final CameraInfo cameraInfo;

  /** The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet */
  private int cameraIndex;

  /** True if the device has front and back cameras */
  private final boolean hasFrontAndBackCamera;

  /** True if the camera should be open (may not yet be actually open) */
  private boolean openRequested;

  /** The preview view to show the preview on */
  private CameraPreview cameraPreview;

  /** The helper classs to handle orientation changes */
  private OrientationHandler orientationHandler;

  /** Tracks whether the preview has hardware acceleration */
  private boolean isHardwareAccelerationSupported;

  /**
   * The task for opening the camera, so it doesn't block the UI thread Using AsyncTask rather than
   * SafeAsyncTask because the tasks need to be serialized, but don't need to be on the UI thread
   * TODO(blemmon): If we have other AyncTasks (not SafeAsyncTasks) this may contend and we may need
   * to create a dedicated thread, or synchronize the threads in the thread pool
   */
  private AsyncTask<Integer, Void, Camera> openCameraTask;

  /**
   * The camera index that is queued to be opened, but not completed yet, or NO_CAMERA_SELECTED if
   * no open task is pending
   */
  private int pendingOpenCameraIndex = NO_CAMERA_SELECTED;

  /** The instance of the currently opened camera */
  private Camera camera;

  /** The rotation of the screen relative to the camera's natural orientation */
  private int rotation;

  /** The callback to notify when errors or other events occur */
  private CameraManagerListener listener;

  /** True if the camera is currently in the process of taking an image */
  private boolean takingPicture;

  /** Manages auto focus visual and behavior */
  private final FocusOverlayManager focusOverlayManager;

  private CameraManager() {
    this.cameraInfo = new CameraInfo();
    cameraIndex = NO_CAMERA_SELECTED;

    // Check to see if a front and back camera exist
    boolean hasFrontCamera = false;
    boolean hasBackCamera = false;
    final CameraInfo cameraInfo = new CameraInfo();
    final int cameraCount = Camera.getNumberOfCameras();
    try {
      for (int i = 0; i < cameraCount; i++) {
        Camera.getCameraInfo(i, cameraInfo);
        if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
          hasFrontCamera = true;
        } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
          hasBackCamera = true;
        }
        if (hasFrontCamera && hasBackCamera) {
          break;
        }
      }
    } catch (final RuntimeException e) {
      LogUtil.e("CameraManager.CameraManager", "Unable to load camera info", e);
    }
    hasFrontAndBackCamera = hasFrontCamera && hasBackCamera;
    focusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper());

    // Assume the best until we are proven otherwise
    isHardwareAccelerationSupported = true;
  }

  /** Gets the singleton instance */
  public static CameraManager get() {
    if (instance == null) {
      instance = new CameraManager();
    }
    return instance;
  }

  /**
   * Sets the surface to use to display the preview This must only be called AFTER the CameraPreview
   * has a texture ready
   *
   * @param preview The preview surface view
   */
  void setSurface(final CameraPreview preview) {
    if (preview == cameraPreview) {
      return;
    }

    if (preview != null) {
      Assert.checkArgument(preview.isValid());
      preview.setOnTouchListener(
          new View.OnTouchListener() {
            @Override
            public boolean onTouch(final View view, final MotionEvent motionEvent) {
              if ((motionEvent.getActionMasked() & MotionEvent.ACTION_UP)
                  == MotionEvent.ACTION_UP) {
                focusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight());
                focusOverlayManager.onSingleTapUp(
                    (int) motionEvent.getX() + view.getLeft(),
                    (int) motionEvent.getY() + view.getTop());
              }
              view.performClick();
              return true;
            }
          });
    }
    cameraPreview = preview;
    tryShowPreview();
  }

  public void setRenderOverlay(final RenderOverlay renderOverlay) {
    focusOverlayManager.setFocusRenderer(
        renderOverlay != null ? renderOverlay.getPieRenderer() : null);
  }

  /** Convenience function to swap between front and back facing cameras */
  public void swapCamera() {
    Assert.checkState(cameraIndex >= 0);
    selectCamera(
        cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT
            ? CameraInfo.CAMERA_FACING_BACK
            : CameraInfo.CAMERA_FACING_FRONT);
  }

  /**
   * Selects the first camera facing the desired direction, or the first camera if there is no
   * camera in the desired direction
   *
   * @param desiredFacing One of the CameraInfo.CAMERA_FACING_* constants
   * @return True if a camera was selected, or false if selecting a camera failed
   */
  public boolean selectCamera(final int desiredFacing) {
    try {
      // We already selected a camera facing that direction
      if (cameraIndex >= 0 && this.cameraInfo.facing == desiredFacing) {
        return true;
      }

      final int cameraCount = Camera.getNumberOfCameras();
      Assert.checkState(cameraCount > 0);

      cameraIndex = NO_CAMERA_SELECTED;
      setCamera(null);
      final CameraInfo cameraInfo = new CameraInfo();
      for (int i = 0; i < cameraCount; i++) {
        Camera.getCameraInfo(i, cameraInfo);
        if (cameraInfo.facing == desiredFacing) {
          cameraIndex = i;
          Camera.getCameraInfo(i, this.cameraInfo);
          break;
        }
      }

      // There's no camera in the desired facing direction, just select the first camera
      // regardless of direction
      if (cameraIndex < 0) {
        cameraIndex = 0;
        Camera.getCameraInfo(0, this.cameraInfo);
      }

      if (openRequested) {
        // The camera is open, so reopen with the newly selected camera
        openCamera();
      }
      return true;
    } catch (final RuntimeException e) {
      LogUtil.e("CameraManager.selectCamera", "RuntimeException in CameraManager.selectCamera", e);
      if (listener != null) {
        listener.onCameraError(ERROR_OPENING_CAMERA, e);
      }
      return false;
    }
  }

  public int getCameraIndex() {
    return cameraIndex;
  }

  public void selectCameraByIndex(final int cameraIndex) {
    if (this.cameraIndex == cameraIndex) {
      return;
    }

    try {
      this.cameraIndex = cameraIndex;
      Camera.getCameraInfo(this.cameraIndex, cameraInfo);
      if (openRequested) {
        openCamera();
      }
    } catch (final RuntimeException e) {
      LogUtil.e(
          "CameraManager.selectCameraByIndex",
          "RuntimeException in CameraManager.selectCameraByIndex",
          e);
      if (listener != null) {
        listener.onCameraError(ERROR_OPENING_CAMERA, e);
      }
    }
  }

  @Nullable
  @VisibleForTesting
  public CameraInfo getCameraInfo() {
    if (cameraIndex == NO_CAMERA_SELECTED) {
      return null;
    }
    return cameraInfo;
  }

  /** @return True if the device has both a front and back camera */
  public boolean hasFrontAndBackCamera() {
    return hasFrontAndBackCamera;
  }

  /** Opens the camera on a separate thread and initiates the preview if one is available */
  void openCamera() {
    if (this.cameraIndex == NO_CAMERA_SELECTED) {
      // Ensure a selected camera if none is currently selected. This may happen if the
      // camera chooser is not the default media chooser.
      selectCamera(CameraInfo.CAMERA_FACING_BACK);
    }
    openRequested = true;
    // We're already opening the camera or already have the camera handle, nothing more to do
    if (pendingOpenCameraIndex == this.cameraIndex || this.camera != null) {
      return;
    }

    // True if the task to open the camera has to be delayed until the current one completes
    boolean delayTask = false;

    // Cancel any previous open camera tasks
    if (openCameraTask != null) {
      pendingOpenCameraIndex = NO_CAMERA_SELECTED;
      delayTask = true;
    }

    pendingOpenCameraIndex = this.cameraIndex;
    openCameraTask =
        new AsyncTask<Integer, Void, Camera>() {
          private Exception exception;

          @Override
          protected Camera doInBackground(final Integer... params) {
            try {
              final int cameraIndex = params[0];
              LogUtil.v(
                  "CameraManager.doInBackground",
                  "Opening camera " + CameraManager.this.cameraIndex);
              return Camera.open(cameraIndex);
            } catch (final Exception e) {
              LogUtil.e("CameraManager.doInBackground", "Exception while opening camera", e);
              exception = e;
              return null;
            }
          }

          @Override
          protected void onPostExecute(final Camera camera) {
            // If we completed, but no longer want this camera, then release the camera
            if (openCameraTask != this || !openRequested) {
              releaseCamera(camera);
              cleanup();
              return;
            }

            cleanup();

            LogUtil.v(
                "CameraManager.onPostExecute",
                "Opened camera " + CameraManager.this.cameraIndex + " " + (camera != null));
            setCamera(camera);
            if (camera == null) {
              if (listener != null) {
                listener.onCameraError(ERROR_OPENING_CAMERA, exception);
              }
              LogUtil.e("CameraManager.onPostExecute", "Error opening camera");
            }
          }

          @Override
          protected void onCancelled() {
            super.onCancelled();
            cleanup();
          }

          private void cleanup() {
            pendingOpenCameraIndex = NO_CAMERA_SELECTED;
            if (openCameraTask != null && openCameraTask.getStatus() == Status.PENDING) {
              // If there's another task waiting on this one to complete, start it now
              openCameraTask.execute(CameraManager.this.cameraIndex);
            } else {
              openCameraTask = null;
            }
          }
        };
    LogUtil.v("CameraManager.openCamera", "Start opening camera " + this.cameraIndex);
    if (!delayTask) {
      openCameraTask.execute(this.cameraIndex);
    }
  }

  /** Closes the camera releasing the resources it uses */
  void closeCamera() {
    openRequested = false;
    setCamera(null);
  }

  /**
   * Sets the listener which will be notified of errors or other events in the camera
   *
   * @param listener The listener to notify
   */
  public void setListener(final CameraManagerListener listener) {
    Assert.isMainThread();
    this.listener = listener;
    if (!isHardwareAccelerationSupported && this.listener != null) {
      this.listener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null);
    }
  }

  public void takePicture(final float heightPercent, @NonNull final MediaCallback callback) {
    Assert.checkState(!takingPicture);
    Assert.isNotNull(callback);
    cameraPreview.setFocusable(false);
    focusOverlayManager.cancelAutoFocus();
    if (this.camera == null) {
      // The caller should have checked isCameraAvailable first, but just in case, protect
      // against a null camera by notifying the callback that taking the picture didn't work
      callback.onMediaFailed(null);
      return;
    }
    final Camera.PictureCallback jpegCallback =
        new Camera.PictureCallback() {
          @Override
          public void onPictureTaken(final byte[] bytes, final Camera camera) {
            takingPicture = false;
            if (CameraManager.this.camera != camera) {
              // This may happen if the camera was changed between front/back while the
              // picture is being taken.
              callback.onMediaInfo(MediaCallback.MEDIA_CAMERA_CHANGED);
              return;
            }

            if (bytes == null) {
              callback.onMediaInfo(MediaCallback.MEDIA_NO_DATA);
              return;
            }

            final Camera.Size size = camera.getParameters().getPictureSize();
            int width;
            int height;
            if (rotation == 90 || rotation == 270) {
              // Is rotated, so swapping dimensions is desired
              // noinspection SuspiciousNameCombination
              width = size.height;
              // noinspection SuspiciousNameCombination
              height = size.width;
            } else {
              width = size.width;
              height = size.height;
            }
            LogUtil.i(
                "CameraManager.onPictureTaken", "taken picture size: " + bytes.length + " bytes");
            DialerExecutorComponent.get(cameraPreview.getContext())
                .dialerExecutorFactory()
                .createNonUiTaskBuilder(
                    new ImagePersistWorker(
                        width, height, heightPercent, bytes, cameraPreview.getContext()))
                .onSuccess(
                    (result) -> {
                      callback.onMediaReady(
                          result.getUri(), "image/jpeg", result.getWidth(), result.getHeight());
                    })
                .onFailure(
                    (throwable) -> {
                      callback.onMediaFailed(new Exception("Persisting image failed", throwable));
                    })
                .build()
                .executeSerial(null);
          }
        };

    takingPicture = true;
    try {
      this.camera.takePicture(
          // A shutter callback is required to enable shutter sound
              NOOP_SHUTTER_CALLBACK, null /* raw */, null /* postView */, jpegCallback);
    } catch (final RuntimeException e) {
      LogUtil.e("CameraManager.takePicture", "RuntimeException in CameraManager.takePicture", e);
      takingPicture = false;
      if (listener != null) {
        listener.onCameraError(ERROR_TAKING_PICTURE, e);
      }
    }
  }

  /**
   * Asynchronously releases a camera
   *
   * @param camera The camera to release
   */
  private void releaseCamera(final Camera camera) {
    if (camera == null) {
      return;
    }

    focusOverlayManager.onCameraReleased();

    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(final Void... params) {
        LogUtil.v("CameraManager.doInBackground", "Releasing camera " + cameraIndex);
        camera.release();
        return null;
      }
    }.execute();
  }

  /**
   * Updates the orientation of the {@link Camera} w.r.t. the orientation of the device and the
   * orientation that the physical camera is mounted on the device.
   *
   * @param camera that needs to be reorientated
   * @param screenRotation rotation of the physical device
   * @param cameraOrientation {@link CameraInfo#orientation}
   * @param cameraIsFrontFacing {@link CameraInfo#CAMERA_FACING_FRONT}
   * @return rotation that images returned from {@link
   *     android.hardware.Camera.PictureCallback#onPictureTaken(byte[], Camera)} will be rotated.
   */
  @VisibleForTesting
  static int updateCameraRotation(
      @NonNull Camera camera,
      int screenRotation,
      int cameraOrientation,
      boolean cameraIsFrontFacing) {
    Assert.isNotNull(camera);
    Assert.checkArgument(cameraOrientation % 90 == 0);

    int rotation = screenRotationToDegress(screenRotation);
    boolean portrait = rotation == 0 || rotation == 180;

    if (!portrait && !cameraIsFrontFacing) {
      rotation += 180;
    }
    rotation += cameraOrientation;
    rotation %= 360;

    // Rotate the camera
    if (portrait && cameraIsFrontFacing) {
      camera.setDisplayOrientation((rotation + 180) % 360);
    } else {
      camera.setDisplayOrientation(rotation);
    }

    // Rotate the images returned when a picture is taken
    Camera.Parameters params = camera.getParameters();
    params.setRotation(rotation);
    camera.setParameters(params);
    return rotation;
  }

  private static int screenRotationToDegress(int screenRotation) {
    switch (screenRotation) {
      case Surface.ROTATION_0:
        return 0;
      case Surface.ROTATION_90:
        return 90;
      case Surface.ROTATION_180:
        return 180;
      case Surface.ROTATION_270:
        return 270;
      default:
        throw Assert.createIllegalStateFailException("Invalid surface rotation.");
    }
  }

  /** Sets the current camera, releasing any previously opened camera */
  private void setCamera(final Camera camera) {
    if (this.camera == camera) {
      return;
    }

    releaseCamera(this.camera);
    this.camera = camera;
    tryShowPreview();
    if (listener != null) {
      listener.onCameraChanged();
    }
  }

  /** Shows the preview if the camera is open and the preview is loaded */
  private void tryShowPreview() {
    if (cameraPreview == null || this.camera == null) {
      if (orientationHandler != null) {
        orientationHandler.disable();
        orientationHandler = null;
      }
      focusOverlayManager.onPreviewStopped();
      return;
    }
    try {
      this.camera.stopPreview();
      if (!takingPicture) {
        rotation =
            updateCameraRotation(
                this.camera,
                getScreenRotation(),
                cameraInfo.orientation,
                cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT);
      }

      final Camera.Parameters params = this.camera.getParameters();
      final Camera.Size pictureSize = chooseBestPictureSize();
      final Camera.Size previewSize = chooseBestPreviewSize(pictureSize);
      params.setPreviewSize(previewSize.width, previewSize.height);
      params.setPictureSize(pictureSize.width, pictureSize.height);
      logCameraSize("Setting preview size: ", previewSize);
      logCameraSize("Setting picture size: ", pictureSize);
      cameraPreview.setSize(previewSize, cameraInfo.orientation);
      for (final String focusMode : params.getSupportedFocusModes()) {
        if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
          // Use continuous focus if available
          params.setFocusMode(focusMode);
          break;
        }
      }

      this.camera.setParameters(params);
      cameraPreview.startPreview(this.camera);
      this.camera.startPreview();
      this.camera.setAutoFocusMoveCallback(
          new Camera.AutoFocusMoveCallback() {
            @Override
            public void onAutoFocusMoving(final boolean start, final Camera camera) {
              focusOverlayManager.onAutoFocusMoving(start);
            }
          });
      focusOverlayManager.setParameters(this.camera.getParameters());
      focusOverlayManager.setMirror(cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK);
      focusOverlayManager.onPreviewStarted();
      if (orientationHandler == null) {
        orientationHandler = new OrientationHandler(cameraPreview.getContext());
        orientationHandler.enable();
      }
    } catch (final IOException e) {
      LogUtil.e("CameraManager.tryShowPreview", "IOException in CameraManager.tryShowPreview", e);
      if (listener != null) {
        listener.onCameraError(ERROR_SHOWING_PREVIEW, e);
      }
    } catch (final RuntimeException e) {
      LogUtil.e(
          "CameraManager.tryShowPreview", "RuntimeException in CameraManager.tryShowPreview", e);
      if (listener != null) {
        listener.onCameraError(ERROR_SHOWING_PREVIEW, e);
      }
    }
  }

  private int getScreenRotation() {
    return cameraPreview
        .getContext()
        .getSystemService(WindowManager.class)
        .getDefaultDisplay()
        .getRotation();
  }

  public boolean isCameraAvailable() {
    return camera != null && !takingPicture && isHardwareAccelerationSupported;
  }

  /**
   * Choose the best picture size by trying to find a size close to the MmsConfig's max size, which
   * is closest to the screen aspect ratio. In case of RCS conversation returns default size.
   */
  private Camera.Size chooseBestPictureSize() {
    return camera.getParameters().getPictureSize();
  }

  /**
   * Chose the best preview size based on the picture size. Try to find a size with the same aspect
   * ratio and size as the picture if possible
   */
  private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) {
    final List<Camera.Size> sizes =
        new ArrayList<Camera.Size>(camera.getParameters().getSupportedPreviewSizes());
    final float aspectRatio = pictureSize.width / (float) pictureSize.height;
    final int capturePixels = pictureSize.width * pictureSize.height;

    // Sort the sizes so the best size is first
    Collections.sort(
        sizes,
        new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, aspectRatio, capturePixels));

    return sizes.get(0);
  }

  private class OrientationHandler extends OrientationEventListener {
    OrientationHandler(final Context context) {
      super(context);
    }

    @Override
    public void onOrientationChanged(final int orientation) {
      if (!takingPicture) {
        rotation =
            updateCameraRotation(
                camera,
                getScreenRotation(),
                cameraInfo.orientation,
                cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT);
      }
    }
  }

  private static class SizeComparator implements Comparator<Camera.Size> {
    private static final int PREFER_LEFT = -1;
    private static final int PREFER_RIGHT = 1;

    // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit
    private final int maxWidth;
    private final int maxHeight;

    // The desired aspect ratio
    private final float targetAspectRatio;

    // The desired size (width x height) to try to match
    private final int targetPixels;

    public SizeComparator(
        final int maxWidth,
        final int maxHeight,
        final float targetAspectRatio,
        final int targetPixels) {
      this.maxWidth = maxWidth;
      this.maxHeight = maxHeight;
      this.targetAspectRatio = targetAspectRatio;
      this.targetPixels = targetPixels;
    }

    /**
     * Returns a negative value if left is a better choice than right, or a positive value if right
     * is a better choice is better than left. 0 if they are equal
     */
    @Override
    public int compare(final Camera.Size left, final Camera.Size right) {
      // If one size is less than the max size prefer it over the other
      if ((left.width <= maxWidth && left.height <= maxHeight)
          != (right.width <= maxWidth && right.height <= maxHeight)) {
        return left.width <= maxWidth ? PREFER_LEFT : PREFER_RIGHT;
      }

      // If one is closer to the target aspect ratio, prefer it.
      final float leftAspectRatio = left.width / (float) left.height;
      final float rightAspectRatio = right.width / (float) right.height;
      final float leftAspectRatioDiff = Math.abs(leftAspectRatio - targetAspectRatio);
      final float rightAspectRatioDiff = Math.abs(rightAspectRatio - targetAspectRatio);
      if (leftAspectRatioDiff != rightAspectRatioDiff) {
        return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? PREFER_LEFT : PREFER_RIGHT;
      }

      // At this point they have the same aspect ratio diff and are either both bigger
      // than the max size or both smaller than the max size, so prefer the one closest
      // to target size
      final int leftDiff = Math.abs((left.width * left.height) - targetPixels);
      final int rightDiff = Math.abs((right.width * right.height) - targetPixels);
      return leftDiff - rightDiff;
    }
  }

  @Override // From FocusOverlayManager.Listener
  public void autoFocus() {
    if (this.camera == null) {
      return;
    }

    try {
      this.camera.autoFocus(
          new Camera.AutoFocusCallback() {
            @Override
            public void onAutoFocus(final boolean success, final Camera camera) {
              focusOverlayManager.onAutoFocus(success, false /* shutterDown */);
            }
          });
    } catch (final RuntimeException e) {
      LogUtil.e("CameraManager.autoFocus", "RuntimeException in CameraManager.autoFocus", e);
      // If autofocus fails, the camera should have called the callback with success=false,
      // but some throw an exception here
      focusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/);
    }
  }

  @Override // From FocusOverlayManager.Listener
  public void cancelAutoFocus() {
    if (camera == null) {
      return;
    }
    try {
      camera.cancelAutoFocus();
    } catch (final RuntimeException e) {
      // Ignore
      LogUtil.e(
          "CameraManager.cancelAutoFocus", "RuntimeException in CameraManager.cancelAutoFocus", e);
    }
  }

  @Override // From FocusOverlayManager.Listener
  public boolean capture() {
    return false;
  }

  @Override // From FocusOverlayManager.Listener
  public void setFocusParameters() {
    if (camera == null) {
      return;
    }
    try {
      final Camera.Parameters parameters = camera.getParameters();
      parameters.setFocusMode(focusOverlayManager.getFocusMode());
      if (parameters.getMaxNumFocusAreas() > 0) {
        // Don't set focus areas (even to null) if focus areas aren't supported, camera may
        // crash
        parameters.setFocusAreas(focusOverlayManager.getFocusAreas());
      }
      parameters.setMeteringAreas(focusOverlayManager.getMeteringAreas());
      camera.setParameters(parameters);
    } catch (final RuntimeException e) {
      // This occurs when the device is out of space or when the camera is locked
      LogUtil.e(
          "CameraManager.setFocusParameters",
          "RuntimeException in CameraManager setFocusParameters");
    }
  }

  public void resetPreview() {
    camera.startPreview();
    if (cameraPreview != null) {
      cameraPreview.setFocusable(true);
    }
  }

  private void logCameraSize(final String prefix, final Camera.Size size) {
    // Log the camera size and aspect ratio for help when examining bug reports for camera
    // failures
    LogUtil.i(
        "CameraManager.logCameraSize",
        prefix + size.width + "x" + size.height + " (" + (size.width / (float) size.height) + ")");
  }

  @VisibleForTesting
  public void resetCameraManager() {
    instance = null;
  }
}
