/**
* 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 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.*;
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 downloader
RequestKey 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 etc
downloaderContext.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 it
DownloaderContext 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() {
@Override
public 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 it
DownloaderContext 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 requests
issueResponse = 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 it
DownloaderContext 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 available
stream = ImageResponseCache.interceptAndCacheImageStream(context, connection);
bitmap = BitmapFactory.decodeStream(stream);
break;
default:
stream = connection.getErrorStream();
InputStreamReader reader = new InputStreamReader(stream);
char[] buffer = new char[128];
int bufferLength;
StringBuilder errorMessageBuilder = new StringBuilder();
while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) {
errorMessageBuilder.append(buffer, 0, bufferLength);
}
Utility.closeQuietly(reader);
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 number
private static final int HASH_MULTIPLIER = 37; // Some random prime number
URI uri;
Object tag;
RequestKey(URI url, Object tag) {
this.uri = url;
this.tag = tag;
}
@Override
public int hashCode() {
int result = HASH_SEED;
result = (result * HASH_MULTIPLIER) + uri.hashCode();
result = (result * HASH_MULTIPLIER) + tag.hashCode();
return result;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public void run() {
download(key, context);
}
}
}