/* * 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.Context; import android.graphics.Bitmap; import android.graphics.Color; 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.lang.ref.ReferenceQueue; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ExecutorService; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; import static com.squareup.picasso.Action.RequestWeakReference; import static com.squareup.picasso.Dispatcher.HUNTER_BATCH_COMPLETE; import static com.squareup.picasso.Dispatcher.REQUEST_GCED; import static com.squareup.picasso.Utils.THREAD_PREFIX; /** * Image downloading, transformation, and caching manager. *

* Use {@link #with(android.content.Context)} for the global singleton instance or construct your * own instance with {@link Builder}. */ public class Picasso { /** 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); } /** * A transformer that is called immediately before every request is submitted. This can be used to * modify any information about a request. *

* For example, if you use a CDN you can change the hostname for the image based on the current * location of the user in order to get faster download speeds. *

* NOTE: This is a beta feature. The API is subject to change in a backwards incompatible * way at any time. */ public interface RequestTransformer { /** * Transform a request before it is submitted to be processed. * * @return The original request or a new request to replace it. Must not be null. */ Request transformRequest(Request request); /** A {@link RequestTransformer} which returns the original request. */ RequestTransformer IDENTITY = new RequestTransformer() { @Override public Request transformRequest(Request request) { return request; } }; } static final Handler HANDLER = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { case HUNTER_BATCH_COMPLETE: { @SuppressWarnings("unchecked") List batch = (List) msg.obj; for (BitmapHunter hunter : batch) { hunter.picasso.complete(hunter); } break; } case REQUEST_GCED: { Action action = (Action) msg.obj; action.picasso.cancelExistingRequest(action.getTarget()); break; } default: throw new AssertionError("Unknown handler message received: " + msg.what); } } }; static Picasso singleton = null; private final Listener listener; private final RequestTransformer requestTransformer; private final CleanupThread cleanupThread; final Context context; final Dispatcher dispatcher; final Cache cache; final Stats stats; final Map targetToAction; final Map targetToDeferredRequestCreator; final ReferenceQueue referenceQueue; boolean debugging; boolean shutdown; Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener, RequestTransformer requestTransformer, Stats stats, boolean debugging) { this.context = context; this.dispatcher = dispatcher; this.cache = cache; this.listener = listener; this.requestTransformer = requestTransformer; this.stats = stats; this.targetToAction = new WeakHashMap(); this.targetToDeferredRequestCreator = new WeakHashMap(); this.debugging = debugging; this.referenceQueue = new ReferenceQueue(); this.cleanupThread = new CleanupThread(referenceQueue, HANDLER); this.cleanupThread.start(); } /** Cancel any existing requests for the specified target {@link ImageView}. */ public void cancelRequest(ImageView view) { cancelExistingRequest(view); } /** Cancel any existing requests for the specified {@link Target} instance. */ public void cancelRequest(Target target) { cancelExistingRequest(target); } /** * Start an image request using the specified URI. *

* 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 RequestCreator load(Uri uri) { return new RequestCreator(this, uri, 0); } /** * Start an image request using the specified path. This is a convenience method for calling * {@link #load(Uri)}. *

* 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:}. *

* 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 RequestCreator load(String path) { if (path == null) { return new RequestCreator(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)}. *

* 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 RequestCreator load(File file) { if (file == null) { return new RequestCreator(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 RequestCreator load(int resourceId) { if (resourceId == 0) { throw new IllegalArgumentException("Resource ID must not be zero."); } return new RequestCreator(this, null, resourceId); } /** {@code true} if debug display, logging, and statistics are enabled. */ @SuppressWarnings("UnusedDeclaration") public boolean isDebugging() { return debugging; } /** Toggle whether debug display, logging, and statistics are enabled. */ @SuppressWarnings("UnusedDeclaration") public void setDebugging(boolean debugging) { this.debugging = debugging; } /** Creates a {@link StatsSnapshot} of the current stats for this instance. */ @SuppressWarnings("UnusedDeclaration") public StatsSnapshot getSnapshot() { return stats.createSnapshot(); } /** Stops this instance from accepting further requests. */ public void shutdown() { if (this == singleton) { throw new UnsupportedOperationException("Default singleton instance cannot be shutdown."); } if (shutdown) { return; } cache.clear(); cleanupThread.shutdown(); stats.shutdown(); dispatcher.shutdown(); for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) { deferredRequestCreator.cancel(); } targetToDeferredRequestCreator.clear(); shutdown = true; } Request transformRequest(Request request) { Request transformed = requestTransformer.transformRequest(request); if (transformed == null) { throw new IllegalStateException("Request transformer " + requestTransformer.getClass().getCanonicalName() + " returned null for " + request); } return transformed; } void defer(ImageView view, DeferredRequestCreator request) { targetToDeferredRequestCreator.put(view, request); } void enqueueAndSubmit(Action action) { Object target = action.getTarget(); if (target != null) { cancelExistingRequest(target); targetToAction.put(target, action); } submit(action); } void submit(Action action) { dispatcher.dispatchSubmit(action); } Bitmap quickMemoryCacheCheck(String key) { Bitmap cached = cache.get(key); if (cached != null) { stats.dispatchCacheHit(); } else { stats.dispatchCacheMiss(); } return cached; } void complete(BitmapHunter hunter) { List joined = hunter.getActions(); if (joined.isEmpty()) { return; } Uri uri = hunter.getData().uri; Exception exception = hunter.getException(); Bitmap result = hunter.getResult(); LoadedFrom from = hunter.getLoadedFrom(); for (Action join : joined) { if (join.isCancelled()) { continue; } targetToAction.remove(join.getTarget()); if (result != null) { if (from == null) { throw new AssertionError("LoadedFrom cannot be null."); } join.complete(result, from); } else { join.error(); } } if (listener != null && exception != null) { listener.onImageLoadFailed(this, uri, exception); } } private void cancelExistingRequest(Object target) { Action action = targetToAction.remove(target); if (action != null) { action.cancel(); dispatcher.dispatchCancel(action); } if (target instanceof ImageView) { ImageView targetImageView = (ImageView) target; DeferredRequestCreator deferredRequestCreator = targetToDeferredRequestCreator.remove(targetImageView); if (deferredRequestCreator != null) { deferredRequestCreator.cancel(); } } } private 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(THREAD_PREFIX + "refQueue"); } @Override public void run() { Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND); while (true) { try { RequestWeakReference remove = (RequestWeakReference) referenceQueue.remove(); handler.sendMessage(handler.obtainMessage(REQUEST_GCED, remove.action)); } catch (InterruptedException e) { break; } catch (final Exception e) { handler.post(new Runnable() { @Override public void run() { throw new RuntimeException(e); } }); break; } } } void shutdown() { interrupt(); } } /** * The global default {@link Picasso} instance. *

* This instance is automatically initialized with defaults that are suitable to most * implementations. *

    *
  • LRU memory cache of 15% the available application RAM
  • *
  • Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only * available on API 14+ or if you are using a standalone library that provides a disk * cache on all API levels like OkHttp)
  • *
  • Three download threads for disk and network access.
  • *
*

* 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 cache; private Listener listener; private RequestTransformer transformer; 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.cache != null) { throw new IllegalStateException("Memory cache already set."); } this.cache = 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; } /** * Specify a transformer for all incoming requests. *

* NOTE: This is a beta feature. The API is subject to change in a backwards incompatible * way at any time. */ public Builder requestTransformer(RequestTransformer transformer) { if (transformer == null) { throw new IllegalArgumentException("Transformer must not be null."); } if (this.transformer != null) { throw new IllegalStateException("Transformer already set."); } this.transformer = transformer; 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 (cache == null) { cache = new LruCache(context); } if (service == null) { service = new PicassoExecutorService(); } if (transformer == null) { transformer = RequestTransformer.IDENTITY; } Stats stats = new Stats(cache); Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats); return new Picasso(context, dispatcher, cache, listener, transformer, stats, debugging); } } /** Describes where the image was loaded from. */ public enum LoadedFrom { MEMORY(Color.GREEN), DISK(Color.YELLOW), NETWORK(Color.RED); final int debugColor; private LoadedFrom(int debugColor) { this.debugColor = debugColor; } } }