/*
* Copyright (C) 2013 Square, Inc.
*
* 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 com.squareup.picasso;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.widget.ImageView;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.ReferenceQueue;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE;
import static android.content.ContentResolver.SCHEME_CONTENT;
import static android.content.ContentResolver.SCHEME_FILE;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.provider.ContactsContract.Contacts;
import static com.squareup.picasso.Downloader.Response;
import static com.squareup.picasso.Request.RequestWeakReference;
import static com.squareup.picasso.Utils.calculateInSampleSize;
/**
* Image downloading, transformation, and caching manager.
* <p/>
* Use {@link #with(android.content.Context)} for the global singleton instance or construct your
* own instance with {@link Builder}.
*/
public class Picasso {
private static final int RETRY_DELAY = 500;
private static final int REQUEST_COMPLETE = 1;
private static final int REQUEST_RETRY = 2;
private static final int REQUEST_DECODE_FAILED = 3;
private static final int REQUEST_CANCEL_GC = 4;
/**
* Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since
* this will only ever happen in background threads we help avoid excessive memory thrashing as
* well as potential OOMs. Shamelessly stolen from Volley.
*/
private static final Object DECODE_LOCK = new Object();
/** Callbacks for Picasso events. */
public interface Listener {
/**
* Invoked when an image has failed to load. This is useful for reporting image failures to a
* remote analytics service, for example.
*/
void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception);
}
// TODO This should be static.
final Handler handler = new Handler(Looper.getMainLooper()) {
@Override public void handleMessage(Message msg) {
Request request = (Request) msg.obj;
if (request.future.isCancelled() || request.retryCancelled) {
return;
}
Picasso picasso = request.picasso;
switch (msg.what) {
case REQUEST_COMPLETE:
picasso.targetsToRequests.remove(request.getTarget());
request.complete();
break;
case REQUEST_RETRY:
picasso.retry(request);
break;
case REQUEST_DECODE_FAILED:
picasso.error(request);
break;
case REQUEST_CANCEL_GC:
cancelExistingRequest(request, null);
break;
default:
throw new AssertionError("Unknown handler message received: " + msg.what);
}
}
};
static Picasso singleton = null;
final Context context;
final Downloader downloader;
final ExecutorService service;
final Cache cache;
final Listener listener;
final Stats stats;
final Map<Object, Request> targetsToRequests;
final ReferenceQueue<Object> referenceQueue;
boolean debugging;
Picasso(Context context, Downloader downloader, ExecutorService service, Cache cache,
Listener listener, Stats stats, boolean debugging) {
this.context = context;
this.downloader = downloader;
this.service = service;
this.cache = cache;
this.listener = listener;
this.stats = stats;
this.debugging = debugging;
this.targetsToRequests = new WeakHashMap<Object, Request>();
this.referenceQueue = new ReferenceQueue<Object>();
new CleanupThread(referenceQueue, handler).start();
}
/** Cancel any existing requests for the specified target {@link ImageView}. */
public void cancelRequest(ImageView view) {
cancelExistingRequest(view, null);
}
/** Cancel and existing requests for the specified {@link Target} instance. */
public void cancelRequest(Target target) {
cancelExistingRequest(target, null);
}
/**
* Start an image request using the specified URI.
* <p>
* Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
* if one is specified.
*
* @see #load(File)
* @see #load(String)
* @see #load(int)
*/
public RequestBuilder load(Uri uri) {
return new RequestBuilder(this, uri, 0);
}
/**
* Start an image request using the specified path. This is a convenience method for calling
* {@link #load(Uri)}.
* <p>
* This path may be a remote URL, file resource (prefixed with {@code file:}), content resource
* (prefixed with {@code content:}), or android resource (prefixed with {@code
* android.resource:}.
* <p>
* Passing {@code null} as a {@code path} will not trigger any request but will set a
* placeholder, if one is specified.
*
* @see #load(Uri)
* @see #load(File)
* @see #load(int)
*/
public RequestBuilder load(String path) {
if (path == null) {
return new RequestBuilder(this, null, 0);
}
if (path.trim().length() == 0) {
throw new IllegalArgumentException("Path must not be empty.");
}
return load(Uri.parse(path));
}
/**
* Start an image request using the specified image file. This is a convenience method for
* calling {@link #load(Uri)}.
* <p>
* Passing {@code null} as a {@code file} will not trigger any request but will set a
* placeholder, if one is specified.
*
* @see #load(Uri)
* @see #load(String)
* @see #load(int)
*/
public RequestBuilder load(File file) {
if (file == null) {
return new RequestBuilder(this, null, 0);
}
return load(Uri.fromFile(file));
}
/**
* Start an image request using the specified drawable resource ID.
*
* @see #load(Uri)
* @see #load(String)
* @see #load(File)
*/
public RequestBuilder load(int resourceId) {
if (resourceId == 0) {
throw new IllegalArgumentException("Resource ID must not be zero.");
}
return new RequestBuilder(this, null, resourceId);
}
/** {@code true} if debug display, logging, and statistics are enabled. */
public boolean isDebugging() {
return debugging;
}
/** Toggle whether debug display, logging, and statistics are enabled. */
public void setDebugging(boolean debugging) {
this.debugging = debugging;
}
/** Creates a {@link StatsSnapshot} of the current stats for this instance. */
public StatsSnapshot getSnapshot() {
return stats.createSnapshot();
}
void submit(Request request) {
Object target = request.getTarget();
if (target == null) return;
cancelExistingRequest(target, request.uri);
targetsToRequests.put(target, request);
request.future = service.submit(request);
}
void run(Request request) {
try {
Bitmap result = resolveRequest(request);
if (result == null) {
handler.sendMessage(handler.obtainMessage(REQUEST_DECODE_FAILED, request));
return;
}
request.result = result;
handler.sendMessage(handler.obtainMessage(REQUEST_COMPLETE, request));
} catch (IOException e) {
if (listener != null && request.uri != null) {
listener.onImageLoadFailed(this, request.uri, e);
}
handler.sendMessageDelayed(handler.obtainMessage(REQUEST_RETRY, request), RETRY_DELAY);
}
}
Bitmap resolveRequest(Request request) throws IOException {
Bitmap bitmap = loadFromCache(request);
if (bitmap == null) {
stats.cacheMiss();
try {
bitmap = loadFromType(request);
} catch (OutOfMemoryError e) {
throw new IOException("Failed to decode request: " + request, e);
}
if (bitmap != null && !request.skipCache) {
cache.set(request.key, bitmap);
}
} else {
stats.cacheHit();
}
return bitmap;
}
Bitmap quickMemoryCacheCheck(Object target, Uri uri, String key) {
Bitmap cached = cache.get(key);
cancelExistingRequest(target, uri);
if (cached != null) {
stats.cacheHit();
}
return cached;
}
void retry(Request request) {
if (request.retryCancelled) return;
if (request.retryCount > 0) {
request.retryCount--;
submit(request);
} else {
targetsToRequests.remove(request.getTarget());
request.error();
}
}
void error(Request request) {
targetsToRequests.remove(request.getTarget());
request.error();
}
Bitmap decodeStream(InputStream stream, PicassoBitmapOptions bitmapOptions) throws IOException {
if (stream == null) {
return null;
}
try {
if (bitmapOptions != null && bitmapOptions.inJustDecodeBounds) {
MarkableInputStream markStream = new MarkableInputStream(stream);
stream = markStream;
long mark = markStream.savePosition(1024); // Mirrors BitmapFactory.cpp value.
BitmapFactory.decodeStream(stream, null, bitmapOptions);
calculateInSampleSize(bitmapOptions);
markStream.reset(mark);
}
return BitmapFactory.decodeStream(stream, null, bitmapOptions);
} finally {
Utils.closeQuietly(stream);
}
}
Bitmap decodeContentStream(Uri path, PicassoBitmapOptions bitmapOptions) throws IOException {
ContentResolver contentResolver = context.getContentResolver();
if (bitmapOptions != null && bitmapOptions.inJustDecodeBounds) {
BitmapFactory.decodeStream(contentResolver.openInputStream(path), null, bitmapOptions);
calculateInSampleSize(bitmapOptions);
}
return BitmapFactory.decodeStream(contentResolver.openInputStream(path), null, bitmapOptions);
}
Bitmap decodeResource(Resources resources, int resourceId, PicassoBitmapOptions bitmapOptions) {
if (bitmapOptions != null && bitmapOptions.inJustDecodeBounds) {
BitmapFactory.decodeResource(resources, resourceId, bitmapOptions);
calculateInSampleSize(bitmapOptions);
}
return BitmapFactory.decodeResource(resources, resourceId, bitmapOptions);
}
private void cancelExistingRequest(Object target, Uri uri) {
Request existing = targetsToRequests.remove(target);
cancelExistingRequest(existing, uri);
}
private void cancelExistingRequest(Request request, Uri uri) {
if (request != null) {
if (!request.future.isDone()) {
request.future.cancel(true);
} else if (uri == null || !uri.equals(request.uri)) {
request.retryCancelled = true;
}
}
}
private Bitmap loadFromCache(Request request) {
if (request.skipCache) return null;
Bitmap cached = cache.get(request.key);
if (cached != null) {
request.loadedFrom = Request.LoadedFrom.MEMORY;
}
return cached;
}
private Bitmap loadFromType(Request request) throws IOException {
PicassoBitmapOptions options = request.options;
int exifRotation = 0;
Bitmap result = null;
Uri uri = request.uri;
int resourceId = request.resourceId;
if (resourceId != 0) {
result = decodeResource(context.getResources(), resourceId, options);
request.loadedFrom = Request.LoadedFrom.DISK;
} else {
String scheme = uri.getScheme();
if (SCHEME_CONTENT.equals(scheme)) {
ContentResolver contentResolver = context.getContentResolver();
if (Contacts.CONTENT_URI.getHost().equals(uri.getHost()) //
&& !uri.getPathSegments().contains(Contacts.Photo.CONTENT_DIRECTORY)) {
InputStream contactStream = Utils.getContactPhotoStream(contentResolver, uri);
result = decodeStream(contactStream, options);
} else {
exifRotation = Utils.getContentProviderExifRotation(contentResolver, uri);
result = decodeContentStream(uri, options);
}
request.loadedFrom = Request.LoadedFrom.DISK;
} else if (SCHEME_FILE.equals(scheme)) {
exifRotation = Utils.getFileExifRotation(uri.getPath());
result = decodeContentStream(uri, options);
request.loadedFrom = Request.LoadedFrom.DISK;
} else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
result = decodeContentStream(uri, options);
request.loadedFrom = Request.LoadedFrom.DISK;
} else {
Response response = null;
try {
response = downloader.load(uri, request.retryCount == 0);
if (response == null) {
return null;
}
result = decodeStream(response.stream, options);
} finally {
if (response != null && response.stream != null) {
try {
response.stream.close();
} catch (IOException ignored) {
}
}
}
request.loadedFrom = response.cached ? Request.LoadedFrom.DISK : Request.LoadedFrom.NETWORK;
}
}
if (result == null) {
return null;
}
stats.bitmapDecoded(result);
// If the caller wants deferred resize, try to load the target ImageView's measured size.
if (options != null && options.deferredResize) {
ImageView target = request.target.get();
if (target != null) {
int targetWidth = target.getMeasuredWidth();
int targetHeight = target.getMeasuredHeight();
if (targetWidth != 0 && targetHeight != 0) {
options.targetWidth = targetWidth;
options.targetHeight = targetHeight;
}
}
}
if (options != null || exifRotation != 0) {
result = transformResult(options, result, exifRotation);
}
List<Transformation> transformations = request.transformations;
if (transformations != null) {
result = applyCustomTransformations(transformations, result);
stats.bitmapTransformed(result);
}
return result;
}
static class CleanupThread extends Thread {
private final ReferenceQueue<?> referenceQueue;
private final Handler handler;
CleanupThread(ReferenceQueue<?> referenceQueue, Handler handler) {
this.referenceQueue = referenceQueue;
this.handler = handler;
setDaemon(true);
setName(Utils.THREAD_PREFIX + "refQueue");
}
public void run() {
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
while (true) {
try {
RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove();
handler.sendMessage(handler.obtainMessage(REQUEST_CANCEL_GC, remove.request));
} catch (final Exception e) {
handler.post(new Runnable() {
@Override public void run() {
throw new RuntimeException(e);
}
});
break;
}
}
}
}
static Bitmap transformResult(PicassoBitmapOptions options, Bitmap result, int exifRotation) {
int inWidth = result.getWidth();
int inHeight = result.getHeight();
int drawX = 0;
int drawY = 0;
int drawWidth = inWidth;
int drawHeight = inHeight;
Matrix matrix = new Matrix();
if (options != null) {
int targetWidth = options.targetWidth;
int targetHeight = options.targetHeight;
float targetRotation = options.targetRotation;
if (targetRotation != 0) {
if (options.hasRotationPivot) {
matrix.setRotate(targetRotation, options.targetPivotX, options.targetPivotY);
} else {
matrix.setRotate(targetRotation);
}
}
if (options.centerCrop) {
float widthRatio = targetWidth / (float) inWidth;
float heightRatio = targetHeight / (float) inHeight;
float scale;
if (widthRatio > heightRatio) {
scale = widthRatio;
int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio));
drawY = (inHeight - newSize) / 2;
drawHeight = newSize;
} else {
scale = heightRatio;
int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio));
drawX = (inWidth - newSize) / 2;
drawWidth = newSize;
}
matrix.preScale(scale, scale);
} else if (options.centerInside) {
float widthRatio = targetWidth / (float) inWidth;
float heightRatio = targetHeight / (float) inHeight;
float scale = widthRatio < heightRatio ? widthRatio : heightRatio;
matrix.preScale(scale, scale);
} else if (targetWidth != 0 && targetHeight != 0 //
&& (targetWidth != inWidth || targetHeight != inHeight)) {
// If an explicit target size has been specified and they do not match the results bounds,
// pre-scale the existing matrix appropriately.
float sx = targetWidth / (float) inWidth;
float sy = targetHeight / (float) inHeight;
matrix.preScale(sx, sy);
}
float targetScaleX = options.targetScaleX;
float targetScaleY = options.targetScaleY;
if (targetScaleX != 0 || targetScaleY != 0) {
matrix.setScale(targetScaleX, targetScaleY);
}
}
if (exifRotation != 0) {
matrix.preRotate(exifRotation);
}
synchronized (DECODE_LOCK) {
Bitmap newResult =
Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, false);
if (newResult != result) {
result.recycle();
result = newResult;
}
}
return result;
}
static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) {
for (int i = 0, count = transformations.size(); i < count; i++) {
Transformation transformation = transformations.get(i);
Bitmap newResult = transformation.transform(result);
if (newResult == null) {
StringBuilder builder = new StringBuilder() //
.append("Transformation ")
.append(transformation.key())
.append(" returned null after ")
.append(i)
.append(" previous transformation(s).\n\nTransformation list:\n");
for (Transformation t : transformations) {
builder.append(t.key()).append('\n');
}
throw new NullPointerException(builder.toString());
}
if (newResult == result && result.isRecycled()) {
throw new IllegalStateException(
"Transformation " + transformation.key() + " returned input Bitmap but recycled it.");
}
// If the transformation returned a new bitmap ensure they recycled the original.
if (newResult != result && !result.isRecycled()) {
throw new IllegalStateException("Transformation "
+ transformation.key()
+ " mutated input Bitmap but failed to recycle the original.");
}
result = newResult;
}
return result;
}
/**
* The global default {@link Picasso} instance.
* <p>
* This instance is automatically initialized with defaults that are suitable to most
* implementations.
* <ul>
* <li>LRU memory cache of 15% the available application RAM up to 20MB</li>
* <li>Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only
* available on API 14+ <em>or</em> if you are using a standalone library that provides a disk
* cache on all API levels like OkHttp)</li>
* <li>Three download threads for disk and network access.</li>
* </ul>
* <p>
* If these settings do not meet the requirements of your application you can construct your own
* instance with full control over the configuration by using {@link Picasso.Builder}.
*/
public static Picasso with(Context context) {
if (singleton == null) {
singleton = new Builder(context).build();
}
return singleton;
}
/** Fluent API for creating {@link Picasso} instances. */
@SuppressWarnings("UnusedDeclaration") // Public API.
public static class Builder {
private final Context context;
private Downloader downloader;
private ExecutorService service;
private Cache memoryCache;
private Listener listener;
private boolean debugging;
/** Start building a new {@link Picasso} instance. */
public Builder(Context context) {
if (context == null) {
throw new IllegalArgumentException("Context must not be null.");
}
this.context = context.getApplicationContext();
}
/** Specify the {@link Downloader} that will be used for downloading images. */
public Builder downloader(Downloader downloader) {
if (downloader == null) {
throw new IllegalArgumentException("Downloader must not be null.");
}
if (this.downloader != null) {
throw new IllegalStateException("Downloader already set.");
}
this.downloader = downloader;
return this;
}
/** Specify the executor service for loading images in the background. */
public Builder executor(ExecutorService executorService) {
if (executorService == null) {
throw new IllegalArgumentException("Executor service must not be null.");
}
if (this.service != null) {
throw new IllegalStateException("Executor service already set.");
}
this.service = executorService;
return this;
}
/** Specify the memory cache used for the most recent images. */
public Builder memoryCache(Cache memoryCache) {
if (memoryCache == null) {
throw new IllegalArgumentException("Memory cache must not be null.");
}
if (this.memoryCache != null) {
throw new IllegalStateException("Memory cache already set.");
}
this.memoryCache = memoryCache;
return this;
}
/** Specify a listener for interesting events. */
public Builder listener(Listener listener) {
if (listener == null) {
throw new IllegalArgumentException("Listener must not be null.");
}
if (this.listener != null) {
throw new IllegalStateException("Listener already set.");
}
this.listener = listener;
return this;
}
/** Whether debugging is enabled or not. */
public Builder debugging(boolean debugging) {
this.debugging = debugging;
return this;
}
/** Create the {@link Picasso} instance. */
public Picasso build() {
Context context = this.context;
if (downloader == null) {
downloader = Utils.createDefaultDownloader(context);
}
if (memoryCache == null) {
memoryCache = new LruCache(context);
}
if (service == null) {
service = Executors.newFixedThreadPool(3, new Utils.PicassoThreadFactory());
}
Stats stats = new Stats(memoryCache);
return new Picasso(context, downloader, service, memoryCache, listener, stats, debugging);
}
}
}