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