/** * Copyright 2010 Facebook, Inc. * * 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; import android.content.Context; import android.util.Log; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.EnumSet; class ImageResponseCache { static final String TAG = ImageResponseCache.class.getSimpleName(); private static final String REDIRECT_CONTENT_TAG = TAG + "_Redirect"; private volatile static FileLruCache imageCache; static InputStream getCachedImageStream(URL url, Context context) { return getCachedImageStream(url, context, Options.NONE); } // Get stream from cache, or return null if the image is not cached. // Does not throw if there was an error. static InputStream getCachedImageStream(URL url, Context context, EnumSet<Options> options) { InputStream imageStream = null; if (url != null) { if (options.contains(Options.FOLLOW_REDIRECTS)) { url = getRedirectedURL(context, url.toString()); } if (isCDNURL(url)) { try { FileLruCache cache = getCache(context); imageStream = cache.get(url.toString()); } catch (IOException e) { Logger.log(LoggingBehaviors.CACHE, Log.WARN, TAG, e.toString()); } } } return imageStream; } synchronized static FileLruCache getCache(Context context) throws IOException{ if (imageCache == null) { imageCache = new FileLruCache(context.getApplicationContext(), TAG, new FileLruCache.Limits()); } return imageCache; } static InputStream getImageStream(URL url, Context context) throws IOException { return getImageStream( url, context, EnumSet.of(Options.FOLLOW_REDIRECTS, Options.RETURN_STREAM_ON_HTTP_ERROR)); } // Get stream from cache if present, otherwise get from web. // If not cached and the uri points to a CDN, store the result in cache. static InputStream getImageStream(URL url, Context context, EnumSet<Options> options) throws IOException { Validate.notNull(url, "url"); Validate.notNull(context, "context"); InputStream stream = null; boolean performRequest = true; while (performRequest) { performRequest = false; // See if the url has been cached stream = getCachedImageStream(url, context); if (stream != null) { break; } // Since it isn't cached, make the network call HttpURLConnection connection = (HttpURLConnection)url.openConnection(); connection.setInstanceFollowRedirects(options.contains(Options.FOLLOW_REDIRECTS)); switch (connection.getResponseCode()) { case HttpURLConnection.HTTP_MOVED_PERM: case HttpURLConnection.HTTP_MOVED_TEMP: // redirect. So we need to perform further requests String redirectLocation = connection.getHeaderField("location"); if (!Utility.isNullOrEmpty(redirectLocation)) { cacheImageRedirect(context, url, redirectLocation); url = new URL(redirectLocation); performRequest = true; } break; case HttpURLConnection.HTTP_OK: // image should be available stream = cacheImageFromStream( context, url, new BufferedHttpInputStream(connection.getInputStream(), connection)); break; default: if (options.contains(Options.RETURN_STREAM_ON_HTTP_ERROR)) { // If response is not HTTP_OK, return error stream stream = new BufferedHttpInputStream(connection.getErrorStream(), connection); } break; } } return stream; } private static InputStream cacheImageFromStream(Context context, URL url, InputStream stream) { if (isCDNURL(url)) { try { FileLruCache cache = getCache(context); // Wrap stream with a caching stream stream = cache.interceptAndPut(url.toString(), stream); } catch (IOException e) { // Caching is best effort } } return stream; } private static void cacheImageRedirect(Context context, URL fromUrl, String toUrl) { OutputStream redirectStream = null; try { FileLruCache cache = getCache(context); redirectStream = cache.openPutStream(fromUrl.toString(), REDIRECT_CONTENT_TAG); redirectStream.write(toUrl.getBytes()); } catch (IOException e) { // Caching is best effort } finally { Utility.closeQuietly(redirectStream); } } private static URL getRedirectedURL(Context context, String url) { URL finalUrl = null; InputStreamReader reader = null; try { InputStream stream; FileLruCache cache = getCache(context); boolean redirectExists = false; while ((stream = cache.get(url, REDIRECT_CONTENT_TAG)) != null) { redirectExists = true; // Get the redirected url reader = new InputStreamReader(stream); char[] buffer = new char[128]; int bufferLength; StringBuilder urlBuilder = new StringBuilder(); while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) { urlBuilder.append(buffer, 0, bufferLength); } Utility.closeQuietly(reader); // Iterate to the next url in the redirection url = urlBuilder.toString(); } if (redirectExists) { finalUrl = new URL(url); } } catch (MalformedURLException e) { // caching is best effort, so ignore the exception } catch (IOException ioe) { } finally { Utility.closeQuietly(reader); } return finalUrl; } private static boolean isCDNURL(URL url) { if (url != null) { String uriHost = url.getHost(); if (uriHost.endsWith("fbcdn.net")) { return true; } if (uriHost.startsWith("fbcdn") && uriHost.endsWith("akamaihd.net")) { return true; } } return false; } private static class BufferedHttpInputStream extends BufferedInputStream { HttpURLConnection connection; BufferedHttpInputStream(InputStream stream, HttpURLConnection connection) { super(stream, Utility.DEFAULT_STREAM_BUFFER_SIZE); this.connection = connection; } @Override public void close() throws IOException { super.close(); Utility.disconnectQuietly(connection); } } enum Options { FOLLOW_REDIRECTS, RETURN_STREAM_ON_HTTP_ERROR; public static final EnumSet<Options> NONE = EnumSet.noneOf(Options.class); } }