/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.imagepipeline.producers;

import android.content.ContentResolver;
import android.database.Cursor;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.MediaStore;

import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.logging.FLog;
import com.facebook.imagepipeline.common.ResizeOptions;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.memory.PooledByteBufferFactory;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imageutils.JfifUtil;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executor;

import javax.annotation.Nullable;

/**
 * Represents a local content Uri fetch producer.
 */
public class LocalContentUriFetchProducer extends LocalFetchProducer {

  private static final Class<?> TAG = LocalContentUriFetchProducer.class;

  @VisibleForTesting static final String PRODUCER_NAME = "LocalContentUriFetchProducer";
  private static final String DISPLAY_PHOTO_PATH =
      Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "display_photo").getPath();
  private static final String[] PROJECTION = new String[] {
      MediaStore.Images.Media._ID,
      MediaStore.Images.ImageColumns.DATA
  };
  private static final String[] THUMBNAIL_PROJECTION = new String[] {
      MediaStore.Images.Thumbnails.DATA
  };

  private static final Rect MINI_THUMBNAIL_DIMENSIONS = new Rect(0, 0, 512, 384);
  private static final Rect MICRO_THUMBNAIL_DIMENSIONS = new Rect(0, 0, 96, 96);
  private static final float ACCEPTABLE_REQUESTED_TO_ACTUAL_SIZE_RATIO = 4.0f/3;

  private static final int NO_THUMBNAIL = 0;

  private final ContentResolver mContentResolver;

  public LocalContentUriFetchProducer(
      Executor executor,
      PooledByteBufferFactory pooledByteBufferFactory,
      ContentResolver contentResolver,
      boolean decodeFileDescriptorEnabled) {
    super(executor, pooledByteBufferFactory,decodeFileDescriptorEnabled);
    mContentResolver = contentResolver;
  }

  @Override
  protected EncodedImage getEncodedImage(ImageRequest imageRequest) throws IOException {
    Uri uri = imageRequest.getSourceUri();
    if (isContactUri(uri)) {
      final InputStream inputStream;
      if (uri.toString().endsWith("/photo")) {
        inputStream =  mContentResolver.openInputStream(uri);
      } else {
        inputStream = ContactsContract.Contacts.openContactPhotoInputStream(mContentResolver, uri);
      }
      // If a Contact URI is provided, use the special helper to open that contact's photo.
      return getEncodedImage(
          inputStream,
          EncodedImage.UNKNOWN_STREAM_SIZE);
    }

    if (isCameraUri(uri)) {
      EncodedImage cameraImage = getCameraImage(uri, imageRequest.getResizeOptions());
      if (cameraImage != null) {
        return cameraImage;
      }
    }

    return getEncodedImage(
        mContentResolver.openInputStream(uri),
        EncodedImage.UNKNOWN_STREAM_SIZE);
  }

  /**
   * Checks if the given URI is a general Contact URI, and not a specific display photo.
   * @param uri the URI to check
   * @return true if the uri is a a Contact URI, and is not already specifying a display photo.
   */
  private static boolean isContactUri(Uri uri) {
    return ContactsContract.AUTHORITY.equals(uri.getAuthority()) &&
        !uri.getPath().startsWith(DISPLAY_PHOTO_PATH);
  }

  private static boolean isCameraUri(Uri uri) {
    String uriString = uri.toString();
    return uriString.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) ||
        uriString.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString());
  }

  private @Nullable EncodedImage getCameraImage(
      Uri uri,
      ResizeOptions resizeOptions) throws IOException{
    Cursor cursor = mContentResolver.query(uri, PROJECTION, null, null, null);
    if (cursor == null) {
      return null;
    }
    try {
      if (cursor.getCount() == 0) {
        return null;
      }
      cursor.moveToFirst();
      final String pathname =
          cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
      if (resizeOptions != null) {
        int imageIdColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media._ID);
        EncodedImage thumbnail = getThumbnail(resizeOptions, cursor.getInt(imageIdColumnIndex));
        if (thumbnail != null) {
          thumbnail.setRotationAngle(getRotationAngle(pathname));
          return thumbnail;
        }
      }
      if (pathname != null) {
        return getEncodedImage(new FileInputStream(pathname), getLength(pathname));
      }
    } finally {
      cursor.close();
    }
    return null;
  }

  // Gets the smallest possible thumbnail that is bigger than the requested size in the resize
  // options or null if either the thumbnails are smaller than the requested size or there are no
  // stored thumbnails.
  private EncodedImage getThumbnail(ResizeOptions resizeOptions, int imageId) throws IOException{
    int thumbnailKind = getThumbnailKind(resizeOptions);
    if (thumbnailKind == NO_THUMBNAIL) {
      return null;
    }
    Cursor thumbnailCursor = null;
    try {
      thumbnailCursor = MediaStore.Images.Thumbnails.queryMiniThumbnail(
          mContentResolver,
          imageId,
          thumbnailKind,
          THUMBNAIL_PROJECTION);
      if (thumbnailCursor == null) {
         return null;
      }
      thumbnailCursor.moveToFirst();
      if (thumbnailCursor.getCount() > 0) {
        final String thumbnailUri = thumbnailCursor.getString(
            thumbnailCursor.getColumnIndex(MediaStore.Images.Thumbnails.DATA));
        if (new File(thumbnailUri).exists()) {
          return getEncodedImage(new FileInputStream(thumbnailUri), getLength(thumbnailUri));
        }
      }
    } finally {
      if (thumbnailCursor != null) {
        thumbnailCursor.close();
      }
    }
    return null;
  }

  // Returns the smallest possible thumbnail kind that has an acceptable size (meaning the resize
  // options requested size is smaller than 4/3 its size).
  // We can add a small interval of acceptance over the size of the thumbnail since the quality lost
  // when scaling it to fit a view will not be significant.
  private static int getThumbnailKind(ResizeOptions resizeOptions) {
    if (isThumbnailBigEnough(resizeOptions, MICRO_THUMBNAIL_DIMENSIONS)) {
      return MediaStore.Images.Thumbnails.MICRO_KIND;
    } else if (isThumbnailBigEnough(resizeOptions, MINI_THUMBNAIL_DIMENSIONS)) {
      return MediaStore.Images.Thumbnails.MINI_KIND;
    }
    return NO_THUMBNAIL;
  }

  @VisibleForTesting
  static boolean isThumbnailBigEnough(ResizeOptions resizeOptions, Rect thumbnailDimensions) {
    return resizeOptions.width <=
        thumbnailDimensions.width() * ACCEPTABLE_REQUESTED_TO_ACTUAL_SIZE_RATIO
        && resizeOptions.height <=
        thumbnailDimensions.height() * ACCEPTABLE_REQUESTED_TO_ACTUAL_SIZE_RATIO;
  }

  private static int getLength(String pathname) {
    return pathname == null ? -1 : (int) new File(pathname).length();
  }

  @Override
  protected String getProducerName() {
    return PRODUCER_NAME;
  }

  private static int getRotationAngle(String pathname) {
    if (pathname != null) {
      try {
        ExifInterface exif = new ExifInterface(pathname);
        return JfifUtil.getAutoRotateAngleFromOrientation(exif.getAttributeInt(
            ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL));
      } catch (IOException ioe) {
        FLog.e(TAG, ioe, "Unable to retrieve thumbnail rotation for %s", pathname);
      }
    }
    return 0;
  }
}

@@ -2927,24 +2927,142 @@
 lver, uri);%0A
+        if (inputStream == null) %7B%0A          throw new IOException(%22Contact photo does not exist: %22 + uri);%0A        %7D%0A
       %7D%0A    

throw IOException (instead of NullPointerException) if no contact photo is present