/* * 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.core; import javax.annotation.concurrent.ThreadSafe; import java.lang.Exception; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicLong; import android.net.Uri; import com.facebook.cache.common.CacheKey; import com.facebook.common.internal.Objects; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.Predicate; import com.facebook.common.internal.Supplier; import com.facebook.common.references.CloseableReference; import com.facebook.common.util.UriUtil; import com.facebook.datasource.DataSource; import com.facebook.datasource.DataSources; import com.facebook.datasource.SettableDataSource; import com.facebook.imagepipeline.cache.BitmapMemoryCacheKey; import com.facebook.imagepipeline.cache.BufferedDiskCache; import com.facebook.imagepipeline.cache.MemoryCache; import com.facebook.imagepipeline.cache.CacheKeyFactory; import com.facebook.imagepipeline.common.Priority; import com.facebook.imagepipeline.datasource.CloseableProducerToDataSourceAdapter; import com.facebook.imagepipeline.datasource.ProducerToDataSourceAdapter; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.memory.PooledByteBuffer; import com.facebook.imagepipeline.producers.Producer; import com.facebook.imagepipeline.producers.SettableProducerContext; import com.facebook.imagepipeline.listener.ForwardingRequestListener; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.listener.RequestListener; import bolts.Continuation; import com.facebook.imagepipeline.request.ImageRequestBuilder; import bolts.Task; /** * The entry point for the image pipeline. */ @ThreadSafe public class ImagePipeline { private static final CancellationException PREFETCH_EXCEPTION = new CancellationException("Prefetching is not enabled"); private final ProducerSequenceFactory mProducerSequenceFactory; private final RequestListener mRequestListener; private final Supplier<Boolean> mIsPrefetchEnabledSupplier; private final MemoryCache<CacheKey, CloseableImage> mBitmapMemoryCache; private final MemoryCache<CacheKey, PooledByteBuffer> mEncodedMemoryCache; private final BufferedDiskCache mMainBufferedDiskCache; private final BufferedDiskCache mSmallImageBufferedDiskCache; private final CacheKeyFactory mCacheKeyFactory; private AtomicLong mIdCounter; public ImagePipeline( ProducerSequenceFactory producerSequenceFactory, Set<RequestListener> requestListeners, Supplier<Boolean> isPrefetchEnabledSupplier, MemoryCache<CacheKey, CloseableImage> bitmapMemoryCache, MemoryCache<CacheKey, PooledByteBuffer> encodedMemoryCache, BufferedDiskCache mainBufferedDiskCache, BufferedDiskCache smallImageBufferedDiskCache, CacheKeyFactory cacheKeyFactory) { mIdCounter = new AtomicLong(); mProducerSequenceFactory = producerSequenceFactory; mRequestListener = new ForwardingRequestListener(requestListeners); mIsPrefetchEnabledSupplier = isPrefetchEnabledSupplier; mBitmapMemoryCache = bitmapMemoryCache; mEncodedMemoryCache = encodedMemoryCache; mMainBufferedDiskCache = mainBufferedDiskCache; mSmallImageBufferedDiskCache = smallImageBufferedDiskCache; mCacheKeyFactory = cacheKeyFactory; } /** * Generates unique id for RequestFuture. * * @return unique id */ private String generateUniqueFutureId() { return String.valueOf(mIdCounter.getAndIncrement()); } /** * Returns a DataSource supplier that will on get submit the request for execution and return a * DataSource representing the pending results of the task. * * @param imageRequest the request to submit (what to execute). * @param bitmapCacheOnly whether to only look for the image in the bitmap cache * @return a DataSource representing pending results and completion of the request */ public Supplier<DataSource<CloseableReference<CloseableImage>>> getDataSourceSupplier( final ImageRequest imageRequest, final Object callerContext, final boolean bitmapCacheOnly) { return new Supplier<DataSource<CloseableReference<CloseableImage>>>() { @Override public DataSource<CloseableReference<CloseableImage>> get() { if (bitmapCacheOnly) { return fetchImageFromBitmapCache(imageRequest, callerContext); } else { return fetchDecodedImage(imageRequest, callerContext); } } @Override public String toString() { return Objects.toStringHelper(this) .add("uri", imageRequest.getSourceUri()) .toString(); } }; } /** * Returns a DataSource supplier that will on get submit the request for execution and return a * DataSource representing the pending results of the task. * * @param imageRequest the request to submit (what to execute). * @return a DataSource representing pending results and completion of the request */ public Supplier<DataSource<CloseableReference<PooledByteBuffer>>> getEncodedImageDataSourceSupplier( final ImageRequest imageRequest, final Object callerContext) { return new Supplier<DataSource<CloseableReference<PooledByteBuffer>>>() { @Override public DataSource<CloseableReference<PooledByteBuffer>> get() { return fetchEncodedImage(imageRequest, callerContext); } @Override public String toString() { return Objects.toStringHelper(this) .add("uri", imageRequest.getSourceUri()) .toString(); } }; } /** * Submits a request for bitmap cache lookup. * * @param imageRequest the request to submit * @return a DataSource representing the image */ public DataSource<CloseableReference<CloseableImage>> fetchImageFromBitmapCache( ImageRequest imageRequest, Object callerContext) { try { Producer<CloseableReference<CloseableImage>> producerSequence = mProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest); return submitFetchRequest( producerSequence, imageRequest, ImageRequest.RequestLevel.BITMAP_MEMORY_CACHE, callerContext); } catch (Exception exception) { return DataSources.immediateFailedDataSource(exception); } } /** * Submits a request for execution and returns a DataSource representing the pending decoded * image(s). * <p>The returned DataSource must be closed once the client has finished with it. * @param imageRequest the request to submit * @return a DataSource representing the pending decoded image(s) */ public DataSource<CloseableReference<CloseableImage>> fetchDecodedImage( ImageRequest imageRequest, Object callerContext) { try { Producer<CloseableReference<CloseableImage>> producerSequence = mProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest); return submitFetchRequest( producerSequence, imageRequest, ImageRequest.RequestLevel.FULL_FETCH, callerContext); } catch (Exception exception) { return DataSources.immediateFailedDataSource(exception); } } /** * Submits a request for execution and returns a DataSource representing the pending encoded * image(s). * * <p> The ResizeOptions in the imageRequest will be ignored for this fetch * * <p>The returned DataSource must be closed once the client has finished with it. * * @param imageRequest the request to submit * @return a DataSource representing the pending encoded image(s) */ public DataSource<CloseableReference<PooledByteBuffer>> fetchEncodedImage( ImageRequest imageRequest, Object callerContext) { Preconditions.checkNotNull(imageRequest.getSourceUri()); try { Producer<CloseableReference<PooledByteBuffer>> producerSequence = mProducerSequenceFactory.getEncodedImageProducerSequence(imageRequest); // The resize options are used to determine whether images are going to be downsampled during // decode or not. For the case where the image has to be downsampled and it's a local image it // will be kept as a FileInputStream until decoding instead of reading it in memory. Since // this method returns an encoded image, it should always be read into memory. Therefore, the // resize options are ignored to avoid treating the image as if it was to be downsampled // during decode. if (imageRequest.getResizeOptions() != null) { imageRequest = ImageRequestBuilder.fromRequest(imageRequest) .setResizeOptions(null) .build(); } return submitFetchRequest( producerSequence, imageRequest, ImageRequest.RequestLevel.FULL_FETCH, callerContext); } catch (Exception exception) { return DataSources.immediateFailedDataSource(exception); } } /** * Submits a request for prefetching to the bitmap cache. * @param imageRequest the request to submit * @return a DataSource that can safely be ignored. */ public DataSource<Void> prefetchToBitmapCache( ImageRequest imageRequest, Object callerContext) { if (!mIsPrefetchEnabledSupplier.get()) { return DataSources.immediateFailedDataSource(PREFETCH_EXCEPTION); } try { Producer<Void> producerSequence = mProducerSequenceFactory.getDecodedImagePrefetchProducerSequence(imageRequest); return submitPrefetchRequest( producerSequence, imageRequest, ImageRequest.RequestLevel.FULL_FETCH, callerContext); } catch (Exception exception) { return DataSources.immediateFailedDataSource(exception); } } /** * Submits a request for prefetching to the disk cache. * @param imageRequest the request to submit * @return a DataSource that can safely be ignored. */ public DataSource<Void> prefetchToDiskCache( ImageRequest imageRequest, Object callerContext) { if (!mIsPrefetchEnabledSupplier.get()) { return DataSources.immediateFailedDataSource(PREFETCH_EXCEPTION); } try { Producer<Void> producerSequence = mProducerSequenceFactory.getEncodedImagePrefetchProducerSequence(imageRequest); return submitPrefetchRequest( producerSequence, imageRequest, ImageRequest.RequestLevel.FULL_FETCH, callerContext); } catch (Exception exception) { return DataSources.immediateFailedDataSource(exception); } } /** * Removes all images with the specified {@link Uri} from memory cache. * @param uri The uri of the image to evict */ public void evictFromMemoryCache(final Uri uri) { Predicate<CacheKey> bitmapCachePredicate = predicateForUri(uri); mBitmapMemoryCache.removeAll(bitmapCachePredicate); final String cacheKeySourceString = mCacheKeyFactory.getCacheKeySourceUri(uri).toString(); Predicate<CacheKey> encodedCachePredicate = new Predicate<CacheKey>() { @Override public boolean apply(CacheKey key) { return key.toString().equals(cacheKeySourceString); } }; mEncodedMemoryCache.removeAll(encodedCachePredicate); } /** * <p>If you have supplied your own cache key factory when configuring the pipeline, this method * may not work correctly. It will only work if the custom factory builds the cache key entirely * from the URI. If that is not the case, use {@link #evictFromDiskCache(ImageRequest)}. * @param uri The uri of the image to evict */ public void evictFromDiskCache(final Uri uri) { evictFromDiskCache(ImageRequest.fromUri(uri)); } /** * Removes all images with the specified {@link Uri} from disk cache. * * @param imageRequest The imageRequest for the image to evict from disk cache */ public void evictFromDiskCache(final ImageRequest imageRequest) { final CacheKey cacheKey = mCacheKeyFactory.getEncodedCacheKey(imageRequest); mMainBufferedDiskCache.remove(cacheKey); mSmallImageBufferedDiskCache.remove(cacheKey); } /** * <p>If you have supplied your own cache key factory when configuring the pipeline, this method * may not work correctly. It will only work if the custom factory builds the cache key entirely * from the URI. If that is not the case, use {@link #evictFromMemoryCache(Uri)} and * {@link #evictFromDiskCache(ImageRequest)} separately. * @param uri The uri of the image to evict */ public void evictFromCache(final Uri uri) { evictFromMemoryCache(uri); evictFromDiskCache(uri); } /** * Clear the memory caches */ public void clearMemoryCaches() { Predicate<CacheKey> allPredicate = new Predicate<CacheKey>() { @Override public boolean apply(CacheKey key) { return true; } }; mBitmapMemoryCache.removeAll(allPredicate); mEncodedMemoryCache.removeAll(allPredicate); } /** * Clear disk caches */ public void clearDiskCaches() { mMainBufferedDiskCache.clearAll(); mSmallImageBufferedDiskCache.clearAll(); } /** * Clear all the caches (memory and disk) */ public void clearCaches() { clearMemoryCaches(); clearDiskCaches(); } /** * Returns whether the image is stored in the bitmap memory cache. * * @param uri the uri for the image to be looked up. * @return true if the image was found in the bitmap memory cache, false otherwise */ public boolean isInBitmapMemoryCache(final Uri uri) { Predicate<CacheKey> bitmapCachePredicate = predicateForUri(uri); return mBitmapMemoryCache.contains(bitmapCachePredicate); } /** * Returns whether the image is stored in the bitmap memory cache. * * @param imageRequest the imageRequest for the image to be looked up. * @return true if the image was found in the bitmap memory cache, false otherwise. */ public boolean isInBitmapMemoryCache(final ImageRequest imageRequest) { final CacheKey cacheKey = mCacheKeyFactory.getBitmapCacheKey(imageRequest); CloseableReference<CloseableImage> ref = mBitmapMemoryCache.get(cacheKey); try { return CloseableReference.isValid(ref); } finally { CloseableReference.closeSafely(ref); } } /** * Returns whether the image is stored in the disk cache. * * <p>If you have supplied your own cache key factory when configuring the pipeline, this method * may not work correctly. It will only work if the custom factory builds the cache key entirely * from the URI. If that is not the case, use {@link #isInDiskCache(ImageRequest)}. * * @param uri the uri for the image to be looked up. * @return true if the image was found in the disk cache, false otherwise. */ public DataSource<Boolean> isInDiskCache(final Uri uri) { return isInDiskCache(ImageRequest.fromUri(uri)); } /** * Returns whether the image is stored in the disk cache. * * @param imageRequest the imageRequest for the image to be looked up. * @return true if the image was found in the disk cache, false otherwise. */ public DataSource<Boolean> isInDiskCache(final ImageRequest imageRequest) { final CacheKey cacheKey = mCacheKeyFactory.getEncodedCacheKey(imageRequest); final SettableDataSource<Boolean> dataSource = SettableDataSource.create(); mMainBufferedDiskCache.contains(cacheKey) .continueWithTask( new Continuation<Boolean, Task<Boolean>>() { @Override public Task<Boolean> then(Task<Boolean> task) throws Exception { if (!task.isCancelled() && !task.isFaulted() && task.getResult()) { return Task.forResult(true); } return mSmallImageBufferedDiskCache.contains(cacheKey); } }) .continueWith( new Continuation<Boolean, Void>() { @Override public Void then(Task<Boolean> task) throws Exception { dataSource.setResult(!task.isCancelled() && !task.isFaulted() && task.getResult()); return null; } }); return dataSource; } private <T> DataSource<CloseableReference<T>> submitFetchRequest( Producer<CloseableReference<T>> producerSequence, ImageRequest imageRequest, ImageRequest.RequestLevel lowestPermittedRequestLevelOnSubmit, Object callerContext) { try { ImageRequest.RequestLevel lowestPermittedRequestLevel = ImageRequest.RequestLevel.getMax( imageRequest.getLowestPermittedRequestLevel(), lowestPermittedRequestLevelOnSubmit); SettableProducerContext settableProducerContext = new SettableProducerContext( imageRequest, generateUniqueFutureId(), mRequestListener, callerContext, lowestPermittedRequestLevel, /* isPrefetch */ false, imageRequest.getProgressiveRenderingEnabled() || !UriUtil.isNetworkUri(imageRequest.getSourceUri()), imageRequest.getPriority()); return CloseableProducerToDataSourceAdapter.create( producerSequence, settableProducerContext, mRequestListener); } catch (Exception exception) { return DataSources.immediateFailedDataSource(exception); } } private DataSource<Void> submitPrefetchRequest( Producer<Void> producerSequence, ImageRequest imageRequest, ImageRequest.RequestLevel lowestPermittedRequestLevelOnSubmit, Object callerContext) { try { ImageRequest.RequestLevel lowestPermittedRequestLevel = ImageRequest.RequestLevel.getMax( imageRequest.getLowestPermittedRequestLevel(), lowestPermittedRequestLevelOnSubmit); SettableProducerContext settableProducerContext = new SettableProducerContext( imageRequest, generateUniqueFutureId(), mRequestListener, callerContext, lowestPermittedRequestLevel, /* isPrefetch */ true, /* isIntermediateResultExpected */ false, Priority.LOW); return ProducerToDataSourceAdapter.create( producerSequence, settableProducerContext, mRequestListener); } catch (Exception exception) { return DataSources.immediateFailedDataSource(exception); } } private Predicate<CacheKey> predicateForUri(Uri uri) { final String cacheKeySourceString = mCacheKeyFactory.getCacheKeySourceUri(uri).toString(); return new Predicate<CacheKey>() { @Override public boolean apply(CacheKey key) { if (key instanceof BitmapMemoryCacheKey) { return ((BitmapMemoryCacheKey) key).getSourceUriString().equals(cacheKeySourceString); } return false; } }; } }