// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

package net.realify.lib.androidimagecropper;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.exifinterface.media.ExifInterface;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;

import java.lang.ref.WeakReference;
import java.util.UUID;

/** Custom view that provides cropping capabilities to an image. */
public class CropImageView extends FrameLayout {

  // region: Fields and Consts

  /** Image view widget used to show the image for cropping. */
  private final ImageView mImageView;

  /** Overlay over the image view to show cropping UI. */
  private final CropOverlayView mCropOverlayView;

  /** The matrix used to transform the cropping image in the image view */
  private final Matrix mImageMatrix = new Matrix();

  /** Reusing matrix instance for reverse matrix calculations. */
  private final Matrix mImageInverseMatrix = new Matrix();

  /** Progress bar widget to show progress bar on async image loading and cropping. */
  private final ProgressBar mProgressBar;

  /** Rectangle used in image matrix transformation calculation (reusing rect instance) */
  private final float[] mImagePoints = new float[8];

  /** Rectangle used in image matrix transformation for scale calculation (reusing rect instance) */
  private final float[] mScaleImagePoints = new float[8];

  /** Animation class to smooth animate zoom-in/out */
  private CropImageAnimation mAnimation;

  private Bitmap mBitmap;

  /** The image rotation value used during loading of the image so we can reset to it */
  private int mInitialDegreesRotated;

  /** How much the image is rotated from original clockwise */
  private int mDegreesRotated;

  /** if the image flipped horizontally */
  private boolean mFlipHorizontally;

  /** if the image flipped vertically */
  private boolean mFlipVertically;

  private int mLayoutWidth;

  private int mLayoutHeight;

  private int mImageResource;

  /** The initial scale type of the image in the crop image view */
  private ScaleType mScaleType;

  /**
   * if to save bitmap on save instance state.<br>
   * It is best to avoid it by using URI in setting image for cropping.<br>
   * If false the bitmap is not saved and if restore is required to view will be empty, storing the
   * bitmap requires saving it to file which can be expensive. default: false.
   */
  private boolean mSaveBitmapToInstanceState = false;

  /**
   * if to show crop overlay UI what contains the crop window UI surrounded by background over the
   * cropping image.<br>
   * default: true, may disable for animation or frame transition.
   */
  private boolean mShowCropOverlay = true;

  /**
   * if to show progress bar when image async loading/cropping is in progress.<br>
   * default: true, disable to provide custom progress bar UI.
   */
  private boolean mShowProgressBar = true;

  /**
   * if auto-zoom functionality is enabled.<br>
   * default: true.
   */
  private boolean mAutoZoomEnabled = true;

  /** The max zoom allowed during cropping */
  private int mMaxZoom;

  /** callback to be invoked when crop overlay is released. */
  private OnSetCropOverlayReleasedListener mOnCropOverlayReleasedListener;

  /** callback to be invoked when crop overlay is moved. */
  private OnSetCropOverlayMovedListener mOnSetCropOverlayMovedListener;

  /** callback to be invoked when crop window is changed. */
  private OnSetCropWindowChangeListener mOnSetCropWindowChangeListener;

  /** callback to be invoked when image async loading is complete. */
  private OnSetImageUriCompleteListener mOnSetImageUriCompleteListener;

  /** callback to be invoked when image async cropping is complete. */
  private OnCropImageCompleteListener mOnCropImageCompleteListener;

  /** The URI that the image was loaded from (if loaded from URI) */
  private Uri mLoadedImageUri;

  /** The sample size the image was loaded by if was loaded by URI */
  private int mLoadedSampleSize = 1;

  /** The current zoom level to to scale the cropping image */
  private float mZoom = 1;

  /** The X offset that the cropping image was translated after zooming */
  private float mZoomOffsetX;

  /** The Y offset that the cropping image was translated after zooming */
  private float mZoomOffsetY;

  /** Used to restore the cropping windows rectangle after state restore */
  private RectF mRestoreCropWindowRect;

  /** Used to restore image rotation after state restore */
  private int mRestoreDegreesRotated;

  /**
   * Used to detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean,
   * boolean)} in {@link #layout(int, int, int, int)}.
   */
  private boolean mSizeChanged;

  /**
   * Temp URI used to save bitmap image to disk to preserve for instance state in case cropped was
   * set with bitmap
   */
  private Uri mSaveInstanceStateBitmapUri;

  /** Task used to load bitmap async from UI thread */
  private WeakReference<BitmapLoadingWorkerTask> mBitmapLoadingWorkerTask;

  /** Task used to crop bitmap async from UI thread */
  private WeakReference<BitmapCroppingWorkerTask> mBitmapCroppingWorkerTask;
  // endregion

  public CropImageView(Context context) {
    this(context, null);
  }

  public CropImageView(Context context, AttributeSet attrs) {
    super(context, attrs);

    CropImageOptions options = null;
    Intent intent = context instanceof Activity ? ((Activity) context).getIntent() : null;
    if (intent != null) {
      Bundle bundle = intent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
      if (bundle != null) {
        options = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
      }
    }

    if (options == null) {

      options = new CropImageOptions();

      if (attrs != null) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0);
        try {
          options.fixAspectRatio =
              ta.getBoolean(R.styleable.CropImageView_cropFixAspectRatio, options.fixAspectRatio);
          options.aspectRatioX =
              ta.getInteger(R.styleable.CropImageView_cropAspectRatioX, options.aspectRatioX);
          options.aspectRatioY =
              ta.getInteger(R.styleable.CropImageView_cropAspectRatioY, options.aspectRatioY);
          options.scaleType =
              ScaleType.values()[
                  ta.getInt(R.styleable.CropImageView_cropScaleType, options.scaleType.ordinal())];
          options.autoZoomEnabled =
              ta.getBoolean(R.styleable.CropImageView_cropAutoZoomEnabled, options.autoZoomEnabled);
          options.multiTouchEnabled =
              ta.getBoolean(
                  R.styleable.CropImageView_cropMultiTouchEnabled, options.multiTouchEnabled);
          options.maxZoom = ta.getInteger(R.styleable.CropImageView_cropMaxZoom, options.maxZoom);
          options.cropShape =
              CropShape.values()[
                  ta.getInt(R.styleable.CropImageView_cropShape, options.cropShape.ordinal())];
          options.guidelines =
              Guidelines.values()[
                  ta.getInt(
                      R.styleable.CropImageView_cropGuidelines, options.guidelines.ordinal())];
          options.snapRadius =
              ta.getDimension(R.styleable.CropImageView_cropSnapRadius, options.snapRadius);
          options.touchRadius =
              ta.getDimension(R.styleable.CropImageView_cropTouchRadius, options.touchRadius);
          options.initialCropWindowPaddingRatio =
              ta.getFloat(
                  R.styleable.CropImageView_cropInitialCropWindowPaddingRatio,
                  options.initialCropWindowPaddingRatio);
          options.borderLineThickness =
              ta.getDimension(
                  R.styleable.CropImageView_cropBorderLineThickness, options.borderLineThickness);
          options.borderLineColor =
              ta.getInteger(R.styleable.CropImageView_cropBorderLineColor, options.borderLineColor);
          options.borderCornerThickness =
              ta.getDimension(
                  R.styleable.CropImageView_cropBorderCornerThickness,
                  options.borderCornerThickness);
          options.borderCornerOffset =
              ta.getDimension(
                  R.styleable.CropImageView_cropBorderCornerOffset, options.borderCornerOffset);
          options.borderCornerLength =
              ta.getDimension(
                  R.styleable.CropImageView_cropBorderCornerLength, options.borderCornerLength);
          options.borderCornerColor =
              ta.getInteger(
                  R.styleable.CropImageView_cropBorderCornerColor, options.borderCornerColor);
          options.guidelinesThickness =
              ta.getDimension(
                  R.styleable.CropImageView_cropGuidelinesThickness, options.guidelinesThickness);
          options.guidelinesColor =
              ta.getInteger(R.styleable.CropImageView_cropGuidelinesColor, options.guidelinesColor);
          options.backgroundColor =
              ta.getInteger(R.styleable.CropImageView_cropBackgroundColor, options.backgroundColor);
          options.showCropOverlay =
              ta.getBoolean(R.styleable.CropImageView_cropShowCropOverlay, mShowCropOverlay);
          options.showProgressBar =
              ta.getBoolean(R.styleable.CropImageView_cropShowProgressBar, mShowProgressBar);
          options.borderCornerThickness =
              ta.getDimension(
                  R.styleable.CropImageView_cropBorderCornerThickness,
                  options.borderCornerThickness);
          options.minCropWindowWidth =
              (int)
                  ta.getDimension(
                      R.styleable.CropImageView_cropMinCropWindowWidth, options.minCropWindowWidth);
          options.minCropWindowHeight =
              (int)
                  ta.getDimension(
                      R.styleable.CropImageView_cropMinCropWindowHeight,
                      options.minCropWindowHeight);
          options.minCropResultWidth =
              (int)
                  ta.getFloat(
                      R.styleable.CropImageView_cropMinCropResultWidthPX,
                      options.minCropResultWidth);
          options.minCropResultHeight =
              (int)
                  ta.getFloat(
                      R.styleable.CropImageView_cropMinCropResultHeightPX,
                      options.minCropResultHeight);
          options.maxCropResultWidth =
              (int)
                  ta.getFloat(
                      R.styleable.CropImageView_cropMaxCropResultWidthPX,
                      options.maxCropResultWidth);
          options.maxCropResultHeight =
              (int)
                  ta.getFloat(
                      R.styleable.CropImageView_cropMaxCropResultHeightPX,
                      options.maxCropResultHeight);
          options.flipHorizontally =
              ta.getBoolean(
                  R.styleable.CropImageView_cropFlipHorizontally, options.flipHorizontally);
          options.flipVertically =
              ta.getBoolean(R.styleable.CropImageView_cropFlipHorizontally, options.flipVertically);

          mSaveBitmapToInstanceState =
              ta.getBoolean(
                  R.styleable.CropImageView_cropSaveBitmapToInstanceState,
                  mSaveBitmapToInstanceState);

          // if aspect ratio is set then set fixed to true
          if (ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
              && ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
              && !ta.hasValue(R.styleable.CropImageView_cropFixAspectRatio)) {
            options.fixAspectRatio = true;
          }
        } finally {
          ta.recycle();
        }
      }
    }

    options.validate();

    mScaleType = options.scaleType;
    mAutoZoomEnabled = options.autoZoomEnabled;
    mMaxZoom = options.maxZoom;
    mShowCropOverlay = options.showCropOverlay;
    mShowProgressBar = options.showProgressBar;
    mFlipHorizontally = options.flipHorizontally;
    mFlipVertically = options.flipVertically;

    LayoutInflater inflater = LayoutInflater.from(context);
    View v = inflater.inflate(R.layout.crop_image_view, this, true);

    mImageView = v.findViewById(R.id.ImageView_image);
    mImageView.setScaleType(ImageView.ScaleType.MATRIX);

    mCropOverlayView = v.findViewById(R.id.CropOverlayView);
    mCropOverlayView.setCropWindowChangeListener(
        new CropOverlayView.CropWindowChangeListener() {
          @Override
          public void onCropWindowChanged(boolean inProgress) {
            handleCropWindowChanged(inProgress, true);
            OnSetCropOverlayReleasedListener listener = mOnCropOverlayReleasedListener;
            if (listener != null && !inProgress) {
              listener.onCropOverlayReleased(getCropRect());
            }
            OnSetCropOverlayMovedListener movedListener = mOnSetCropOverlayMovedListener;
            if (movedListener != null && inProgress) {
              movedListener.onCropOverlayMoved(getCropRect());
            }
          }
        });
    mCropOverlayView.setInitialAttributeValues(options);

    mProgressBar = v.findViewById(R.id.CropProgressBar);
    setProgressBarVisibility();
  }

  /** Get the scale type of the image in the crop view. */
  public ScaleType getScaleType() {
    return mScaleType;
  }

  /** Set the scale type of the image in the crop view */
  public void setScaleType(ScaleType scaleType) {
    if (scaleType != mScaleType) {
      mScaleType = scaleType;
      mZoom = 1;
      mZoomOffsetX = mZoomOffsetY = 0;
      mCropOverlayView.resetCropOverlayView();
      requestLayout();
    }
  }

  /** The shape of the cropping area - rectangle/circular. */
  public CropShape getCropShape() {
    return mCropOverlayView.getCropShape();
  }

  /**
   * The shape of the cropping area - rectangle/circular.<br>
   * To set square/circle crop shape set aspect ratio to 1:1.
   */
  public void setCropShape(CropShape cropShape) {
    mCropOverlayView.setCropShape(cropShape);
  }

  /** if auto-zoom functionality is enabled. default: true. */
  public boolean isAutoZoomEnabled() {
    return mAutoZoomEnabled;
  }

  /** Set auto-zoom functionality to enabled/disabled. */
  public void setAutoZoomEnabled(boolean autoZoomEnabled) {
    if (mAutoZoomEnabled != autoZoomEnabled) {
      mAutoZoomEnabled = autoZoomEnabled;
      handleCropWindowChanged(false, false);
      mCropOverlayView.invalidate();
    }
  }

  /** Set multi touch functionality to enabled/disabled. */
  public void setMultiTouchEnabled(boolean multiTouchEnabled) {
    if (mCropOverlayView.setMultiTouchEnabled(multiTouchEnabled)) {
      handleCropWindowChanged(false, false);
      mCropOverlayView.invalidate();
    }
  }

  /** The max zoom allowed during cropping. */
  public int getMaxZoom() {
    return mMaxZoom;
  }

  /** The max zoom allowed during cropping. */
  public void setMaxZoom(int maxZoom) {
    if (mMaxZoom != maxZoom && maxZoom > 0) {
      mMaxZoom = maxZoom;
      handleCropWindowChanged(false, false);
      mCropOverlayView.invalidate();
    }
  }

  /**
   * the min size the resulting cropping image is allowed to be, affects the cropping window limits
   * (in pixels).<br>
   */
  public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
    mCropOverlayView.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
  }

  /**
   * the max size the resulting cropping image is allowed to be, affects the cropping window limits
   * (in pixels).<br>
   */
  public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
    mCropOverlayView.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
  }

  /**
   * Get the amount of degrees the cropping image is rotated cloackwise.<br>
   *
   * @return 0-360
   */
  public int getRotatedDegrees() {
    return mDegreesRotated;
  }

  /**
   * Set the amount of degrees the cropping image is rotated cloackwise.<br>
   *
   * @param degrees 0-360
   */
  public void setRotatedDegrees(int degrees) {
    if (mDegreesRotated != degrees) {
      rotateImage(degrees - mDegreesRotated);
    }
  }

  /**
   * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to
   * be changed.
   */
  public boolean isFixAspectRatio() {
    return mCropOverlayView.isFixAspectRatio();
  }

  /**
   * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows
   * it to be changed.
   */
  public void setFixedAspectRatio(boolean fixAspectRatio) {
    mCropOverlayView.setFixedAspectRatio(fixAspectRatio);
  }

  /** whether the image should be flipped horizontally */
  public boolean isFlippedHorizontally() {
    return mFlipHorizontally;
  }

  /** Sets whether the image should be flipped horizontally */
  public void setFlippedHorizontally(boolean flipHorizontally) {
    if (mFlipHorizontally != flipHorizontally) {
      mFlipHorizontally = flipHorizontally;
      applyImageMatrix(getWidth(), getHeight(), true, false);
    }
  }

  /** whether the image should be flipped vertically */
  public boolean isFlippedVertically() {
    return mFlipVertically;
  }

  /** Sets whether the image should be flipped vertically */
  public void setFlippedVertically(boolean flipVertically) {
    if (mFlipVertically != flipVertically) {
      mFlipVertically = flipVertically;
      applyImageMatrix(getWidth(), getHeight(), true, false);
    }
  }

  /** Get the current guidelines option set. */
  public Guidelines getGuidelines() {
    return mCropOverlayView.getGuidelines();
  }

  /**
   * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the
   * application.
   */
  public void setGuidelines(Guidelines guidelines) {
    mCropOverlayView.setGuidelines(guidelines);
  }

  /** both the X and Y values of the aspectRatio. */
  public Pair<Integer, Integer> getAspectRatio() {
    return new Pair<>(mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
  }

  /**
   * Sets the both the X and Y values of the aspectRatio.<br>
   * Sets fixed aspect ratio to TRUE.
   *
   * @param aspectRatioX int that specifies the new X value of the aspect ratio
   * @param aspectRatioY int that specifies the new Y value of the aspect ratio
   */
  public void setAspectRatio(int aspectRatioX, int aspectRatioY) {
    mCropOverlayView.setAspectRatioX(aspectRatioX);
    mCropOverlayView.setAspectRatioY(aspectRatioY);
    setFixedAspectRatio(true);
  }

  /** Clears set aspect ratio values and sets fixed aspect ratio to FALSE. */
  public void clearAspectRatio() {
    mCropOverlayView.setAspectRatioX(1);
    mCropOverlayView.setAspectRatioY(1);
    setFixedAspectRatio(false);
  }

  /**
   * An edge of the crop window will snap to the corresponding edge of a specified bounding box when
   * the crop window edge is less than or equal to this distance (in pixels) away from the bounding
   * box edge. (default: 3dp)
   */
  public void setSnapRadius(float snapRadius) {
    if (snapRadius >= 0) {
      mCropOverlayView.setSnapRadius(snapRadius);
    }
  }

  /**
   * if to show progress bar when image async loading/cropping is in progress.<br>
   * default: true, disable to provide custom progress bar UI.
   */
  public boolean isShowProgressBar() {
    return mShowProgressBar;
  }

  /**
   * if to show progress bar when image async loading/cropping is in progress.<br>
   * default: true, disable to provide custom progress bar UI.
   */
  public void setShowProgressBar(boolean showProgressBar) {
    if (mShowProgressBar != showProgressBar) {
      mShowProgressBar = showProgressBar;
      setProgressBarVisibility();
    }
  }

  /**
   * if to show crop overlay UI what contains the crop window UI surrounded by background over the
   * cropping image.<br>
   * default: true, may disable for animation or frame transition.
   */
  public boolean isShowCropOverlay() {
    return mShowCropOverlay;
  }

  /**
   * if to show crop overlay UI what contains the crop window UI surrounded by background over the
   * cropping image.<br>
   * default: true, may disable for animation or frame transition.
   */
  public void setShowCropOverlay(boolean showCropOverlay) {
    if (mShowCropOverlay != showCropOverlay) {
      mShowCropOverlay = showCropOverlay;
      setCropOverlayVisibility();
    }
  }

  /**
   * if to save bitmap on save instance state.<br>
   * It is best to avoid it by using URI in setting image for cropping.<br>
   * If false the bitmap is not saved and if restore is required to view will be empty, storing the
   * bitmap requires saving it to file which can be expensive. default: false.
   */
  public boolean isSaveBitmapToInstanceState() {
    return mSaveBitmapToInstanceState;
  }

  /**
   * if to save bitmap on save instance state.<br>
   * It is best to avoid it by using URI in setting image for cropping.<br>
   * If false the bitmap is not saved and if restore is required to view will be empty, storing the
   * bitmap requires saving it to file which can be expensive. default: false.
   */
  public void setSaveBitmapToInstanceState(boolean saveBitmapToInstanceState) {
    mSaveBitmapToInstanceState = saveBitmapToInstanceState;
  }

  /** Returns the integer of the imageResource */
  public int getImageResource() {
    return mImageResource;
  }

  /** Get the URI of an image that was set by URI, null otherwise. */
  public Uri getImageUri() {
    return mLoadedImageUri;
  }

  /**
   * Gets the source Bitmap's dimensions. This represents the largest possible crop rectangle.
   *
   * @return a Rect instance dimensions of the source Bitmap
   */
  public Rect getWholeImageRect() {
    int loadedSampleSize = mLoadedSampleSize;
    Bitmap bitmap = mBitmap;
    if (bitmap == null) {
      return null;
    }

    int orgWidth = bitmap.getWidth() * loadedSampleSize;
    int orgHeight = bitmap.getHeight() * loadedSampleSize;
    return new Rect(0, 0, orgWidth, orgHeight);
  }

  /**
   * Gets the crop window's position relative to the source Bitmap (not the image displayed in the
   * CropImageView) using the original image rotation.
   *
   * @return a Rect instance containing cropped area boundaries of the source Bitmap
   */
  public Rect getCropRect() {
    int loadedSampleSize = mLoadedSampleSize;
    Bitmap bitmap = mBitmap;
    if (bitmap == null) {
      return null;
    }

    // get the points of the crop rectangle adjusted to source bitmap
    float[] points = getCropPoints();

    int orgWidth = bitmap.getWidth() * loadedSampleSize;
    int orgHeight = bitmap.getHeight() * loadedSampleSize;

    // get the rectangle for the points (it may be larger than original if rotation is not stright)
    return BitmapUtils.getRectFromPoints(
        points,
        orgWidth,
        orgHeight,
        mCropOverlayView.isFixAspectRatio(),
        mCropOverlayView.getAspectRatioX(),
        mCropOverlayView.getAspectRatioY());
  }

  /**
   * Gets the crop window's position relative to the parent's view at screen.
   *
   * @return a Rect instance containing cropped area boundaries of the source Bitmap
   */
  public RectF getCropWindowRect() {
    if (mCropOverlayView == null) {
      return null;
    }
    return mCropOverlayView.getCropWindowRect();
  }

  /**
   * Gets the 4 points of crop window's position relative to the source Bitmap (not the image
   * displayed in the CropImageView) using the original image rotation.<br>
   * Note: the 4 points may not be a rectangle if the image was rotates to NOT stright angle (!=
   * 90/180/270).
   *
   * @return 4 points (x0,y0,x1,y1,x2,y2,x3,y3) of cropped area boundaries
   */
  public float[] getCropPoints() {

    // Get crop window position relative to the displayed image.
    RectF cropWindowRect = mCropOverlayView.getCropWindowRect();

    float[] points =
        new float[] {
          cropWindowRect.left,
          cropWindowRect.top,
          cropWindowRect.right,
          cropWindowRect.top,
          cropWindowRect.right,
          cropWindowRect.bottom,
          cropWindowRect.left,
          cropWindowRect.bottom
        };

    mImageMatrix.invert(mImageInverseMatrix);
    mImageInverseMatrix.mapPoints(points);

    for (int i = 0; i < points.length; i++) {
      points[i] *= mLoadedSampleSize;
    }

    return points;
  }

  /**
   * Set the crop window position and size to the given rectangle.<br>
   * Image to crop must be first set before invoking this, for async - after complete callback.
   *
   * @param rect window rectangle (position and size) relative to source bitmap
   */
  public void setCropRect(Rect rect) {
    mCropOverlayView.setInitialCropWindowRect(rect);
  }

  /** Reset crop window to initial rectangle. */
  public void resetCropRect() {
    mZoom = 1;
    mZoomOffsetX = 0;
    mZoomOffsetY = 0;
    mDegreesRotated = mInitialDegreesRotated;
    mFlipHorizontally = false;
    mFlipVertically = false;
    applyImageMatrix(getWidth(), getHeight(), false, false);
    mCropOverlayView.resetCropWindowRect();
  }

  /**
   * Gets the cropped image based on the current crop window.
   *
   * @return a new Bitmap representing the cropped image
   */
  public Bitmap getCroppedImage() {
    return getCroppedImage(0, 0, RequestSizeOptions.NONE);
  }

  /**
   * Gets the cropped image based on the current crop window.<br>
   * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
   *
   * @param reqWidth the width to resize the cropped image to
   * @param reqHeight the height to resize the cropped image to
   * @return a new Bitmap representing the cropped image
   */
  public Bitmap getCroppedImage(int reqWidth, int reqHeight) {
    return getCroppedImage(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
  }

  /**
   * Gets the cropped image based on the current crop window.<br>
   *
   * @param reqWidth the width to resize the cropped image to (see options)
   * @param reqHeight the height to resize the cropped image to (see options)
   * @param options the resize method to use, see its documentation
   * @return a new Bitmap representing the cropped image
   */
  public Bitmap getCroppedImage(int reqWidth, int reqHeight, RequestSizeOptions options) {
    Bitmap croppedBitmap = null;
    if (mBitmap != null) {
      mImageView.clearAnimation();

      reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
      reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;

      if (mLoadedImageUri != null
          && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
        int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
        int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
        BitmapUtils.BitmapSampled bitmapSampled =
            BitmapUtils.cropBitmap(
                getContext(),
                mLoadedImageUri,
                getCropPoints(),
                mDegreesRotated,
                orgWidth,
                orgHeight,
                mCropOverlayView.isFixAspectRatio(),
                mCropOverlayView.getAspectRatioX(),
                mCropOverlayView.getAspectRatioY(),
                reqWidth,
                reqHeight,
                mFlipHorizontally,
                mFlipVertically);
        croppedBitmap = bitmapSampled.bitmap;
      } else {
        croppedBitmap =
            BitmapUtils.cropBitmapObjectHandleOOM(
                    mBitmap,
                    getCropPoints(),
                    mDegreesRotated,
                    mCropOverlayView.isFixAspectRatio(),
                    mCropOverlayView.getAspectRatioX(),
                    mCropOverlayView.getAspectRatioY(),
                    mFlipHorizontally,
                    mFlipVertically)
                .bitmap;
      }

      croppedBitmap = BitmapUtils.resizeBitmap(croppedBitmap, reqWidth, reqHeight, options);
    }

    return croppedBitmap;
  }

  /**
   * Gets the cropped image based on the current crop window.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   */
  public void getCroppedImageAsync() {
    getCroppedImageAsync(0, 0, RequestSizeOptions.NONE);
  }

  /**
   * Gets the cropped image based on the current crop window.<br>
   * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   *
   * @param reqWidth the width to resize the cropped image to
   * @param reqHeight the height to resize the cropped image to
   */
  public void getCroppedImageAsync(int reqWidth, int reqHeight) {
    getCroppedImageAsync(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
  }

  /**
   * Gets the cropped image based on the current crop window.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   *
   * @param reqWidth the width to resize the cropped image to (see options)
   * @param reqHeight the height to resize the cropped image to (see options)
   * @param options the resize method to use, see its documentation
   */
  public void getCroppedImageAsync(int reqWidth, int reqHeight, RequestSizeOptions options) {
    if (mOnCropImageCompleteListener == null) {
      throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
    }
    startCropWorkerTask(reqWidth, reqHeight, options, null, null, 0);
  }

  /**
   * Save the cropped image based on the current crop window to the given uri.<br>
   * Uses JPEG image compression with 90 compression quality.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   *
   * @param saveUri the Android Uri to save the cropped image to
   */
  public void saveCroppedImageAsync(Uri saveUri) {
    saveCroppedImageAsync(saveUri, Bitmap.CompressFormat.JPEG, 90, 0, 0, RequestSizeOptions.NONE);
  }

  /**
   * Save the cropped image based on the current crop window to the given uri.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   *
   * @param saveUri the Android Uri to save the cropped image to
   * @param saveCompressFormat the compression format to use when writing the image
   * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
   */
  public void saveCroppedImageAsync(
      Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
    saveCroppedImageAsync(
        saveUri, saveCompressFormat, saveCompressQuality, 0, 0, RequestSizeOptions.NONE);
  }

  /**
   * Save the cropped image based on the current crop window to the given uri.<br>
   * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   *
   * @param saveUri the Android Uri to save the cropped image to
   * @param saveCompressFormat the compression format to use when writing the image
   * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
   * @param reqWidth the width to resize the cropped image to
   * @param reqHeight the height to resize the cropped image to
   */
  public void saveCroppedImageAsync(
      Uri saveUri,
      Bitmap.CompressFormat saveCompressFormat,
      int saveCompressQuality,
      int reqWidth,
      int reqHeight) {
    saveCroppedImageAsync(
        saveUri,
        saveCompressFormat,
        saveCompressQuality,
        reqWidth,
        reqHeight,
        RequestSizeOptions.RESIZE_INSIDE);
  }

  /**
   * Save the cropped image based on the current crop window to the given uri.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   *
   * @param saveUri the Android Uri to save the cropped image to
   * @param saveCompressFormat the compression format to use when writing the image
   * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
   * @param reqWidth the width to resize the cropped image to (see options)
   * @param reqHeight the height to resize the cropped image to (see options)
   * @param options the resize method to use, see its documentation
   */
  public void saveCroppedImageAsync(
      Uri saveUri,
      Bitmap.CompressFormat saveCompressFormat,
      int saveCompressQuality,
      int reqWidth,
      int reqHeight,
      RequestSizeOptions options) {
    if (mOnCropImageCompleteListener == null) {
      throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
    }
    startCropWorkerTask(
        reqWidth, reqHeight, options, saveUri, saveCompressFormat, saveCompressQuality);
  }

  /** Set the callback t */
  public void setOnSetCropOverlayReleasedListener(OnSetCropOverlayReleasedListener listener) {
    mOnCropOverlayReleasedListener = listener;
  }

  /** Set the callback when the cropping is moved */
  public void setOnSetCropOverlayMovedListener(OnSetCropOverlayMovedListener listener) {
    mOnSetCropOverlayMovedListener = listener;
  }

  /** Set the callback when the crop window is changed */
  public void setOnCropWindowChangedListener(OnSetCropWindowChangeListener listener) {
    mOnSetCropWindowChangeListener = listener;
  }

  /**
   * Set the callback to be invoked when image async loading ({@link #setImageUriAsync(Uri)}) is
   * complete (successful or failed).
   */
  public void setOnSetImageUriCompleteListener(OnSetImageUriCompleteListener listener) {
    mOnSetImageUriCompleteListener = listener;
  }

  /**
   * Set the callback to be invoked when image async cropping image ({@link #getCroppedImageAsync()}
   * or {@link #saveCroppedImageAsync(Uri)}) is complete (successful or failed).
   */
  public void setOnCropImageCompleteListener(OnCropImageCompleteListener listener) {
    mOnCropImageCompleteListener = listener;
  }

  /**
   * Sets a Bitmap as the content of the CropImageView.
   *
   * @param bitmap the Bitmap to set
   */
  public void setImageBitmap(Bitmap bitmap) {
    mCropOverlayView.setInitialCropWindowRect(null);
    setBitmap(bitmap, 0, null, 1, 0);
  }

  /**
   * Sets a Bitmap and initializes the image rotation according to the EXIT data.<br>
   * <br>
   * The EXIF can be retrieved by doing the following: <code>
   * ExifInterface exif = new ExifInterface(path);</code>
   *
   * @param bitmap the original bitmap to set; if null, this
   * @param exif the EXIF information about this bitmap; may be null
   */
  public void setImageBitmap(Bitmap bitmap, ExifInterface exif) {
    Bitmap setBitmap;
    int degreesRotated = 0;
    if (bitmap != null && exif != null) {
      BitmapUtils.RotateBitmapResult result = BitmapUtils.rotateBitmapByExif(bitmap, exif);
      setBitmap = result.bitmap;
      degreesRotated = result.degrees;
      mInitialDegreesRotated = result.degrees;
    } else {
      setBitmap = bitmap;
    }
    mCropOverlayView.setInitialCropWindowRect(null);
    setBitmap(setBitmap, 0, null, 1, degreesRotated);
  }

  /**
   * Sets a Drawable as the content of the CropImageView.
   *
   * @param resId the drawable resource ID to set
   */
  public void setImageResource(int resId) {
    if (resId != 0) {
      mCropOverlayView.setInitialCropWindowRect(null);
      Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
      setBitmap(bitmap, resId, null, 1, 0);
    }
  }

  /**
   * Sets a bitmap loaded from the given Android URI as the content of the CropImageView.<br>
   * Can be used with URI from gallery or camera source.<br>
   * Will rotate the image by exif data.<br>
   *
   * @param uri the URI to load the image from
   */
  public void setImageUriAsync(Uri uri) {
    if (uri != null) {
      BitmapLoadingWorkerTask currentTask =
          mBitmapLoadingWorkerTask != null ? mBitmapLoadingWorkerTask.get() : null;
      if (currentTask != null) {
        // cancel previous loading (no check if the same URI because camera URI can be the same for
        // different images)
        currentTask.cancel(true);
      }

      // either no existing task is working or we canceled it, need to load new URI
      clearImageInt();
      mRestoreCropWindowRect = null;
      mRestoreDegreesRotated = 0;
      mCropOverlayView.setInitialCropWindowRect(null);
      mBitmapLoadingWorkerTask = new WeakReference<>(new BitmapLoadingWorkerTask(this, uri));
      mBitmapLoadingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
      setProgressBarVisibility();
    }
  }

  /** Clear the current image set for cropping. */
  public void clearImage() {
    clearImageInt();
    mCropOverlayView.setInitialCropWindowRect(null);
  }

  /**
   * Rotates image by the specified number of degrees clockwise.<br>
   * Negative values represent counter-clockwise rotations.
   *
   * @param degrees Integer specifying the number of degrees to rotate.
   */
  public void rotateImage(int degrees) {
    if (mBitmap != null) {
      // Force degrees to be a non-zero value between 0 and 360 (inclusive)
      if (degrees < 0) {
        degrees = (degrees % 360) + 360;
      } else {
        degrees = degrees % 360;
      }

      boolean flipAxes =
          !mCropOverlayView.isFixAspectRatio()
              && ((degrees > 45 && degrees < 135) || (degrees > 215 && degrees < 305));
      BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
      float halfWidth = (flipAxes ? BitmapUtils.RECT.height() : BitmapUtils.RECT.width()) / 2f;
      float halfHeight = (flipAxes ? BitmapUtils.RECT.width() : BitmapUtils.RECT.height()) / 2f;
      if (flipAxes) {
        boolean isFlippedHorizontally = mFlipHorizontally;
        mFlipHorizontally = mFlipVertically;
        mFlipVertically = isFlippedHorizontally;
      }

      mImageMatrix.invert(mImageInverseMatrix);

      BitmapUtils.POINTS[0] = BitmapUtils.RECT.centerX();
      BitmapUtils.POINTS[1] = BitmapUtils.RECT.centerY();
      BitmapUtils.POINTS[2] = 0;
      BitmapUtils.POINTS[3] = 0;
      BitmapUtils.POINTS[4] = 1;
      BitmapUtils.POINTS[5] = 0;
      mImageInverseMatrix.mapPoints(BitmapUtils.POINTS);

      // This is valid because degrees is not negative.
      mDegreesRotated = (mDegreesRotated + degrees) % 360;

      applyImageMatrix(getWidth(), getHeight(), true, false);

      // adjust the zoom so the crop window size remains the same even after image scale change
      mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
      mZoom /=
          Math.sqrt(
              Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
                  + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
      mZoom = Math.max(mZoom, 1);

      applyImageMatrix(getWidth(), getHeight(), true, false);

      mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);

      // adjust the width/height by the changes in scaling to the image
      double change =
          Math.sqrt(
              Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
                  + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
      halfWidth *= change;
      halfHeight *= change;

      // calculate the new crop window rectangle to center in the same location and have proper
      // width/height
      BitmapUtils.RECT.set(
          BitmapUtils.POINTS2[0] - halfWidth,
          BitmapUtils.POINTS2[1] - halfHeight,
          BitmapUtils.POINTS2[0] + halfWidth,
          BitmapUtils.POINTS2[1] + halfHeight);

      mCropOverlayView.resetCropOverlayView();
      mCropOverlayView.setCropWindowRect(BitmapUtils.RECT);
      applyImageMatrix(getWidth(), getHeight(), true, false);
      handleCropWindowChanged(false, false);

      // make sure the crop window rectangle is within the cropping image bounds after all the
      // changes
      mCropOverlayView.fixCurrentCropWindowRect();
    }
  }

  /** Flips the image horizontally. */
  public void flipImageHorizontally() {
    mFlipHorizontally = !mFlipHorizontally;
    applyImageMatrix(getWidth(), getHeight(), true, false);
  }

  /** Flips the image vertically. */
  public void flipImageVertically() {
    mFlipVertically = !mFlipVertically;
    applyImageMatrix(getWidth(), getHeight(), true, false);
  }

  // region: Private methods

  /**
   * On complete of the async bitmap loading by {@link #setImageUriAsync(Uri)} set the result to the
   * widget if still relevant and call listener if set.
   *
   * @param result the result of bitmap loading
   */
  void onSetImageUriAsyncComplete(BitmapLoadingWorkerTask.Result result) {

    mBitmapLoadingWorkerTask = null;
    setProgressBarVisibility();

    if (result.error == null) {
      mInitialDegreesRotated = result.degreesRotated;
      setBitmap(result.bitmap, 0, result.uri, result.loadSampleSize, result.degreesRotated);
    }

    OnSetImageUriCompleteListener listener = mOnSetImageUriCompleteListener;
    if (listener != null) {
      listener.onSetImageUriComplete(this, result.uri, result.error);
    }
  }

  /**
   * On complete of the async bitmap cropping by {@link #getCroppedImageAsync()} call listener if
   * set.
   *
   * @param result the result of bitmap cropping
   */
  void onImageCroppingAsyncComplete(BitmapCroppingWorkerTask.Result result) {

    mBitmapCroppingWorkerTask = null;
    setProgressBarVisibility();

    OnCropImageCompleteListener listener = mOnCropImageCompleteListener;
    if (listener != null) {
      CropResult cropResult =
          new CropResult(
              mBitmap,
              mLoadedImageUri,
              result.bitmap,
              result.uri,
              result.error,
              getCropPoints(),
              getCropRect(),
              getWholeImageRect(),
              getRotatedDegrees(),
              result.sampleSize);
      listener.onCropImageComplete(this, cropResult);
    }
  }

  /**
   * Set the given bitmap to be used in for cropping<br>
   * Optionally clear full if the bitmap is new, or partial clear if the bitmap has been
   * manipulated.
   */
  private void setBitmap(
      Bitmap bitmap, int imageResource, Uri imageUri, int loadSampleSize, int degreesRotated) {
    if (mBitmap == null || !mBitmap.equals(bitmap)) {

      mImageView.clearAnimation();

      clearImageInt();

      mBitmap = bitmap;
      mImageView.setImageBitmap(mBitmap);

      mLoadedImageUri = imageUri;
      mImageResource = imageResource;
      mLoadedSampleSize = loadSampleSize;
      mDegreesRotated = degreesRotated;

      applyImageMatrix(getWidth(), getHeight(), true, false);

      if (mCropOverlayView != null) {
        mCropOverlayView.resetCropOverlayView();
        setCropOverlayVisibility();
      }
    }
  }

  /**
   * Clear the current image set for cropping.<br>
   * Full clear will also clear the data of the set image like Uri or Resource id while partial
   * clear will only clear the bitmap and recycle if required.
   */
  private void clearImageInt() {

    // if we allocated the bitmap, release it as fast as possible
    if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) {
      mBitmap.recycle();
    }
    mBitmap = null;

    // clean the loaded image flags for new image
    mImageResource = 0;
    mLoadedImageUri = null;
    mLoadedSampleSize = 1;
    mDegreesRotated = 0;
    mZoom = 1;
    mZoomOffsetX = 0;
    mZoomOffsetY = 0;
    mImageMatrix.reset();
    mSaveInstanceStateBitmapUri = null;

    mImageView.setImageBitmap(null);

    setCropOverlayVisibility();
  }

  /**
   * Gets the cropped image based on the current crop window.<br>
   * If (reqWidth,reqHeight) is given AND image is loaded from URI cropping will try to use sample
   * size to fit in the requested width and height down-sampling if possible - optimization to get
   * best size to quality.<br>
   * The result will be invoked to listener set by {@link
   * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
   *
   * @param reqWidth the width to resize the cropped image to (see options)
   * @param reqHeight the height to resize the cropped image to (see options)
   * @param options the resize method to use on the cropped bitmap
   * @param saveUri optional: to save the cropped image to
   * @param saveCompressFormat if saveUri is given, the given compression will be used for saving
   *     the image
   * @param saveCompressQuality if saveUri is given, the given quality will be used for the
   *     compression.
   */
  public void startCropWorkerTask(
      int reqWidth,
      int reqHeight,
      RequestSizeOptions options,
      Uri saveUri,
      Bitmap.CompressFormat saveCompressFormat,
      int saveCompressQuality) {
    Bitmap bitmap = mBitmap;
    if (bitmap != null) {
      mImageView.clearAnimation();

      BitmapCroppingWorkerTask currentTask =
          mBitmapCroppingWorkerTask != null ? mBitmapCroppingWorkerTask.get() : null;
      if (currentTask != null) {
        // cancel previous cropping
        currentTask.cancel(true);
      }

      reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
      reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;

      int orgWidth = bitmap.getWidth() * mLoadedSampleSize;
      int orgHeight = bitmap.getHeight() * mLoadedSampleSize;
      if (mLoadedImageUri != null
          && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
        mBitmapCroppingWorkerTask =
            new WeakReference<>(
                new BitmapCroppingWorkerTask(
                    this,
                    mLoadedImageUri,
                    getCropPoints(),
                    mDegreesRotated,
                    orgWidth,
                    orgHeight,
                    mCropOverlayView.isFixAspectRatio(),
                    mCropOverlayView.getAspectRatioX(),
                    mCropOverlayView.getAspectRatioY(),
                    reqWidth,
                    reqHeight,
                    mFlipHorizontally,
                    mFlipVertically,
                    options,
                    saveUri,
                    saveCompressFormat,
                    saveCompressQuality));
      } else {
        mBitmapCroppingWorkerTask =
            new WeakReference<>(
                new BitmapCroppingWorkerTask(
                    this,
                    bitmap,
                    getCropPoints(),
                    mDegreesRotated,
                    mCropOverlayView.isFixAspectRatio(),
                    mCropOverlayView.getAspectRatioX(),
                    mCropOverlayView.getAspectRatioY(),
                    reqWidth,
                    reqHeight,
                    mFlipHorizontally,
                    mFlipVertically,
                    options,
                    saveUri,
                    saveCompressFormat,
                    saveCompressQuality));
      }
      mBitmapCroppingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
      setProgressBarVisibility();
    }
  }

  @Override
  public Parcelable onSaveInstanceState() {
    if (mLoadedImageUri == null && mBitmap == null && mImageResource < 1) {
      return super.onSaveInstanceState();
    }

    Bundle bundle = new Bundle();
    Uri imageUri = mLoadedImageUri;
    if (mSaveBitmapToInstanceState && imageUri == null && mImageResource < 1) {
      mSaveInstanceStateBitmapUri =
          imageUri =
              BitmapUtils.writeTempStateStoreBitmap(
                  getContext(), mBitmap, mSaveInstanceStateBitmapUri);
    }
    if (imageUri != null && mBitmap != null) {
      String key = UUID.randomUUID().toString();
      BitmapUtils.mStateBitmap = new Pair<>(key, new WeakReference<>(mBitmap));
      bundle.putString("LOADED_IMAGE_STATE_BITMAP_KEY", key);
    }
    if (mBitmapLoadingWorkerTask != null) {
      BitmapLoadingWorkerTask task = mBitmapLoadingWorkerTask.get();
      if (task != null) {
        bundle.putParcelable("LOADING_IMAGE_URI", task.getUri());
      }
    }
    bundle.putParcelable("instanceState", super.onSaveInstanceState());
    bundle.putParcelable("LOADED_IMAGE_URI", imageUri);
    bundle.putInt("LOADED_IMAGE_RESOURCE", mImageResource);
    bundle.putInt("LOADED_SAMPLE_SIZE", mLoadedSampleSize);
    bundle.putInt("DEGREES_ROTATED", mDegreesRotated);
    bundle.putParcelable("INITIAL_CROP_RECT", mCropOverlayView.getInitialCropWindowRect());

    BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());

    mImageMatrix.invert(mImageInverseMatrix);
    mImageInverseMatrix.mapRect(BitmapUtils.RECT);

    bundle.putParcelable("CROP_WINDOW_RECT", BitmapUtils.RECT);
    bundle.putString("CROP_SHAPE", mCropOverlayView.getCropShape().name());
    bundle.putBoolean("CROP_AUTO_ZOOM_ENABLED", mAutoZoomEnabled);
    bundle.putInt("CROP_MAX_ZOOM", mMaxZoom);
    bundle.putBoolean("CROP_FLIP_HORIZONTALLY", mFlipHorizontally);
    bundle.putBoolean("CROP_FLIP_VERTICALLY", mFlipVertically);

    return bundle;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state) {

    if (state instanceof Bundle) {
      Bundle bundle = (Bundle) state;

      // prevent restoring state if already set by outside code
      if (mBitmapLoadingWorkerTask == null
          && mLoadedImageUri == null
          && mBitmap == null
          && mImageResource == 0) {

        Uri uri = bundle.getParcelable("LOADED_IMAGE_URI");
        if (uri != null) {
          String key = bundle.getString("LOADED_IMAGE_STATE_BITMAP_KEY");
          if (key != null) {
            Bitmap stateBitmap =
                BitmapUtils.mStateBitmap != null && BitmapUtils.mStateBitmap.first.equals(key)
                    ? BitmapUtils.mStateBitmap.second.get()
                    : null;
            BitmapUtils.mStateBitmap = null;
            if (stateBitmap != null && !stateBitmap.isRecycled()) {
              setBitmap(stateBitmap, 0, uri, bundle.getInt("LOADED_SAMPLE_SIZE"), 0);
            }
          }
          if (mLoadedImageUri == null) {
            setImageUriAsync(uri);
          }
        } else {
          int resId = bundle.getInt("LOADED_IMAGE_RESOURCE");
          if (resId > 0) {
            setImageResource(resId);
          } else {
            uri = bundle.getParcelable("LOADING_IMAGE_URI");
            if (uri != null) {
              setImageUriAsync(uri);
            }
          }
        }

        mDegreesRotated = mRestoreDegreesRotated = bundle.getInt("DEGREES_ROTATED");

        Rect initialCropRect = bundle.getParcelable("INITIAL_CROP_RECT");
        if (initialCropRect != null
            && (initialCropRect.width() > 0 || initialCropRect.height() > 0)) {
          mCropOverlayView.setInitialCropWindowRect(initialCropRect);
        }

        RectF cropWindowRect = bundle.getParcelable("CROP_WINDOW_RECT");
        if (cropWindowRect != null && (cropWindowRect.width() > 0 || cropWindowRect.height() > 0)) {
          mRestoreCropWindowRect = cropWindowRect;
        }

        mCropOverlayView.setCropShape(CropShape.valueOf(bundle.getString("CROP_SHAPE")));

        mAutoZoomEnabled = bundle.getBoolean("CROP_AUTO_ZOOM_ENABLED");
        mMaxZoom = bundle.getInt("CROP_MAX_ZOOM");

        mFlipHorizontally = bundle.getBoolean("CROP_FLIP_HORIZONTALLY");
        mFlipVertically = bundle.getBoolean("CROP_FLIP_VERTICALLY");
      }

      super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
    } else {
      super.onRestoreInstanceState(state);
    }
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (mBitmap != null) {

      // Bypasses a baffling bug when used within a ScrollView, where heightSize is set to 0.
      if (heightSize == 0) {
        heightSize = mBitmap.getHeight();
      }

      int desiredWidth;
      int desiredHeight;

      double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
      double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;

      // Checks if either width or height needs to be fixed
      if (widthSize < mBitmap.getWidth()) {
        viewToBitmapWidthRatio = (double) widthSize / (double) mBitmap.getWidth();
      }
      if (heightSize < mBitmap.getHeight()) {
        viewToBitmapHeightRatio = (double) heightSize / (double) mBitmap.getHeight();
      }

      // If either needs to be fixed, choose smallest ratio and calculate from there
      if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY
          || viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
        if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
          desiredWidth = widthSize;
          desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio);
        } else {
          desiredHeight = heightSize;
          desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio);
        }
      } else {
        // Otherwise, the picture is within frame layout bounds. Desired width is simply picture
        // size
        desiredWidth = mBitmap.getWidth();
        desiredHeight = mBitmap.getHeight();
      }

      int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth);
      int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight);

      mLayoutWidth = width;
      mLayoutHeight = height;

      setMeasuredDimension(mLayoutWidth, mLayoutHeight);

    } else {
      setMeasuredDimension(widthSize, heightSize);
    }
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {

    super.onLayout(changed, l, t, r, b);

    if (mLayoutWidth > 0 && mLayoutHeight > 0) {
      // Gets original parameters, and creates the new parameters
      ViewGroup.LayoutParams origParams = this.getLayoutParams();
      origParams.width = mLayoutWidth;
      origParams.height = mLayoutHeight;
      setLayoutParams(origParams);

      if (mBitmap != null) {
        applyImageMatrix(r - l, b - t, true, false);

        // after state restore we want to restore the window crop, possible only after widget size
        // is known
        if (mRestoreCropWindowRect != null) {
          if (mRestoreDegreesRotated != mInitialDegreesRotated) {
            mDegreesRotated = mRestoreDegreesRotated;
            applyImageMatrix(r - l, b - t, true, false);
          }
          mImageMatrix.mapRect(mRestoreCropWindowRect);
          mCropOverlayView.setCropWindowRect(mRestoreCropWindowRect);
          handleCropWindowChanged(false, false);
          mCropOverlayView.fixCurrentCropWindowRect();
          mRestoreCropWindowRect = null;
        } else if (mSizeChanged) {
          mSizeChanged = false;
          handleCropWindowChanged(false, false);
        }
      } else {
        updateImageBounds(true);
      }
    } else {
      updateImageBounds(true);
    }
  }

  /**
   * Detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean, boolean)}
   * in {@link #layout(int, int, int, int)}.
   */
  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mSizeChanged = oldw > 0 && oldh > 0;
  }

  /**
   * Handle crop window change to:<br>
   * 1. Execute auto-zoom-in/out depending on the area covered of cropping window relative to the
   * available view area.<br>
   * 2. Slide the zoomed sub-area if the cropping window is outside of the visible view sub-area.
   * <br>
   *
   * @param inProgress is the crop window change is still in progress by the user
   * @param animate if to animate the change to the image matrix, or set it directly
   */
  private void handleCropWindowChanged(boolean inProgress, boolean animate) {
    int width = getWidth();
    int height = getHeight();
    if (mBitmap != null && width > 0 && height > 0) {

      RectF cropRect = mCropOverlayView.getCropWindowRect();
      if (inProgress) {
        if (cropRect.left < 0
            || cropRect.top < 0
            || cropRect.right > width
            || cropRect.bottom > height) {
          applyImageMatrix(width, height, false, false);
        }
      } else if (mAutoZoomEnabled || mZoom > 1) {
        float newZoom = 0;
        // keep the cropping window covered area to 50%-65% of zoomed sub-area
        if (mZoom < mMaxZoom
            && cropRect.width() < width * 0.5f
            && cropRect.height() < height * 0.5f) {
          newZoom =
              Math.min(
                  mMaxZoom,
                  Math.min(
                      width / (cropRect.width() / mZoom / 0.64f),
                      height / (cropRect.height() / mZoom / 0.64f)));
        }
        if (mZoom > 1 && (cropRect.width() > width * 0.65f || cropRect.height() > height * 0.65f)) {
          newZoom =
              Math.max(
                  1,
                  Math.min(
                      width / (cropRect.width() / mZoom / 0.51f),
                      height / (cropRect.height() / mZoom / 0.51f)));
        }
        if (!mAutoZoomEnabled) {
          newZoom = 1;
        }

        if (newZoom > 0 && newZoom != mZoom) {
          if (animate) {
            if (mAnimation == null) {
              // lazy create animation single instance
              mAnimation = new CropImageAnimation(mImageView, mCropOverlayView);
            }
            // set the state for animation to start from
            mAnimation.setStartState(mImagePoints, mImageMatrix);
          }

          mZoom = newZoom;

          applyImageMatrix(width, height, true, animate);
        }
      }
      if (mOnSetCropWindowChangeListener != null && !inProgress) {
        mOnSetCropWindowChangeListener.onCropWindowChanged();
      }
    }
  }

  /**
   * Apply matrix to handle the image inside the image view.
   *
   * @param width the width of the image view
   * @param height the height of the image view
   */
  private void applyImageMatrix(float width, float height, boolean center, boolean animate) {
    if (mBitmap != null && width > 0 && height > 0) {

      mImageMatrix.invert(mImageInverseMatrix);
      RectF cropRect = mCropOverlayView.getCropWindowRect();
      mImageInverseMatrix.mapRect(cropRect);

      mImageMatrix.reset();

      // move the image to the center of the image view first so we can manipulate it from there
      mImageMatrix.postTranslate(
          (width - mBitmap.getWidth()) / 2, (height - mBitmap.getHeight()) / 2);
      mapImagePointsByImageMatrix();

      // rotate the image the required degrees from center of image
      if (mDegreesRotated > 0) {
        mImageMatrix.postRotate(
            mDegreesRotated,
            BitmapUtils.getRectCenterX(mImagePoints),
            BitmapUtils.getRectCenterY(mImagePoints));
        mapImagePointsByImageMatrix();
      }

      // scale the image to the image view, image rect transformed to know new width/height
      float scale =
          Math.min(
              width / BitmapUtils.getRectWidth(mImagePoints),
              height / BitmapUtils.getRectHeight(mImagePoints));
      if (mScaleType == ScaleType.FIT_CENTER
          || (mScaleType == ScaleType.CENTER_INSIDE && scale < 1)
          || (scale > 1 && mAutoZoomEnabled)) {
        mImageMatrix.postScale(
            scale,
            scale,
            BitmapUtils.getRectCenterX(mImagePoints),
            BitmapUtils.getRectCenterY(mImagePoints));
        mapImagePointsByImageMatrix();
      }

      // scale by the current zoom level
      float scaleX = mFlipHorizontally ? -mZoom : mZoom;
      float scaleY = mFlipVertically ? -mZoom : mZoom;
      mImageMatrix.postScale(
          scaleX,
          scaleY,
          BitmapUtils.getRectCenterX(mImagePoints),
          BitmapUtils.getRectCenterY(mImagePoints));
      mapImagePointsByImageMatrix();

      mImageMatrix.mapRect(cropRect);

      if (center) {
        // set the zoomed area to be as to the center of cropping window as possible
        mZoomOffsetX =
            width > BitmapUtils.getRectWidth(mImagePoints)
                ? 0
                : Math.max(
                        Math.min(
                            width / 2 - cropRect.centerX(), -BitmapUtils.getRectLeft(mImagePoints)),
                        getWidth() - BitmapUtils.getRectRight(mImagePoints))
                    / scaleX;
        mZoomOffsetY =
            height > BitmapUtils.getRectHeight(mImagePoints)
                ? 0
                : Math.max(
                        Math.min(
                            height / 2 - cropRect.centerY(), -BitmapUtils.getRectTop(mImagePoints)),
                        getHeight() - BitmapUtils.getRectBottom(mImagePoints))
                    / scaleY;
      } else {
        // adjust the zoomed area so the crop window rectangle will be inside the area in case it
        // was moved outside
        mZoomOffsetX =
            Math.min(Math.max(mZoomOffsetX * scaleX, -cropRect.left), -cropRect.right + width)
                / scaleX;
        mZoomOffsetY =
            Math.min(Math.max(mZoomOffsetY * scaleY, -cropRect.top), -cropRect.bottom + height)
                / scaleY;
      }

      // apply to zoom offset translate and update the crop rectangle to offset correctly
      mImageMatrix.postTranslate(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
      cropRect.offset(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
      mCropOverlayView.setCropWindowRect(cropRect);
      mapImagePointsByImageMatrix();
      mCropOverlayView.invalidate();

      // set matrix to apply
      if (animate) {
        // set the state for animation to end in, start animation now
        mAnimation.setEndState(mImagePoints, mImageMatrix);
        mImageView.startAnimation(mAnimation);
      } else {
        mImageView.setImageMatrix(mImageMatrix);
      }

      // update the image rectangle in the crop overlay
      updateImageBounds(false);
    }
  }

  /**
   * Adjust the given image rectangle by image transformation matrix to know the final rectangle of
   * the image.<br>
   * To get the proper rectangle it must be first reset to original image rectangle.
   */
  private void mapImagePointsByImageMatrix() {
    mImagePoints[0] = 0;
    mImagePoints[1] = 0;
    mImagePoints[2] = mBitmap.getWidth();
    mImagePoints[3] = 0;
    mImagePoints[4] = mBitmap.getWidth();
    mImagePoints[5] = mBitmap.getHeight();
    mImagePoints[6] = 0;
    mImagePoints[7] = mBitmap.getHeight();
    mImageMatrix.mapPoints(mImagePoints);
    mScaleImagePoints[0] = 0;
    mScaleImagePoints[1] = 0;
    mScaleImagePoints[2] = 100;
    mScaleImagePoints[3] = 0;
    mScaleImagePoints[4] = 100;
    mScaleImagePoints[5] = 100;
    mScaleImagePoints[6] = 0;
    mScaleImagePoints[7] = 100;
    mImageMatrix.mapPoints(mScaleImagePoints);
  }

  /**
   * Determines the specs for the onMeasure function. Calculates the width or height depending on
   * the mode.
   *
   * @param measureSpecMode The mode of the measured width or height.
   * @param measureSpecSize The size of the measured width or height.
   * @param desiredSize The desired size of the measured width or height.
   * @return The final size of the width or height.
   */
  private static int getOnMeasureSpec(int measureSpecMode, int measureSpecSize, int desiredSize) {

    // Measure Width
    int spec;
    if (measureSpecMode == MeasureSpec.EXACTLY) {
      // Must be this size
      spec = measureSpecSize;
    } else if (measureSpecMode == MeasureSpec.AT_MOST) {
      // Can't be bigger than...; match_parent value
      spec = Math.min(desiredSize, measureSpecSize);
    } else {
      // Be whatever you want; wrap_content
      spec = desiredSize;
    }

    return spec;
  }

  /**
   * Set visibility of crop overlay to hide it when there is no image or specificly set by client.
   */
  private void setCropOverlayVisibility() {
    if (mCropOverlayView != null) {
      mCropOverlayView.setVisibility(mShowCropOverlay && mBitmap != null ? VISIBLE : INVISIBLE);
    }
  }

  /**
   * Set visibility of progress bar when async loading/cropping is in process and show is enabled.
   */
  private void setProgressBarVisibility() {
    boolean visible =
        mShowProgressBar
            && (mBitmap == null && mBitmapLoadingWorkerTask != null
                || mBitmapCroppingWorkerTask != null);
    mProgressBar.setVisibility(visible ? VISIBLE : INVISIBLE);
  }

  /** Update the scale factor between the actual image bitmap and the shown image.<br> */
  private void updateImageBounds(boolean clear) {
    if (mBitmap != null && !clear) {

      // Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for
      // width/height.
      float scaleFactorWidth =
          100f * mLoadedSampleSize / BitmapUtils.getRectWidth(mScaleImagePoints);
      float scaleFactorHeight =
          100f * mLoadedSampleSize / BitmapUtils.getRectHeight(mScaleImagePoints);
      mCropOverlayView.setCropWindowLimits(
          getWidth(), getHeight(), scaleFactorWidth, scaleFactorHeight);
    }

    // set the bitmap rectangle and update the crop window after scale factor is set
    mCropOverlayView.setBounds(clear ? null : mImagePoints, getWidth(), getHeight());
  }
  // endregion

  // region: Inner class: CropShape

  /**
   * The possible cropping area shape.<br>
   * To set square/circle crop shape set aspect ratio to 1:1.
   */
  public enum CropShape {
    RECTANGLE,
    OVAL
  }
  // endregion

  // region: Inner class: ScaleType

  /**
   * Options for scaling the bounds of cropping image to the bounds of Crop Image View.<br>
   * Note: Some options are affected by auto-zoom, if enabled.
   */
  public enum ScaleType {

    /**
     * Scale the image uniformly (maintain the image's aspect ratio) to fit in crop image view.<br>
     * The largest dimension will be equals to crop image view and the second dimension will be
     * smaller.
     */
    FIT_CENTER,

    /**
     * Center the image in the view, but perform no scaling.<br>
     * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
     * will be scaled uniformly to fit the crop image view.
     */
    CENTER,

    /**
     * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
     * and height) of the image will be equal to or <b>larger</b> than the corresponding dimension
     * of the view (minus padding).<br>
     * The image is then centered in the view.
     */
    CENTER_CROP,

    /**
     * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
     * and height) of the image will be equal to or <b>less</b> than the corresponding dimension of
     * the view (minus padding).<br>
     * The image is then centered in the view.<br>
     * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
     * will be scaled uniformly to fit the crop image view.
     */
    CENTER_INSIDE
  }
  // endregion

  // region: Inner class: Guidelines

  /** The possible guidelines showing types. */
  public enum Guidelines {
    /** Never show */
    OFF,

    /** Show when crop move action is live */
    ON_TOUCH,

    /** Always show */
    ON
  }
  // endregion

  // region: Inner class: RequestSizeOptions

  /** Possible options for handling requested width/height for cropping. */
  public enum RequestSizeOptions {

    /** No resize/sampling is done unless required for memory management (OOM). */
    NONE,

    /**
     * Only sample the image during loading (if image set using URI) so the smallest of the image
     * dimensions will be between the requested size and x2 requested size.<br>
     * NOTE: resulting image will not be exactly requested width/height see: <a
     * href="http://developer.android.com/training/displaying-bitmaps/load-bitmap.html">Loading
     * Large Bitmaps Efficiently</a>.
     */
    SAMPLING,

    /**
     * Resize the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
     * and height) of the image will be equal to or <b>less</b> than the corresponding requested
     * dimension.<br>
     * If the image is smaller than the requested size it will NOT change.
     */
    RESIZE_INSIDE,

    /**
     * Resize the image uniformly (maintain the image's aspect ratio) to fit in the given
     * width/height.<br>
     * The largest dimension will be equals to the requested and the second dimension will be
     * smaller.<br>
     * If the image is smaller than the requested size it will enlarge it.
     */
    RESIZE_FIT,

    /**
     * Resize the image to fit exactly in the given width/height.<br>
     * This resize method does NOT preserve aspect ratio.<br>
     * If the image is smaller than the requested size it will enlarge it.
     */
    RESIZE_EXACT
  }
  // endregion

  // region: Inner class: OnSetImageUriCompleteListener

  /** Interface definition for a callback to be invoked when the crop overlay is released. */
  public interface OnSetCropOverlayReleasedListener {

    /**
     * Called when the crop overlay changed listener is called and inProgress is false.
     *
     * @param rect The rect coordinates of the cropped overlay
     */
    void onCropOverlayReleased(Rect rect);
  }

  /** Interface definition for a callback to be invoked when the crop overlay is released. */
  public interface OnSetCropOverlayMovedListener {

    /**
     * Called when the crop overlay is moved
     *
     * @param rect The rect coordinates of the cropped overlay
     */
    void onCropOverlayMoved(Rect rect);
  }

  /** Interface definition for a callback to be invoked when the crop overlay is released. */
  public interface OnSetCropWindowChangeListener {

    /** Called when the crop window is changed */
    void onCropWindowChanged();
  }

  /** Interface definition for a callback to be invoked when image async loading is complete. */
  public interface OnSetImageUriCompleteListener {

    /**
     * Called when a crop image view has completed loading image for cropping.<br>
     * If loading failed error parameter will contain the error.
     *
     * @param view The crop image view that loading of image was complete.
     * @param uri the URI of the image that was loading
     * @param error if error occurred during loading will contain the error, otherwise null.
     */
    void onSetImageUriComplete(CropImageView view, Uri uri, Exception error);
  }
  // endregion

  // region: Inner class: OnGetCroppedImageCompleteListener

  /** Interface definition for a callback to be invoked when image async crop is complete. */
  public interface OnCropImageCompleteListener {

    /**
     * Called when a crop image view has completed cropping image.<br>
     * Result object contains the cropped bitmap, saved cropped image uri, crop points data or the
     * error occured during cropping.
     *
     * @param view The crop image view that cropping of image was complete.
     * @param result the crop image result data (with cropped image or error)
     */
    void onCropImageComplete(CropImageView view, CropResult result);
  }
  // endregion

  // region: Inner class: ActivityResult

  /** Result data of crop image. */
  public static class CropResult {

    /**
     * The image bitmap of the original image loaded for cropping.<br>
     * Null if uri used to load image or activity result is used.
     */
    private final Bitmap mOriginalBitmap;

    /**
     * The Android uri of the original image loaded for cropping.<br>
     * Null if bitmap was used to load image.
     */
    private final Uri mOriginalUri;

    /**
     * The cropped image bitmap result.<br>
     * Null if save cropped image was executed, no output requested or failure.
     */
    private final Bitmap mBitmap;

    /**
     * The Android uri of the saved cropped image result.<br>
     * Null if get cropped image was executed, no output requested or failure.
     */
    private final Uri mUri;

    /** The error that failed the loading/cropping (null if successful) */
    private final Exception mError;

    /** The 4 points of the cropping window in the source image */
    private final float[] mCropPoints;

    /** The rectangle of the cropping window in the source image */
    private final Rect mCropRect;

    /** The rectangle of the source image dimensions */
    private final Rect mWholeImageRect;

    /** The final rotation of the cropped image relative to source */
    private final int mRotation;

    /** sample size used creating the crop bitmap to lower its size */
    private final int mSampleSize;

    CropResult(
        Bitmap originalBitmap,
        Uri originalUri,
        Bitmap bitmap,
        Uri uri,
        Exception error,
        float[] cropPoints,
        Rect cropRect,
        Rect wholeImageRect,
        int rotation,
        int sampleSize) {
      mOriginalBitmap = originalBitmap;
      mOriginalUri = originalUri;
      mBitmap = bitmap;
      mUri = uri;
      mError = error;
      mCropPoints = cropPoints;
      mCropRect = cropRect;
      mWholeImageRect = wholeImageRect;
      mRotation = rotation;
      mSampleSize = sampleSize;
    }

    /**
     * The image bitmap of the original image loaded for cropping.<br>
     * Null if uri used to load image or activity result is used.
     */
    public Bitmap getOriginalBitmap() {
      return mOriginalBitmap;
    }

    /**
     * The Android uri of the original image loaded for cropping.<br>
     * Null if bitmap was used to load image.
     */
    public Uri getOriginalUri() {
      return mOriginalUri;
    }

    /** Is the result is success or error. */
    public boolean isSuccessful() {
      return mError == null;
    }

    /**
     * The cropped image bitmap result.<br>
     * Null if save cropped image was executed, no output requested or failure.
     */
    public Bitmap getBitmap() {
      return mBitmap;
    }

    /**
     * The Android uri of the saved cropped image result Null if get cropped image was executed, no
     * output requested or failure.
     */
    public Uri getUri() {
      return mUri;
    }

    /** The error that failed the loading/cropping (null if successful) */
    public Exception getError() {
      return mError;
    }

    /** The 4 points of the cropping window in the source image */
    public float[] getCropPoints() {
      return mCropPoints;
    }

    /** The rectangle of the cropping window in the source image */
    public Rect getCropRect() {
      return mCropRect;
    }

    /** The rectangle of the source image dimensions */
    public Rect getWholeImageRect() {
      return mWholeImageRect;
    }

    /** The final rotation of the cropped image relative to source */
    public int getRotation() {
      return mRotation;
    }

    /** sample size used creating the crop bitmap to lower its size */
    public int getSampleSize() {
      return mSampleSize;
    }
  }
  // endregion
}
