michael@0: /*
michael@0: * Copyright (C) 2013 Square, Inc.
michael@0: *
michael@0: * Licensed under the Apache License, Version 2.0 (the "License");
michael@0: * you may not use this file except in compliance with the License.
michael@0: * You may obtain a copy of the License at
michael@0: *
michael@0: * http://www.apache.org/licenses/LICENSE-2.0
michael@0: *
michael@0: * Unless required by applicable law or agreed to in writing, software
michael@0: * distributed under the License is distributed on an "AS IS" BASIS,
michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
michael@0: * See the License for the specific language governing permissions and
michael@0: * limitations under the License.
michael@0: */
michael@0: package com.squareup.picasso;
michael@0:
michael@0: import android.content.Context;
michael@0: import android.graphics.Bitmap;
michael@0: import android.graphics.Color;
michael@0: import android.net.Uri;
michael@0: import android.os.Handler;
michael@0: import android.os.Looper;
michael@0: import android.os.Message;
michael@0: import android.os.Process;
michael@0: import android.widget.ImageView;
michael@0: import java.io.File;
michael@0: import java.lang.ref.ReferenceQueue;
michael@0: import java.util.List;
michael@0: import java.util.Map;
michael@0: import java.util.WeakHashMap;
michael@0: import java.util.concurrent.ExecutorService;
michael@0:
michael@0: import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
michael@0: import static com.squareup.picasso.Action.RequestWeakReference;
michael@0: import static com.squareup.picasso.Dispatcher.HUNTER_BATCH_COMPLETE;
michael@0: import static com.squareup.picasso.Dispatcher.REQUEST_GCED;
michael@0: import static com.squareup.picasso.Utils.THREAD_PREFIX;
michael@0:
michael@0: /**
michael@0: * Image downloading, transformation, and caching manager.
michael@0: *
michael@0: * Use {@link #with(android.content.Context)} for the global singleton instance or construct your
michael@0: * own instance with {@link Builder}.
michael@0: */
michael@0: public class Picasso {
michael@0:
michael@0: /** Callbacks for Picasso events. */
michael@0: public interface Listener {
michael@0: /**
michael@0: * Invoked when an image has failed to load. This is useful for reporting image failures to a
michael@0: * remote analytics service, for example.
michael@0: */
michael@0: void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception);
michael@0: }
michael@0:
michael@0: /**
michael@0: * A transformer that is called immediately before every request is submitted. This can be used to
michael@0: * modify any information about a request.
michael@0: *
michael@0: * For example, if you use a CDN you can change the hostname for the image based on the current
michael@0: * location of the user in order to get faster download speeds.
michael@0: *
michael@0: * NOTE: This is a beta feature. The API is subject to change in a backwards incompatible
michael@0: * way at any time.
michael@0: */
michael@0: public interface RequestTransformer {
michael@0: /**
michael@0: * Transform a request before it is submitted to be processed.
michael@0: *
michael@0: * @return The original request or a new request to replace it. Must not be null.
michael@0: */
michael@0: Request transformRequest(Request request);
michael@0:
michael@0: /** A {@link RequestTransformer} which returns the original request. */
michael@0: RequestTransformer IDENTITY = new RequestTransformer() {
michael@0: @Override public Request transformRequest(Request request) {
michael@0: return request;
michael@0: }
michael@0: };
michael@0: }
michael@0:
michael@0: static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
michael@0: @Override public void handleMessage(Message msg) {
michael@0: switch (msg.what) {
michael@0: case HUNTER_BATCH_COMPLETE: {
michael@0: @SuppressWarnings("unchecked") List batch = (List) msg.obj;
michael@0: for (BitmapHunter hunter : batch) {
michael@0: hunter.picasso.complete(hunter);
michael@0: }
michael@0: break;
michael@0: }
michael@0: case REQUEST_GCED: {
michael@0: Action action = (Action) msg.obj;
michael@0: action.picasso.cancelExistingRequest(action.getTarget());
michael@0: break;
michael@0: }
michael@0: default:
michael@0: throw new AssertionError("Unknown handler message received: " + msg.what);
michael@0: }
michael@0: }
michael@0: };
michael@0:
michael@0: static Picasso singleton = null;
michael@0:
michael@0: private final Listener listener;
michael@0: private final RequestTransformer requestTransformer;
michael@0: private final CleanupThread cleanupThread;
michael@0:
michael@0: final Context context;
michael@0: final Dispatcher dispatcher;
michael@0: final Cache cache;
michael@0: final Stats stats;
michael@0: final Map targetToAction;
michael@0: final Map targetToDeferredRequestCreator;
michael@0: final ReferenceQueue referenceQueue;
michael@0:
michael@0: boolean debugging;
michael@0: boolean shutdown;
michael@0:
michael@0: Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
michael@0: RequestTransformer requestTransformer, Stats stats, boolean debugging) {
michael@0: this.context = context;
michael@0: this.dispatcher = dispatcher;
michael@0: this.cache = cache;
michael@0: this.listener = listener;
michael@0: this.requestTransformer = requestTransformer;
michael@0: this.stats = stats;
michael@0: this.targetToAction = new WeakHashMap();
michael@0: this.targetToDeferredRequestCreator = new WeakHashMap();
michael@0: this.debugging = debugging;
michael@0: this.referenceQueue = new ReferenceQueue();
michael@0: this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);
michael@0: this.cleanupThread.start();
michael@0: }
michael@0:
michael@0: /** Cancel any existing requests for the specified target {@link ImageView}. */
michael@0: public void cancelRequest(ImageView view) {
michael@0: cancelExistingRequest(view);
michael@0: }
michael@0:
michael@0: /** Cancel any existing requests for the specified {@link Target} instance. */
michael@0: public void cancelRequest(Target target) {
michael@0: cancelExistingRequest(target);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Start an image request using the specified URI.
michael@0: *
michael@0: * Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
michael@0: * if one is specified.
michael@0: *
michael@0: * @see #load(File)
michael@0: * @see #load(String)
michael@0: * @see #load(int)
michael@0: */
michael@0: public RequestCreator load(Uri uri) {
michael@0: return new RequestCreator(this, uri, 0);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Start an image request using the specified path. This is a convenience method for calling
michael@0: * {@link #load(Uri)}.
michael@0: *
michael@0: * This path may be a remote URL, file resource (prefixed with {@code file:}), content resource
michael@0: * (prefixed with {@code content:}), or android resource (prefixed with {@code
michael@0: * android.resource:}.
michael@0: *
michael@0: * Passing {@code null} as a {@code path} will not trigger any request but will set a
michael@0: * placeholder, if one is specified.
michael@0: *
michael@0: * @see #load(Uri)
michael@0: * @see #load(File)
michael@0: * @see #load(int)
michael@0: */
michael@0: public RequestCreator load(String path) {
michael@0: if (path == null) {
michael@0: return new RequestCreator(this, null, 0);
michael@0: }
michael@0: if (path.trim().length() == 0) {
michael@0: throw new IllegalArgumentException("Path must not be empty.");
michael@0: }
michael@0: return load(Uri.parse(path));
michael@0: }
michael@0:
michael@0: /**
michael@0: * Start an image request using the specified image file. This is a convenience method for
michael@0: * calling {@link #load(Uri)}.
michael@0: *
michael@0: * Passing {@code null} as a {@code file} will not trigger any request but will set a
michael@0: * placeholder, if one is specified.
michael@0: *
michael@0: * @see #load(Uri)
michael@0: * @see #load(String)
michael@0: * @see #load(int)
michael@0: */
michael@0: public RequestCreator load(File file) {
michael@0: if (file == null) {
michael@0: return new RequestCreator(this, null, 0);
michael@0: }
michael@0: return load(Uri.fromFile(file));
michael@0: }
michael@0:
michael@0: /**
michael@0: * Start an image request using the specified drawable resource ID.
michael@0: *
michael@0: * @see #load(Uri)
michael@0: * @see #load(String)
michael@0: * @see #load(File)
michael@0: */
michael@0: public RequestCreator load(int resourceId) {
michael@0: if (resourceId == 0) {
michael@0: throw new IllegalArgumentException("Resource ID must not be zero.");
michael@0: }
michael@0: return new RequestCreator(this, null, resourceId);
michael@0: }
michael@0:
michael@0: /** {@code true} if debug display, logging, and statistics are enabled. */
michael@0: @SuppressWarnings("UnusedDeclaration") public boolean isDebugging() {
michael@0: return debugging;
michael@0: }
michael@0:
michael@0: /** Toggle whether debug display, logging, and statistics are enabled. */
michael@0: @SuppressWarnings("UnusedDeclaration") public void setDebugging(boolean debugging) {
michael@0: this.debugging = debugging;
michael@0: }
michael@0:
michael@0: /** Creates a {@link StatsSnapshot} of the current stats for this instance. */
michael@0: @SuppressWarnings("UnusedDeclaration") public StatsSnapshot getSnapshot() {
michael@0: return stats.createSnapshot();
michael@0: }
michael@0:
michael@0: /** Stops this instance from accepting further requests. */
michael@0: public void shutdown() {
michael@0: if (this == singleton) {
michael@0: throw new UnsupportedOperationException("Default singleton instance cannot be shutdown.");
michael@0: }
michael@0: if (shutdown) {
michael@0: return;
michael@0: }
michael@0: cache.clear();
michael@0: cleanupThread.shutdown();
michael@0: stats.shutdown();
michael@0: dispatcher.shutdown();
michael@0: for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) {
michael@0: deferredRequestCreator.cancel();
michael@0: }
michael@0: targetToDeferredRequestCreator.clear();
michael@0: shutdown = true;
michael@0: }
michael@0:
michael@0: Request transformRequest(Request request) {
michael@0: Request transformed = requestTransformer.transformRequest(request);
michael@0: if (transformed == null) {
michael@0: throw new IllegalStateException("Request transformer "
michael@0: + requestTransformer.getClass().getCanonicalName()
michael@0: + " returned null for "
michael@0: + request);
michael@0: }
michael@0: return transformed;
michael@0: }
michael@0:
michael@0: void defer(ImageView view, DeferredRequestCreator request) {
michael@0: targetToDeferredRequestCreator.put(view, request);
michael@0: }
michael@0:
michael@0: void enqueueAndSubmit(Action action) {
michael@0: Object target = action.getTarget();
michael@0: if (target != null) {
michael@0: cancelExistingRequest(target);
michael@0: targetToAction.put(target, action);
michael@0: }
michael@0: submit(action);
michael@0: }
michael@0:
michael@0: void submit(Action action) {
michael@0: dispatcher.dispatchSubmit(action);
michael@0: }
michael@0:
michael@0: Bitmap quickMemoryCacheCheck(String key) {
michael@0: Bitmap cached = cache.get(key);
michael@0: if (cached != null) {
michael@0: stats.dispatchCacheHit();
michael@0: } else {
michael@0: stats.dispatchCacheMiss();
michael@0: }
michael@0: return cached;
michael@0: }
michael@0:
michael@0: void complete(BitmapHunter hunter) {
michael@0: List joined = hunter.getActions();
michael@0: if (joined.isEmpty()) {
michael@0: return;
michael@0: }
michael@0:
michael@0: Uri uri = hunter.getData().uri;
michael@0: Exception exception = hunter.getException();
michael@0: Bitmap result = hunter.getResult();
michael@0: LoadedFrom from = hunter.getLoadedFrom();
michael@0:
michael@0: for (Action join : joined) {
michael@0: if (join.isCancelled()) {
michael@0: continue;
michael@0: }
michael@0: targetToAction.remove(join.getTarget());
michael@0: if (result != null) {
michael@0: if (from == null) {
michael@0: throw new AssertionError("LoadedFrom cannot be null.");
michael@0: }
michael@0: join.complete(result, from);
michael@0: } else {
michael@0: join.error();
michael@0: }
michael@0: }
michael@0:
michael@0: if (listener != null && exception != null) {
michael@0: listener.onImageLoadFailed(this, uri, exception);
michael@0: }
michael@0: }
michael@0:
michael@0: private void cancelExistingRequest(Object target) {
michael@0: Action action = targetToAction.remove(target);
michael@0: if (action != null) {
michael@0: action.cancel();
michael@0: dispatcher.dispatchCancel(action);
michael@0: }
michael@0: if (target instanceof ImageView) {
michael@0: ImageView targetImageView = (ImageView) target;
michael@0: DeferredRequestCreator deferredRequestCreator =
michael@0: targetToDeferredRequestCreator.remove(targetImageView);
michael@0: if (deferredRequestCreator != null) {
michael@0: deferredRequestCreator.cancel();
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: private static class CleanupThread extends Thread {
michael@0: private final ReferenceQueue> referenceQueue;
michael@0: private final Handler handler;
michael@0:
michael@0: CleanupThread(ReferenceQueue> referenceQueue, Handler handler) {
michael@0: this.referenceQueue = referenceQueue;
michael@0: this.handler = handler;
michael@0: setDaemon(true);
michael@0: setName(THREAD_PREFIX + "refQueue");
michael@0: }
michael@0:
michael@0: @Override public void run() {
michael@0: Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
michael@0: while (true) {
michael@0: try {
michael@0: RequestWeakReference> remove = (RequestWeakReference>) referenceQueue.remove();
michael@0: handler.sendMessage(handler.obtainMessage(REQUEST_GCED, remove.action));
michael@0: } catch (InterruptedException e) {
michael@0: break;
michael@0: } catch (final Exception e) {
michael@0: handler.post(new Runnable() {
michael@0: @Override public void run() {
michael@0: throw new RuntimeException(e);
michael@0: }
michael@0: });
michael@0: break;
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: void shutdown() {
michael@0: interrupt();
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * The global default {@link Picasso} instance.
michael@0: *
michael@0: * This instance is automatically initialized with defaults that are suitable to most
michael@0: * implementations.
michael@0: *
michael@0: * LRU memory cache of 15% the available application RAM
michael@0: * Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only
michael@0: * available on API 14+ or if you are using a standalone library that provides a disk
michael@0: * cache on all API levels like OkHttp)
michael@0: * Three download threads for disk and network access.
michael@0: *
michael@0: *
michael@0: * If these settings do not meet the requirements of your application you can construct your own
michael@0: * instance with full control over the configuration by using {@link Picasso.Builder}.
michael@0: */
michael@0: public static Picasso with(Context context) {
michael@0: if (singleton == null) {
michael@0: singleton = new Builder(context).build();
michael@0: }
michael@0: return singleton;
michael@0: }
michael@0:
michael@0: /** Fluent API for creating {@link Picasso} instances. */
michael@0: @SuppressWarnings("UnusedDeclaration") // Public API.
michael@0: public static class Builder {
michael@0: private final Context context;
michael@0: private Downloader downloader;
michael@0: private ExecutorService service;
michael@0: private Cache cache;
michael@0: private Listener listener;
michael@0: private RequestTransformer transformer;
michael@0: private boolean debugging;
michael@0:
michael@0: /** Start building a new {@link Picasso} instance. */
michael@0: public Builder(Context context) {
michael@0: if (context == null) {
michael@0: throw new IllegalArgumentException("Context must not be null.");
michael@0: }
michael@0: this.context = context.getApplicationContext();
michael@0: }
michael@0:
michael@0: /** Specify the {@link Downloader} that will be used for downloading images. */
michael@0: public Builder downloader(Downloader downloader) {
michael@0: if (downloader == null) {
michael@0: throw new IllegalArgumentException("Downloader must not be null.");
michael@0: }
michael@0: if (this.downloader != null) {
michael@0: throw new IllegalStateException("Downloader already set.");
michael@0: }
michael@0: this.downloader = downloader;
michael@0: return this;
michael@0: }
michael@0:
michael@0: /** Specify the executor service for loading images in the background. */
michael@0: public Builder executor(ExecutorService executorService) {
michael@0: if (executorService == null) {
michael@0: throw new IllegalArgumentException("Executor service must not be null.");
michael@0: }
michael@0: if (this.service != null) {
michael@0: throw new IllegalStateException("Executor service already set.");
michael@0: }
michael@0: this.service = executorService;
michael@0: return this;
michael@0: }
michael@0:
michael@0: /** Specify the memory cache used for the most recent images. */
michael@0: public Builder memoryCache(Cache memoryCache) {
michael@0: if (memoryCache == null) {
michael@0: throw new IllegalArgumentException("Memory cache must not be null.");
michael@0: }
michael@0: if (this.cache != null) {
michael@0: throw new IllegalStateException("Memory cache already set.");
michael@0: }
michael@0: this.cache = memoryCache;
michael@0: return this;
michael@0: }
michael@0:
michael@0: /** Specify a listener for interesting events. */
michael@0: public Builder listener(Listener listener) {
michael@0: if (listener == null) {
michael@0: throw new IllegalArgumentException("Listener must not be null.");
michael@0: }
michael@0: if (this.listener != null) {
michael@0: throw new IllegalStateException("Listener already set.");
michael@0: }
michael@0: this.listener = listener;
michael@0: return this;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Specify a transformer for all incoming requests.
michael@0: *
michael@0: * NOTE: This is a beta feature. The API is subject to change in a backwards incompatible
michael@0: * way at any time.
michael@0: */
michael@0: public Builder requestTransformer(RequestTransformer transformer) {
michael@0: if (transformer == null) {
michael@0: throw new IllegalArgumentException("Transformer must not be null.");
michael@0: }
michael@0: if (this.transformer != null) {
michael@0: throw new IllegalStateException("Transformer already set.");
michael@0: }
michael@0: this.transformer = transformer;
michael@0: return this;
michael@0: }
michael@0:
michael@0: /** Whether debugging is enabled or not. */
michael@0: public Builder debugging(boolean debugging) {
michael@0: this.debugging = debugging;
michael@0: return this;
michael@0: }
michael@0:
michael@0: /** Create the {@link Picasso} instance. */
michael@0: public Picasso build() {
michael@0: Context context = this.context;
michael@0:
michael@0: if (downloader == null) {
michael@0: downloader = Utils.createDefaultDownloader(context);
michael@0: }
michael@0: if (cache == null) {
michael@0: cache = new LruCache(context);
michael@0: }
michael@0: if (service == null) {
michael@0: service = new PicassoExecutorService();
michael@0: }
michael@0: if (transformer == null) {
michael@0: transformer = RequestTransformer.IDENTITY;
michael@0: }
michael@0:
michael@0: Stats stats = new Stats(cache);
michael@0:
michael@0: Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
michael@0:
michael@0: return new Picasso(context, dispatcher, cache, listener, transformer, stats, debugging);
michael@0: }
michael@0: }
michael@0:
michael@0: /** Describes where the image was loaded from. */
michael@0: public enum LoadedFrom {
michael@0: MEMORY(Color.GREEN),
michael@0: DISK(Color.YELLOW),
michael@0: NETWORK(Color.RED);
michael@0:
michael@0: final int debugColor;
michael@0:
michael@0: private LoadedFrom(int debugColor) {
michael@0: this.debugColor = debugColor;
michael@0: }
michael@0: }
michael@0: }