Blame | Last modification | View Log | RSS feed
/*** Copyright 2010-present Facebook.** 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.facebook.internal;import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.Handler;import android.os.Looper;import com.facebook.FacebookException;import com.facebook.android.R;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.net.HttpURLConnection;import java.net.URI;import java.net.URISyntaxException;import java.net.URL;import java.util.HashMap;import java.util.Map;public class ImageDownloader {private static final int DOWNLOAD_QUEUE_MAX_CONCURRENT = WorkQueue.DEFAULT_MAX_CONCURRENT;private static final int CACHE_READ_QUEUE_MAX_CONCURRENT = 2;private static Handler handler;private static WorkQueue downloadQueue = new WorkQueue(DOWNLOAD_QUEUE_MAX_CONCURRENT);private static WorkQueue cacheReadQueue = new WorkQueue(CACHE_READ_QUEUE_MAX_CONCURRENT);private static final Map<RequestKey, DownloaderContext> pendingRequests = new HashMap<RequestKey, DownloaderContext>();/*** Downloads the image specified in the passed in request.* If a callback is specified, it is guaranteed to be invoked on the calling thread.* @param request Request to process*/public static void downloadAsync(ImageRequest request) {if (request == null) {return;}// NOTE: This is the ONLY place where the original request's Url is read. From here on,// we will keep track of the Url separately. This is because we might be dealing with a// redirect response and the Url might change. We can't create our own new ImageRequests// for these changed Urls since the caller might be doing some book-keeping with the request's// object reference. So we keep the old references and just map them to new urls in the downloaderRequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag());synchronized (pendingRequests) {DownloaderContext downloaderContext = pendingRequests.get(key);if (downloaderContext != null) {downloaderContext.request = request;downloaderContext.isCancelled = false;downloaderContext.workItem.moveToFront();} else {enqueueCacheRead(request, key, request.isCachedRedirectAllowed());}}}public static boolean cancelRequest(ImageRequest request) {boolean cancelled = false;RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag());synchronized (pendingRequests) {DownloaderContext downloaderContext = pendingRequests.get(key);if (downloaderContext != null) {// If we were able to find the request in our list of pending requests, then we will// definitely be able to prevent an ImageResponse from being issued. This is regardless// of whether a cache-read or network-download is underway for this request.cancelled = true;if (downloaderContext.workItem.cancel()) {pendingRequests.remove(key);} else {// May be attempting a cache-read right now. So keep track of the cancellation// to prevent network calls etcdownloaderContext.isCancelled = true;}}}return cancelled;}public static void prioritizeRequest(ImageRequest request) {RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag());synchronized (pendingRequests) {DownloaderContext downloaderContext = pendingRequests.get(key);if (downloaderContext != null) {downloaderContext.workItem.moveToFront();}}}public static void clearCache(Context context) {ImageResponseCache.clearCache(context);UrlRedirectCache.clearCache(context);}private static void enqueueCacheRead(ImageRequest request, RequestKey key, boolean allowCachedRedirects) {enqueueRequest(request,key,cacheReadQueue,new CacheReadWorkItem(request.getContext(), key, allowCachedRedirects));}private static void enqueueDownload(ImageRequest request, RequestKey key) {enqueueRequest(request,key,downloadQueue,new DownloadImageWorkItem(request.getContext(), key));}private static void enqueueRequest(ImageRequest request,RequestKey key,WorkQueue workQueue,Runnable workItem) {synchronized (pendingRequests) {DownloaderContext downloaderContext = new DownloaderContext();downloaderContext.request = request;pendingRequests.put(key, downloaderContext);// The creation of the WorkItem should be done after the pending request has been registered.// This is necessary since the WorkItem might kick off right away and attempt to retrieve// the request's DownloaderContext prior to it being ready for access.//// It is also necessary to hold on to the lock until after the workItem is created, since// calls to cancelRequest or prioritizeRequest might come in and expect a registered// request to have a workItem available as well.downloaderContext.workItem = workQueue.addActiveWorkItem(workItem);}}private static void issueResponse(RequestKey key,final Exception error,final Bitmap bitmap,final boolean isCachedRedirect) {// Once the old downloader context is removed, we are thread-safe since this is the// only reference to itDownloaderContext completedRequestContext = removePendingRequest(key);if (completedRequestContext != null && !completedRequestContext.isCancelled) {final ImageRequest request = completedRequestContext.request;final ImageRequest.Callback callback = request.getCallback();if (callback != null) {getHandler().post(new Runnable() {@Overridepublic void run() {ImageResponse response = new ImageResponse(request,error,isCachedRedirect,bitmap);callback.onCompleted(response);}});}}}private static void readFromCache(RequestKey key, Context context, boolean allowCachedRedirects) {InputStream cachedStream = null;boolean isCachedRedirect = false;if (allowCachedRedirects) {URI redirectUri = UrlRedirectCache.getRedirectedUri(context, key.uri);if (redirectUri != null) {cachedStream = ImageResponseCache.getCachedImageStream(redirectUri, context);isCachedRedirect = cachedStream != null;}}if (!isCachedRedirect) {cachedStream = ImageResponseCache.getCachedImageStream(key.uri, context);}if (cachedStream != null) {// We were able to find a cached image.Bitmap bitmap = BitmapFactory.decodeStream(cachedStream);Utility.closeQuietly(cachedStream);issueResponse(key, null, bitmap, isCachedRedirect);} else {// Once the old downloader context is removed, we are thread-safe since this is the// only reference to itDownloaderContext downloaderContext = removePendingRequest(key);if (downloaderContext != null && !downloaderContext.isCancelled) {enqueueDownload(downloaderContext.request, key);}}}private static void download(RequestKey key, Context context) {HttpURLConnection connection = null;InputStream stream = null;Exception error = null;Bitmap bitmap = null;boolean issueResponse = true;try {URL url = new URL(key.uri.toString());connection = (HttpURLConnection) url.openConnection();connection.setInstanceFollowRedirects(false);switch (connection.getResponseCode()) {case HttpURLConnection.HTTP_MOVED_PERM:case HttpURLConnection.HTTP_MOVED_TEMP:// redirect. So we need to perform further requestsissueResponse = false;String redirectLocation = connection.getHeaderField("location");if (!Utility.isNullOrEmpty(redirectLocation)) {URI redirectUri = new URI(redirectLocation);UrlRedirectCache.cacheUriRedirect(context, key.uri, redirectUri);// Once the old downloader context is removed, we are thread-safe since this is the// only reference to itDownloaderContext downloaderContext = removePendingRequest(key);if (downloaderContext != null && !downloaderContext.isCancelled) {enqueueCacheRead(downloaderContext.request,new RequestKey(redirectUri, key.tag),false);}}break;case HttpURLConnection.HTTP_OK:// image should be availablestream = ImageResponseCache.interceptAndCacheImageStream(context, connection);bitmap = BitmapFactory.decodeStream(stream);break;default:stream = connection.getErrorStream();StringBuilder errorMessageBuilder = new StringBuilder();if (stream != null) {InputStreamReader reader = new InputStreamReader(stream);char[] buffer = new char[128];int bufferLength;while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) {errorMessageBuilder.append(buffer, 0, bufferLength);}Utility.closeQuietly(reader);} else {errorMessageBuilder.append(context.getString(R.string.com_facebook_image_download_unknown_error));}error = new FacebookException(errorMessageBuilder.toString());break;}} catch (IOException e) {error = e;} catch (URISyntaxException e) {error = e;} finally {Utility.closeQuietly(stream);Utility.disconnectQuietly(connection);}if (issueResponse) {issueResponse(key, error, bitmap, false);}}private static synchronized Handler getHandler() {if (handler == null) {handler = new Handler(Looper.getMainLooper());}return handler;}private static DownloaderContext removePendingRequest(RequestKey key) {synchronized (pendingRequests) {return pendingRequests.remove(key);}}private static class RequestKey {private static final int HASH_SEED = 29; // Some random prime numberprivate static final int HASH_MULTIPLIER = 37; // Some random prime numberURI uri;Object tag;RequestKey(URI url, Object tag) {this.uri = url;this.tag = tag;}@Overridepublic int hashCode() {int result = HASH_SEED;result = (result * HASH_MULTIPLIER) + uri.hashCode();result = (result * HASH_MULTIPLIER) + tag.hashCode();return result;}@Overridepublic boolean equals(Object o) {boolean isEqual = false;if (o != null && o instanceof RequestKey) {RequestKey compareTo = (RequestKey)o;isEqual = compareTo.uri == uri && compareTo.tag == tag;}return isEqual;}}private static class DownloaderContext {WorkQueue.WorkItem workItem;ImageRequest request;boolean isCancelled;}private static class CacheReadWorkItem implements Runnable {private Context context;private RequestKey key;private boolean allowCachedRedirects;CacheReadWorkItem(Context context, RequestKey key, boolean allowCachedRedirects) {this.context = context;this.key = key;this.allowCachedRedirects = allowCachedRedirects;}@Overridepublic void run() {readFromCache(key, context, allowCachedRedirects);}}private static class DownloadImageWorkItem implements Runnable {private Context context;private RequestKey key;DownloadImageWorkItem(Context context, RequestKey key) {this.context = context;this.key = key;}@Overridepublic void run() {download(key, context);}}}