Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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