content: /*
   * Copyright (C) 2007 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.onegravity.rteditor.media.crop;
  
  import android.app.Activity;
  import android.content.Intent;
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.graphics.Canvas;
  import android.graphics.Matrix;
  import android.graphics.Path;
  import android.graphics.PointF;
  import android.graphics.PorterDuff;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.graphics.Region;
  import android.media.FaceDetector;
  import android.net.Uri;
  import android.os.Bundle;
  import android.util.Log;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.Surface;
  import android.view.View;
  
  import com.onegravity.rteditor.R;
  import com.onegravity.rteditor.media.MediaUtils;
  import com.onegravity.rteditor.media.MonitoredActivity;
  import com.onegravity.rteditor.utils.Helper;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  
  /**
   * The activity can crop specific region of interest from an image.
   */
  public class CropImageActivity extends MonitoredActivity {
  
      private final float IMAGE_MAX_SIZE = 1024f;
  
      public static final String IMAGE_SOURCE_FILE = "image-source-file";
      public static final String IMAGE_DESTINATION_FILE = "image-dest-file";
      public static final String SCALE = "scale";
      public static final String ORIENTATION_IN_DEGREES = "orientation_in_degrees";
      public static final String ASPECT_X = "aspectX";
      public static final String ASPECT_Y = "aspectY";
      public static final String OUTPUT_X = "outputX";
      public static final String OUTPUT_Y = "outputY";
      public static final String SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
      public static final String CIRCLE_CROP = "circleCrop";
      public static final String RETURN_DATA = "return-data";
      public static final String RETURN_DATA_AS_BITMAP = "data";
      public static final String ACTION_INLINE_DATA = "inline-data";
  
      // These are various options can be specified in the intent.
      private Bitmap.CompressFormat mOutputFormat = Bitmap.CompressFormat.JPEG;
      private Uri mSaveUri = null;
      private boolean mDoFaceDetection = false;
      private boolean mCircleCrop = false;
  
      private int mAspectX;
      private int mAspectY;
      private int mOutputX;
      private int mOutputY;
      private boolean mScale;
      private CropImageView mImageView;
      private Bitmap mBitmap;
      private String mImageSource;
      private String mImageDest;
  
      boolean mWaitingToPick; // Whether we are wait the user to pick a face.
      boolean mSaving; // Whether the "save" button is already clicked.
      HighlightView mCrop;
  
      // These options specify the output image size and whether we should
      // scale the output to fit it (or just crop it).
      private boolean mScaleUp = true;
  
      private final BitmapManager.ThreadSet mDecodingThreads = new BitmapManager.ThreadSet();
  
      // ****************************************** Lifecycle Methods *******************************************
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
  
          setContentView(R.layout.rte_crop_image);
          mImageView = (CropImageView) findViewById(R.id.image);
  
          Intent intent = getIntent();
          Bundle extras = intent.getExtras();
          if (extras != null) {
  
              if (extras.getString(CIRCLE_CROP) != null) {
                  mImageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
                  mCircleCrop = true;
                  mAspectX = 1;
                  mAspectY = 1;
              }
  
              mImageSource = extras.getString(IMAGE_SOURCE_FILE);
              mBitmap = getBitmap(mImageSource);
  
              mImageDest = extras.getString(IMAGE_DESTINATION_FILE);
              if (mImageDest == null) {
                  mImageDest = mImageSource;
              }
              mSaveUri = Uri.fromFile(new File(mImageDest));
  
              if (extras.containsKey(ASPECT_X)
                      && extras.get(ASPECT_X) instanceof Integer) {
                  mAspectX = extras.getInt(ASPECT_X);
              } else {
                  throw new IllegalArgumentException("aspect_x must be integer");
              }
              if (extras.containsKey(ASPECT_Y)
                      && extras.get(ASPECT_Y) instanceof Integer) {
                  mAspectY = extras.getInt(ASPECT_Y);
              } else {
                  throw new IllegalArgumentException("aspect_y must be integer");
              }
              mOutputX = extras.getInt(OUTPUT_X);
              mOutputY = extras.getInt(OUTPUT_Y);
              mScale = extras.getBoolean(SCALE, true);
              mScaleUp = extras.getBoolean(SCALE_UP_IF_NEEDED, true);
          }
  
          if (mBitmap == null) {
              finish();
              return;
          }
  
          startFaceDetection();
      }
  
      // ****************************************** ActionBar / Option Menu *******************************************
  
      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
          super.onCreateOptionsMenu(menu);
          getMenuInflater().inflate(R.menu.rte_crop_image, menu);
          return true;
      }
  
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
          int itemId = item.getItemId();
          if (itemId == R.id.save) {
              try {
                  onSave();
              } catch (Exception e) {
                  Log.e(getClass().getSimpleName(), e.getMessage(), e);
                  finish();
              }
              return true;
          } else if (itemId == R.id.cancel) {
              setResult(RESULT_CANCELED);
              finish();
              return true;
          } else if (itemId == R.id.rotate_left) {
              mBitmap = rotateImage(mBitmap, -90);
              RotateBitmap rotateBitmap = new RotateBitmap(mBitmap);
              mImageView.setImageRotateBitmapResetBase(rotateBitmap, true);
              mRunFaceDetection.run();
              return true;
          } else if (itemId == R.id.rotate_right) {
              mBitmap = rotateImage(mBitmap, 90);
              RotateBitmap rotateBitmap = new RotateBitmap(mBitmap);
              mImageView.setImageRotateBitmapResetBase(rotateBitmap, true);
              mRunFaceDetection.run();
              return true;
          }
          return super.onOptionsItemSelected(item);
      }
  
      // ****************************************** Private Methods *******************************************
  
      private Bitmap getBitmap(String path) {
          Uri uri = MediaUtils.createFileUri(path);
          InputStream in = null;
          try {
              in = getContentResolver().openInputStream(uri);
  
              // Decode image size
              BitmapFactory.Options options = new BitmapFactory.Options();
              options.inJustDecodeBounds = true;
              BitmapFactory.decodeStream(in, null, options);
              Helper.closeQuietly(in);
  
              // compute scale factor to ensure that the image is smaller than
              // IMAGE_MAX_SIZE
              float maxSize = Math.max(options.outWidth, options.outHeight);
              float scale = maxSize > IMAGE_MAX_SIZE ? maxSize / IMAGE_MAX_SIZE
                      : 1.0f;
  
              while ((maxSize / scale) > 8) {
                  try {
                      return getBitmap(in, uri, scale);
                  } catch (Throwable e) {
                      Log.w(getClass().getSimpleName(),
                              "bitmap could not be created (probably out of memory), decreasing size and retrying");
                      scale *= 2f;
                  }
              }
          } catch (IOException e) {
              Log.e(getClass().getSimpleName(), "file " + path + " not found");
          } catch (Exception e) {
              Log.e(getClass().getSimpleName(), "error while opening image", e);
          } finally {
              Helper.closeQuietly(in);
          }
          return null;
      }
  
      private Bitmap getBitmap(InputStream in, Uri uri, float scale)
              throws Throwable {
          // decode image
          BitmapFactory.Options options = new BitmapFactory.Options();
          options.inJustDecodeBounds = false;
          options.inSampleSize = Math.round(scale);
          in = getContentResolver().openInputStream(uri);
          return BitmapFactory.decodeStream(in, null, options);
      }
  
      private void startFaceDetection() {
  
          if (isFinishing()) {
              return;
          }
  
          mImageView.setImageBitmapResetBase(mBitmap, true);
  
          startBackgroundJob(R.string.rte_processing_image, new Runnable() {
              public void run() {
                  final Bitmap b = mBitmap;
                  if (b != mBitmap && b != null) {
                      mImageView.setImageBitmapResetBase(b, true);
                      mBitmap.recycle();
                      mBitmap = b;
                  }
                  if (mImageView.getScale() == 1F) {
                      mImageView.center(true, true);
                  }
                  mRunFaceDetection.run();
              }
          });
      }
  
      private void onSave() throws Exception {
          // TODO this code needs to change to use the decode/crop/encode single
          // step api so that we don't require that the whole (possibly large)
          // bitmap doesn't have to be read into memory
          if (mSaving)
              return;
  
          if (mCrop == null) {
  
              return;
          }
  
          mSaving = true;
  
          Rect r = mCrop.getCropRect();
  
          int width = r.width();
          int height = r.height();
  
          // If we are circle cropping, we want alpha channel, which is the third
          // param here.
          Bitmap croppedImage;
          try {
              croppedImage = Bitmap.createBitmap(width, height, mCircleCrop ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
          } catch (Exception e) {
              throw e;
          }
          if (croppedImage == null) {
              return;
          }
  
          {
              Canvas canvas = new Canvas(croppedImage);
              Rect dstRect = new Rect(0, 0, width, height);
              canvas.drawBitmap(mBitmap, r, dstRect, null);
          }
  
          if (mCircleCrop) {
              // OK, so what's all this about?
              // Bitmaps are inherently rectangular but we want to return
              // something that's basically a circle. So we fill in the
              // area around the circle with alpha. Note the all important
              // PortDuff.Mode.CLEAR.
              Canvas c = new Canvas(croppedImage);
              Path p = new Path();
              p.addCircle(width / 2F, height / 2F, width / 2F, Path.Direction.CW);
              c.clipPath(p, Region.Op.DIFFERENCE);
              c.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
          }
  
          /* If the output is required to a specific size then scale or fill */
          if (mOutputX != 0 && mOutputY != 0) {
  
              if (mScale) {
  
                  /* Scale the image to the required dimensions */
                  Bitmap old = croppedImage;
                  croppedImage = transform(new Matrix(), croppedImage, mOutputX,
                          mOutputY, mScaleUp);
                  if (old != croppedImage) {
  
                      old.recycle();
                  }
              } else {
  
                  /*
                   * Don't scale the image crop it to the size requested. Create
                   * an new image with the cropped image in the center and the
                   * extra space filled.
                   */
  
                  // Don't scale the image but instead fill it so it's the
                  // required dimension
                  Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY,
                          Bitmap.Config.RGB_565);
                  Canvas canvas = new Canvas(b);
  
                  Rect srcRect = mCrop.getCropRect();
                  Rect dstRect = new Rect(0, 0, mOutputX, mOutputY);
  
                  int dx = (srcRect.width() - dstRect.width()) / 2;
                  int dy = (srcRect.height() - dstRect.height()) / 2;
  
                  /* If the srcRect is too big, use the center part of it. */
                  srcRect.inset(Math.max(0, dx), Math.max(0, dy));
  
                  /* If the dstRect is too big, use the center part of it. */
                  dstRect.inset(Math.max(0, -dx), Math.max(0, -dy));
  
                  /* Draw the cropped bitmap in the center */
                  canvas.drawBitmap(mBitmap, srcRect, dstRect, null);
  
                  /* Set the cropped bitmap as the new bitmap */
                  croppedImage.recycle();
                  croppedImage = b;
              }
          }
  
          // Return the cropped image directly or save it to the specified URI.
          Bundle myExtras = getIntent().getExtras();
          if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean(RETURN_DATA))) {
              Bundle extras = new Bundle();
              extras.putParcelable(RETURN_DATA_AS_BITMAP, croppedImage);
              setResult(RESULT_OK, (new Intent()).setAction(ACTION_INLINE_DATA).putExtras(extras));
              finish();
          } else {
              final Bitmap b = croppedImage;
              startBackgroundJob(R.string.rte_processing_image, new Runnable() {
                  public void run() {
                      saveOutput(b);
                  }
              });
          }
      }
  
      private void saveOutput(Bitmap croppedImage) {
          if (mSaveUri != null) {
              OutputStream out = null;
              try {
                  out = getContentResolver().openOutputStream(mSaveUri);
                  if (out != null) {
                      croppedImage.compress(mOutputFormat, 90, out);
                  }
              } catch (IOException ex) {
                  Log.e(getClass().getSimpleName(), "Cannot open file: " + mSaveUri, ex);
                  setResult(RESULT_CANCELED);
                  finish();
                  return;
              } finally {
                  Helper.closeQuietly(out);
              }
  
              Bundle extras = new Bundle();
              Intent intent = new Intent(mSaveUri.toString());
              intent.putExtras(extras);
              intent.putExtra(IMAGE_SOURCE_FILE, mImageSource);
              intent.putExtra(IMAGE_DESTINATION_FILE, mImageDest);
              intent.putExtra(ORIENTATION_IN_DEGREES,
                      getOrientationInDegree(this));
              setResult(RESULT_OK, intent);
  
          } else {
              Log.e(getClass().getSimpleName(), "not defined image url");
          }
          croppedImage.recycle();
          finish();
      }
  
      @Override
      protected void onPause() {
          super.onPause();
          BitmapManager.instance().cancelThreadDecoding(mDecodingThreads);
      }
  
      @Override
      protected void onDestroy() {
          super.onDestroy();
          if (mBitmap != null) {
              mBitmap.recycle();
          }
      }
  
      Runnable mRunFaceDetection = new Runnable() {
          float mScale = 1F;
          Matrix mImageMatrix;
          FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
          int mNumFaces;
  
          // For each face, we create a HightlightView for it.
          private void handleFace(FaceDetector.Face f) {
  
              PointF midPoint = new PointF();
  
              int r = ((int) (f.eyesDistance() * mScale)) * 2;
              f.getMidPoint(midPoint);
              midPoint.x *= mScale;
              midPoint.y *= mScale;
  
              int midX = (int) midPoint.x;
              int midY = (int) midPoint.y;
  
              HighlightView hv = new HighlightView(mImageView);
  
              int width = mBitmap.getWidth();
              int height = mBitmap.getHeight();
  
              Rect imageRect = new Rect(0, 0, width, height);
  
              RectF faceRect = new RectF(midX, midY, midX, midY);
              faceRect.inset(-r, -r);
              if (faceRect.left < 0) {
                  faceRect.inset(-faceRect.left, -faceRect.left);
              }
  
              if (faceRect.top < 0) {
                  faceRect.inset(-faceRect.top, -faceRect.top);
              }
  
              if (faceRect.right > imageRect.right) {
                  faceRect.inset(faceRect.right - imageRect.right, faceRect.right
                          - imageRect.right);
              }
  
              if (faceRect.bottom > imageRect.bottom) {
                  faceRect.inset(faceRect.bottom - imageRect.bottom,
                          faceRect.bottom - imageRect.bottom);
              }
  
              hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop,
                      mAspectX != 0 && mAspectY != 0);
  
              mImageView.add(hv);
          }
  
          // Create a default HightlightView if we found no face in the picture.
          private void makeDefault() {
  
              HighlightView hv = new HighlightView(mImageView);
  
              int width = mBitmap.getWidth();
              int height = mBitmap.getHeight();
  
              Rect imageRect = new Rect(0, 0, width, height);
  
              // make the default size about 4/5 of the width or height
              /*
               * int cropWidth = Math.min(width, height) * 4 / 5; int cropHeight =
               * cropWidth;
               */
              int cropWidth = width;
              int cropHeight = height;
  
              if (mAspectX != 0 && mAspectY != 0) {
  
                  if (mAspectX > mAspectY) {
  
                      cropHeight = cropWidth * mAspectY / mAspectX;
                  } else {
  
                      cropWidth = cropHeight * mAspectX / mAspectY;
                  }
              }
  
              int x = (width - cropWidth) / 2;
              int y = (height - cropHeight) / 2;
  
              RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
              hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop,
                      mAspectX != 0 && mAspectY != 0);
  
              mImageView.mHighlightViews.clear(); // Thong added for rotate
  
              mImageView.add(hv);
          }
  
          // Scale the image down for faster face detection.
          private Bitmap prepareBitmap() {
  
              if (mBitmap == null) {
  
                  return null;
              }
  
              // 256 pixels wide is enough.
              if (mBitmap.getWidth() > 256) {
  
                  mScale = 256.0F / mBitmap.getWidth();
              }
              Matrix matrix = new Matrix();
              matrix.setScale(mScale, mScale);
              return Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(),
                      mBitmap.getHeight(), matrix, true);
          }
  
          public void run() {
  
              mImageMatrix = mImageView.getImageMatrix();
              Bitmap faceBitmap = prepareBitmap();
  
              mScale = 1.0F / mScale;
              if (faceBitmap != null && mDoFaceDetection) {
                  FaceDetector detector = new FaceDetector(faceBitmap.getWidth(),
                          faceBitmap.getHeight(), mFaces.length);
                  mNumFaces = detector.findFaces(faceBitmap, mFaces);
              }
  
              if (faceBitmap != null && faceBitmap != mBitmap) {
                  faceBitmap.recycle();
              }
  
              mHandler.post(new Runnable() {
                  public void run() {
                      mWaitingToPick = mNumFaces > 1;
                      if (mNumFaces > 0) {
                          for (int i = 0; i < mNumFaces; i++) {
                              handleFace(mFaces[i]);
                          }
                      } else {
                          makeDefault();
                      }
                      mImageView.invalidate();
                      if (mImageView.mHighlightViews.size() == 1) {
                          mCrop = mImageView.mHighlightViews.get(0);
                          mCrop.setFocus(true);
                      }
                  }
              });
          }
      };
  
      /*
       * Compute the sample size as a function of minSideLength and
       * maxNumOfPixels. minSideLength is used to specify that minimal width or
       * height of a bitmap. maxNumOfPixels is used to specify the maximal size in
       * pixels that are tolerable in terms of memory usage.
       *
       * The function returns a sample size based on the constraints. Both size
       * and minSideLength can be passed in as IImage.UNCONSTRAINED, which
       * indicates no care of the corresponding constraint. The functions prefers
       * returning a sample size that generates a smaller bitmap, unless
       * minSideLength = IImage.UNCONSTRAINED.
       */
      private Bitmap transform(Matrix scaler, Bitmap source, int targetWidth,
                               int targetHeight, boolean scaleUp) {
  
          int deltaX = source.getWidth() - targetWidth;
          int deltaY = source.getHeight() - targetHeight;
          if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
              /*
               * In this case the bitmap is smaller, at least in one dimension,
               * than the target. Transform it by placing as much of the image as
               * possible into the target and leaving the top/bottom or left/right
               * (or both) black.
               */
              Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
                      Bitmap.Config.ARGB_8888);
              Canvas c = new Canvas(b2);
  
              int deltaXHalf = Math.max(0, deltaX / 2);
              int deltaYHalf = Math.max(0, deltaY / 2);
              Rect src = new Rect(deltaXHalf, deltaYHalf, deltaXHalf
                      + Math.min(targetWidth, source.getWidth()), deltaYHalf
                      + Math.min(targetHeight, source.getHeight()));
              int dstX = (targetWidth - src.width()) / 2;
              int dstY = (targetHeight - src.height()) / 2;
              Rect dst = new Rect(dstX, dstY, targetWidth - dstX, targetHeight
                      - dstY);
              c.drawBitmap(source, src, dst, null);
              return b2;
          }
          float bitmapWidthF = source.getWidth();
          float bitmapHeightF = source.getHeight();
  
          float bitmapAspect = bitmapWidthF / bitmapHeightF;
          float viewAspect = (float) targetWidth / targetHeight;
  
          if (bitmapAspect > viewAspect) {
              float scale = targetHeight / bitmapHeightF;
              if (scale < .9F || scale > 1F) {
                  scaler.setScale(scale, scale);
              } else {
                  scaler = null;
              }
          } else {
              float scale = targetWidth / bitmapWidthF;
              if (scale < .9F || scale > 1F) {
                  scaler.setScale(scale, scale);
              } else {
                  scaler = null;
              }
          }
  
          Bitmap b1;
          if (scaler != null) {
              // this is used for minithumb and crop, so we want to mFilter here.
              b1 = Bitmap.createBitmap(source, 0, 0, source.getWidth(),
                      source.getHeight(), scaler, true);
          } else {
              b1 = source;
          }
  
          int dx1 = Math.max(0, b1.getWidth() - targetWidth);
          int dy1 = Math.max(0, b1.getHeight() - targetHeight);
  
          Bitmap b2 = Bitmap.createBitmap(b1, dx1 / 2, dy1 / 2, targetWidth,
                  targetHeight);
  
          if (b1 != source) {
              b1.recycle();
          }
  
          return b2;
      }
  
      // Thong added for rotate
      private Bitmap rotateImage(Bitmap src, float degree) {
          // create new matrix
          Matrix matrix = new Matrix();
          // setup rotation degree
          matrix.postRotate(degree);
          Bitmap bmp = Bitmap.createBitmap(src, 0, 0, src.getWidth(),
                  src.getHeight(), matrix, true);
          return bmp;
      }
  
      private int getOrientationInDegree(Activity activity) {
          int rotation = activity.getWindowManager().getDefaultDisplay()
                  .getRotation();
          int degrees = 0;
  
          switch (rotation) {
              case Surface.ROTATION_0:
                  degrees = 0;
                  break;
              case Surface.ROTATION_90:
                  degrees = 90;
                  break;
              case Surface.ROTATION_180:
                  degrees = 180;
                  break;
              case Surface.ROTATION_270:
                  degrees = 270;
                  break;
          }
  
          return degrees;
      }
  
  }
  