Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
14792 manas 1
/**
2
 * Copyright 2010-present Facebook.
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
 
17
package com.facebook.internal;
18
 
19
import android.content.Context;
20
import android.graphics.Bitmap;
21
import android.graphics.BitmapFactory;
22
import android.os.Handler;
23
import android.os.Looper;
24
import com.facebook.FacebookException;
25
import com.facebook.android.R;
26
 
27
import java.io.IOException;
28
import java.io.InputStream;
29
import java.io.InputStreamReader;
30
import java.net.HttpURLConnection;
31
import java.net.URI;
32
import java.net.URISyntaxException;
33
import java.net.URL;
34
import java.util.HashMap;
35
import java.util.Map;
36
 
37
public class ImageDownloader {
38
    private static final int DOWNLOAD_QUEUE_MAX_CONCURRENT = WorkQueue.DEFAULT_MAX_CONCURRENT;
39
    private static final int CACHE_READ_QUEUE_MAX_CONCURRENT = 2;
40
    private static Handler handler;
41
    private static WorkQueue downloadQueue = new WorkQueue(DOWNLOAD_QUEUE_MAX_CONCURRENT);
42
    private static WorkQueue cacheReadQueue = new WorkQueue(CACHE_READ_QUEUE_MAX_CONCURRENT);
43
 
44
    private static final Map<RequestKey, DownloaderContext> pendingRequests = new HashMap<RequestKey, DownloaderContext>();
45
 
46
    /**
47
     * Downloads the image specified in the passed in request.
48
     * If a callback is specified, it is guaranteed to be invoked on the calling thread.
49
     * @param request Request to process
50
     */
51
    public static void downloadAsync(ImageRequest request) {
52
        if (request == null) {
53
            return;
54
        }
55
 
56
        // NOTE: This is the ONLY place where the original request's Url is read. From here on,
57
        // we will keep track of the Url separately. This is because we might be dealing with a
58
        // redirect response and the Url might change. We can't create our own new ImageRequests
59
        // for these changed Urls since the caller might be doing some book-keeping with the request's
60
        // object reference. So we keep the old references and just map them to new urls in the downloader
61
        RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag());
62
        synchronized (pendingRequests) {
63
            DownloaderContext downloaderContext = pendingRequests.get(key);
64
            if (downloaderContext != null) {
65
                downloaderContext.request = request;
66
                downloaderContext.isCancelled = false;
67
                downloaderContext.workItem.moveToFront();
68
            } else {
69
                enqueueCacheRead(request, key, request.isCachedRedirectAllowed());
70
            }
71
        }
72
    }
73
 
74
    public static boolean cancelRequest(ImageRequest request) {
75
        boolean cancelled = false;
76
        RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag());
77
        synchronized (pendingRequests) {
78
            DownloaderContext downloaderContext = pendingRequests.get(key);
79
            if (downloaderContext != null) {
80
                // If we were able to find the request in our list of pending requests, then we will
81
                // definitely be able to prevent an ImageResponse from being issued. This is regardless
82
                // of whether a cache-read or network-download is underway for this request.
83
                cancelled = true;
84
 
85
                if (downloaderContext.workItem.cancel()) {
86
                    pendingRequests.remove(key);
87
                } else {
88
                    // May be attempting a cache-read right now. So keep track of the cancellation
89
                    // to prevent network calls etc
90
                    downloaderContext.isCancelled = true;
91
                }
92
            }
93
        }
94
 
95
        return cancelled;
96
    }
97
 
98
    public static void prioritizeRequest(ImageRequest request) {
99
        RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag());
100
        synchronized (pendingRequests) {
101
            DownloaderContext downloaderContext = pendingRequests.get(key);
102
            if (downloaderContext != null) {
103
                downloaderContext.workItem.moveToFront();
104
            }
105
        }
106
    }
107
 
108
    public static void clearCache(Context context) {
109
        ImageResponseCache.clearCache(context);
110
        UrlRedirectCache.clearCache(context);
111
    }
112
 
113
    private static void enqueueCacheRead(ImageRequest request, RequestKey key, boolean allowCachedRedirects) {
114
        enqueueRequest(
115
                request,
116
                key,
117
                cacheReadQueue,
118
                new CacheReadWorkItem(request.getContext(), key, allowCachedRedirects));
119
    }
120
 
121
    private static void enqueueDownload(ImageRequest request, RequestKey key) {
122
        enqueueRequest(
123
                request,
124
                key,
125
                downloadQueue,
126
                new DownloadImageWorkItem(request.getContext(), key));
127
    }
128
 
129
    private static void enqueueRequest(
130
            ImageRequest request,
131
            RequestKey key,
132
            WorkQueue workQueue,
133
            Runnable workItem) {
134
        synchronized (pendingRequests) {
135
            DownloaderContext downloaderContext = new DownloaderContext();
136
            downloaderContext.request = request;
137
            pendingRequests.put(key, downloaderContext);
138
 
139
            // The creation of the WorkItem should be done after the pending request has been registered.
140
            // This is necessary since the WorkItem might kick off right away and attempt to retrieve
141
            // the request's DownloaderContext prior to it being ready for access.
142
            //
143
            // It is also necessary to hold on to the lock until after the workItem is created, since
144
            // calls to cancelRequest or prioritizeRequest might come in and expect a registered
145
            // request to have a workItem available as well.
146
            downloaderContext.workItem = workQueue.addActiveWorkItem(workItem);
147
        }
148
    }
149
 
150
    private static void issueResponse(
151
            RequestKey key,
152
            final Exception error,
153
            final Bitmap bitmap,
154
            final boolean isCachedRedirect) {
155
        // Once the old downloader context is removed, we are thread-safe since this is the
156
        // only reference to it
157
        DownloaderContext completedRequestContext = removePendingRequest(key);
158
        if (completedRequestContext != null && !completedRequestContext.isCancelled) {
159
            final ImageRequest request = completedRequestContext.request;
160
            final ImageRequest.Callback callback = request.getCallback();
161
            if (callback != null) {
162
                getHandler().post(new Runnable() {
163
                    @Override
164
                    public void run() {
165
                        ImageResponse response = new ImageResponse(
166
                                request,
167
                                error,
168
                                isCachedRedirect,
169
                                bitmap);
170
                        callback.onCompleted(response);
171
                    }
172
                });
173
            }
174
        }
175
    }
176
 
177
    private static void readFromCache(RequestKey key, Context context, boolean allowCachedRedirects) {
178
        InputStream cachedStream = null;
179
        boolean isCachedRedirect = false;
180
        if (allowCachedRedirects) {
181
            URI redirectUri = UrlRedirectCache.getRedirectedUri(context, key.uri);
182
            if (redirectUri != null) {
183
                cachedStream = ImageResponseCache.getCachedImageStream(redirectUri, context);
184
                isCachedRedirect = cachedStream != null;
185
            }
186
        }
187
 
188
        if (!isCachedRedirect) {
189
            cachedStream = ImageResponseCache.getCachedImageStream(key.uri, context);
190
        }
191
 
192
        if (cachedStream != null) {
193
            // We were able to find a cached image.
194
            Bitmap bitmap = BitmapFactory.decodeStream(cachedStream);
195
            Utility.closeQuietly(cachedStream);
196
            issueResponse(key, null, bitmap, isCachedRedirect);
197
        } else {
198
            // Once the old downloader context is removed, we are thread-safe since this is the
199
            // only reference to it
200
            DownloaderContext downloaderContext = removePendingRequest(key);
201
            if (downloaderContext != null && !downloaderContext.isCancelled) {
202
                enqueueDownload(downloaderContext.request, key);
203
            }
204
        }
205
    }
206
 
207
    private static void download(RequestKey key, Context context) {
208
        HttpURLConnection connection = null;
209
        InputStream stream = null;
210
        Exception error = null;
211
        Bitmap bitmap = null;
212
        boolean issueResponse = true;
213
 
214
        try {
215
            URL url = new URL(key.uri.toString());
216
            connection = (HttpURLConnection) url.openConnection();
217
            connection.setInstanceFollowRedirects(false);
218
 
219
            switch (connection.getResponseCode()) {
220
                case HttpURLConnection.HTTP_MOVED_PERM:
221
                case HttpURLConnection.HTTP_MOVED_TEMP:
222
                    // redirect. So we need to perform further requests
223
                    issueResponse = false;
224
 
225
                    String redirectLocation = connection.getHeaderField("location");
226
                    if (!Utility.isNullOrEmpty(redirectLocation)) {
227
                        URI redirectUri = new URI(redirectLocation);
228
                        UrlRedirectCache.cacheUriRedirect(context, key.uri, redirectUri);
229
 
230
                        // Once the old downloader context is removed, we are thread-safe since this is the
231
                        // only reference to it
232
                        DownloaderContext downloaderContext = removePendingRequest(key);
233
                        if (downloaderContext != null && !downloaderContext.isCancelled) {
234
                            enqueueCacheRead(
235
                                    downloaderContext.request,
236
                                    new RequestKey(redirectUri, key.tag),
237
                                    false);
238
                        }
239
                    }
240
                    break;
241
 
242
                case HttpURLConnection.HTTP_OK:
243
                    // image should be available
244
                    stream = ImageResponseCache.interceptAndCacheImageStream(context, connection);
245
                    bitmap = BitmapFactory.decodeStream(stream);
246
                    break;
247
 
248
                default:
249
                    stream = connection.getErrorStream();
250
                    StringBuilder errorMessageBuilder = new StringBuilder();
251
                    if (stream != null) {
252
                        InputStreamReader reader = new InputStreamReader(stream);
253
                        char[] buffer = new char[128];
254
                        int bufferLength;
255
                        while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) {
256
                            errorMessageBuilder.append(buffer, 0, bufferLength);
257
                        }
258
                        Utility.closeQuietly(reader);
259
                    } else {
260
                        errorMessageBuilder.append(
261
                            context.getString(R.string.com_facebook_image_download_unknown_error));
262
                    }
263
                    error = new FacebookException(errorMessageBuilder.toString());
264
                    break;
265
            }
266
        } catch (IOException e) {
267
            error = e;
268
        } catch (URISyntaxException e) {
269
            error = e;
270
        } finally {
271
            Utility.closeQuietly(stream);
272
            Utility.disconnectQuietly(connection);
273
        }
274
 
275
        if (issueResponse) {
276
            issueResponse(key, error, bitmap, false);
277
        }
278
    }
279
 
280
    private static synchronized Handler getHandler() {
281
        if (handler == null) {
282
            handler = new Handler(Looper.getMainLooper());
283
        }
284
        return handler;
285
    }
286
 
287
    private static DownloaderContext removePendingRequest(RequestKey key) {
288
        synchronized (pendingRequests) {
289
            return pendingRequests.remove(key);
290
        }
291
    }
292
 
293
    private static class RequestKey {
294
        private static final int HASH_SEED = 29; // Some random prime number
295
        private static final int HASH_MULTIPLIER = 37; // Some random prime number
296
 
297
        URI uri;
298
        Object tag;
299
 
300
        RequestKey(URI url, Object tag) {
301
            this.uri = url;
302
            this.tag = tag;
303
        }
304
 
305
        @Override
306
        public int hashCode() {
307
            int result = HASH_SEED;
308
 
309
            result = (result * HASH_MULTIPLIER) + uri.hashCode();
310
            result = (result * HASH_MULTIPLIER) + tag.hashCode();
311
 
312
            return result;
313
        }
314
 
315
        @Override
316
        public boolean equals(Object o) {
317
            boolean isEqual = false;
318
 
319
            if (o != null && o instanceof RequestKey) {
320
                RequestKey compareTo = (RequestKey)o;
321
                isEqual = compareTo.uri == uri && compareTo.tag == tag;
322
            }
323
 
324
            return isEqual;
325
        }
326
    }
327
 
328
    private static class DownloaderContext {
329
        WorkQueue.WorkItem workItem;
330
        ImageRequest request;
331
        boolean isCancelled;
332
    }
333
 
334
    private static class CacheReadWorkItem implements Runnable {
335
        private Context context;
336
        private RequestKey key;
337
        private boolean allowCachedRedirects;
338
 
339
        CacheReadWorkItem(Context context, RequestKey key, boolean allowCachedRedirects) {
340
            this.context = context;
341
            this.key = key;
342
            this.allowCachedRedirects = allowCachedRedirects;
343
        }
344
 
345
        @Override
346
        public void run() {
347
            readFromCache(key, context, allowCachedRedirects);
348
        }
349
    }
350
 
351
    private static class DownloadImageWorkItem implements Runnable {
352
        private Context context;
353
        private RequestKey key;
354
 
355
        DownloadImageWorkItem(Context context, RequestKey key) {
356
            this.context = context;
357
            this.key = key;
358
        }
359
 
360
        @Override
361
        public void run() {
362
            download(key, context);
363
        }
364
 
365
    }
366
}