/*
* 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.support.annotation.DrawableRes;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.ImageView;
import android.widget.RemoteViews;
import java.io.File;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.Collections;
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_BATCH_RESUME;
import static com.squareup.picasso.Dispatcher.REQUEST_GCED;
import static com.squareup.picasso.MemoryPolicy.shouldReadFromMemoryCache;
import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
import static com.squareup.picasso.Utils.OWNER_MAIN;
import static com.squareup.picasso.Utils.THREAD_LEAK_CLEANING_MS;
import static com.squareup.picasso.Utils.THREAD_PREFIX;
import static com.squareup.picasso.Utils.VERB_CANCELED;
import static com.squareup.picasso.Utils.VERB_COMPLETED;
import static com.squareup.picasso.Utils.VERB_ERRORED;
import static com.squareup.picasso.Utils.VERB_RESUMED;
import static com.squareup.picasso.Utils.checkMain;
import static com.squareup.picasso.Utils.log;
/**
* 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;
}
};
}
/**
* The priority of a request.
*
* @see RequestCreator#priority(Priority)
*/
public enum Priority {
LOW,
NORMAL,
HIGH
}
static final String TAG = "Picasso";
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;
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = batch.size(); i < n; i++) {
BitmapHunter hunter = batch.get(i);
hunter.picasso.complete(hunter);
}
break;
}
case REQUEST_GCED: {
Action action = (Action) msg.obj;
if (action.getPicasso().loggingEnabled) {
log(OWNER_MAIN, VERB_CANCELED, action.request.logId(), "target got garbage collected");
}
action.picasso.cancelExistingRequest(action.getTarget());
break;
}
case REQUEST_BATCH_RESUME:
@SuppressWarnings("unchecked") List<Action> batch = (List<Action>) msg.obj;
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = batch.size(); i < n; i++) {
Action action = batch.get(i);
action.picasso.resumeAction(action);
}
break;
default:
throw new AssertionError("Unknown handler message received: " + msg.what);
}
}
};
static volatile Picasso singleton = null;
private final Listener listener;
private final RequestTransformer requestTransformer;
private final CleanupThread cleanupThread;
private final List<RequestHandler> requestHandlers;
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;
final Bitmap.Config defaultBitmapConfig;
boolean indicatorsEnabled;
volatile boolean loggingEnabled;
boolean shutdown;
Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats,
Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) {
this.context = context;
this.dispatcher = dispatcher;
this.cache = cache;
this.listener = listener;
this.requestTransformer = requestTransformer;
this.defaultBitmapConfig = defaultBitmapConfig;
int builtInHandlers = 7; // Adjust this as internal handlers are added or removed.
int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);
List<RequestHandler> allRequestHandlers = new ArrayList<>(builtInHandlers + extraCount);
// ResourceRequestHandler needs to be the first in the list to avoid
// forcing other RequestHandlers to perform null checks on request.uri
// to cover the (request.resourceId != 0) case.
allRequestHandlers.add(new ResourceRequestHandler(context));
if (extraRequestHandlers != null) {
allRequestHandlers.addAll(extraRequestHandlers);
}
allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
allRequestHandlers.add(new MediaStoreRequestHandler(context));
allRequestHandlers.add(new ContentStreamRequestHandler(context));
allRequestHandlers.add(new AssetRequestHandler(context));
allRequestHandlers.add(new FileRequestHandler(context));
allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));
requestHandlers = Collections.unmodifiableList(allRequestHandlers);
this.stats = stats;
this.targetToAction = new WeakHashMap<>();
this.targetToDeferredRequestCreator = new WeakHashMap<>();
this.indicatorsEnabled = indicatorsEnabled;
this.loggingEnabled = loggingEnabled;
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(@NonNull ImageView view) {
// checkMain() is called from cancelExistingRequest()
if (view == null) {
throw new IllegalArgumentException("view cannot be null.");
}
cancelExistingRequest(view);
}
/** Cancel any existing requests for the specified {@link Target} instance. */
public void cancelRequest(@NonNull Target target) {
// checkMain() is called from cancelExistingRequest()
if (target == null) {
throw new IllegalArgumentException("target cannot be null.");
}
cancelExistingRequest(target);
}
/**
* Cancel any existing requests for the specified {@link RemoteViews} target with the given {@code
* viewId}.
*/
public void cancelRequest(@NonNull RemoteViews remoteViews, @IdRes int viewId) {
// checkMain() is called from cancelExistingRequest()
if (remoteViews == null) {
throw new IllegalArgumentException("remoteViews cannot be null.");
}
cancelExistingRequest(new RemoteViewsAction.RemoteViewsTarget(remoteViews, viewId));
}
/**
* Cancel any existing requests with given tag. You can set a tag
* on new requests with {@link RequestCreator#tag(Object)}.
*
* @see RequestCreator#tag(Object)
*/
public void cancelTag(@NonNull Object tag) {
checkMain();
if (tag == null) {
throw new IllegalArgumentException("Cannot cancel requests with null tag.");
}
List<Action> actions = new ArrayList<>(targetToAction.values());
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = actions.size(); i < n; i++) {
Action action = actions.get(i);
if (tag.equals(action.getTag())) {
cancelExistingRequest(action.getTarget());
}
}
List<DeferredRequestCreator> deferredRequestCreators =
new ArrayList<>(targetToDeferredRequestCreator.values());
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = deferredRequestCreators.size(); i < n; i++) {
DeferredRequestCreator deferredRequestCreator = deferredRequestCreators.get(i);
if (tag.equals(deferredRequestCreator.getTag())) {
deferredRequestCreator.cancel();
}
}
}
/**
* Pause existing requests with the given tag. Use {@link #resumeTag(Object)}
* to resume requests with the given tag.
*
* @see #resumeTag(Object)
* @see RequestCreator#tag(Object)
*/
public void pauseTag(@NonNull Object tag) {
if (tag == null) {
throw new IllegalArgumentException("tag == null");
}
dispatcher.dispatchPauseTag(tag);
}
/**
* Resume paused requests with the given tag. Use {@link #pauseTag(Object)}
* to pause requests with the given tag.
*
* @see #pauseTag(Object)
* @see RequestCreator#tag(Object)
*/
public void resumeTag(@NonNull Object tag) {
if (tag == null) {
throw new IllegalArgumentException("tag == null");
}
dispatcher.dispatchResumeTag(tag);
}
/**
* 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(@Nullable 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)
* @throws IllegalArgumentException if {@code path} is empty or blank string.
*/
public RequestCreator load(@Nullable 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.
* <p>
* Equivalent to calling {@link #load(Uri) load(Uri.fromFile(file))}.
*
* @see #load(Uri)
* @see #load(String)
* @see #load(int)
*/
public RequestCreator load(@NonNull 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(@DrawableRes int resourceId) {
if (resourceId == 0) {
throw new IllegalArgumentException("Resource ID must not be zero.");
}
return new RequestCreator(this, null, resourceId);
}
/**
* Invalidate all memory cached images for the specified {@code uri}.
*
* @see #invalidate(String)
* @see #invalidate(File)
*/
public void invalidate(@Nullable Uri uri) {
if (uri != null) {
cache.clearKeyUri(uri.toString());
}
}
/**
* Invalidate all memory cached images for the specified {@code path}. You can also pass a
* {@linkplain RequestCreator#stableKey stable key}.
*
* @see #invalidate(Uri)
* @see #invalidate(File)
*/
public void invalidate(@Nullable String path) {
if (path != null) {
invalidate(Uri.parse(path));
}
}
/**
* Invalidate all memory cached images for the specified {@code file}.
*
* @see #invalidate(Uri)
* @see #invalidate(String)
*/
public void invalidate(@NonNull File file) {
if (file == null) {
throw new IllegalArgumentException("file == null");
}
invalidate(Uri.fromFile(file));
}
/** Toggle whether to display debug indicators on images. */
@SuppressWarnings("UnusedDeclaration") public void setIndicatorsEnabled(boolean enabled) {
indicatorsEnabled = enabled;
}
/** {@code true} if debug indicators should are displayed on images. */
@SuppressWarnings("UnusedDeclaration") public boolean areIndicatorsEnabled() {
return indicatorsEnabled;
}
/**
* Toggle whether debug logging is enabled.
* <p>
* <b>WARNING:</b> Enabling this will result in excessive object allocation. This should be only
* be used for debugging Picasso behavior. Do NOT pass {@code BuildConfig.DEBUG}.
*/
@SuppressWarnings("UnusedDeclaration") // Public API.
public void setLoggingEnabled(boolean enabled) {
loggingEnabled = enabled;
}
/** {@code true} if debug logging is enabled. */
public boolean isLoggingEnabled() {
return loggingEnabled;
}
/**
* Creates a {@link StatsSnapshot} of the current stats for this instance.
* <p>
* <b>NOTE:</b> The snapshot may not always be completely up-to-date if requests are still in
* progress.
*/
@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;
}
List<RequestHandler> getRequestHandlers() {
return requestHandlers;
}
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) {
// If there is already a deferred request, cancel it.
if (targetToDeferredRequestCreator.containsKey(view)) {
cancelExistingRequest(view);
}
targetToDeferredRequestCreator.put(view, request);
}
void enqueueAndSubmit(Action action) {
Object target = action.getTarget();
if (target != null && targetToAction.get(target) != action) {
// This will also check we are on the main thread.
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) {
Action single = hunter.getAction();
List<Action> joined = hunter.getActions();
boolean hasMultiple = joined != null && !joined.isEmpty();
boolean shouldDeliver = single != null || hasMultiple;
if (!shouldDeliver) {
return;
}
Uri uri = hunter.getData().uri;
Exception exception = hunter.getException();
Bitmap result = hunter.getResult();
LoadedFrom from = hunter.getLoadedFrom();
if (single != null) {
deliverAction(result, from, single, exception);
}
if (hasMultiple) {
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = joined.size(); i < n; i++) {
Action join = joined.get(i);
deliverAction(result, from, join, exception);
}
}
if (listener != null && exception != null) {
listener.onImageLoadFailed(this, uri, exception);
}
}
void resumeAction(Action action) {
Bitmap bitmap = null;
if (shouldReadFromMemoryCache(action.memoryPolicy)) {
bitmap = quickMemoryCacheCheck(action.getKey());
}
if (bitmap != null) {
// Resumed action is cached, complete immediately.
deliverAction(bitmap, MEMORY, action, null);
if (loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, action.request.logId(), "from " + MEMORY);
}
} else {
// Re-submit the action to the executor.
enqueueAndSubmit(action);
if (loggingEnabled) {
log(OWNER_MAIN, VERB_RESUMED, action.request.logId());
}
}
}
private void deliverAction(Bitmap result, LoadedFrom from, Action action, Exception e) {
if (action.isCancelled()) {
return;
}
if (!action.willReplay()) {
targetToAction.remove(action.getTarget());
}
if (result != null) {
if (from == null) {
throw new AssertionError("LoadedFrom cannot be null.");
}
action.complete(result, from);
if (loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, action.request.logId(), "from " + from);
}
} else {
action.error(e);
if (loggingEnabled) {
log(OWNER_MAIN, VERB_ERRORED, action.request.logId(), e.getMessage());
}
}
}
void cancelExistingRequest(Object target) {
checkMain();
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();
}
}
}
/**
* When the target of an action is weakly reachable but the request hasn't been canceled, it
* gets added to the reference queue. This thread empties the reference queue and cancels the
* request.
*/
private static class CleanupThread extends Thread {
private final ReferenceQueue<Object> referenceQueue;
private final Handler handler;
CleanupThread(ReferenceQueue<Object> 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 {
// Prior to Android 5.0, even when there is no local variable, the result from
// remove() & obtainMessage() is kept as a stack local variable.
// We're forcing this reference to be cleared and replaced by looping every second
// when there is nothing to do.
// This behavior has been tested and reproduced with heap dumps.
RequestWeakReference<?> remove =
(RequestWeakReference<?>) referenceQueue.remove(THREAD_LEAK_CLEANING_MS);
Message message = handler.obtainMessage();
if (remove != null) {
message.what = REQUEST_GCED;
message.obj = remove.action;
handler.sendMessage(message);
} else {
message.recycle();
}
} 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
* with full control over the configuration by using {@link Picasso.Builder} to create a
* {@link Picasso} instance. You can either use this directly or by setting it as the global
* instance with {@link #setSingletonInstance}.
*/
public static Picasso with(@NonNull Context context) {
if (context == null) {
throw new IllegalArgumentException("context == null");
}
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}
/**
* Set the global instance returned from {@link #with}.
* <p>
* This method must be called before any calls to {@link #with} and may only be called once.
*/
public static void setSingletonInstance(@NonNull Picasso picasso) {
if (picasso == null) {
throw new IllegalArgumentException("Picasso must not be null.");
}
synchronized (Picasso.class) {
if (singleton != null) {
throw new IllegalStateException("Singleton instance already exists.");
}
singleton = picasso;
}
}
/** 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 List<RequestHandler> requestHandlers;
private Bitmap.Config defaultBitmapConfig;
private boolean indicatorsEnabled;
private boolean loggingEnabled;
/** Start building a new {@link Picasso} instance. */
public Builder(@NonNull Context context) {
if (context == null) {
throw new IllegalArgumentException("Context must not be null.");
}
this.context = context.getApplicationContext();
}
/**
* Specify the default {@link Bitmap.Config} used when decoding images. This can be overridden
* on a per-request basis using {@link RequestCreator#config(Bitmap.Config) config(..)}.
*/
public Builder defaultBitmapConfig(@NonNull Bitmap.Config bitmapConfig) {
if (bitmapConfig == null) {
throw new IllegalArgumentException("Bitmap config must not be null.");
}
this.defaultBitmapConfig = bitmapConfig;
return this;
}
/** Specify the {@link Downloader} that will be used for downloading images. */
public Builder downloader(@NonNull 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.
* <p>
* Note: Calling {@link Picasso#shutdown() shutdown()} will not shutdown supplied executors.
*/
public Builder executor(@NonNull 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(@NonNull 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(@NonNull 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(@NonNull 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;
}
/** Register a {@link RequestHandler}. */
public Builder addRequestHandler(@NonNull RequestHandler requestHandler) {
if (requestHandler == null) {
throw new IllegalArgumentException("RequestHandler must not be null.");
}
if (requestHandlers == null) {
requestHandlers = new ArrayList<>();
}
if (requestHandlers.contains(requestHandler)) {
throw new IllegalStateException("RequestHandler already registered.");
}
requestHandlers.add(requestHandler);
return this;
}
/** Toggle whether to display debug indicators on images. */
public Builder indicatorsEnabled(boolean enabled) {
this.indicatorsEnabled = enabled;
return this;
}
/**
* Toggle whether debug logging is enabled.
* <p>
* <b>WARNING:</b> Enabling this will result in excessive object allocation. This should be only
* be used for debugging purposes. Do NOT pass {@code BuildConfig.DEBUG}.
*/
public Builder loggingEnabled(boolean enabled) {
this.loggingEnabled = enabled;
return this;
}
/** Create the {@link Picasso} instance. */
public Picasso build() {
Context context = this.context;
if (downloader == null) {
downloader = new OkHttp3Downloader(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, requestHandlers, stats,
defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}
}
/** Describes where the image was loaded from. */
public enum LoadedFrom {
MEMORY(Color.GREEN),
DISK(Color.BLUE),
NETWORK(Color.RED);
final int debugColor;
LoadedFrom(int debugColor) {
this.debugColor = debugColor;
}
}
}