mobile/android/base/favicons/LoadFaviconTask.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.favicons;
     8 import android.content.ContentResolver;
     9 import android.graphics.Bitmap;
    10 import android.net.http.AndroidHttpClient;
    11 import android.os.Handler;
    12 import android.text.TextUtils;
    13 import android.util.Log;
    14 import org.apache.http.Header;
    15 import org.apache.http.HttpEntity;
    16 import org.apache.http.HttpResponse;
    17 import org.apache.http.client.methods.HttpGet;
    18 import org.mozilla.gecko.GeckoAppShell;
    19 import org.mozilla.gecko.db.BrowserDB;
    20 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
    21 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
    22 import org.mozilla.gecko.util.GeckoJarReader;
    23 import org.mozilla.gecko.util.ThreadUtils;
    24 import org.mozilla.gecko.util.UiAsyncTask;
    25 import static org.mozilla.gecko.favicons.Favicons.context;
    27 import java.io.IOException;
    28 import java.io.InputStream;
    29 import java.net.URI;
    30 import java.net.URISyntaxException;
    31 import java.util.HashMap;
    32 import java.util.HashSet;
    33 import java.util.LinkedList;
    34 import java.util.concurrent.atomic.AtomicInteger;
    36 /**
    37  * Class representing the asynchronous task to load a Favicon which is not currently in the in-memory
    38  * cache.
    39  * The implementation initially tries to get the Favicon from the database. Upon failure, the icon
    40  * is loaded from the internet.
    41  */
    42 public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
    43     private static final String LOGTAG = "LoadFaviconTask";
    45     // Access to this map needs to be synchronized prevent multiple jobs loading the same favicon
    46     // from executing concurrently.
    47     private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<String, LoadFaviconTask>();
    49     public static final int FLAG_PERSIST = 1;
    50     public static final int FLAG_SCALE = 2;
    51     private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
    52     // The default size of the buffer to use for downloading Favicons in the event no size is given
    53     // by the server.
    54     private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000;
    56     private static AtomicInteger nextFaviconLoadId = new AtomicInteger(0);
    57     private int id;
    58     private String pageUrl;
    59     private String faviconURL;
    60     private OnFaviconLoadedListener listener;
    61     private int flags;
    63     private final boolean onlyFromLocal;
    65     // Assuming square favicons, judging by width only is acceptable.
    66     protected int targetWidth;
    67     private LinkedList<LoadFaviconTask> chainees;
    68     private boolean isChaining;
    70     static AndroidHttpClient httpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString());
    72     public LoadFaviconTask(Handler backgroundThreadHandler,
    73                            String pageUrl, String faviconUrl, int flags,
    74                            OnFaviconLoadedListener listener) {
    75         this(backgroundThreadHandler, pageUrl, faviconUrl, flags, listener, -1, false);
    76     }
    77     public LoadFaviconTask(Handler backgroundThreadHandler,
    78                            String pageUrl, String faviconUrl, int flags,
    79                            OnFaviconLoadedListener listener, int targetWidth, boolean onlyFromLocal) {
    80         super(backgroundThreadHandler);
    82         id = nextFaviconLoadId.incrementAndGet();
    84         this.pageUrl = pageUrl;
    85         this.faviconURL = faviconUrl;
    86         this.listener = listener;
    87         this.flags = flags;
    88         this.targetWidth = targetWidth;
    89         this.onlyFromLocal = onlyFromLocal;
    90     }
    92     // Runs in background thread
    93     private LoadFaviconResult loadFaviconFromDb() {
    94         ContentResolver resolver = context.getContentResolver();
    95         return BrowserDB.getFaviconForFaviconUrl(resolver, faviconURL);
    96     }
    98     // Runs in background thread
    99     private void saveFaviconToDb(final byte[] encodedFavicon) {
   100         if (encodedFavicon == null) {
   101             return;
   102         }
   104         if ((flags & FLAG_PERSIST) == 0) {
   105             return;
   106         }
   108         ContentResolver resolver = context.getContentResolver();
   109         BrowserDB.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL);
   110     }
   112     /**
   113      * Helper method for trying the download request to grab a Favicon.
   114      * @param faviconURI URL of Favicon to try and download
   115      * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise.
   116      */
   117     private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException {
   118         HashSet<String> visitedLinkSet = new HashSet<String>();
   119         visitedLinkSet.add(faviconURI.toString());
   120         return tryDownloadRecurse(faviconURI, visitedLinkSet);
   121     }
   122     private HttpResponse tryDownloadRecurse(URI faviconURI, HashSet<String> visited) throws URISyntaxException, IOException {
   123         if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) {
   124             return null;
   125         }
   127         HttpGet request = new HttpGet(faviconURI);
   128         HttpResponse response = httpClient.execute(request);
   129         if (response == null) {
   130             return null;
   131         }
   133         if (response.getStatusLine() != null) {
   135             // Was the response a failure?
   136             int status = response.getStatusLine().getStatusCode();
   138             // Handle HTTP status codes requesting a redirect.
   139             if (status >= 300 && status < 400) {
   140                 Header header = response.getFirstHeader("Location");
   142                 // Handle mad webservers.
   143                 if (header == null) {
   144                     return null;
   145                 }
   147                 String newURI = header.getValue();
   148                 if (newURI == null || newURI.equals(faviconURI.toString())) {
   149                     return null;
   150                 }
   152                 if (visited.contains(newURI)) {
   153                     // Already been redirected here - abort.
   154                     return null;
   155                 }
   157                 visited.add(newURI);
   158                 return tryDownloadRecurse(new URI(newURI), visited);
   159             }
   161             if (status >= 400) {
   162                 return null;
   163             }
   164         }
   165         return response;
   166     }
   168     /**
   169      * Retrieve the specified favicon from the JAR, returning null if it's not
   170      * a JAR URI.
   171      */
   172     private static Bitmap fetchJARFavicon(String uri) {
   173         if (uri == null) {
   174             return null;
   175         }
   176         if (uri.startsWith("jar:jar:")) {
   177             Log.d(LOGTAG, "Fetching favicon from JAR.");
   178             try {
   179                 return GeckoJarReader.getBitmap(context.getResources(), uri);
   180             } catch (Exception e) {
   181                 // Just about anything could happen here.
   182                 Log.w(LOGTAG, "Error fetching favicon from JAR.", e);
   183                 return null;
   184             }
   185         }
   186         return null;
   187     }
   189     // Runs in background thread.
   190     // Does not attempt to fetch from JARs.
   191     private LoadFaviconResult downloadFavicon(URI targetFaviconURI) {
   192         if (targetFaviconURI == null) {
   193             return null;
   194         }
   196         // Only get favicons for HTTP/HTTPS.
   197         String scheme = targetFaviconURI.getScheme();
   198         if (!"http".equals(scheme) && !"https".equals(scheme)) {
   199             return null;
   200         }
   202         LoadFaviconResult result = null;
   204         try {
   205             result = downloadAndDecodeImage(targetFaviconURI);
   206         } catch (Exception e) {
   207             Log.e(LOGTAG, "Error reading favicon", e);
   208         }
   210         return result;
   211     }
   213     /**
   214      * Download the Favicon from the given URL and pass it to the decoder function.
   215      *
   216      * @param targetFaviconURL URL of the favicon to download.
   217      * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or
   218      *         null if no or corrupt data ware received.
   219      * @throws IOException If attempts to fully read the stream result in such an exception, such as
   220      *                     in the event of a transient connection failure.
   221      * @throws URISyntaxException If the underlying call to tryDownload retries and raises such an
   222      *                            exception trying a fallback URL.
   223      */
   224     private LoadFaviconResult downloadAndDecodeImage(URI targetFaviconURL) throws IOException, URISyntaxException {
   225         // Try the URL we were given.
   226         HttpResponse response = tryDownload(targetFaviconURL);
   227         if (response == null) {
   228             return null;
   229         }
   231         HttpEntity entity = response.getEntity();
   232         if (entity == null) {
   233             return null;
   234         }
   236         // This may not be provided, but if it is, it's useful.
   237         final long entityReportedLength = entity.getContentLength();
   238         int bufferSize;
   239         if (entityReportedLength > 0) {
   240             // The size was reported and sane, so let's use that.
   241             // Integer overflow should not be a problem for Favicon sizes...
   242             bufferSize = (int) entityReportedLength + 1;
   243         } else {
   244             // No declared size, so guess and reallocate later if it turns out to be too small.
   245             bufferSize = DEFAULT_FAVICON_BUFFER_SIZE;
   246         }
   248         // Allocate a buffer to hold the raw favicon data downloaded.
   249         byte[] buffer = new byte[bufferSize];
   251         // The offset of the start of the buffer's free space.
   252         int bPointer = 0;
   254         // The quantity of bytes the last call to read yielded.
   255         int lastRead = 0;
   256         InputStream contentStream = entity.getContent();
   257         try {
   258             // Fully read the entity into the buffer - decoding of streams is not supported
   259             // (and questionably pointful - what would one do with a half-decoded Favicon?)
   260             while (lastRead != -1) {
   261                 // Read as many bytes as are currently available into the buffer.
   262                 lastRead = contentStream.read(buffer, bPointer, buffer.length - bPointer);
   263                 bPointer += lastRead;
   265                 // If buffer has overflowed, double its size and carry on.
   266                 if (bPointer == buffer.length) {
   267                     bufferSize *= 2;
   268                     byte[] newBuffer = new byte[bufferSize];
   270                     // Copy the contents of the old buffer into the new buffer.
   271                     System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
   272                     buffer = newBuffer;
   273                 }
   274             }
   275         } finally {
   276             contentStream.close();
   277         }
   279         // Having downloaded the image, decode it.
   280         return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1);
   281     }
   283     @Override
   284     protected Bitmap doInBackground(Void... unused) {
   285         if (isCancelled()) {
   286             return null;
   287         }
   289         String storedFaviconUrl;
   290         boolean isUsingDefaultURL = false;
   292         // Handle the case of malformed favicon URL.
   293         // If favicon is empty, fall back to the stored one.
   294         if (TextUtils.isEmpty(faviconURL)) {
   295             // Try to get the favicon URL from the memory cache.
   296             storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(pageUrl);
   298             // If that failed, try to get the URL from the database.
   299             if (storedFaviconUrl == null) {
   300                 storedFaviconUrl = Favicons.getFaviconURLForPageURL(pageUrl);
   301                 if (storedFaviconUrl != null) {
   302                     // If that succeeded, cache the URL loaded from the database in memory.
   303                     Favicons.putFaviconURLForPageURLInCache(pageUrl, storedFaviconUrl);
   304                 }
   305             }
   307             // If we found a faviconURL - use it.
   308             if (storedFaviconUrl != null) {
   309                 faviconURL = storedFaviconUrl;
   310             } else {
   311                 // If we don't have a stored one, fall back to the default.
   312                 faviconURL = Favicons.guessDefaultFaviconURL(pageUrl);
   314                 if (TextUtils.isEmpty(faviconURL)) {
   315                     return null;
   316                 }
   317                 isUsingDefaultURL = true;
   318             }
   319         }
   321         // Check if favicon has failed - if so, give up. We need this check because, sometimes, we
   322         // didn't know the real Favicon URL until we asked the database.
   323         if (Favicons.isFailedFavicon(faviconURL)) {
   324             return null;
   325         }
   327         if (isCancelled()) {
   328             return null;
   329         }
   331         Bitmap image;
   332         // Determine if there is already an ongoing task to fetch the Favicon we desire.
   333         // If there is, just join the queue and wait for it to finish. If not, we carry on.
   334         synchronized(loadsInFlight) {
   335             // Another load of the current Favicon is already underway
   336             LoadFaviconTask existingTask = loadsInFlight.get(faviconURL);
   337             if (existingTask != null && !existingTask.isCancelled()) {
   338                 existingTask.chainTasks(this);
   339                 isChaining = true;
   341                 // If we are chaining, we want to keep the first task started to do this job as the one
   342                 // in the hashmap so subsequent tasks will add themselves to its chaining list.
   343                 return null;
   344             }
   346             // We do not want to update the hashmap if the task has chained - other tasks need to
   347             // chain onto the same parent task.
   348             loadsInFlight.put(faviconURL, this);
   349         }
   351         if (isCancelled()) {
   352             return null;
   353         }
   355         // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null.
   356         LoadFaviconResult loadedBitmaps = loadFaviconFromDb();
   357         if (loadedBitmaps != null) {
   358             return pushToCacheAndGetResult(loadedBitmaps);
   359         }
   361         if (onlyFromLocal || isCancelled()) {
   362             return null;
   363         }
   365         // Let's see if it's in a JAR.
   366         image = fetchJARFavicon(faviconURL);
   367         if (imageIsValid(image)) {
   368             // We don't want to put this into the DB.
   369             Favicons.putFaviconInMemCache(faviconURL, image);
   370             return image;
   371         }
   373         try {
   374             loadedBitmaps = downloadFavicon(new URI(faviconURL));
   375         } catch (URISyntaxException e) {
   376             Log.e(LOGTAG, "The provided favicon URL is not valid");
   377             return null;
   378         } catch (Exception e) {
   379             Log.e(LOGTAG, "Couldn't download favicon.", e);
   380         }
   382         if (loadedBitmaps != null) {
   383             // Fetching bytes to store can fail. saveFaviconToDb will
   384             // do the right thing, but we still choose to cache the
   385             // downloaded icon in memory.
   386             saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
   387             return pushToCacheAndGetResult(loadedBitmaps);
   388         }
   390         if (isUsingDefaultURL) {
   391             Favicons.putFaviconInFailedCache(faviconURL);
   392             return null;
   393         }
   395         if (isCancelled()) {
   396             return null;
   397         }
   399         // If we're not already trying the default URL, try it now.
   400         final String guessed = Favicons.guessDefaultFaviconURL(pageUrl);
   401         if (guessed == null) {
   402             Favicons.putFaviconInFailedCache(faviconURL);
   403             return null;
   404         }
   406         image = fetchJARFavicon(guessed);
   407         if (imageIsValid(image)) {
   408             // We don't want to put this into the DB.
   409             Favicons.putFaviconInMemCache(faviconURL, image);
   410             return image;
   411         }
   413         try {
   414             loadedBitmaps = downloadFavicon(new URI(guessed));
   415         } catch (Exception e) {
   416             // Not interesting. It was an educated guess, anyway.
   417             return null;
   418         }
   420         if (loadedBitmaps != null) {
   421             saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
   422             return pushToCacheAndGetResult(loadedBitmaps);
   423         }
   425         return null;
   426     }
   428     /**
   429      * Helper method to put the result of a favicon load into the memory cache and then query the
   430      * cache for the particular bitmap we want for this request.
   431      * This call is certain to succeed, provided there was enough memory to decode this favicon.
   432      *
   433      * @param loadedBitmaps LoadFaviconResult to store.
   434      * @return The optimal favicon available to satisfy this LoadFaviconTask's request, or null if
   435      *         we are under extreme memory pressure and find ourselves dropping the cache immediately.
   436      */
   437     private Bitmap pushToCacheAndGetResult(LoadFaviconResult loadedBitmaps) {
   438         Favicons.putFaviconsInMemCache(faviconURL, loadedBitmaps.getBitmaps());
   439         Bitmap result = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth);
   440         return result;
   441     }
   443     private static boolean imageIsValid(final Bitmap image) {
   444         return image != null &&
   445                image.getWidth() > 0 &&
   446                image.getHeight() > 0;
   447     }
   449     @Override
   450     protected void onPostExecute(Bitmap image) {
   451         if (isChaining) {
   452             return;
   453         }
   455         // Process the result, scale for the listener, etc.
   456         processResult(image);
   458         synchronized (loadsInFlight) {
   459             // Prevent any other tasks from chaining on this one.
   460             loadsInFlight.remove(faviconURL);
   461         }
   463         // Since any update to chainees is done while holding the loadsInFlight lock, once we reach
   464         // this point no further updates to that list can possibly take place (As far as other tasks
   465         // are concerned, there is no longer a task to chain from. The above block will have waited
   466         // for any tasks that were adding themselves to the list before reaching this point.)
   468         // As such, I believe we're safe to do the following without holding the lock.
   469         // This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely
   470         // actually happens outside of the strange situations unit tests create.
   472         // Share the result with all chained tasks.
   473         if (chainees != null) {
   474             for (LoadFaviconTask t : chainees) {
   475                 // In the case that we just decoded multiple favicons, either we're passing the right
   476                 // image now, or the call into the cache in processResult will fetch the right one.
   477                 t.processResult(image);
   478             }
   479         }
   480     }
   482     private void processResult(Bitmap image) {
   483         Favicons.removeLoadTask(id);
   484         Bitmap scaled = image;
   486         // Notify listeners, scaling if required.
   487         if (targetWidth != -1 && image != null &&  image.getWidth() != targetWidth) {
   488             scaled = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth);
   489         }
   491         Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener);
   492     }
   494     @Override
   495     protected void onCancelled() {
   496         Favicons.removeLoadTask(id);
   498         synchronized(loadsInFlight) {
   499             // Only remove from the hashmap if the task there is the one that's being canceled.
   500             // Cancellation of a task that would have chained is not interesting to the hashmap.
   501             final LoadFaviconTask primary = loadsInFlight.get(faviconURL);
   502             if (primary == this) {
   503                 loadsInFlight.remove(faviconURL);
   504                 return;
   505             }
   506             if (primary == null) {
   507                 // This shouldn't happen.
   508                 return;
   509             }
   510             if (primary.chainees != null) {
   511               primary.chainees.remove(this);
   512             }
   513         }
   515         // Note that we don't call the listener callback if the
   516         // favicon load is cancelled.
   517     }
   519     /**
   520      * When the result of this job is ready, also notify the chainee of the result.
   521      * Used for aggregating concurrent requests for the same Favicon into a single actual request.
   522      * (Don't want to download a hundred instances of Google's Favicon at once, for example).
   523      * The loadsInFlight lock must be held when calling this function.
   524      *
   525      * @param aChainee LoadFaviconTask
   526      */
   527     private void chainTasks(LoadFaviconTask aChainee) {
   528         if (chainees == null) {
   529             chainees = new LinkedList<LoadFaviconTask>();
   530         }
   532         chainees.add(aChainee);
   533     }
   535     int getId() {
   536         return id;
   537     }
   539     static void closeHTTPClient() {
   540         // This work must be done on a background thread because it shuts down
   541         // the connection pool, which typically involves closing a connection --
   542         // which counts as network activity.
   543         if (ThreadUtils.isOnBackgroundThread()) {
   544             if (httpClient != null) {
   545                 httpClient.close();
   546             }
   547             return;
   548         }
   550         ThreadUtils.postToBackgroundThread(new Runnable() {
   551             @Override
   552             public void run() {
   553                 LoadFaviconTask.closeHTTPClient();
   554             }
   555         });
   556     }
   557 }

mercurial