Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
18084 manas 1
/**
2
 * Copyright (C) 2013 The Android Open Source Project
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.android.volley.toolbox;
17
 
18
import android.graphics.Bitmap;
19
import android.graphics.Bitmap.Config;
20
import android.os.Handler;
21
import android.os.Looper;
22
import android.widget.ImageView;
23
import android.widget.ImageView.ScaleType;
24
import com.android.volley.Request;
25
import com.android.volley.RequestQueue;
26
import com.android.volley.Response.ErrorListener;
27
import com.android.volley.Response.Listener;
28
import com.android.volley.VolleyError;
29
 
30
import java.util.HashMap;
31
import java.util.LinkedList;
32
 
33
/**
34
 * Helper that handles loading and caching images from remote URLs.
35
 *
36
 * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
37
 * and to pass in the default image listener provided by
38
 * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
39
 * this class must be made from the main thead, and all responses will be delivered to the main
40
 * thread as well.
41
 */
42
public class ImageLoader {
43
    /** RequestQueue for dispatching ImageRequests onto. */
44
    private final RequestQueue mRequestQueue;
45
 
46
    /** Amount of time to wait after first response arrives before delivering all responses. */
47
    private int mBatchResponseDelayMs = 100;
48
 
49
    /** The cache implementation to be used as an L1 cache before calling into volley. */
50
    private final ImageCache mCache;
51
 
52
    /**
53
     * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
54
     * that we can coalesce multiple requests to the same URL into a single network request.
55
     */
56
    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
57
            new HashMap<String, BatchedImageRequest>();
58
 
59
    /** HashMap of the currently pending responses (waiting to be delivered). */
60
    private final HashMap<String, BatchedImageRequest> mBatchedResponses =
61
            new HashMap<String, BatchedImageRequest>();
62
 
63
    /** Handler to the main thread. */
64
    private final Handler mHandler = new Handler(Looper.getMainLooper());
65
 
66
    /** Runnable for in-flight response delivery. */
67
    private Runnable mRunnable;
68
 
69
    /**
70
     * Simple cache adapter interface. If provided to the ImageLoader, it
71
     * will be used as an L1 cache before dispatch to Volley. Implementations
72
     * must not block. Implementation with an LruCache is recommended.
73
     */
74
    public interface ImageCache {
75
        public Bitmap getBitmap(String url);
76
        public void putBitmap(String url, Bitmap bitmap);
77
    }
78
 
79
    /**
80
     * Constructs a new ImageLoader.
81
     * @param queue The RequestQueue to use for making image requests.
82
     * @param imageCache The cache to use as an L1 cache.
83
     */
84
    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
85
        mRequestQueue = queue;
86
        mCache = imageCache;
87
    }
88
 
89
    /**
90
     * The default implementation of ImageListener which handles basic functionality
91
     * of showing a default image until the network response is received, at which point
92
     * it will switch to either the actual image or the error image.
93
     * @param view The imageView that the listener is associated with.
94
     * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
95
     * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
96
     */
97
    public static ImageListener getImageListener(final ImageView view,
98
            final int defaultImageResId, final int errorImageResId) {
99
        return new ImageListener() {
100
            @Override
101
            public void onErrorResponse(VolleyError error) {
102
                if (errorImageResId != 0) {
103
                    view.setImageResource(errorImageResId);
104
                }
105
            }
106
 
107
            @Override
108
            public void onResponse(ImageContainer response, boolean isImmediate) {
109
                if (response.getBitmap() != null) {
110
                    view.setImageBitmap(response.getBitmap());
111
                } else if (defaultImageResId != 0) {
112
                    view.setImageResource(defaultImageResId);
113
                }
114
            }
115
        };
116
    }
117
 
118
    /**
119
     * Interface for the response handlers on image requests.
120
     *
121
     * The call flow is this:
122
     * 1. Upon being  attached to a request, onResponse(response, true) will
123
     * be invoked to reflect any cached data that was already available. If the
124
     * data was available, response.getBitmap() will be non-null.
125
     *
126
     * 2. After a network response returns, only one of the following cases will happen:
127
     *   - onResponse(response, false) will be called if the image was loaded.
128
     *   or
129
     *   - onErrorResponse will be called if there was an error loading the image.
130
     */
131
    public interface ImageListener extends ErrorListener {
132
        /**
133
         * Listens for non-error changes to the loading of the image request.
134
         *
135
         * @param response Holds all information pertaining to the request, as well
136
         * as the bitmap (if it is loaded).
137
         * @param isImmediate True if this was called during ImageLoader.get() variants.
138
         * This can be used to differentiate between a cached image loading and a network
139
         * image loading in order to, for example, run an animation to fade in network loaded
140
         * images.
141
         */
142
        public void onResponse(ImageContainer response, boolean isImmediate);
143
    }
144
 
145
    /**
146
     * Checks if the item is available in the cache.
147
     * @param requestUrl The url of the remote image
148
     * @param maxWidth The maximum width of the returned image.
149
     * @param maxHeight The maximum height of the returned image.
150
     * @return True if the item exists in cache, false otherwise.
151
     */
152
    public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
153
        return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
154
    }
155
 
156
    /**
157
     * Checks if the item is available in the cache.
158
     *
159
     * @param requestUrl The url of the remote image
160
     * @param maxWidth   The maximum width of the returned image.
161
     * @param maxHeight  The maximum height of the returned image.
162
     * @param scaleType  The scaleType of the imageView.
163
     * @return True if the item exists in cache, false otherwise.
164
     */
165
    public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
166
        throwIfNotOnMainThread();
167
 
168
        String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
169
        return mCache.getBitmap(cacheKey) != null;
170
    }
171
 
172
    /**
173
     * Returns an ImageContainer for the requested URL.
174
     *
175
     * The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
176
     * If the default was returned, the {@link ImageLoader} will be invoked when the
177
     * request is fulfilled.
178
     *
179
     * @param requestUrl The URL of the image to be loaded.
180
     */
181
    public ImageContainer get(String requestUrl, final ImageListener listener) {
182
        return get(requestUrl, listener, 0, 0);
183
    }
184
 
185
    /**
186
     * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with
187
     * {@code Scaletype == ScaleType.CENTER_INSIDE}.
188
     */
189
    public ImageContainer get(String requestUrl, ImageListener imageListener,
190
            int maxWidth, int maxHeight) {
191
        return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
192
    }
193
 
194
    /**
195
     * Issues a bitmap request with the given URL if that image is not available
196
     * in the cache, and returns a bitmap container that contains all of the data
197
     * relating to the request (as well as the default image if the requested
198
     * image is not available).
199
     * @param requestUrl The url of the remote image
200
     * @param imageListener The listener to call when the remote image is loaded
201
     * @param maxWidth The maximum width of the returned image.
202
     * @param maxHeight The maximum height of the returned image.
203
     * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
204
     * @return A container object that contains all of the properties of the request, as well as
205
     *     the currently available image (default if remote is not loaded).
206
     */
207
    public ImageContainer get(String requestUrl, ImageListener imageListener,
208
            int maxWidth, int maxHeight, ScaleType scaleType) {
209
 
210
        // only fulfill requests that were initiated from the main thread.
211
        throwIfNotOnMainThread();
212
 
213
        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
214
 
215
        // Try to look up the request in the cache of remote images.
216
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
217
        if (cachedBitmap != null) {
218
            // Return the cached bitmap.
219
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
220
            imageListener.onResponse(container, true);
221
            return container;
222
        }
223
 
224
        // The bitmap did not exist in the cache, fetch it!
225
        ImageContainer imageContainer =
226
                new ImageContainer(null, requestUrl, cacheKey, imageListener);
227
 
228
        // Update the caller to let them know that they should use the default bitmap.
229
        imageListener.onResponse(imageContainer, true);
230
 
231
        // Check to see if a request is already in-flight.
232
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
233
        if (request != null) {
234
            // If it is, add this request to the list of listeners.
235
            request.addContainer(imageContainer);
236
            return imageContainer;
237
        }
238
 
239
        // The request is not already in flight. Send the new request to the network and
240
        // track it.
241
        Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
242
                cacheKey);
243
 
244
        mRequestQueue.add(newRequest);
245
        mInFlightRequests.put(cacheKey,
246
                new BatchedImageRequest(newRequest, imageContainer));
247
        return imageContainer;
248
    }
249
 
250
    protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
251
            ScaleType scaleType, final String cacheKey) {
252
        return new ImageRequest(requestUrl, new Listener<Bitmap>() {
253
            @Override
254
            public void onResponse(Bitmap response) {
255
                onGetImageSuccess(cacheKey, response);
256
            }
257
        }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
258
            @Override
259
            public void onErrorResponse(VolleyError error) {
260
                onGetImageError(cacheKey, error);
261
            }
262
        });
263
    }
264
 
265
    /**
266
     * Sets the amount of time to wait after the first response arrives before delivering all
267
     * responses. Batching can be disabled entirely by passing in 0.
268
     * @param newBatchedResponseDelayMs The time in milliseconds to wait.
269
     */
270
    public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
271
        mBatchResponseDelayMs = newBatchedResponseDelayMs;
272
    }
273
 
274
    /**
275
     * Handler for when an image was successfully loaded.
276
     * @param cacheKey The cache key that is associated with the image request.
277
     * @param response The bitmap that was returned from the network.
278
     */
279
    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
280
        // cache the image that was fetched.
281
        mCache.putBitmap(cacheKey, response);
282
 
283
        // remove the request from the list of in-flight requests.
284
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
285
 
286
        if (request != null) {
287
            // Update the response bitmap.
288
            request.mResponseBitmap = response;
289
 
290
            // Send the batched response
291
            batchResponse(cacheKey, request);
292
        }
293
    }
294
 
295
    /**
296
     * Handler for when an image failed to load.
297
     * @param cacheKey The cache key that is associated with the image request.
298
     */
299
    protected void onGetImageError(String cacheKey, VolleyError error) {
300
        // Notify the requesters that something failed via a null result.
301
        // Remove this request from the list of in-flight requests.
302
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
303
 
304
        if (request != null) {
305
            // Set the error for this request
306
            request.setError(error);
307
 
308
            // Send the batched response
309
            batchResponse(cacheKey, request);
310
        }
311
    }
312
 
313
    /**
314
     * Container object for all of the data surrounding an image request.
315
     */
316
    public class ImageContainer {
317
        /**
318
         * The most relevant bitmap for the container. If the image was in cache, the
319
         * Holder to use for the final bitmap (the one that pairs to the requested URL).
320
         */
321
        private Bitmap mBitmap;
322
 
323
        private final ImageListener mListener;
324
 
325
        /** The cache key that was associated with the request */
326
        private final String mCacheKey;
327
 
328
        /** The request URL that was specified */
329
        private final String mRequestUrl;
330
 
331
        /**
332
         * Constructs a BitmapContainer object.
333
         * @param bitmap The final bitmap (if it exists).
334
         * @param requestUrl The requested URL for this container.
335
         * @param cacheKey The cache key that identifies the requested URL for this container.
336
         */
337
        public ImageContainer(Bitmap bitmap, String requestUrl,
338
                String cacheKey, ImageListener listener) {
339
            mBitmap = bitmap;
340
            mRequestUrl = requestUrl;
341
            mCacheKey = cacheKey;
342
            mListener = listener;
343
        }
344
 
345
        /**
346
         * Releases interest in the in-flight request (and cancels it if no one else is listening).
347
         */
348
        public void cancelRequest() {
349
            if (mListener == null) {
350
                return;
351
            }
352
 
353
            BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
354
            if (request != null) {
355
                boolean canceled = request.removeContainerAndCancelIfNecessary(this);
356
                if (canceled) {
357
                    mInFlightRequests.remove(mCacheKey);
358
                }
359
            } else {
360
                // check to see if it is already batched for delivery.
361
                request = mBatchedResponses.get(mCacheKey);
362
                if (request != null) {
363
                    request.removeContainerAndCancelIfNecessary(this);
364
                    if (request.mContainers.size() == 0) {
365
                        mBatchedResponses.remove(mCacheKey);
366
                    }
367
                }
368
            }
369
        }
370
 
371
        /**
372
         * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
373
         */
374
        public Bitmap getBitmap() {
375
            return mBitmap;
376
        }
377
 
378
        /**
379
         * Returns the requested URL for this container.
380
         */
381
        public String getRequestUrl() {
382
            return mRequestUrl;
383
        }
384
    }
385
 
386
    /**
387
     * Wrapper class used to map a Request to the set of active ImageContainer objects that are
388
     * interested in its results.
389
     */
390
    private class BatchedImageRequest {
391
        /** The request being tracked */
392
        private final Request<?> mRequest;
393
 
394
        /** The result of the request being tracked by this item */
395
        private Bitmap mResponseBitmap;
396
 
397
        /** Error if one occurred for this response */
398
        private VolleyError mError;
399
 
400
        /** List of all of the active ImageContainers that are interested in the request */
401
        private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
402
 
403
        /**
404
         * Constructs a new BatchedImageRequest object
405
         * @param request The request being tracked
406
         * @param container The ImageContainer of the person who initiated the request.
407
         */
408
        public BatchedImageRequest(Request<?> request, ImageContainer container) {
409
            mRequest = request;
410
            mContainers.add(container);
411
        }
412
 
413
        /**
414
         * Set the error for this response
415
         */
416
        public void setError(VolleyError error) {
417
            mError = error;
418
        }
419
 
420
        /**
421
         * Get the error for this response
422
         */
423
        public VolleyError getError() {
424
            return mError;
425
        }
426
 
427
        /**
428
         * Adds another ImageContainer to the list of those interested in the results of
429
         * the request.
430
         */
431
        public void addContainer(ImageContainer container) {
432
            mContainers.add(container);
433
        }
434
 
435
        /**
436
         * Detatches the bitmap container from the request and cancels the request if no one is
437
         * left listening.
438
         * @param container The container to remove from the list
439
         * @return True if the request was canceled, false otherwise.
440
         */
441
        public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
442
            mContainers.remove(container);
443
            if (mContainers.size() == 0) {
444
                mRequest.cancel();
445
                return true;
446
            }
447
            return false;
448
        }
449
    }
450
 
451
    /**
452
     * Starts the runnable for batched delivery of responses if it is not already started.
453
     * @param cacheKey The cacheKey of the response being delivered.
454
     * @param request The BatchedImageRequest to be delivered.
455
     */
456
    private void batchResponse(String cacheKey, BatchedImageRequest request) {
457
        mBatchedResponses.put(cacheKey, request);
458
        // If we don't already have a batch delivery runnable in flight, make a new one.
459
        // Note that this will be used to deliver responses to all callers in mBatchedResponses.
460
        if (mRunnable == null) {
461
            mRunnable = new Runnable() {
462
                @Override
463
                public void run() {
464
                    for (BatchedImageRequest bir : mBatchedResponses.values()) {
465
                        for (ImageContainer container : bir.mContainers) {
466
                            // If one of the callers in the batched request canceled the request
467
                            // after the response was received but before it was delivered,
468
                            // skip them.
469
                            if (container.mListener == null) {
470
                                continue;
471
                            }
472
                            if (bir.getError() == null) {
473
                                container.mBitmap = bir.mResponseBitmap;
474
                                container.mListener.onResponse(container, false);
475
                            } else {
476
                                container.mListener.onErrorResponse(bir.getError());
477
                            }
478
                        }
479
                    }
480
                    mBatchedResponses.clear();
481
                    mRunnable = null;
482
                }
483
 
484
            };
485
            // Post the runnable.
486
            mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
487
        }
488
    }
489
 
490
    private void throwIfNotOnMainThread() {
491
        if (Looper.myLooper() != Looper.getMainLooper()) {
492
            throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
493
        }
494
    }
495
    /**
496
     * Creates a cache key for use with the L1 cache.
497
     * @param url The URL of the request.
498
     * @param maxWidth The max-width of the output.
499
     * @param maxHeight The max-height of the output.
500
     * @param scaleType The scaleType of the imageView.
501
     */
502
    private static String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
503
        return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
504
                .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
505
                .toString();
506
    }
507
}