/* * 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 java.io.File; import java.io.IOException; import java.util.Map; import java.util.concurrent.Executor; import android.content.ContentResolver; import android.database.Cursor; import android.media.ExifInterface; import android.net.Uri; import android.provider.MediaStore; import android.util.Pair; import com.facebook.common.internal.ImmutableMap; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.references.CloseableReference; import com.facebook.common.util.UriUtil; import com.facebook.imageformat.ImageFormat; import com.facebook.imagepipeline.image.EncodedImage; import com.facebook.imagepipeline.memory.PooledByteBuffer; import com.facebook.imagepipeline.memory.PooledByteBufferFactory; import com.facebook.imagepipeline.memory.PooledByteBufferInputStream; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imageutils.BitmapUtil; import com.facebook.imageutils.JfifUtil; /** * A producer that retrieves exif thumbnails. * * <p>At present, these thumbnails are retrieved on the java heap before being put into native * memory. */ public class LocalExifThumbnailProducer implements Producer<EncodedImage> { @VisibleForTesting static final String PRODUCER_NAME = "LocalExifThumbnailProducer"; @VisibleForTesting static final String CREATED_THUMBNAIL = "createdThumbnail"; private final Executor mExecutor; private final PooledByteBufferFactory mPooledByteBufferFactory; private final ContentResolver mContentResolver; public LocalExifThumbnailProducer( Executor executor, PooledByteBufferFactory pooledByteBufferFactory, ContentResolver contentResolver) { mExecutor = executor; mPooledByteBufferFactory = pooledByteBufferFactory; mContentResolver = contentResolver; } @Override public void produceResults( final Consumer<EncodedImage> consumer, final ProducerContext producerContext) { final ProducerListener listener = producerContext.getListener(); final String requestId = producerContext.getId(); final ImageRequest imageRequest = producerContext.getImageRequest(); final StatefulProducerRunnable cancellableProducerRunnable = new StatefulProducerRunnable<EncodedImage>( consumer, listener, PRODUCER_NAME, requestId) { @Override protected EncodedImage getResult() throws Exception { final Uri sourceUri = imageRequest.getSourceUri(); final ExifInterface exifInterface = getExifInterface(sourceUri); if (exifInterface == null || !exifInterface.hasThumbnail()) { return null; } byte[] bytes = exifInterface.getThumbnail(); PooledByteBuffer pooledByteBuffer = mPooledByteBufferFactory.newByteBuffer(bytes); return buildEncodedImage(pooledByteBuffer, exifInterface); } @Override protected void disposeResult(EncodedImage result) { EncodedImage.closeSafely(result); } @Override protected Map<String, String> getExtraMapOnSuccess(final EncodedImage result) { return ImmutableMap.of(CREATED_THUMBNAIL, Boolean.toString(result != null)); } }; producerContext.addCallbacks( new BaseProducerContextCallbacks() { @Override public void onCancellationRequested() { cancellableProducerRunnable.cancel(); } }); mExecutor.execute(cancellableProducerRunnable); } @VisibleForTesting ExifInterface getExifInterface(Uri uri) throws IOException { final String realPath = getRealPathFromUri(uri); if (canReadAsFile(realPath)) { return new ExifInterface(realPath); } return null; } private EncodedImage buildEncodedImage( PooledByteBuffer imageBytes, ExifInterface exifInterface) { Pair<Integer, Integer> dimensions = BitmapUtil.decodeDimensions(new PooledByteBufferInputStream(imageBytes)); int rotationAngle = getRotationAngle(exifInterface); int width = dimensions != null ? dimensions.first : EncodedImage.UNKNOWN_WIDTH; int height = dimensions != null ? dimensions.second : EncodedImage.UNKNOWN_HEIGHT; EncodedImage encodedImage = new EncodedImage(CloseableReference.of(imageBytes)); encodedImage.setImageFormat(ImageFormat.JPEG); encodedImage.setRotationAngle(rotationAngle); encodedImage.setWidth(width); encodedImage.setHeight(height); return encodedImage; } // Gets the correction angle based on the image's orientation private int getRotationAngle(final ExifInterface exifInterface) { return JfifUtil.getAutoRotateAngleFromOrientation( Integer.parseInt(exifInterface.getAttribute(ExifInterface.TAG_ORIENTATION))); } /** * Get the path of a file from the Uri * @param srcUri The source uri * @return The Path for the file or null if doesn't exists */ private String getRealPathFromUri(final Uri srcUri) { String result = null; if (UriUtil.isLocalContentUri(srcUri)) { Cursor cursor = mContentResolver.query(srcUri, null, null, null, null); if (cursor != null) { cursor.moveToFirst(); int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); result = cursor.getString(idx); cursor.close(); } } else if (UriUtil.isLocalFileUri(srcUri)) { result = srcUri.getPath(); } return result; } @VisibleForTesting boolean canReadAsFile(String realPath) throws IOException { if (realPath == null) { return false; } final File file = new File(realPath); return file.exists() && file.canRead(); } }