/* * 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; } }