content: /*
   * Copyright (C) 2012 CyberAgent
   *
   * 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 jp.co.cyberagent.android.gpuimage;
  
  import android.annotation.TargetApi;
  import android.app.ActivityManager;
  import android.content.Context;
  import android.content.pm.ConfigurationInfo;
  import android.database.Cursor;
  import android.graphics.Bitmap;
  import android.graphics.Bitmap.CompressFormat;
  import android.graphics.BitmapFactory;
  import android.graphics.Matrix;
  import android.graphics.PixelFormat;
  import android.hardware.Camera;
  import android.media.ExifInterface;
  import android.media.MediaScannerConnection;
  import android.net.Uri;
  import android.opengl.GLSurfaceView;
  import android.os.AsyncTask;
  import android.os.Build;
  import android.os.Environment;
  import android.os.Handler;
  import android.provider.MediaStore;
  import android.view.Display;
  import android.view.WindowManager;
  
  import java.io.*;
  import java.net.URL;
  import java.util.List;
  import java.util.concurrent.Semaphore;
  
  /**
   * The main accessor for GPUImage functionality. This class helps to do common
   * tasks through a simple interface.
   */
  public class GPUImage {
      private final Context mContext;
      private final GPUImageRenderer mRenderer;
      private GLSurfaceView mGlSurfaceView;
      private GPUImageFilter mFilter;
      private Bitmap mCurrentBitmap;
      private ScaleType mScaleType = ScaleType.CENTER_CROP;
  
      /**
       * Instantiates a new GPUImage object.
       *
       * @param context the context
       */
      public GPUImage(final Context context) {
          if (!supportsOpenGLES2(context)) {
              throw new IllegalStateException("OpenGL ES 2.0 is not supported on this phone.");
          }
  
          mContext = context;
          mFilter = new GPUImageFilter();
          mRenderer = new GPUImageRenderer(mFilter);
      }
  
      /**
       * Checks if OpenGL ES 2.0 is supported on the current device.
       *
       * @param context the context
       * @return true, if successful
       */
      private boolean supportsOpenGLES2(final Context context) {
          final ActivityManager activityManager = (ActivityManager)
                  context.getSystemService(Context.ACTIVITY_SERVICE);
          final ConfigurationInfo configurationInfo =
                  activityManager.getDeviceConfigurationInfo();
          return configurationInfo.reqGlEsVersion >= 0x20000;
      }
  
      /**
       * Sets the GLSurfaceView which will display the preview.
       *
       * @param view the GLSurfaceView
       */
      public void setGLSurfaceView(final GLSurfaceView view) {
          mGlSurfaceView = view;
          mGlSurfaceView.setEGLContextClientVersion(2);
          mGlSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
          mGlSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
          mGlSurfaceView.setRenderer(mRenderer);
          mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
          mGlSurfaceView.requestRender();
      }
  
      /**
       * Sets the background color
       *
       * @param red red color value
       * @param green green color value
       * @param blue red color value
       */
      public void setBackgroundColor(float red, float green, float blue) {
          mRenderer.setBackgroundColor(red, green, blue);
      }
  
      /**
       * Request the preview to be rendered again.
       */
      public void requestRender() {
          if (mGlSurfaceView != null) {
              mGlSurfaceView.requestRender();
          }
      }
  
      /**
       * Sets the up camera to be connected to GPUImage to get a filtered preview.
       *
       * @param camera the camera
       */
      public void setUpCamera(final Camera camera) {
          setUpCamera(camera, 0, false, false);
      }
  
      /**
       * Sets the up camera to be connected to GPUImage to get a filtered preview.
       *
       * @param camera the camera
       * @param degrees by how many degrees the image should be rotated
       * @param flipHorizontal if the image should be flipped horizontally
       * @param flipVertical if the image should be flipped vertically
       */
      public void setUpCamera(final Camera camera, final int degrees, final boolean flipHorizontal,
              final boolean flipVertical) {
          mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
          if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
              setUpCameraGingerbread(camera);
          } else {
              camera.setPreviewCallback(mRenderer);
              camera.startPreview();
          }
          Rotation rotation = Rotation.NORMAL;
          switch (degrees) {
              case 90:
                  rotation = Rotation.ROTATION_90;
                  break;
              case 180:
                  rotation = Rotation.ROTATION_180;
                  break;
              case 270:
                  rotation = Rotation.ROTATION_270;
                  break;
          }
          mRenderer.setRotationCamera(rotation, flipHorizontal, flipVertical);
      }
  
      @TargetApi(11)
      private void setUpCameraGingerbread(final Camera camera) {
          mRenderer.setUpSurfaceTexture(camera);
      }
  
      /**
       * Sets the filter which should be applied to the image which was (or will
       * be) set by setImage(...).
       *
       * @param filter the new filter
       */
      public void setFilter(final GPUImageFilter filter) {
          mFilter = filter;
          mRenderer.setFilter(mFilter);
          requestRender();
      }
  
      /**
       * Sets the image on which the filter should be applied.
       *
       * @param bitmap the new image
       */
      public void setImage(final Bitmap bitmap) {
          mCurrentBitmap = bitmap;
          mRenderer.setImageBitmap(bitmap, false);
          requestRender();
      }
  
      /**
       * This sets the scale type of GPUImage. This has to be run before setting the image.
       * If image is set and scale type changed, image needs to be reset.
       *
       * @param scaleType The new ScaleType
       */
      public void setScaleType(ScaleType scaleType) {
          mScaleType = scaleType;
          mRenderer.setScaleType(scaleType);
          mRenderer.deleteImage();
          mCurrentBitmap = null;
          requestRender();
      }
  
      /**
       * Sets the rotation of the displayed image.
       *
       * @param rotation new rotation
       */
      public void setRotation(Rotation rotation) {
          mRenderer.setRotation(rotation);
      }
  
      /**
       * Sets the rotation of the displayed image with flip options.
       *
       * @param rotation new rotation
       */
      public void setRotation(Rotation rotation, boolean flipHorizontal, boolean flipVertical) {
          mRenderer.setRotation(rotation, flipHorizontal, flipVertical);
      }
  
      /**
       * Deletes the current image.
       */
      public void deleteImage() {
          mRenderer.deleteImage();
          mCurrentBitmap = null;
          requestRender();
      }
  
      /**
       * Sets the image on which the filter should be applied from a Uri.
       *
       * @param uri the uri of the new image
       */
      public void setImage(final Uri uri) {
          new LoadImageUriTask(this, uri).execute();
      }
  
      /**
       * Sets the image on which the filter should be applied from a File.
       *
       * @param file the file of the new image
       */
      public void setImage(final File file) {
          new LoadImageFileTask(this, file).execute();
      }
  
      private String getPath(final Uri uri) {
          String[] projection = {
                  MediaStore.Images.Media.DATA,
          };
          Cursor cursor = mContext.getContentResolver()
                  .query(uri, projection, null, null, null);
          int pathIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
          String path = null;
          if (cursor.moveToFirst()) {
              path = cursor.getString(pathIndex);
          }
          cursor.close();
          return path;
      }
  
      /**
       * Gets the current displayed image with applied filter as a Bitmap.
       *
       * @return the current image with filter applied
       */
      public Bitmap getBitmapWithFilterApplied() {
          return getBitmapWithFilterApplied(mCurrentBitmap);
      }
  
      /**
       * Gets the given bitmap with current filter applied as a Bitmap.
       *
       * @param bitmap the bitmap on which the current filter should be applied
       * @return the bitmap with filter applied
       */
      public Bitmap getBitmapWithFilterApplied(final Bitmap bitmap) {
          if (mGlSurfaceView != null) {
              mRenderer.deleteImage();
              mRenderer.runOnDraw(new Runnable() {
  
                  @Override
                  public void run() {
                      synchronized(mFilter) {
                          mFilter.destroy();
                          mFilter.notify();
                      }
                  }
              });
              synchronized(mFilter) {
                  requestRender();
                  try {
                      mFilter.wait();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
  
          GPUImageRenderer renderer = new GPUImageRenderer(mFilter);
          renderer.setRotation(Rotation.NORMAL,
                  mRenderer.isFlippedHorizontally(), mRenderer.isFlippedVertically());
          renderer.setScaleType(mScaleType);
          PixelBuffer buffer = new PixelBuffer(bitmap.getWidth(), bitmap.getHeight());
          buffer.setRenderer(renderer);
          renderer.setImageBitmap(bitmap, false);
          Bitmap result = buffer.getBitmap();
          mFilter.destroy();
          renderer.deleteImage();
          buffer.destroy();
  
          mRenderer.setFilter(mFilter);
          if (mCurrentBitmap != null) {
              mRenderer.setImageBitmap(mCurrentBitmap, false);
          }
          requestRender();
  
          return result;
      }
  
      /**
       * Gets the images for multiple filters on a image. This can be used to
       * quickly get thumbnail images for filters. <br>
       * Whenever a new Bitmap is ready, the listener will be called with the
       * bitmap. The order of the calls to the listener will be the same as the
       * filter order.
       *
       * @param bitmap the bitmap on which the filters will be applied
       * @param filters the filters which will be applied on the bitmap
       * @param listener the listener on which the results will be notified
       */
      public static void getBitmapForMultipleFilters(final Bitmap bitmap,
              final List<GPUImageFilter> filters, final ResponseListener<Bitmap> listener) {
          if (filters.isEmpty()) {
              return;
          }
          GPUImageRenderer renderer = new GPUImageRenderer(filters.get(0));
          renderer.setImageBitmap(bitmap, false);
          PixelBuffer buffer = new PixelBuffer(bitmap.getWidth(), bitmap.getHeight());
          buffer.setRenderer(renderer);
  
          for (GPUImageFilter filter : filters) {
              renderer.setFilter(filter);
              listener.response(buffer.getBitmap());
              filter.destroy();
          }
          renderer.deleteImage();
          buffer.destroy();
      }
  
      /**
       * Deprecated: Please use
       * {@link GPUImageView#saveToPictures(String, String, jp.co.cyberagent.android.gpuimage.GPUImageView.OnPictureSavedListener)}
       *
       * Save current image with applied filter to Pictures. It will be stored on
       * the default Picture folder on the phone below the given folderName and
       * fileName. <br>
       * This method is async and will notify when the image was saved through the
       * listener.
       *
       * @param folderName the folder name
       * @param fileName the file name
       * @param listener the listener
       */
      @Deprecated
      public void saveToPictures(final String folderName, final String fileName,
              final OnPictureSavedListener listener) {
          saveToPictures(mCurrentBitmap, folderName, fileName, listener);
      }
  
      /**
       * Deprecated: Please use
       * {@link GPUImageView#saveToPictures(String, String, jp.co.cyberagent.android.gpuimage.GPUImageView.OnPictureSavedListener)}
       *
       * Apply and save the given bitmap with applied filter to Pictures. It will
       * be stored on the default Picture folder on the phone below the given
       * folerName and fileName. <br>
       * This method is async and will notify when the image was saved through the
       * listener.
       *
       * @param bitmap the bitmap
       * @param folderName the folder name
       * @param fileName the file name
       * @param listener the listener
       */
      @Deprecated
      public void saveToPictures(final Bitmap bitmap, final String folderName, final String fileName,
              final OnPictureSavedListener listener) {
          new SaveTask(bitmap, folderName, fileName, listener).execute();
      }
  
      /**
       * Runs the given Runnable on the OpenGL thread.
       *
       * @param runnable The runnable to be run on the OpenGL thread.
       */
      void runOnGLThread(Runnable runnable) {
          mRenderer.runOnDrawEnd(runnable);
      }
  
      private int getOutputWidth() {
          if (mRenderer != null && mRenderer.getFrameWidth() != 0) {
              return mRenderer.getFrameWidth();
          } else if (mCurrentBitmap != null) {
              return mCurrentBitmap.getWidth();
          } else {
              WindowManager windowManager =
                      (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
              Display display = windowManager.getDefaultDisplay();
              return display.getWidth();
          }
      }
  
      private int getOutputHeight() {
          if (mRenderer != null && mRenderer.getFrameHeight() != 0) {
              return mRenderer.getFrameHeight();
          } else if (mCurrentBitmap != null) {
              return mCurrentBitmap.getHeight();
          } else {
              WindowManager windowManager =
                      (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
              Display display = windowManager.getDefaultDisplay();
              return display.getHeight();
          }
      }
  
      @Deprecated
      private class SaveTask extends AsyncTask<Void, Void, Void> {
  
          private final Bitmap mBitmap;
          private final String mFolderName;
          private final String mFileName;
          private final OnPictureSavedListener mListener;
          private final Handler mHandler;
  
          public SaveTask(final Bitmap bitmap, final String folderName, final String fileName,
                  final OnPictureSavedListener listener) {
              mBitmap = bitmap;
              mFolderName = folderName;
              mFileName = fileName;
              mListener = listener;
              mHandler = new Handler();
          }
  
          @Override
          protected Void doInBackground(final Void... params) {
              Bitmap result = getBitmapWithFilterApplied(mBitmap);
              saveImage(mFolderName, mFileName, result);
              return null;
          }
  
          private void saveImage(final String folderName, final String fileName, final Bitmap image) {
              File path = Environment
                      .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
              File file = new File(path, folderName + "/" + fileName);
              try {
                  file.getParentFile().mkdirs();
                  image.compress(CompressFormat.JPEG, 80, new FileOutputStream(file));
                  MediaScannerConnection.scanFile(mContext,
                          new String[] {
                              file.toString()
                          }, null,
                          new MediaScannerConnection.OnScanCompletedListener() {
                              @Override
                              public void onScanCompleted(final String path, final Uri uri) {
                                  if (mListener != null) {
                                      mHandler.post(new Runnable() {
  
                                          @Override
                                          public void run() {
                                              mListener.onPictureSaved(uri);
                                          }
                                      });
                                  }
                              }
                          });
              } catch (FileNotFoundException e) {
                  e.printStackTrace();
              }
          }
      }
  
      public interface OnPictureSavedListener {
          void onPictureSaved(Uri uri);
      }
  
      private class LoadImageUriTask extends LoadImageTask {
  
          private final Uri mUri;
  
          public LoadImageUriTask(GPUImage gpuImage, Uri uri) {
              super(gpuImage);
              mUri = uri;
          }
  
          @Override
          protected Bitmap decode(BitmapFactory.Options options) {
              try {
                  InputStream inputStream;
                  if (mUri.getScheme().startsWith("http") || mUri.getScheme().startsWith("https")) {
                      inputStream = new URL(mUri.toString()).openStream();
                  } else {
                      inputStream = mContext.getContentResolver().openInputStream(mUri);
                  }
                  return BitmapFactory.decodeStream(inputStream, null, options);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              return null;
          }
  
          @Override
          protected int getImageOrientation() throws IOException {
              Cursor cursor = mContext.getContentResolver().query(mUri,
                      new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null);
  
              if (cursor == null || cursor.getCount() != 1) {
                  return 0;
              }
  
              cursor.moveToFirst();
              int orientation = cursor.getInt(0);
              cursor.close();
              return orientation;
          }
      }
  
      private class LoadImageFileTask extends LoadImageTask {
  
          private final File mImageFile;
  
          public LoadImageFileTask(GPUImage gpuImage, File file) {
              super(gpuImage);
              mImageFile = file;
          }
  
          @Override
          protected Bitmap decode(BitmapFactory.Options options) {
              return BitmapFactory.decodeFile(mImageFile.getAbsolutePath(), options);
          }
  
          @Override
          protected int getImageOrientation() throws IOException {
              ExifInterface exif = new ExifInterface(mImageFile.getAbsolutePath());
              int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
              switch (orientation) {
                  case ExifInterface.ORIENTATION_NORMAL:
                      return 0;
                  case ExifInterface.ORIENTATION_ROTATE_90:
                      return 90;
                  case ExifInterface.ORIENTATION_ROTATE_180:
                      return 180;
                  case ExifInterface.ORIENTATION_ROTATE_270:
                      return 270;
                  default:
                      return 0;
              }
          }
      }
  
      private abstract class LoadImageTask extends AsyncTask<Void, Void, Bitmap> {
  
          private final GPUImage mGPUImage;
          private int mOutputWidth;
          private int mOutputHeight;
  
          @SuppressWarnings("deprecation")
          public LoadImageTask(final GPUImage gpuImage) {
              mGPUImage = gpuImage;
          }
  
          @Override
          protected Bitmap doInBackground(Void... params) {
              if (mRenderer != null && mRenderer.getFrameWidth() == 0) {
                  try {
                      synchronized (mRenderer.mSurfaceChangedWaiter) {
                          mRenderer.mSurfaceChangedWaiter.wait(3000);
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
              mOutputWidth = getOutputWidth();
              mOutputHeight = getOutputHeight();
              return loadResizedImage();
          }
  
          @Override
          protected void onPostExecute(Bitmap bitmap) {
              super.onPostExecute(bitmap);
              mGPUImage.deleteImage();
              mGPUImage.setImage(bitmap);
          }
  
          protected abstract Bitmap decode(BitmapFactory.Options options);
  
          private Bitmap loadResizedImage() {
              BitmapFactory.Options options = new BitmapFactory.Options();
              options.inJustDecodeBounds = true;
              decode(options);
              int scale = 1;
              while (checkSize(options.outWidth / scale > mOutputWidth, options.outHeight / scale > mOutputHeight)) {
                  scale++;
              }
  
              scale--;
              if (scale < 1) {
                  scale = 1;
              }
              options = new BitmapFactory.Options();
              options.inSampleSize = scale;
              options.inPreferredConfig = Bitmap.Config.RGB_565;
              options.inPurgeable = true;
              options.inTempStorage = new byte[32 * 1024];
              Bitmap bitmap = decode(options);
              if (bitmap == null) {
                  return null;
              }
              bitmap = rotateImage(bitmap);
              bitmap = scaleBitmap(bitmap);
              return bitmap;
          }
  
          private Bitmap scaleBitmap(Bitmap bitmap) {
              // resize to desired dimensions
              int width = bitmap.getWidth();
              int height = bitmap.getHeight();
              int[] newSize = getScaleSize(width, height);
              Bitmap workBitmap = Bitmap.createScaledBitmap(bitmap, newSize[0], newSize[1], true);
              if (workBitmap != bitmap) {
                  bitmap.recycle();
                  bitmap = workBitmap;
                  System.gc();
              }
  
              if (mScaleType == ScaleType.CENTER_CROP) {
                  // Crop it
                  int diffWidth = newSize[0] - mOutputWidth;
                  int diffHeight = newSize[1] - mOutputHeight;
                  workBitmap = Bitmap.createBitmap(bitmap, diffWidth / 2, diffHeight / 2,
                          newSize[0] - diffWidth, newSize[1] - diffHeight);
                  if (workBitmap != bitmap) {
                      bitmap.recycle();
                      bitmap = workBitmap;
                  }
              }
  
              return bitmap;
          }
  
          /**
           * Retrieve the scaling size for the image dependent on the ScaleType.<br>
           * <br>
           * If CROP: sides are same size or bigger than output's sides<br>
           * Else   : sides are same size or smaller than output's sides
           */
          private int[] getScaleSize(int width, int height) {
              float newWidth;
              float newHeight;
  
              float withRatio = (float) width / mOutputWidth;
              float heightRatio = (float) height / mOutputHeight;
  
              boolean adjustWidth = mScaleType == ScaleType.CENTER_CROP
                      ? withRatio > heightRatio : withRatio < heightRatio;
  
              if (adjustWidth) {
                  newHeight = mOutputHeight;
                  newWidth = (newHeight / height) * width;
              } else {
                  newWidth = mOutputWidth;
                  newHeight = (newWidth / width) * height;
              }
              return new int[]{Math.round(newWidth), Math.round(newHeight)};
          }
  
          private boolean checkSize(boolean widthBigger, boolean heightBigger) {
              if (mScaleType == ScaleType.CENTER_CROP) {
                  return widthBigger && heightBigger;
              } else {
                  return widthBigger || heightBigger;
              }
          }
  
          private Bitmap rotateImage(final Bitmap bitmap) {
              if (bitmap == null) {
                  return null;
              }
              Bitmap rotatedBitmap = bitmap;
              try {
                  int orientation = getImageOrientation();
                  if (orientation != 0) {
                      Matrix matrix = new Matrix();
                      matrix.postRotate(orientation);
                      rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                              bitmap.getHeight(), matrix, true);
                      bitmap.recycle();
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
              return rotatedBitmap;
          }
  
          protected abstract int getImageOrientation() throws IOException;
      }
  
      public interface ResponseListener<T> {
          void response(T item);
      }
  
      public enum ScaleType { CENTER_INSIDE, CENTER_CROP }
  }
  