/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
* copy, modify, and distribute this software in source code or binary form for use
* in connection with the web services and APIs provided by Facebook.
*
* As with any software that integrates with the Facebook platform, your use of
* this software is subject to the Facebook Developer Principles and Policies
* [http://developers.facebook.com/policy/]. This copyright notice shall be
* included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.facebook.internal;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import com.facebook.FacebookException;
import com.facebook.R;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
/**
* com.facebook.internal is solely for the use of other packages within the
* Facebook SDK for Android. Use of any of the classes in this package is
* unsupported, and they may be modified or removed without warning at any time.
*/
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
// requests 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();
}
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(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 = Uri.parse(redirectLocation);
UrlRedirectCache.cacheUriRedirect(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();
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;
} 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);
}
}
}