diff options
Diffstat (limited to 'mobile/android/thirdparty/com/squareup/picasso/Picasso.java')
-rw-r--r-- | mobile/android/thirdparty/com/squareup/picasso/Picasso.java | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Picasso.java b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java new file mode 100644 index 000000000..9b510f977 --- /dev/null +++ b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java @@ -0,0 +1,522 @@ +/* + * 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. + * <p/> + * 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. + * <p> + * 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. + * <p> + * <b>NOTE:</b> 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<BitmapHunter> batch = (List<BitmapHunter>) 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<Object, Action> targetToAction; + final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator; + final ReferenceQueue<Object> 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<Object, Action>(); + this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>(); + this.debugging = debugging; + this.referenceQueue = new ReferenceQueue<Object>(); + 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. + * <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 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)}. + * <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 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)}. + * <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 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<Action> 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. + * <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</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 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. + * <p> + * <b>NOTE:</b> 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; + } + } +} |