|
1 /* |
|
2 * Copyright (C) 2013 Square, Inc. |
|
3 * |
|
4 * Licensed under the Apache License, Version 2.0 (the "License"); |
|
5 * you may not use this file except in compliance with the License. |
|
6 * You may obtain a copy of the License at |
|
7 * |
|
8 * http://www.apache.org/licenses/LICENSE-2.0 |
|
9 * |
|
10 * Unless required by applicable law or agreed to in writing, software |
|
11 * distributed under the License is distributed on an "AS IS" BASIS, |
|
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
13 * See the License for the specific language governing permissions and |
|
14 * limitations under the License. |
|
15 */ |
|
16 package com.squareup.picasso; |
|
17 |
|
18 import android.content.Context; |
|
19 import android.graphics.Bitmap; |
|
20 import android.graphics.Color; |
|
21 import android.net.Uri; |
|
22 import android.os.Handler; |
|
23 import android.os.Looper; |
|
24 import android.os.Message; |
|
25 import android.os.Process; |
|
26 import android.widget.ImageView; |
|
27 import java.io.File; |
|
28 import java.lang.ref.ReferenceQueue; |
|
29 import java.util.List; |
|
30 import java.util.Map; |
|
31 import java.util.WeakHashMap; |
|
32 import java.util.concurrent.ExecutorService; |
|
33 |
|
34 import static android.os.Process.THREAD_PRIORITY_BACKGROUND; |
|
35 import static com.squareup.picasso.Action.RequestWeakReference; |
|
36 import static com.squareup.picasso.Dispatcher.HUNTER_BATCH_COMPLETE; |
|
37 import static com.squareup.picasso.Dispatcher.REQUEST_GCED; |
|
38 import static com.squareup.picasso.Utils.THREAD_PREFIX; |
|
39 |
|
40 /** |
|
41 * Image downloading, transformation, and caching manager. |
|
42 * <p/> |
|
43 * Use {@link #with(android.content.Context)} for the global singleton instance or construct your |
|
44 * own instance with {@link Builder}. |
|
45 */ |
|
46 public class Picasso { |
|
47 |
|
48 /** Callbacks for Picasso events. */ |
|
49 public interface Listener { |
|
50 /** |
|
51 * Invoked when an image has failed to load. This is useful for reporting image failures to a |
|
52 * remote analytics service, for example. |
|
53 */ |
|
54 void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception); |
|
55 } |
|
56 |
|
57 /** |
|
58 * A transformer that is called immediately before every request is submitted. This can be used to |
|
59 * modify any information about a request. |
|
60 * <p> |
|
61 * For example, if you use a CDN you can change the hostname for the image based on the current |
|
62 * location of the user in order to get faster download speeds. |
|
63 * <p> |
|
64 * <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible |
|
65 * way at any time. |
|
66 */ |
|
67 public interface RequestTransformer { |
|
68 /** |
|
69 * Transform a request before it is submitted to be processed. |
|
70 * |
|
71 * @return The original request or a new request to replace it. Must not be null. |
|
72 */ |
|
73 Request transformRequest(Request request); |
|
74 |
|
75 /** A {@link RequestTransformer} which returns the original request. */ |
|
76 RequestTransformer IDENTITY = new RequestTransformer() { |
|
77 @Override public Request transformRequest(Request request) { |
|
78 return request; |
|
79 } |
|
80 }; |
|
81 } |
|
82 |
|
83 static final Handler HANDLER = new Handler(Looper.getMainLooper()) { |
|
84 @Override public void handleMessage(Message msg) { |
|
85 switch (msg.what) { |
|
86 case HUNTER_BATCH_COMPLETE: { |
|
87 @SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj; |
|
88 for (BitmapHunter hunter : batch) { |
|
89 hunter.picasso.complete(hunter); |
|
90 } |
|
91 break; |
|
92 } |
|
93 case REQUEST_GCED: { |
|
94 Action action = (Action) msg.obj; |
|
95 action.picasso.cancelExistingRequest(action.getTarget()); |
|
96 break; |
|
97 } |
|
98 default: |
|
99 throw new AssertionError("Unknown handler message received: " + msg.what); |
|
100 } |
|
101 } |
|
102 }; |
|
103 |
|
104 static Picasso singleton = null; |
|
105 |
|
106 private final Listener listener; |
|
107 private final RequestTransformer requestTransformer; |
|
108 private final CleanupThread cleanupThread; |
|
109 |
|
110 final Context context; |
|
111 final Dispatcher dispatcher; |
|
112 final Cache cache; |
|
113 final Stats stats; |
|
114 final Map<Object, Action> targetToAction; |
|
115 final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator; |
|
116 final ReferenceQueue<Object> referenceQueue; |
|
117 |
|
118 boolean debugging; |
|
119 boolean shutdown; |
|
120 |
|
121 Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener, |
|
122 RequestTransformer requestTransformer, Stats stats, boolean debugging) { |
|
123 this.context = context; |
|
124 this.dispatcher = dispatcher; |
|
125 this.cache = cache; |
|
126 this.listener = listener; |
|
127 this.requestTransformer = requestTransformer; |
|
128 this.stats = stats; |
|
129 this.targetToAction = new WeakHashMap<Object, Action>(); |
|
130 this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>(); |
|
131 this.debugging = debugging; |
|
132 this.referenceQueue = new ReferenceQueue<Object>(); |
|
133 this.cleanupThread = new CleanupThread(referenceQueue, HANDLER); |
|
134 this.cleanupThread.start(); |
|
135 } |
|
136 |
|
137 /** Cancel any existing requests for the specified target {@link ImageView}. */ |
|
138 public void cancelRequest(ImageView view) { |
|
139 cancelExistingRequest(view); |
|
140 } |
|
141 |
|
142 /** Cancel any existing requests for the specified {@link Target} instance. */ |
|
143 public void cancelRequest(Target target) { |
|
144 cancelExistingRequest(target); |
|
145 } |
|
146 |
|
147 /** |
|
148 * Start an image request using the specified URI. |
|
149 * <p> |
|
150 * Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder, |
|
151 * if one is specified. |
|
152 * |
|
153 * @see #load(File) |
|
154 * @see #load(String) |
|
155 * @see #load(int) |
|
156 */ |
|
157 public RequestCreator load(Uri uri) { |
|
158 return new RequestCreator(this, uri, 0); |
|
159 } |
|
160 |
|
161 /** |
|
162 * Start an image request using the specified path. This is a convenience method for calling |
|
163 * {@link #load(Uri)}. |
|
164 * <p> |
|
165 * This path may be a remote URL, file resource (prefixed with {@code file:}), content resource |
|
166 * (prefixed with {@code content:}), or android resource (prefixed with {@code |
|
167 * android.resource:}. |
|
168 * <p> |
|
169 * Passing {@code null} as a {@code path} will not trigger any request but will set a |
|
170 * placeholder, if one is specified. |
|
171 * |
|
172 * @see #load(Uri) |
|
173 * @see #load(File) |
|
174 * @see #load(int) |
|
175 */ |
|
176 public RequestCreator load(String path) { |
|
177 if (path == null) { |
|
178 return new RequestCreator(this, null, 0); |
|
179 } |
|
180 if (path.trim().length() == 0) { |
|
181 throw new IllegalArgumentException("Path must not be empty."); |
|
182 } |
|
183 return load(Uri.parse(path)); |
|
184 } |
|
185 |
|
186 /** |
|
187 * Start an image request using the specified image file. This is a convenience method for |
|
188 * calling {@link #load(Uri)}. |
|
189 * <p> |
|
190 * Passing {@code null} as a {@code file} will not trigger any request but will set a |
|
191 * placeholder, if one is specified. |
|
192 * |
|
193 * @see #load(Uri) |
|
194 * @see #load(String) |
|
195 * @see #load(int) |
|
196 */ |
|
197 public RequestCreator load(File file) { |
|
198 if (file == null) { |
|
199 return new RequestCreator(this, null, 0); |
|
200 } |
|
201 return load(Uri.fromFile(file)); |
|
202 } |
|
203 |
|
204 /** |
|
205 * Start an image request using the specified drawable resource ID. |
|
206 * |
|
207 * @see #load(Uri) |
|
208 * @see #load(String) |
|
209 * @see #load(File) |
|
210 */ |
|
211 public RequestCreator load(int resourceId) { |
|
212 if (resourceId == 0) { |
|
213 throw new IllegalArgumentException("Resource ID must not be zero."); |
|
214 } |
|
215 return new RequestCreator(this, null, resourceId); |
|
216 } |
|
217 |
|
218 /** {@code true} if debug display, logging, and statistics are enabled. */ |
|
219 @SuppressWarnings("UnusedDeclaration") public boolean isDebugging() { |
|
220 return debugging; |
|
221 } |
|
222 |
|
223 /** Toggle whether debug display, logging, and statistics are enabled. */ |
|
224 @SuppressWarnings("UnusedDeclaration") public void setDebugging(boolean debugging) { |
|
225 this.debugging = debugging; |
|
226 } |
|
227 |
|
228 /** Creates a {@link StatsSnapshot} of the current stats for this instance. */ |
|
229 @SuppressWarnings("UnusedDeclaration") public StatsSnapshot getSnapshot() { |
|
230 return stats.createSnapshot(); |
|
231 } |
|
232 |
|
233 /** Stops this instance from accepting further requests. */ |
|
234 public void shutdown() { |
|
235 if (this == singleton) { |
|
236 throw new UnsupportedOperationException("Default singleton instance cannot be shutdown."); |
|
237 } |
|
238 if (shutdown) { |
|
239 return; |
|
240 } |
|
241 cache.clear(); |
|
242 cleanupThread.shutdown(); |
|
243 stats.shutdown(); |
|
244 dispatcher.shutdown(); |
|
245 for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) { |
|
246 deferredRequestCreator.cancel(); |
|
247 } |
|
248 targetToDeferredRequestCreator.clear(); |
|
249 shutdown = true; |
|
250 } |
|
251 |
|
252 Request transformRequest(Request request) { |
|
253 Request transformed = requestTransformer.transformRequest(request); |
|
254 if (transformed == null) { |
|
255 throw new IllegalStateException("Request transformer " |
|
256 + requestTransformer.getClass().getCanonicalName() |
|
257 + " returned null for " |
|
258 + request); |
|
259 } |
|
260 return transformed; |
|
261 } |
|
262 |
|
263 void defer(ImageView view, DeferredRequestCreator request) { |
|
264 targetToDeferredRequestCreator.put(view, request); |
|
265 } |
|
266 |
|
267 void enqueueAndSubmit(Action action) { |
|
268 Object target = action.getTarget(); |
|
269 if (target != null) { |
|
270 cancelExistingRequest(target); |
|
271 targetToAction.put(target, action); |
|
272 } |
|
273 submit(action); |
|
274 } |
|
275 |
|
276 void submit(Action action) { |
|
277 dispatcher.dispatchSubmit(action); |
|
278 } |
|
279 |
|
280 Bitmap quickMemoryCacheCheck(String key) { |
|
281 Bitmap cached = cache.get(key); |
|
282 if (cached != null) { |
|
283 stats.dispatchCacheHit(); |
|
284 } else { |
|
285 stats.dispatchCacheMiss(); |
|
286 } |
|
287 return cached; |
|
288 } |
|
289 |
|
290 void complete(BitmapHunter hunter) { |
|
291 List<Action> joined = hunter.getActions(); |
|
292 if (joined.isEmpty()) { |
|
293 return; |
|
294 } |
|
295 |
|
296 Uri uri = hunter.getData().uri; |
|
297 Exception exception = hunter.getException(); |
|
298 Bitmap result = hunter.getResult(); |
|
299 LoadedFrom from = hunter.getLoadedFrom(); |
|
300 |
|
301 for (Action join : joined) { |
|
302 if (join.isCancelled()) { |
|
303 continue; |
|
304 } |
|
305 targetToAction.remove(join.getTarget()); |
|
306 if (result != null) { |
|
307 if (from == null) { |
|
308 throw new AssertionError("LoadedFrom cannot be null."); |
|
309 } |
|
310 join.complete(result, from); |
|
311 } else { |
|
312 join.error(); |
|
313 } |
|
314 } |
|
315 |
|
316 if (listener != null && exception != null) { |
|
317 listener.onImageLoadFailed(this, uri, exception); |
|
318 } |
|
319 } |
|
320 |
|
321 private void cancelExistingRequest(Object target) { |
|
322 Action action = targetToAction.remove(target); |
|
323 if (action != null) { |
|
324 action.cancel(); |
|
325 dispatcher.dispatchCancel(action); |
|
326 } |
|
327 if (target instanceof ImageView) { |
|
328 ImageView targetImageView = (ImageView) target; |
|
329 DeferredRequestCreator deferredRequestCreator = |
|
330 targetToDeferredRequestCreator.remove(targetImageView); |
|
331 if (deferredRequestCreator != null) { |
|
332 deferredRequestCreator.cancel(); |
|
333 } |
|
334 } |
|
335 } |
|
336 |
|
337 private static class CleanupThread extends Thread { |
|
338 private final ReferenceQueue<?> referenceQueue; |
|
339 private final Handler handler; |
|
340 |
|
341 CleanupThread(ReferenceQueue<?> referenceQueue, Handler handler) { |
|
342 this.referenceQueue = referenceQueue; |
|
343 this.handler = handler; |
|
344 setDaemon(true); |
|
345 setName(THREAD_PREFIX + "refQueue"); |
|
346 } |
|
347 |
|
348 @Override public void run() { |
|
349 Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND); |
|
350 while (true) { |
|
351 try { |
|
352 RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove(); |
|
353 handler.sendMessage(handler.obtainMessage(REQUEST_GCED, remove.action)); |
|
354 } catch (InterruptedException e) { |
|
355 break; |
|
356 } catch (final Exception e) { |
|
357 handler.post(new Runnable() { |
|
358 @Override public void run() { |
|
359 throw new RuntimeException(e); |
|
360 } |
|
361 }); |
|
362 break; |
|
363 } |
|
364 } |
|
365 } |
|
366 |
|
367 void shutdown() { |
|
368 interrupt(); |
|
369 } |
|
370 } |
|
371 |
|
372 /** |
|
373 * The global default {@link Picasso} instance. |
|
374 * <p> |
|
375 * This instance is automatically initialized with defaults that are suitable to most |
|
376 * implementations. |
|
377 * <ul> |
|
378 * <li>LRU memory cache of 15% the available application RAM</li> |
|
379 * <li>Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only |
|
380 * available on API 14+ <em>or</em> if you are using a standalone library that provides a disk |
|
381 * cache on all API levels like OkHttp)</li> |
|
382 * <li>Three download threads for disk and network access.</li> |
|
383 * </ul> |
|
384 * <p> |
|
385 * If these settings do not meet the requirements of your application you can construct your own |
|
386 * instance with full control over the configuration by using {@link Picasso.Builder}. |
|
387 */ |
|
388 public static Picasso with(Context context) { |
|
389 if (singleton == null) { |
|
390 singleton = new Builder(context).build(); |
|
391 } |
|
392 return singleton; |
|
393 } |
|
394 |
|
395 /** Fluent API for creating {@link Picasso} instances. */ |
|
396 @SuppressWarnings("UnusedDeclaration") // Public API. |
|
397 public static class Builder { |
|
398 private final Context context; |
|
399 private Downloader downloader; |
|
400 private ExecutorService service; |
|
401 private Cache cache; |
|
402 private Listener listener; |
|
403 private RequestTransformer transformer; |
|
404 private boolean debugging; |
|
405 |
|
406 /** Start building a new {@link Picasso} instance. */ |
|
407 public Builder(Context context) { |
|
408 if (context == null) { |
|
409 throw new IllegalArgumentException("Context must not be null."); |
|
410 } |
|
411 this.context = context.getApplicationContext(); |
|
412 } |
|
413 |
|
414 /** Specify the {@link Downloader} that will be used for downloading images. */ |
|
415 public Builder downloader(Downloader downloader) { |
|
416 if (downloader == null) { |
|
417 throw new IllegalArgumentException("Downloader must not be null."); |
|
418 } |
|
419 if (this.downloader != null) { |
|
420 throw new IllegalStateException("Downloader already set."); |
|
421 } |
|
422 this.downloader = downloader; |
|
423 return this; |
|
424 } |
|
425 |
|
426 /** Specify the executor service for loading images in the background. */ |
|
427 public Builder executor(ExecutorService executorService) { |
|
428 if (executorService == null) { |
|
429 throw new IllegalArgumentException("Executor service must not be null."); |
|
430 } |
|
431 if (this.service != null) { |
|
432 throw new IllegalStateException("Executor service already set."); |
|
433 } |
|
434 this.service = executorService; |
|
435 return this; |
|
436 } |
|
437 |
|
438 /** Specify the memory cache used for the most recent images. */ |
|
439 public Builder memoryCache(Cache memoryCache) { |
|
440 if (memoryCache == null) { |
|
441 throw new IllegalArgumentException("Memory cache must not be null."); |
|
442 } |
|
443 if (this.cache != null) { |
|
444 throw new IllegalStateException("Memory cache already set."); |
|
445 } |
|
446 this.cache = memoryCache; |
|
447 return this; |
|
448 } |
|
449 |
|
450 /** Specify a listener for interesting events. */ |
|
451 public Builder listener(Listener listener) { |
|
452 if (listener == null) { |
|
453 throw new IllegalArgumentException("Listener must not be null."); |
|
454 } |
|
455 if (this.listener != null) { |
|
456 throw new IllegalStateException("Listener already set."); |
|
457 } |
|
458 this.listener = listener; |
|
459 return this; |
|
460 } |
|
461 |
|
462 /** |
|
463 * Specify a transformer for all incoming requests. |
|
464 * <p> |
|
465 * <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible |
|
466 * way at any time. |
|
467 */ |
|
468 public Builder requestTransformer(RequestTransformer transformer) { |
|
469 if (transformer == null) { |
|
470 throw new IllegalArgumentException("Transformer must not be null."); |
|
471 } |
|
472 if (this.transformer != null) { |
|
473 throw new IllegalStateException("Transformer already set."); |
|
474 } |
|
475 this.transformer = transformer; |
|
476 return this; |
|
477 } |
|
478 |
|
479 /** Whether debugging is enabled or not. */ |
|
480 public Builder debugging(boolean debugging) { |
|
481 this.debugging = debugging; |
|
482 return this; |
|
483 } |
|
484 |
|
485 /** Create the {@link Picasso} instance. */ |
|
486 public Picasso build() { |
|
487 Context context = this.context; |
|
488 |
|
489 if (downloader == null) { |
|
490 downloader = Utils.createDefaultDownloader(context); |
|
491 } |
|
492 if (cache == null) { |
|
493 cache = new LruCache(context); |
|
494 } |
|
495 if (service == null) { |
|
496 service = new PicassoExecutorService(); |
|
497 } |
|
498 if (transformer == null) { |
|
499 transformer = RequestTransformer.IDENTITY; |
|
500 } |
|
501 |
|
502 Stats stats = new Stats(cache); |
|
503 |
|
504 Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats); |
|
505 |
|
506 return new Picasso(context, dispatcher, cache, listener, transformer, stats, debugging); |
|
507 } |
|
508 } |
|
509 |
|
510 /** Describes where the image was loaded from. */ |
|
511 public enum LoadedFrom { |
|
512 MEMORY(Color.GREEN), |
|
513 DISK(Color.YELLOW), |
|
514 NETWORK(Color.RED); |
|
515 |
|
516 final int debugColor; |
|
517 |
|
518 private LoadedFrom(int debugColor) { |
|
519 this.debugColor = debugColor; |
|
520 } |
|
521 } |
|
522 } |