michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.favicons; michael@0: michael@0: michael@0: import android.content.ContentResolver; michael@0: import android.graphics.Bitmap; michael@0: import android.net.http.AndroidHttpClient; michael@0: import android.os.Handler; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: import org.apache.http.Header; michael@0: import org.apache.http.HttpEntity; michael@0: import org.apache.http.HttpResponse; michael@0: import org.apache.http.client.methods.HttpGet; michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.favicons.decoders.FaviconDecoder; michael@0: import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; michael@0: import org.mozilla.gecko.util.GeckoJarReader; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.UiAsyncTask; michael@0: import static org.mozilla.gecko.favicons.Favicons.context; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: import java.net.URI; michael@0: import java.net.URISyntaxException; michael@0: import java.util.HashMap; michael@0: import java.util.HashSet; michael@0: import java.util.LinkedList; michael@0: import java.util.concurrent.atomic.AtomicInteger; michael@0: michael@0: /** michael@0: * Class representing the asynchronous task to load a Favicon which is not currently in the in-memory michael@0: * cache. michael@0: * The implementation initially tries to get the Favicon from the database. Upon failure, the icon michael@0: * is loaded from the internet. michael@0: */ michael@0: public class LoadFaviconTask extends UiAsyncTask { michael@0: private static final String LOGTAG = "LoadFaviconTask"; michael@0: michael@0: // Access to this map needs to be synchronized prevent multiple jobs loading the same favicon michael@0: // from executing concurrently. michael@0: private static final HashMap loadsInFlight = new HashMap(); michael@0: michael@0: public static final int FLAG_PERSIST = 1; michael@0: public static final int FLAG_SCALE = 2; michael@0: private static final int MAX_REDIRECTS_TO_FOLLOW = 5; michael@0: // The default size of the buffer to use for downloading Favicons in the event no size is given michael@0: // by the server. michael@0: private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000; michael@0: michael@0: private static AtomicInteger nextFaviconLoadId = new AtomicInteger(0); michael@0: private int id; michael@0: private String pageUrl; michael@0: private String faviconURL; michael@0: private OnFaviconLoadedListener listener; michael@0: private int flags; michael@0: michael@0: private final boolean onlyFromLocal; michael@0: michael@0: // Assuming square favicons, judging by width only is acceptable. michael@0: protected int targetWidth; michael@0: private LinkedList chainees; michael@0: private boolean isChaining; michael@0: michael@0: static AndroidHttpClient httpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString()); michael@0: michael@0: public LoadFaviconTask(Handler backgroundThreadHandler, michael@0: String pageUrl, String faviconUrl, int flags, michael@0: OnFaviconLoadedListener listener) { michael@0: this(backgroundThreadHandler, pageUrl, faviconUrl, flags, listener, -1, false); michael@0: } michael@0: public LoadFaviconTask(Handler backgroundThreadHandler, michael@0: String pageUrl, String faviconUrl, int flags, michael@0: OnFaviconLoadedListener listener, int targetWidth, boolean onlyFromLocal) { michael@0: super(backgroundThreadHandler); michael@0: michael@0: id = nextFaviconLoadId.incrementAndGet(); michael@0: michael@0: this.pageUrl = pageUrl; michael@0: this.faviconURL = faviconUrl; michael@0: this.listener = listener; michael@0: this.flags = flags; michael@0: this.targetWidth = targetWidth; michael@0: this.onlyFromLocal = onlyFromLocal; michael@0: } michael@0: michael@0: // Runs in background thread michael@0: private LoadFaviconResult loadFaviconFromDb() { michael@0: ContentResolver resolver = context.getContentResolver(); michael@0: return BrowserDB.getFaviconForFaviconUrl(resolver, faviconURL); michael@0: } michael@0: michael@0: // Runs in background thread michael@0: private void saveFaviconToDb(final byte[] encodedFavicon) { michael@0: if (encodedFavicon == null) { michael@0: return; michael@0: } michael@0: michael@0: if ((flags & FLAG_PERSIST) == 0) { michael@0: return; michael@0: } michael@0: michael@0: ContentResolver resolver = context.getContentResolver(); michael@0: BrowserDB.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL); michael@0: } michael@0: michael@0: /** michael@0: * Helper method for trying the download request to grab a Favicon. michael@0: * @param faviconURI URL of Favicon to try and download michael@0: * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise. michael@0: */ michael@0: private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException { michael@0: HashSet visitedLinkSet = new HashSet(); michael@0: visitedLinkSet.add(faviconURI.toString()); michael@0: return tryDownloadRecurse(faviconURI, visitedLinkSet); michael@0: } michael@0: private HttpResponse tryDownloadRecurse(URI faviconURI, HashSet visited) throws URISyntaxException, IOException { michael@0: if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) { michael@0: return null; michael@0: } michael@0: michael@0: HttpGet request = new HttpGet(faviconURI); michael@0: HttpResponse response = httpClient.execute(request); michael@0: if (response == null) { michael@0: return null; michael@0: } michael@0: michael@0: if (response.getStatusLine() != null) { michael@0: michael@0: // Was the response a failure? michael@0: int status = response.getStatusLine().getStatusCode(); michael@0: michael@0: // Handle HTTP status codes requesting a redirect. michael@0: if (status >= 300 && status < 400) { michael@0: Header header = response.getFirstHeader("Location"); michael@0: michael@0: // Handle mad webservers. michael@0: if (header == null) { michael@0: return null; michael@0: } michael@0: michael@0: String newURI = header.getValue(); michael@0: if (newURI == null || newURI.equals(faviconURI.toString())) { michael@0: return null; michael@0: } michael@0: michael@0: if (visited.contains(newURI)) { michael@0: // Already been redirected here - abort. michael@0: return null; michael@0: } michael@0: michael@0: visited.add(newURI); michael@0: return tryDownloadRecurse(new URI(newURI), visited); michael@0: } michael@0: michael@0: if (status >= 400) { michael@0: return null; michael@0: } michael@0: } michael@0: return response; michael@0: } michael@0: michael@0: /** michael@0: * Retrieve the specified favicon from the JAR, returning null if it's not michael@0: * a JAR URI. michael@0: */ michael@0: private static Bitmap fetchJARFavicon(String uri) { michael@0: if (uri == null) { michael@0: return null; michael@0: } michael@0: if (uri.startsWith("jar:jar:")) { michael@0: Log.d(LOGTAG, "Fetching favicon from JAR."); michael@0: try { michael@0: return GeckoJarReader.getBitmap(context.getResources(), uri); michael@0: } catch (Exception e) { michael@0: // Just about anything could happen here. michael@0: Log.w(LOGTAG, "Error fetching favicon from JAR.", e); michael@0: return null; michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: // Runs in background thread. michael@0: // Does not attempt to fetch from JARs. michael@0: private LoadFaviconResult downloadFavicon(URI targetFaviconURI) { michael@0: if (targetFaviconURI == null) { michael@0: return null; michael@0: } michael@0: michael@0: // Only get favicons for HTTP/HTTPS. michael@0: String scheme = targetFaviconURI.getScheme(); michael@0: if (!"http".equals(scheme) && !"https".equals(scheme)) { michael@0: return null; michael@0: } michael@0: michael@0: LoadFaviconResult result = null; michael@0: michael@0: try { michael@0: result = downloadAndDecodeImage(targetFaviconURI); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Error reading favicon", e); michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: /** michael@0: * Download the Favicon from the given URL and pass it to the decoder function. michael@0: * michael@0: * @param targetFaviconURL URL of the favicon to download. michael@0: * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or michael@0: * null if no or corrupt data ware received. michael@0: * @throws IOException If attempts to fully read the stream result in such an exception, such as michael@0: * in the event of a transient connection failure. michael@0: * @throws URISyntaxException If the underlying call to tryDownload retries and raises such an michael@0: * exception trying a fallback URL. michael@0: */ michael@0: private LoadFaviconResult downloadAndDecodeImage(URI targetFaviconURL) throws IOException, URISyntaxException { michael@0: // Try the URL we were given. michael@0: HttpResponse response = tryDownload(targetFaviconURL); michael@0: if (response == null) { michael@0: return null; michael@0: } michael@0: michael@0: HttpEntity entity = response.getEntity(); michael@0: if (entity == null) { michael@0: return null; michael@0: } michael@0: michael@0: // This may not be provided, but if it is, it's useful. michael@0: final long entityReportedLength = entity.getContentLength(); michael@0: int bufferSize; michael@0: if (entityReportedLength > 0) { michael@0: // The size was reported and sane, so let's use that. michael@0: // Integer overflow should not be a problem for Favicon sizes... michael@0: bufferSize = (int) entityReportedLength + 1; michael@0: } else { michael@0: // No declared size, so guess and reallocate later if it turns out to be too small. michael@0: bufferSize = DEFAULT_FAVICON_BUFFER_SIZE; michael@0: } michael@0: michael@0: // Allocate a buffer to hold the raw favicon data downloaded. michael@0: byte[] buffer = new byte[bufferSize]; michael@0: michael@0: // The offset of the start of the buffer's free space. michael@0: int bPointer = 0; michael@0: michael@0: // The quantity of bytes the last call to read yielded. michael@0: int lastRead = 0; michael@0: InputStream contentStream = entity.getContent(); michael@0: try { michael@0: // Fully read the entity into the buffer - decoding of streams is not supported michael@0: // (and questionably pointful - what would one do with a half-decoded Favicon?) michael@0: while (lastRead != -1) { michael@0: // Read as many bytes as are currently available into the buffer. michael@0: lastRead = contentStream.read(buffer, bPointer, buffer.length - bPointer); michael@0: bPointer += lastRead; michael@0: michael@0: // If buffer has overflowed, double its size and carry on. michael@0: if (bPointer == buffer.length) { michael@0: bufferSize *= 2; michael@0: byte[] newBuffer = new byte[bufferSize]; michael@0: michael@0: // Copy the contents of the old buffer into the new buffer. michael@0: System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); michael@0: buffer = newBuffer; michael@0: } michael@0: } michael@0: } finally { michael@0: contentStream.close(); michael@0: } michael@0: michael@0: // Having downloaded the image, decode it. michael@0: return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1); michael@0: } michael@0: michael@0: @Override michael@0: protected Bitmap doInBackground(Void... unused) { michael@0: if (isCancelled()) { michael@0: return null; michael@0: } michael@0: michael@0: String storedFaviconUrl; michael@0: boolean isUsingDefaultURL = false; michael@0: michael@0: // Handle the case of malformed favicon URL. michael@0: // If favicon is empty, fall back to the stored one. michael@0: if (TextUtils.isEmpty(faviconURL)) { michael@0: // Try to get the favicon URL from the memory cache. michael@0: storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(pageUrl); michael@0: michael@0: // If that failed, try to get the URL from the database. michael@0: if (storedFaviconUrl == null) { michael@0: storedFaviconUrl = Favicons.getFaviconURLForPageURL(pageUrl); michael@0: if (storedFaviconUrl != null) { michael@0: // If that succeeded, cache the URL loaded from the database in memory. michael@0: Favicons.putFaviconURLForPageURLInCache(pageUrl, storedFaviconUrl); michael@0: } michael@0: } michael@0: michael@0: // If we found a faviconURL - use it. michael@0: if (storedFaviconUrl != null) { michael@0: faviconURL = storedFaviconUrl; michael@0: } else { michael@0: // If we don't have a stored one, fall back to the default. michael@0: faviconURL = Favicons.guessDefaultFaviconURL(pageUrl); michael@0: michael@0: if (TextUtils.isEmpty(faviconURL)) { michael@0: return null; michael@0: } michael@0: isUsingDefaultURL = true; michael@0: } michael@0: } michael@0: michael@0: // Check if favicon has failed - if so, give up. We need this check because, sometimes, we michael@0: // didn't know the real Favicon URL until we asked the database. michael@0: if (Favicons.isFailedFavicon(faviconURL)) { michael@0: return null; michael@0: } michael@0: michael@0: if (isCancelled()) { michael@0: return null; michael@0: } michael@0: michael@0: Bitmap image; michael@0: // Determine if there is already an ongoing task to fetch the Favicon we desire. michael@0: // If there is, just join the queue and wait for it to finish. If not, we carry on. michael@0: synchronized(loadsInFlight) { michael@0: // Another load of the current Favicon is already underway michael@0: LoadFaviconTask existingTask = loadsInFlight.get(faviconURL); michael@0: if (existingTask != null && !existingTask.isCancelled()) { michael@0: existingTask.chainTasks(this); michael@0: isChaining = true; michael@0: michael@0: // If we are chaining, we want to keep the first task started to do this job as the one michael@0: // in the hashmap so subsequent tasks will add themselves to its chaining list. michael@0: return null; michael@0: } michael@0: michael@0: // We do not want to update the hashmap if the task has chained - other tasks need to michael@0: // chain onto the same parent task. michael@0: loadsInFlight.put(faviconURL, this); michael@0: } michael@0: michael@0: if (isCancelled()) { michael@0: return null; michael@0: } michael@0: michael@0: // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null. michael@0: LoadFaviconResult loadedBitmaps = loadFaviconFromDb(); michael@0: if (loadedBitmaps != null) { michael@0: return pushToCacheAndGetResult(loadedBitmaps); michael@0: } michael@0: michael@0: if (onlyFromLocal || isCancelled()) { michael@0: return null; michael@0: } michael@0: michael@0: // Let's see if it's in a JAR. michael@0: image = fetchJARFavicon(faviconURL); michael@0: if (imageIsValid(image)) { michael@0: // We don't want to put this into the DB. michael@0: Favicons.putFaviconInMemCache(faviconURL, image); michael@0: return image; michael@0: } michael@0: michael@0: try { michael@0: loadedBitmaps = downloadFavicon(new URI(faviconURL)); michael@0: } catch (URISyntaxException e) { michael@0: Log.e(LOGTAG, "The provided favicon URL is not valid"); michael@0: return null; michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Couldn't download favicon.", e); michael@0: } michael@0: michael@0: if (loadedBitmaps != null) { michael@0: // Fetching bytes to store can fail. saveFaviconToDb will michael@0: // do the right thing, but we still choose to cache the michael@0: // downloaded icon in memory. michael@0: saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); michael@0: return pushToCacheAndGetResult(loadedBitmaps); michael@0: } michael@0: michael@0: if (isUsingDefaultURL) { michael@0: Favicons.putFaviconInFailedCache(faviconURL); michael@0: return null; michael@0: } michael@0: michael@0: if (isCancelled()) { michael@0: return null; michael@0: } michael@0: michael@0: // If we're not already trying the default URL, try it now. michael@0: final String guessed = Favicons.guessDefaultFaviconURL(pageUrl); michael@0: if (guessed == null) { michael@0: Favicons.putFaviconInFailedCache(faviconURL); michael@0: return null; michael@0: } michael@0: michael@0: image = fetchJARFavicon(guessed); michael@0: if (imageIsValid(image)) { michael@0: // We don't want to put this into the DB. michael@0: Favicons.putFaviconInMemCache(faviconURL, image); michael@0: return image; michael@0: } michael@0: michael@0: try { michael@0: loadedBitmaps = downloadFavicon(new URI(guessed)); michael@0: } catch (Exception e) { michael@0: // Not interesting. It was an educated guess, anyway. michael@0: return null; michael@0: } michael@0: michael@0: if (loadedBitmaps != null) { michael@0: saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); michael@0: return pushToCacheAndGetResult(loadedBitmaps); michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Helper method to put the result of a favicon load into the memory cache and then query the michael@0: * cache for the particular bitmap we want for this request. michael@0: * This call is certain to succeed, provided there was enough memory to decode this favicon. michael@0: * michael@0: * @param loadedBitmaps LoadFaviconResult to store. michael@0: * @return The optimal favicon available to satisfy this LoadFaviconTask's request, or null if michael@0: * we are under extreme memory pressure and find ourselves dropping the cache immediately. michael@0: */ michael@0: private Bitmap pushToCacheAndGetResult(LoadFaviconResult loadedBitmaps) { michael@0: Favicons.putFaviconsInMemCache(faviconURL, loadedBitmaps.getBitmaps()); michael@0: Bitmap result = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth); michael@0: return result; michael@0: } michael@0: michael@0: private static boolean imageIsValid(final Bitmap image) { michael@0: return image != null && michael@0: image.getWidth() > 0 && michael@0: image.getHeight() > 0; michael@0: } michael@0: michael@0: @Override michael@0: protected void onPostExecute(Bitmap image) { michael@0: if (isChaining) { michael@0: return; michael@0: } michael@0: michael@0: // Process the result, scale for the listener, etc. michael@0: processResult(image); michael@0: michael@0: synchronized (loadsInFlight) { michael@0: // Prevent any other tasks from chaining on this one. michael@0: loadsInFlight.remove(faviconURL); michael@0: } michael@0: michael@0: // Since any update to chainees is done while holding the loadsInFlight lock, once we reach michael@0: // this point no further updates to that list can possibly take place (As far as other tasks michael@0: // are concerned, there is no longer a task to chain from. The above block will have waited michael@0: // for any tasks that were adding themselves to the list before reaching this point.) michael@0: michael@0: // As such, I believe we're safe to do the following without holding the lock. michael@0: // This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely michael@0: // actually happens outside of the strange situations unit tests create. michael@0: michael@0: // Share the result with all chained tasks. michael@0: if (chainees != null) { michael@0: for (LoadFaviconTask t : chainees) { michael@0: // In the case that we just decoded multiple favicons, either we're passing the right michael@0: // image now, or the call into the cache in processResult will fetch the right one. michael@0: t.processResult(image); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void processResult(Bitmap image) { michael@0: Favicons.removeLoadTask(id); michael@0: Bitmap scaled = image; michael@0: michael@0: // Notify listeners, scaling if required. michael@0: if (targetWidth != -1 && image != null && image.getWidth() != targetWidth) { michael@0: scaled = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth); michael@0: } michael@0: michael@0: Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener); michael@0: } michael@0: michael@0: @Override michael@0: protected void onCancelled() { michael@0: Favicons.removeLoadTask(id); michael@0: michael@0: synchronized(loadsInFlight) { michael@0: // Only remove from the hashmap if the task there is the one that's being canceled. michael@0: // Cancellation of a task that would have chained is not interesting to the hashmap. michael@0: final LoadFaviconTask primary = loadsInFlight.get(faviconURL); michael@0: if (primary == this) { michael@0: loadsInFlight.remove(faviconURL); michael@0: return; michael@0: } michael@0: if (primary == null) { michael@0: // This shouldn't happen. michael@0: return; michael@0: } michael@0: if (primary.chainees != null) { michael@0: primary.chainees.remove(this); michael@0: } michael@0: } michael@0: michael@0: // Note that we don't call the listener callback if the michael@0: // favicon load is cancelled. michael@0: } michael@0: michael@0: /** michael@0: * When the result of this job is ready, also notify the chainee of the result. michael@0: * Used for aggregating concurrent requests for the same Favicon into a single actual request. michael@0: * (Don't want to download a hundred instances of Google's Favicon at once, for example). michael@0: * The loadsInFlight lock must be held when calling this function. michael@0: * michael@0: * @param aChainee LoadFaviconTask michael@0: */ michael@0: private void chainTasks(LoadFaviconTask aChainee) { michael@0: if (chainees == null) { michael@0: chainees = new LinkedList(); michael@0: } michael@0: michael@0: chainees.add(aChainee); michael@0: } michael@0: michael@0: int getId() { michael@0: return id; michael@0: } michael@0: michael@0: static void closeHTTPClient() { michael@0: // This work must be done on a background thread because it shuts down michael@0: // the connection pool, which typically involves closing a connection -- michael@0: // which counts as network activity. michael@0: if (ThreadUtils.isOnBackgroundThread()) { michael@0: if (httpClient != null) { michael@0: httpClient.close(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: LoadFaviconTask.closeHTTPClient(); michael@0: } michael@0: }); michael@0: } michael@0: }