1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/favicons/LoadFaviconTask.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,557 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.favicons; 1.9 + 1.10 + 1.11 +import android.content.ContentResolver; 1.12 +import android.graphics.Bitmap; 1.13 +import android.net.http.AndroidHttpClient; 1.14 +import android.os.Handler; 1.15 +import android.text.TextUtils; 1.16 +import android.util.Log; 1.17 +import org.apache.http.Header; 1.18 +import org.apache.http.HttpEntity; 1.19 +import org.apache.http.HttpResponse; 1.20 +import org.apache.http.client.methods.HttpGet; 1.21 +import org.mozilla.gecko.GeckoAppShell; 1.22 +import org.mozilla.gecko.db.BrowserDB; 1.23 +import org.mozilla.gecko.favicons.decoders.FaviconDecoder; 1.24 +import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; 1.25 +import org.mozilla.gecko.util.GeckoJarReader; 1.26 +import org.mozilla.gecko.util.ThreadUtils; 1.27 +import org.mozilla.gecko.util.UiAsyncTask; 1.28 +import static org.mozilla.gecko.favicons.Favicons.context; 1.29 + 1.30 +import java.io.IOException; 1.31 +import java.io.InputStream; 1.32 +import java.net.URI; 1.33 +import java.net.URISyntaxException; 1.34 +import java.util.HashMap; 1.35 +import java.util.HashSet; 1.36 +import java.util.LinkedList; 1.37 +import java.util.concurrent.atomic.AtomicInteger; 1.38 + 1.39 +/** 1.40 + * Class representing the asynchronous task to load a Favicon which is not currently in the in-memory 1.41 + * cache. 1.42 + * The implementation initially tries to get the Favicon from the database. Upon failure, the icon 1.43 + * is loaded from the internet. 1.44 + */ 1.45 +public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> { 1.46 + private static final String LOGTAG = "LoadFaviconTask"; 1.47 + 1.48 + // Access to this map needs to be synchronized prevent multiple jobs loading the same favicon 1.49 + // from executing concurrently. 1.50 + private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<String, LoadFaviconTask>(); 1.51 + 1.52 + public static final int FLAG_PERSIST = 1; 1.53 + public static final int FLAG_SCALE = 2; 1.54 + private static final int MAX_REDIRECTS_TO_FOLLOW = 5; 1.55 + // The default size of the buffer to use for downloading Favicons in the event no size is given 1.56 + // by the server. 1.57 + private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000; 1.58 + 1.59 + private static AtomicInteger nextFaviconLoadId = new AtomicInteger(0); 1.60 + private int id; 1.61 + private String pageUrl; 1.62 + private String faviconURL; 1.63 + private OnFaviconLoadedListener listener; 1.64 + private int flags; 1.65 + 1.66 + private final boolean onlyFromLocal; 1.67 + 1.68 + // Assuming square favicons, judging by width only is acceptable. 1.69 + protected int targetWidth; 1.70 + private LinkedList<LoadFaviconTask> chainees; 1.71 + private boolean isChaining; 1.72 + 1.73 + static AndroidHttpClient httpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString()); 1.74 + 1.75 + public LoadFaviconTask(Handler backgroundThreadHandler, 1.76 + String pageUrl, String faviconUrl, int flags, 1.77 + OnFaviconLoadedListener listener) { 1.78 + this(backgroundThreadHandler, pageUrl, faviconUrl, flags, listener, -1, false); 1.79 + } 1.80 + public LoadFaviconTask(Handler backgroundThreadHandler, 1.81 + String pageUrl, String faviconUrl, int flags, 1.82 + OnFaviconLoadedListener listener, int targetWidth, boolean onlyFromLocal) { 1.83 + super(backgroundThreadHandler); 1.84 + 1.85 + id = nextFaviconLoadId.incrementAndGet(); 1.86 + 1.87 + this.pageUrl = pageUrl; 1.88 + this.faviconURL = faviconUrl; 1.89 + this.listener = listener; 1.90 + this.flags = flags; 1.91 + this.targetWidth = targetWidth; 1.92 + this.onlyFromLocal = onlyFromLocal; 1.93 + } 1.94 + 1.95 + // Runs in background thread 1.96 + private LoadFaviconResult loadFaviconFromDb() { 1.97 + ContentResolver resolver = context.getContentResolver(); 1.98 + return BrowserDB.getFaviconForFaviconUrl(resolver, faviconURL); 1.99 + } 1.100 + 1.101 + // Runs in background thread 1.102 + private void saveFaviconToDb(final byte[] encodedFavicon) { 1.103 + if (encodedFavicon == null) { 1.104 + return; 1.105 + } 1.106 + 1.107 + if ((flags & FLAG_PERSIST) == 0) { 1.108 + return; 1.109 + } 1.110 + 1.111 + ContentResolver resolver = context.getContentResolver(); 1.112 + BrowserDB.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL); 1.113 + } 1.114 + 1.115 + /** 1.116 + * Helper method for trying the download request to grab a Favicon. 1.117 + * @param faviconURI URL of Favicon to try and download 1.118 + * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise. 1.119 + */ 1.120 + private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException { 1.121 + HashSet<String> visitedLinkSet = new HashSet<String>(); 1.122 + visitedLinkSet.add(faviconURI.toString()); 1.123 + return tryDownloadRecurse(faviconURI, visitedLinkSet); 1.124 + } 1.125 + private HttpResponse tryDownloadRecurse(URI faviconURI, HashSet<String> visited) throws URISyntaxException, IOException { 1.126 + if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) { 1.127 + return null; 1.128 + } 1.129 + 1.130 + HttpGet request = new HttpGet(faviconURI); 1.131 + HttpResponse response = httpClient.execute(request); 1.132 + if (response == null) { 1.133 + return null; 1.134 + } 1.135 + 1.136 + if (response.getStatusLine() != null) { 1.137 + 1.138 + // Was the response a failure? 1.139 + int status = response.getStatusLine().getStatusCode(); 1.140 + 1.141 + // Handle HTTP status codes requesting a redirect. 1.142 + if (status >= 300 && status < 400) { 1.143 + Header header = response.getFirstHeader("Location"); 1.144 + 1.145 + // Handle mad webservers. 1.146 + if (header == null) { 1.147 + return null; 1.148 + } 1.149 + 1.150 + String newURI = header.getValue(); 1.151 + if (newURI == null || newURI.equals(faviconURI.toString())) { 1.152 + return null; 1.153 + } 1.154 + 1.155 + if (visited.contains(newURI)) { 1.156 + // Already been redirected here - abort. 1.157 + return null; 1.158 + } 1.159 + 1.160 + visited.add(newURI); 1.161 + return tryDownloadRecurse(new URI(newURI), visited); 1.162 + } 1.163 + 1.164 + if (status >= 400) { 1.165 + return null; 1.166 + } 1.167 + } 1.168 + return response; 1.169 + } 1.170 + 1.171 + /** 1.172 + * Retrieve the specified favicon from the JAR, returning null if it's not 1.173 + * a JAR URI. 1.174 + */ 1.175 + private static Bitmap fetchJARFavicon(String uri) { 1.176 + if (uri == null) { 1.177 + return null; 1.178 + } 1.179 + if (uri.startsWith("jar:jar:")) { 1.180 + Log.d(LOGTAG, "Fetching favicon from JAR."); 1.181 + try { 1.182 + return GeckoJarReader.getBitmap(context.getResources(), uri); 1.183 + } catch (Exception e) { 1.184 + // Just about anything could happen here. 1.185 + Log.w(LOGTAG, "Error fetching favicon from JAR.", e); 1.186 + return null; 1.187 + } 1.188 + } 1.189 + return null; 1.190 + } 1.191 + 1.192 + // Runs in background thread. 1.193 + // Does not attempt to fetch from JARs. 1.194 + private LoadFaviconResult downloadFavicon(URI targetFaviconURI) { 1.195 + if (targetFaviconURI == null) { 1.196 + return null; 1.197 + } 1.198 + 1.199 + // Only get favicons for HTTP/HTTPS. 1.200 + String scheme = targetFaviconURI.getScheme(); 1.201 + if (!"http".equals(scheme) && !"https".equals(scheme)) { 1.202 + return null; 1.203 + } 1.204 + 1.205 + LoadFaviconResult result = null; 1.206 + 1.207 + try { 1.208 + result = downloadAndDecodeImage(targetFaviconURI); 1.209 + } catch (Exception e) { 1.210 + Log.e(LOGTAG, "Error reading favicon", e); 1.211 + } 1.212 + 1.213 + return result; 1.214 + } 1.215 + 1.216 + /** 1.217 + * Download the Favicon from the given URL and pass it to the decoder function. 1.218 + * 1.219 + * @param targetFaviconURL URL of the favicon to download. 1.220 + * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or 1.221 + * null if no or corrupt data ware received. 1.222 + * @throws IOException If attempts to fully read the stream result in such an exception, such as 1.223 + * in the event of a transient connection failure. 1.224 + * @throws URISyntaxException If the underlying call to tryDownload retries and raises such an 1.225 + * exception trying a fallback URL. 1.226 + */ 1.227 + private LoadFaviconResult downloadAndDecodeImage(URI targetFaviconURL) throws IOException, URISyntaxException { 1.228 + // Try the URL we were given. 1.229 + HttpResponse response = tryDownload(targetFaviconURL); 1.230 + if (response == null) { 1.231 + return null; 1.232 + } 1.233 + 1.234 + HttpEntity entity = response.getEntity(); 1.235 + if (entity == null) { 1.236 + return null; 1.237 + } 1.238 + 1.239 + // This may not be provided, but if it is, it's useful. 1.240 + final long entityReportedLength = entity.getContentLength(); 1.241 + int bufferSize; 1.242 + if (entityReportedLength > 0) { 1.243 + // The size was reported and sane, so let's use that. 1.244 + // Integer overflow should not be a problem for Favicon sizes... 1.245 + bufferSize = (int) entityReportedLength + 1; 1.246 + } else { 1.247 + // No declared size, so guess and reallocate later if it turns out to be too small. 1.248 + bufferSize = DEFAULT_FAVICON_BUFFER_SIZE; 1.249 + } 1.250 + 1.251 + // Allocate a buffer to hold the raw favicon data downloaded. 1.252 + byte[] buffer = new byte[bufferSize]; 1.253 + 1.254 + // The offset of the start of the buffer's free space. 1.255 + int bPointer = 0; 1.256 + 1.257 + // The quantity of bytes the last call to read yielded. 1.258 + int lastRead = 0; 1.259 + InputStream contentStream = entity.getContent(); 1.260 + try { 1.261 + // Fully read the entity into the buffer - decoding of streams is not supported 1.262 + // (and questionably pointful - what would one do with a half-decoded Favicon?) 1.263 + while (lastRead != -1) { 1.264 + // Read as many bytes as are currently available into the buffer. 1.265 + lastRead = contentStream.read(buffer, bPointer, buffer.length - bPointer); 1.266 + bPointer += lastRead; 1.267 + 1.268 + // If buffer has overflowed, double its size and carry on. 1.269 + if (bPointer == buffer.length) { 1.270 + bufferSize *= 2; 1.271 + byte[] newBuffer = new byte[bufferSize]; 1.272 + 1.273 + // Copy the contents of the old buffer into the new buffer. 1.274 + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); 1.275 + buffer = newBuffer; 1.276 + } 1.277 + } 1.278 + } finally { 1.279 + contentStream.close(); 1.280 + } 1.281 + 1.282 + // Having downloaded the image, decode it. 1.283 + return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1); 1.284 + } 1.285 + 1.286 + @Override 1.287 + protected Bitmap doInBackground(Void... unused) { 1.288 + if (isCancelled()) { 1.289 + return null; 1.290 + } 1.291 + 1.292 + String storedFaviconUrl; 1.293 + boolean isUsingDefaultURL = false; 1.294 + 1.295 + // Handle the case of malformed favicon URL. 1.296 + // If favicon is empty, fall back to the stored one. 1.297 + if (TextUtils.isEmpty(faviconURL)) { 1.298 + // Try to get the favicon URL from the memory cache. 1.299 + storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(pageUrl); 1.300 + 1.301 + // If that failed, try to get the URL from the database. 1.302 + if (storedFaviconUrl == null) { 1.303 + storedFaviconUrl = Favicons.getFaviconURLForPageURL(pageUrl); 1.304 + if (storedFaviconUrl != null) { 1.305 + // If that succeeded, cache the URL loaded from the database in memory. 1.306 + Favicons.putFaviconURLForPageURLInCache(pageUrl, storedFaviconUrl); 1.307 + } 1.308 + } 1.309 + 1.310 + // If we found a faviconURL - use it. 1.311 + if (storedFaviconUrl != null) { 1.312 + faviconURL = storedFaviconUrl; 1.313 + } else { 1.314 + // If we don't have a stored one, fall back to the default. 1.315 + faviconURL = Favicons.guessDefaultFaviconURL(pageUrl); 1.316 + 1.317 + if (TextUtils.isEmpty(faviconURL)) { 1.318 + return null; 1.319 + } 1.320 + isUsingDefaultURL = true; 1.321 + } 1.322 + } 1.323 + 1.324 + // Check if favicon has failed - if so, give up. We need this check because, sometimes, we 1.325 + // didn't know the real Favicon URL until we asked the database. 1.326 + if (Favicons.isFailedFavicon(faviconURL)) { 1.327 + return null; 1.328 + } 1.329 + 1.330 + if (isCancelled()) { 1.331 + return null; 1.332 + } 1.333 + 1.334 + Bitmap image; 1.335 + // Determine if there is already an ongoing task to fetch the Favicon we desire. 1.336 + // If there is, just join the queue and wait for it to finish. If not, we carry on. 1.337 + synchronized(loadsInFlight) { 1.338 + // Another load of the current Favicon is already underway 1.339 + LoadFaviconTask existingTask = loadsInFlight.get(faviconURL); 1.340 + if (existingTask != null && !existingTask.isCancelled()) { 1.341 + existingTask.chainTasks(this); 1.342 + isChaining = true; 1.343 + 1.344 + // If we are chaining, we want to keep the first task started to do this job as the one 1.345 + // in the hashmap so subsequent tasks will add themselves to its chaining list. 1.346 + return null; 1.347 + } 1.348 + 1.349 + // We do not want to update the hashmap if the task has chained - other tasks need to 1.350 + // chain onto the same parent task. 1.351 + loadsInFlight.put(faviconURL, this); 1.352 + } 1.353 + 1.354 + if (isCancelled()) { 1.355 + return null; 1.356 + } 1.357 + 1.358 + // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null. 1.359 + LoadFaviconResult loadedBitmaps = loadFaviconFromDb(); 1.360 + if (loadedBitmaps != null) { 1.361 + return pushToCacheAndGetResult(loadedBitmaps); 1.362 + } 1.363 + 1.364 + if (onlyFromLocal || isCancelled()) { 1.365 + return null; 1.366 + } 1.367 + 1.368 + // Let's see if it's in a JAR. 1.369 + image = fetchJARFavicon(faviconURL); 1.370 + if (imageIsValid(image)) { 1.371 + // We don't want to put this into the DB. 1.372 + Favicons.putFaviconInMemCache(faviconURL, image); 1.373 + return image; 1.374 + } 1.375 + 1.376 + try { 1.377 + loadedBitmaps = downloadFavicon(new URI(faviconURL)); 1.378 + } catch (URISyntaxException e) { 1.379 + Log.e(LOGTAG, "The provided favicon URL is not valid"); 1.380 + return null; 1.381 + } catch (Exception e) { 1.382 + Log.e(LOGTAG, "Couldn't download favicon.", e); 1.383 + } 1.384 + 1.385 + if (loadedBitmaps != null) { 1.386 + // Fetching bytes to store can fail. saveFaviconToDb will 1.387 + // do the right thing, but we still choose to cache the 1.388 + // downloaded icon in memory. 1.389 + saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); 1.390 + return pushToCacheAndGetResult(loadedBitmaps); 1.391 + } 1.392 + 1.393 + if (isUsingDefaultURL) { 1.394 + Favicons.putFaviconInFailedCache(faviconURL); 1.395 + return null; 1.396 + } 1.397 + 1.398 + if (isCancelled()) { 1.399 + return null; 1.400 + } 1.401 + 1.402 + // If we're not already trying the default URL, try it now. 1.403 + final String guessed = Favicons.guessDefaultFaviconURL(pageUrl); 1.404 + if (guessed == null) { 1.405 + Favicons.putFaviconInFailedCache(faviconURL); 1.406 + return null; 1.407 + } 1.408 + 1.409 + image = fetchJARFavicon(guessed); 1.410 + if (imageIsValid(image)) { 1.411 + // We don't want to put this into the DB. 1.412 + Favicons.putFaviconInMemCache(faviconURL, image); 1.413 + return image; 1.414 + } 1.415 + 1.416 + try { 1.417 + loadedBitmaps = downloadFavicon(new URI(guessed)); 1.418 + } catch (Exception e) { 1.419 + // Not interesting. It was an educated guess, anyway. 1.420 + return null; 1.421 + } 1.422 + 1.423 + if (loadedBitmaps != null) { 1.424 + saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); 1.425 + return pushToCacheAndGetResult(loadedBitmaps); 1.426 + } 1.427 + 1.428 + return null; 1.429 + } 1.430 + 1.431 + /** 1.432 + * Helper method to put the result of a favicon load into the memory cache and then query the 1.433 + * cache for the particular bitmap we want for this request. 1.434 + * This call is certain to succeed, provided there was enough memory to decode this favicon. 1.435 + * 1.436 + * @param loadedBitmaps LoadFaviconResult to store. 1.437 + * @return The optimal favicon available to satisfy this LoadFaviconTask's request, or null if 1.438 + * we are under extreme memory pressure and find ourselves dropping the cache immediately. 1.439 + */ 1.440 + private Bitmap pushToCacheAndGetResult(LoadFaviconResult loadedBitmaps) { 1.441 + Favicons.putFaviconsInMemCache(faviconURL, loadedBitmaps.getBitmaps()); 1.442 + Bitmap result = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth); 1.443 + return result; 1.444 + } 1.445 + 1.446 + private static boolean imageIsValid(final Bitmap image) { 1.447 + return image != null && 1.448 + image.getWidth() > 0 && 1.449 + image.getHeight() > 0; 1.450 + } 1.451 + 1.452 + @Override 1.453 + protected void onPostExecute(Bitmap image) { 1.454 + if (isChaining) { 1.455 + return; 1.456 + } 1.457 + 1.458 + // Process the result, scale for the listener, etc. 1.459 + processResult(image); 1.460 + 1.461 + synchronized (loadsInFlight) { 1.462 + // Prevent any other tasks from chaining on this one. 1.463 + loadsInFlight.remove(faviconURL); 1.464 + } 1.465 + 1.466 + // Since any update to chainees is done while holding the loadsInFlight lock, once we reach 1.467 + // this point no further updates to that list can possibly take place (As far as other tasks 1.468 + // are concerned, there is no longer a task to chain from. The above block will have waited 1.469 + // for any tasks that were adding themselves to the list before reaching this point.) 1.470 + 1.471 + // As such, I believe we're safe to do the following without holding the lock. 1.472 + // This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely 1.473 + // actually happens outside of the strange situations unit tests create. 1.474 + 1.475 + // Share the result with all chained tasks. 1.476 + if (chainees != null) { 1.477 + for (LoadFaviconTask t : chainees) { 1.478 + // In the case that we just decoded multiple favicons, either we're passing the right 1.479 + // image now, or the call into the cache in processResult will fetch the right one. 1.480 + t.processResult(image); 1.481 + } 1.482 + } 1.483 + } 1.484 + 1.485 + private void processResult(Bitmap image) { 1.486 + Favicons.removeLoadTask(id); 1.487 + Bitmap scaled = image; 1.488 + 1.489 + // Notify listeners, scaling if required. 1.490 + if (targetWidth != -1 && image != null && image.getWidth() != targetWidth) { 1.491 + scaled = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth); 1.492 + } 1.493 + 1.494 + Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener); 1.495 + } 1.496 + 1.497 + @Override 1.498 + protected void onCancelled() { 1.499 + Favicons.removeLoadTask(id); 1.500 + 1.501 + synchronized(loadsInFlight) { 1.502 + // Only remove from the hashmap if the task there is the one that's being canceled. 1.503 + // Cancellation of a task that would have chained is not interesting to the hashmap. 1.504 + final LoadFaviconTask primary = loadsInFlight.get(faviconURL); 1.505 + if (primary == this) { 1.506 + loadsInFlight.remove(faviconURL); 1.507 + return; 1.508 + } 1.509 + if (primary == null) { 1.510 + // This shouldn't happen. 1.511 + return; 1.512 + } 1.513 + if (primary.chainees != null) { 1.514 + primary.chainees.remove(this); 1.515 + } 1.516 + } 1.517 + 1.518 + // Note that we don't call the listener callback if the 1.519 + // favicon load is cancelled. 1.520 + } 1.521 + 1.522 + /** 1.523 + * When the result of this job is ready, also notify the chainee of the result. 1.524 + * Used for aggregating concurrent requests for the same Favicon into a single actual request. 1.525 + * (Don't want to download a hundred instances of Google's Favicon at once, for example). 1.526 + * The loadsInFlight lock must be held when calling this function. 1.527 + * 1.528 + * @param aChainee LoadFaviconTask 1.529 + */ 1.530 + private void chainTasks(LoadFaviconTask aChainee) { 1.531 + if (chainees == null) { 1.532 + chainees = new LinkedList<LoadFaviconTask>(); 1.533 + } 1.534 + 1.535 + chainees.add(aChainee); 1.536 + } 1.537 + 1.538 + int getId() { 1.539 + return id; 1.540 + } 1.541 + 1.542 + static void closeHTTPClient() { 1.543 + // This work must be done on a background thread because it shuts down 1.544 + // the connection pool, which typically involves closing a connection -- 1.545 + // which counts as network activity. 1.546 + if (ThreadUtils.isOnBackgroundThread()) { 1.547 + if (httpClient != null) { 1.548 + httpClient.close(); 1.549 + } 1.550 + return; 1.551 + } 1.552 + 1.553 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.554 + @Override 1.555 + public void run() { 1.556 + LoadFaviconTask.closeHTTPClient(); 1.557 + } 1.558 + }); 1.559 + } 1.560 +}