Bug 748100: Augment Favicon decoder with the ability to decode ICO files. r=rnewman
authorChris Kitching <chriskitching@linux.com>
Sat, 18 Jan 2014 03:24:28 +0000
changeset 164171 1c402a47da513701a2959972bf8852eab8809b11
parent 164170 b551eec92a42773c315edfc178e39037d9ea7fda
child 164172 3da20e4c20ec517d07a004fa23879b2726e1757a
push id26027
push userttaubert@mozilla.com
push dateSun, 19 Jan 2014 09:43:10 +0000
treeherdermozilla-central@f038d28c0349 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs748100
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 748100: Augment Favicon decoder with the ability to decode ICO files. r=rnewman
mobile/android/base/BrowserApp.java
mobile/android/base/db/BrowserDB.java
mobile/android/base/db/LocalBrowserDB.java
mobile/android/base/favicons/Favicons.java
mobile/android/base/favicons/LoadFaviconTask.java
mobile/android/base/favicons/cache/FaviconsForURL.java
mobile/android/base/favicons/decoders/FaviconDecoder.java
mobile/android/base/favicons/decoders/ICODecoder.java
mobile/android/base/favicons/decoders/IconDirectoryEntry.java
mobile/android/base/favicons/decoders/LoadFaviconResult.java
mobile/android/base/gfx/BitmapUtils.java
mobile/android/base/moz.build
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko;
 
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
+import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.GeckoLayerClient;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerMarginsAnimator;
 import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.gecko.health.BrowserHealthReporter;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomePager;
@@ -590,16 +591,19 @@ abstract public class BrowserApp extends
 
             @Override
             public boolean isObserver() {
                 // We want to be notified of changes to be able to switch mode
                 // without restarting.
                 return true;
             }
         });
+
+        // Set the maximum bits-per-pixel the favicon system cares about.
+        IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
     }
 
     @Override
     public void onBackPressed() {
         if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
             super.onBackPressed();
             return;
         }
@@ -783,20 +787,20 @@ abstract public class BrowserApp extends
             final String url = tab.getURL();
             final String title = tab.getDisplayTitle();
             if (url == null || title == null) {
                 return true;
             }
 
             final OnFaviconLoadedListener listener = new GeckoAppShell.CreateShortcutFaviconLoadedListener(url, title);
             Favicons.getSizedFavicon(url,
-                                     tab.getFaviconURL(),
-                                     Integer.MAX_VALUE,
-                                     LoadFaviconTask.FLAG_PERSIST,
-                                     listener);
+                    tab.getFaviconURL(),
+                    Integer.MAX_VALUE,
+                    LoadFaviconTask.FLAG_PERSIST,
+                    listener);
             return true;
         }
 
         return false;
     }
 
     @Override
     public void setAccessibilityEnabled(boolean enabled) {
--- a/mobile/android/base/db/BrowserDB.java
+++ b/mobile/android/base/db/BrowserDB.java
@@ -2,16 +2,17 @@
  * 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.db;
 
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
+import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 
 import android.content.ContentResolver;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.CursorWrapper;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
@@ -89,21 +90,21 @@ public class BrowserDB {
 
         @RobocopTarget
         public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
 
         public void addReadingListItem(ContentResolver cr, String title, String uri);
 
         public void removeReadingListItemWithURL(ContentResolver cr, String uri);
 
-        public Bitmap getFaviconForUrl(ContentResolver cr, String uri);
+        public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String uri);
 
         public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url);
 
-        public void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri);
+        public void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri);
 
         public void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail);
 
         @RobocopTarget
         public byte[] getThumbnailForUrl(ContentResolver cr, String uri);
 
         public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls);
 
@@ -252,26 +253,26 @@ public class BrowserDB {
     public static void addReadingListItem(ContentResolver cr, String title, String uri) {
         sDb.addReadingListItem(cr, title, uri);
     }
 
     public static void removeReadingListItemWithURL(ContentResolver cr, String uri) {
         sDb.removeReadingListItemWithURL(cr, uri);
     }
 
-    public static Bitmap getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
+    public static LoadFaviconResult getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
         return sDb.getFaviconForUrl(cr, faviconURL);
     }
 
     public static String getFaviconUrlForHistoryUrl(ContentResolver cr, String url) {
         return sDb.getFaviconUrlForHistoryUrl(cr, url);
     }
 
-    public static void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri) {
-        sDb.updateFaviconForUrl(cr, pageUri, favicon, faviconUri);
+    public static void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri) {
+        sDb.updateFaviconForUrl(cr, pageUri, encodedFavicon, faviconUri);
     }
 
     public static void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail) {
         sDb.updateThumbnailForUrl(cr, uri, thumbnail);
     }
 
     @RobocopTarget
     public static byte[] getThumbnailForUrl(ContentResolver cr, String uri) {
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -10,16 +10,18 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
+import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
+import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.gfx.BitmapUtils;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.CursorWrapper;
@@ -703,17 +705,17 @@ public class LocalBrowserDB implements B
     /**
      * Get the favicon from the database, if any, associated with the given favicon URL. (That is,
      * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
      * @param cr The ContentResolver to use.
      * @param faviconURL The URL of the favicon to fetch from the database.
      * @return The decoded Bitmap from the database, if any. null if none is stored.
      */
     @Override
-    public Bitmap getFaviconForUrl(ContentResolver cr, String faviconURL) {
+    public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) {
         Cursor c = null;
         byte[] b = null;
 
         try {
             c = cr.query(mFaviconsUriWithProfile,
                          new String[] { Favicons.DATA },
                          Favicons.URL + " = ?",
                          new String[] { faviconURL },
@@ -730,17 +732,17 @@ public class LocalBrowserDB implements B
                 c.close();
             }
         }
 
         if (b == null) {
             return null;
         }
 
-        return BitmapUtils.decodeByteArray(b);
+        return FaviconDecoder.decodeFavicon(b);
     }
 
     @Override
     public String getFaviconUrlForHistoryUrl(ContentResolver cr, String uri) {
         Cursor c = null;
 
         try {
             c = cr.query(mHistoryUriWithProfile,
@@ -756,29 +758,21 @@ public class LocalBrowserDB implements B
                 c.close();
         }
 
         return null;
     }
 
     @Override
     public void updateFaviconForUrl(ContentResolver cr, String pageUri,
-            Bitmap favicon, String faviconUri) {
+            byte[] encodedFavicon, String faviconUri) {
         ContentValues values = new ContentValues();
         values.put(Favicons.URL, faviconUri);
         values.put(Favicons.PAGE_URL, pageUri);
-
-        byte[] data = null;
-        ByteArrayOutputStream stream = new ByteArrayOutputStream();
-        if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
-            data = stream.toByteArray();
-        } else {
-            Log.w(LOGTAG, "Favicon compression failed.");
-        }
-        values.put(Favicons.DATA, data);
+        values.put(Favicons.DATA, encodedFavicon);
 
         // Update or insert
         Uri faviconsUri = getAllFaviconsUri().buildUpon().
                 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
 
         cr.update(faviconsUri,
                   values,
                   Favicons.URL + " = ?",
--- a/mobile/android/base/favicons/Favicons.java
+++ b/mobile/android/base/favicons/Favicons.java
@@ -54,16 +54,19 @@ public class Favicons {
     protected static Context sContext;
 
     // The default Favicon to show if no other can be found.
     public static Bitmap sDefaultFavicon;
 
     // The density-adjusted default Favicon dimensions.
     public static int sDefaultFaviconSize;
 
+    // The density-adjusted maximum Favicon dimensions.
+    public static int sLargestFaviconSize;
+
     private static final Map<Integer, LoadFaviconTask> sLoadTasks = Collections.synchronizedMap(new HashMap<Integer, LoadFaviconTask>());
 
     // Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when
     // doing so is not necessary.
     private static final NonEvictingLruCache<String, String> sPageURLMappings = new NonEvictingLruCache<String, String>(NUM_PAGE_URL_MAPPINGS_TO_STORE);
 
     public static String getFaviconURLForPageURLFromCache(String pageURL) {
         return sPageURLMappings.get(pageURL);
@@ -285,20 +288,32 @@ public class Favicons {
 
         return taskId;
     }
 
     public static void putFaviconInMemCache(String pageUrl, Bitmap image) {
         sFaviconsCache.putSingleFavicon(pageUrl, image);
     }
 
+    /**
+     * Adds the bitmaps given by the specified iterator to the cache associated with the url given.
+     * Future requests for images will be able to select the least larger image than the target
+     * size from this new set of images.
+     *
+     * @param pageUrl The URL to associate the new favicons with.
+     * @param images An iterator over the new favicons to put in the cache.
+     */
     public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images, boolean permanently) {
         sFaviconsCache.putFavicons(pageUrl, images, permanently);
     }
 
+    public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) {
+        putFaviconsInMemCache(pageUrl, images, false);
+    }
+
     public static void clearMemCache() {
         sFaviconsCache.evictAll();
         sPageURLMappings.evictAll();
     }
 
     public static void putFaviconInFailedCache(String faviconURL) {
         sFaviconsCache.putFailed(faviconURL);
     }
@@ -361,17 +376,21 @@ public class Favicons {
 
         // Decode the default Favicon ready for use.
         sDefaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon);
         if (sDefaultFavicon == null) {
             throw new Exception("Null default favicon was returned from the resources system!");
         }
 
         sDefaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg);
-        sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, res.getDimensionPixelSize(R.dimen.favicon_largest_interesting_size));
+
+        // Screen-density-adjusted upper limit on favicon size. Favicons larger than this are
+        // downscaled to this size or discarded.
+        sLargestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
+        sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, sLargestFaviconSize);
 
         // Initialize page mappings for each of our special pages.
         for (String url : AboutPages.getDefaultIconPages()) {
             sPageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL);
         }
 
         // Load and cache the built-in favicon in each of its sizes.
         // TODO: don't open the zip twice!
--- a/mobile/android/base/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/favicons/LoadFaviconTask.java
@@ -10,34 +10,33 @@ 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.apache.http.entity.BufferedHttpEntity;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.gfx.BitmapUtils;
+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.sContext;
 
 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;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * 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<Void, Void, Bitmap> {
@@ -45,16 +44,19 @@ public class LoadFaviconTask extends UiA
 
     // Access to this map needs to be synchronized prevent multiple jobs loading the same favicon
     // from executing concurrently.
     private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<String, LoadFaviconTask>();
 
     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 mNextFaviconLoadId = new AtomicInteger(0);
     private int mId;
     private String mPageUrl;
     private String mFaviconUrl;
     private OnFaviconLoadedListener mListener;
     private int mFlags;
 
@@ -83,29 +85,29 @@ public class LoadFaviconTask extends UiA
         mFaviconUrl = faviconUrl;
         mListener = aListener;
         mFlags = flags;
         mTargetWidth = targetSize;
         mOnlyFromLocal = fromLocal;
     }
 
     // Runs in background thread
-    private Bitmap loadFaviconFromDB() {
+    private LoadFaviconResult loadFaviconFromDb() {
         ContentResolver resolver = sContext.getContentResolver();
         return BrowserDB.getFaviconForFaviconUrl(resolver, mFaviconUrl);
     }
 
     // Runs in background thread
-    private void saveFaviconToDb(final Bitmap favicon) {
+    private void saveFaviconToDb(final byte[] encodedFavicon) {
         if ((mFlags & FLAG_PERSIST) == 0) {
             return;
         }
 
         ContentResolver resolver = sContext.getContentResolver();
-        BrowserDB.updateFaviconForUrl(resolver, mPageUrl, favicon, mFaviconUrl);
+        BrowserDB.updateFaviconForUrl(resolver, mPageUrl, encodedFavicon, mFaviconUrl);
     }
 
     /**
      * 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 {
@@ -177,59 +179,106 @@ public class LoadFaviconTask extends UiA
                 return null;
             }
         }
         return null;
     }
 
     // Runs in background thread.
     // Does not attempt to fetch from JARs.
-    private Bitmap downloadFavicon(URI targetFaviconURI) {
+    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;
         }
 
-        Bitmap image = null;
-
-        // skia decoder sometimes returns null; workaround is to use BufferedHttpEntity
-        // http://groups.google.com/group/android-developers/browse_thread/thread/171b8bf35dbbed96/c3ec5f45436ceec8?lnk=raot
-        try {
-            // Try the URL we were given.
-            HttpResponse response = tryDownload(targetFaviconURI);
-            if (response == null) {
-                return null;
-            }
+        LoadFaviconResult result = null;
 
-            HttpEntity entity = response.getEntity();
-            if (entity == null) {
-                return null;
-            }
-
-            BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity);
-            InputStream contentStream = null;
-            try {
-                contentStream = bufferedEntity.getContent();
-                image = BitmapUtils.decodeStream(contentStream);
-                contentStream.close();
-            } finally {
-                if (contentStream != null) {
-                    contentStream.close();
-                }
-            }
+        try {
+            result = downloadAndDecodeImage(targetFaviconURI);
         } catch (Exception e) {
             Log.e(LOGTAG, "Error reading favicon", e);
         }
 
-        return image;
+        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;
         }
 
@@ -294,94 +343,113 @@ public class LoadFaviconTask extends UiA
             // chain onto the same parent task.
             loadsInFlight.put(mFaviconUrl, this);
         }
 
         if (isCancelled()) {
             return null;
         }
 
-        image = loadFaviconFromDB();
-        if (imageIsValid(image)) {
-            return image;
+        // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null.
+        LoadFaviconResult loadedBitmaps = loadFaviconFromDb();
+        if (loadedBitmaps != null) {
+            return pushToCacheAndGetResult(loadedBitmaps);
         }
 
         if (mOnlyFromLocal || isCancelled()) {
             return null;
         }
 
         // Let's see if it's in a JAR.
         image = fetchJARFavicon(mFaviconUrl);
-        if (image != null) {
+        if (imageIsValid(image)) {
             // We don't want to put this into the DB.
+            Favicons.putFaviconInMemCache(mFaviconUrl, image);
             return image;
         }
 
         try {
-            image = downloadFavicon(new URI(mFaviconUrl));
+            loadedBitmaps = downloadFavicon(new URI(mFaviconUrl));
         } 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 (imageIsValid(image)) {
-            saveFaviconToDb(image);
-            return image;
+        if (loadedBitmaps != null) {
+            saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
+            return pushToCacheAndGetResult(loadedBitmaps);
         }
 
         if (isUsingDefaultURL) {
             Favicons.putFaviconInFailedCache(mFaviconUrl);
             return null;
         }
 
+        if (isCancelled()) {
+            return null;
+        }
+
         // If we're not already trying the default URL, try it now.
         final String guessed = Favicons.guessDefaultFaviconURL(mPageUrl);
         if (guessed == null) {
             Favicons.putFaviconInFailedCache(mFaviconUrl);
             return null;
         }
 
         image = fetchJARFavicon(guessed);
         if (imageIsValid(image)) {
             // We don't want to put this into the DB.
+            Favicons.putFaviconInMemCache(mFaviconUrl, image);
             return image;
         }
 
         try {
-            image = downloadFavicon(new URI(guessed));
+            loadedBitmaps = downloadFavicon(new URI(guessed));
         } catch (Exception e) {
             // Not interesting. It was an educated guess, anyway.
             return null;
         }
 
-        if (imageIsValid(image)) {
-            saveFaviconToDb(image);
-            return image;
+        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(mFaviconUrl, loadedBitmaps.getBitmaps());
+        Bitmap result = Favicons.getSizedFaviconFromCache(mFaviconUrl, mTargetWidth);
+        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 (mIsChaining) {
             return;
         }
 
-        // Put what we got in the memcache.
-        Favicons.putFaviconInMemCache(mFaviconUrl, image);
-
         // Process the result, scale for the listener, etc.
         processResult(image);
 
         synchronized (loadsInFlight) {
             // Prevent any other tasks from chaining on this one.
             loadsInFlight.remove(mFaviconUrl);
         }
 
@@ -392,16 +460,18 @@ public class LoadFaviconTask extends UiA
 
         // 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 (mChainees != null) {
             for (LoadFaviconTask t : mChainees) {
+                // 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(mId);
 
--- a/mobile/android/base/favicons/cache/FaviconsForURL.java
+++ b/mobile/android/base/favicons/cache/FaviconsForURL.java
@@ -105,18 +105,21 @@ public class FaviconsForURL {
         final int numIcons = mFavicons.size();
 
         int searchIndex = fromIndex;
         while (searchIndex < numIcons) {
             FaviconCacheElement element = mFavicons.get(searchIndex);
 
             if (element.mIsPrimary) {
                 if (element.mInvalidated) {
-                    // TODO: Replace with `return null` when ICO decoder is introduced.
-                    break;
+                    // We return null here, despite the possible existence of other primaries,
+                    // because we know the most suitable primary for this request exists, but is
+                    // no longer in the cache. By returning null, we cause the caller to load the
+                    // missing primary from the database and call again.
+                    return null;
                 }
                 return element;
             }
             searchIndex++;
         }
 
         // No larger primary available. Let's look for smaller ones...
         searchIndex = fromIndex - 1;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/favicons/decoders/FaviconDecoder.java
@@ -0,0 +1,158 @@
+/* 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.decoders;
+
+import android.graphics.Bitmap;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.BitmapUtils;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Class providing static utility methods for decoding favicons.
+ */
+public class FaviconDecoder {
+    static enum ImageMagicNumbers {
+        // It is irritating that Java bytes are signed...
+        PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}),
+        GIF(new byte[] {0x47, 0x49, 0x46, 0x38}),
+        JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}),
+        BMP(new byte[] {0x42, 0x4d}),
+        WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a});
+
+        public byte[] value;
+
+        private ImageMagicNumbers(byte[] value) {
+            this.value = value;
+        }
+    }
+
+    /**
+     * Check for image format magic numbers of formats supported by Android.
+     * @param buffer Byte buffer to check for magic numbers
+     * @param offset Offset at which to look for magic numbers.
+     * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence
+     *         starting with the magic numbers thereof). false otherwise.
+     */
+    private static boolean isDecodableByAndroid(byte[] buffer, int offset) {
+        for (ImageMagicNumbers m : ImageMagicNumbers.values()) {
+            if (bufferStartsWith(buffer, m.value, offset)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Utility function to check for the existence of a test byte sequence at a given offset in a
+     * buffer.
+     *
+     * @param buffer Byte buffer to search.
+     * @param test Byte sequence to search for.
+     * @param bufferOffset Index in input buffer to expect test sequence.
+     * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false
+     *         otherwise.
+     */
+    static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) {
+        if (buffer.length < test.length) {
+            return false;
+        }
+
+        for (int i = 0; i < test.length; ++i) {
+            if (buffer[bufferOffset + i] != test[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Decode the favicon present in the region of the provided byte[] starting at offset and
+     * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the
+     * given range does not contain a bitmap we know how to decode.
+     *
+     * @param buffer Byte array containing the favicon to decode.
+     * @param offset The index of the first byte in the array of the region of interest.
+     * @param length The length of the region in the array to decode.
+     * @return The decoded version of the bitmap in the described region, or null if none can be
+     *         decoded.
+     */
+    public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) {
+        LoadFaviconResult result;
+        if (isDecodableByAndroid(buffer, offset)) {
+            result = new LoadFaviconResult();
+            result.mOffset = offset;
+            result.mLength = length;
+            result.mHasMultipleBitmaps = false;
+
+            // We assume here that decodeByteArray doesn't hold on to the entire supplied
+            // buffer -- worst case, each of our buffers will be twice the necessary size.
+            result.mBitmapsDecoded = new SingleBitmapIterator(BitmapUtils.decodeByteArray(buffer, offset, length));
+            result.mFaviconBytes = buffer;
+
+            return result;
+        }
+
+        // If it's not decodable by Android, it might be an ICO. Let's try.
+        ICODecoder decoder = new ICODecoder(buffer, offset, length);
+
+        result = decoder.decode();
+
+        if (result == null) {
+            return null;
+        }
+
+        return result;
+    }
+
+    public static LoadFaviconResult decodeFavicon(byte[] buffer) {
+        return decodeFavicon(buffer, 0, buffer.length);
+    }
+
+    /**
+     * Iterator to hold a single bitmap.
+     */
+    static class SingleBitmapIterator implements Iterator<Bitmap> {
+        private Bitmap mBitmap;
+
+        public SingleBitmapIterator(Bitmap b) {
+            mBitmap = b;
+        }
+
+        /**
+         * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure
+         * places where the runtime type of the Iterator under consideration is known and
+         * destruction of it is discouraged.
+         *
+         * @return The bitmap carried by this SingleBitmapIterator.
+         */
+        public Bitmap peek() {
+            return mBitmap;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return mBitmap != null;
+        }
+
+        @Override
+        public Bitmap next() {
+            if (mBitmap == null) {
+                throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
+            }
+
+            Bitmap ret = mBitmap;
+            mBitmap = null;
+            return ret;
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/favicons/decoders/ICODecoder.java
@@ -0,0 +1,380 @@
+/* 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.decoders;
+
+import android.graphics.Bitmap;
+import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.gfx.BitmapUtils;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Utility class for determining the region of a provided array which contains the largest bitmap,
+ * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning
+ * unwanted entries from ICO files, if desired.
+ *
+ * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format.
+ * A mixture of image types may not exist.
+ *
+ * The format consists of a header specifying the number, n,  of images, followed by the Icon Directory.
+ *
+ * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for
+ * the corresponding image, the dimensions, colour information, payload size, and location in the file.
+ *
+ * All numerical fields follow a little-endian byte ordering.
+ *
+ * Header format:
+ *
+ *  0               1               2               3
+ *  0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |  Reserved field. Must be zero |  Type (1 for ICO, 2 for CUR)  |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |         Image count (n)       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * The type field is expected to always be 1. CUR format images should not be used for Favicons.
+ *
+ *
+ * Icon Directory Entry format:
+ *
+ *  0               1               2               3
+ *  0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |  Image width  | Image height  | Palette size  | Reserved (0)  |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |       Colour plane count      |         Bits per pixel        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                   Size of image data, in bytes                |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |      Start of image data, as an offset from start of file     |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Image dimensions of zero are to be interpreted as image dimensions of 256.
+ *
+ * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero
+ * if the payload is a PNG or no palette is in use.
+ *
+ * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be
+ * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field.
+ * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.)
+ *
+ *
+ * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps.
+ *
+ * This class is not thread safe.
+ */
+public class ICODecoder implements Iterable<Bitmap> {
+    // The number of bytes that compacting will save for us to bother doing it.
+    public static final int COMPACT_THRESHOLD = 4000;
+
+    // Some geometry of an ICO file.
+    public static final int ICO_HEADER_LENGTH_BYTES = 6;
+    public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16;
+
+    // The buffer containing bytes to attempt to decode.
+    private byte[] mDecodand;
+
+    // The region of the decodand to decode.
+    private int mOffset;
+    private int mLen;
+
+    private IconDirectoryEntry[] mIconDirectory;
+    private boolean mIsValid;
+    private boolean mHasDecoded;
+
+    public ICODecoder(byte[] buffer, int offset, int len) {
+        mDecodand = buffer;
+        mOffset = offset;
+        mLen = len;
+    }
+
+    /**
+     * Decode the Icon Directory for this ICO and store the result in mIconDirectory.
+     *
+     * @return true if ICO decoding was considered to probably be a success, false if it certainly
+     *         was a failure.
+     */
+    private boolean decodeIconDirectoryAndPossiblyPrune() {
+        mHasDecoded = true;
+
+        // Fail if the end of the described range is out of bounds.
+        if (mOffset + mLen > mDecodand.length) {
+            return false;
+        }
+
+        // Fail if we don't have enough space for the header.
+        if (mLen < ICO_HEADER_LENGTH_BYTES) {
+            return false;
+        }
+
+        // Check that the reserved fields in the header are indeed zero, and that the type field
+        // specifies ICO. If not, we've probably been given something that isn't really an ICO.
+        if (mDecodand[mOffset] != 0 ||
+            mDecodand[mOffset + 1] != 0 ||
+            mDecodand[mOffset + 2] != 1 ||
+            mDecodand[mOffset + 3] != 0) {
+            return false;
+        }
+
+        // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java
+        // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned
+        // interpretation of the byte of interest, we do this.
+        int numEncodedImages = (mDecodand[mOffset + 4] & 0xFF) |
+                               (mDecodand[mOffset + 5] & 0xFF) << 8;
+
+
+        // Fail if there are no images or the field is corrupt.
+        if (numEncodedImages <= 0) {
+            return false;
+        }
+
+        final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+        // Fail if there is not enough space in the buffer for the stated number of icondir entries,
+        // let alone the data.
+        if (mLen < headerAndDirectorySize) {
+            return false;
+        }
+
+        // Put the pointer on the first byte of the first Icon Directory Entry.
+        int bufferIndex = mOffset + ICO_HEADER_LENGTH_BYTES;
+
+        // We now iterate over the Icon Directory, decoding each entry as we go. We also need to
+        // discard all entries except one >= the maximum interesting size.
+
+        // Size of the smallest image larger than the limit encountered.
+        int minimumMaximum = Integer.MAX_VALUE;
+
+        // Used to track the best entry for each size. The entries we want to keep.
+        HashMap<Integer, IconDirectoryEntry> preferenceMap = new HashMap<Integer, IconDirectoryEntry>();
+
+        for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) {
+            // Decode the Icon Directory Entry at this offset.
+            IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(mDecodand, mOffset, mLen, bufferIndex);
+            newEntry.mIndex = i;
+
+            if (newEntry.mIsErroneous) {
+                continue;
+            }
+
+            if (newEntry.mWidth > Favicons.sLargestFaviconSize) {
+                // If we already have a smaller image larger than the maximum size of interest, we
+                // don't care about the new one which is larger than the smallest image larger than
+                // the maximum size.
+                if (newEntry.mWidth >= minimumMaximum) {
+                    continue;
+                }
+
+                // Remove the previous minimum-maximum.
+                if (preferenceMap.containsKey(minimumMaximum)) {
+                    preferenceMap.remove(minimumMaximum);
+                }
+
+                minimumMaximum = newEntry.mWidth;
+            }
+
+            IconDirectoryEntry oldEntry = preferenceMap.get(newEntry.mWidth);
+            if (oldEntry == null) {
+                preferenceMap.put(newEntry.mWidth, newEntry);
+                continue;
+            }
+
+            if (oldEntry.compareTo(newEntry) < 0) {
+                preferenceMap.put(newEntry.mWidth, newEntry);
+            }
+        }
+
+        Collection<IconDirectoryEntry> entriesRetained = preferenceMap.values();
+
+        // Abort if no entries are desired (Perhaps all are corrupt?)
+        if (entriesRetained.isEmpty()) {
+            return false;
+        }
+
+        // Allocate space for the icon directory entries in the decoded directory.
+        mIconDirectory = new IconDirectoryEntry[entriesRetained.size()];
+
+        // The size of the data in the buffer that we find useful.
+        int retainedSpace = ICO_HEADER_LENGTH_BYTES;
+
+        int dirInd = 0;
+        for (IconDirectoryEntry e : entriesRetained) {
+            retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.mPayloadSize;
+            mIconDirectory[dirInd] = e;
+            dirInd++;
+        }
+
+        mIsValid = true;
+
+        // Set the number of images field in the buffer to reflect the number of retained entries.
+        mDecodand[mOffset + 4] = (byte) mIconDirectory.length;
+        mDecodand[mOffset + 5] = (byte) (mIconDirectory.length >>> 8);
+
+        if ((mLen - retainedSpace) > COMPACT_THRESHOLD) {
+            compactingCopy(retainedSpace);
+        }
+
+        return true;
+    }
+
+    /**
+     * Copy the buffer into a new array of exactly the required size, omitting any unwanted data.
+     */
+    private void compactingCopy(int spaceRetained) {
+        byte[] buf = new byte[spaceRetained];
+
+        // Copy the header.
+        System.arraycopy(mDecodand, mOffset, buf, 0, ICO_HEADER_LENGTH_BYTES);
+
+        int headerPtr = ICO_HEADER_LENGTH_BYTES;
+
+        int payloadPtr = ICO_HEADER_LENGTH_BYTES + (mIconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+        int ind = 0;
+        for (IconDirectoryEntry entry : mIconDirectory) {
+            // Copy this entry.
+            System.arraycopy(mDecodand, mOffset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+            // Copy its payload.
+            System.arraycopy(mDecodand, mOffset + entry.mPayloadOffset, buf, payloadPtr, entry.mPayloadSize);
+
+            // Update the offset field.
+            buf[headerPtr + 12] = (byte) payloadPtr;
+            buf[headerPtr + 13] = (byte) (payloadPtr >>> 8);
+            buf[headerPtr + 14] = (byte) (payloadPtr >>> 16);
+            buf[headerPtr + 15] = (byte) (payloadPtr >>> 24);
+
+            entry.mPayloadOffset = payloadPtr;
+            entry.mIndex = ind;
+
+            payloadPtr += entry.mPayloadSize;
+            headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES;
+            ind++;
+        }
+
+        mDecodand = buf;
+        mOffset = 0;
+        mLen = spaceRetained;
+    }
+
+    /**
+     * Decode and return the bitmap represented by the given index in the Icon Directory, if valid.
+     *
+     * @param index The index into the Icon Directory of the image of interest.
+     * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding
+     *         fails.
+     */
+    public Bitmap decodeBitmapAtIndex(int index) {
+        final IconDirectoryEntry iconDirEntry = mIconDirectory[index];
+
+        if (iconDirEntry.mPayloadIsPNG) {
+            // PNG payload. Simply extract it and decode it.
+            return BitmapUtils.decodeByteArray(mDecodand, mOffset + iconDirEntry.mPayloadOffset, iconDirEntry.mPayloadSize);
+        }
+
+        // The payload is a BMP, so we need to do some magic to get the decoder to do what we want.
+        // We construct an ICO containing just the image we want, and let Android do the rest.
+        byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.mPayloadSize];
+
+        // Set the type field in the ICO header.
+        decodeTarget[2] = 1;
+
+        // Set the num-images field in the header to 1.
+        decodeTarget[4] = 1;
+
+        // Copy the ICONDIRENTRY we need into the new buffer.
+        System.arraycopy(mDecodand, mOffset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+        // Copy the payload into the new buffer.
+        final int singlePayloadOffset =  ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES;
+        System.arraycopy(mDecodand, mOffset + iconDirEntry.mPayloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.mPayloadSize);
+
+        // Update the offset field of the ICONDIRENTRY to make the new ICO valid.
+        decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = (byte) singlePayloadOffset;
+        decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (byte) (singlePayloadOffset >>> 8);
+        decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (byte) (singlePayloadOffset >>> 16);
+        decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (byte) (singlePayloadOffset >>> 24);
+
+        // Decode the newly-constructed singleton-ICO.
+        return BitmapUtils.decodeByteArray(decodeTarget);
+    }
+
+    /**
+     * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid.
+     *
+     * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails.
+     */
+    @Override
+    public ICOIterator iterator() {
+        // If a previous call to decode concluded this ICO is invalid, abort.
+        if (mHasDecoded && !mIsValid) {
+            return null;
+        }
+
+        // If we've not been decoded before, but now fail to make any sense of the ICO, abort.
+        if (!mHasDecoded) {
+            if (!decodeIconDirectoryAndPossiblyPrune()) {
+                return null;
+            }
+        }
+
+        // If decoding was a success, return an iterator over the images in this ICO.
+        return new ICOIterator();
+    }
+
+    /**
+     * Decode this ICO and return the result as a LoadFaviconResult.
+     * @return A LoadFaviconResult representing the decoded ICO.
+     */
+    public LoadFaviconResult decode() {
+        // The call to iterator returns null if decoding fails.
+        Iterator<Bitmap> bitmaps = iterator();
+        if (bitmaps == null) {
+            return null;
+        }
+
+        LoadFaviconResult result = new LoadFaviconResult();
+
+        result.mBitmapsDecoded = bitmaps;
+        result.mFaviconBytes = mDecodand;
+        result.mOffset = mOffset;
+        result.mLength = mLen;
+        result.mHasMultipleBitmaps = mIconDirectory.length > 1;
+
+        return result;
+    }
+
+    /**
+     * Inner class to iterate over the elements in the ICO represented by the enclosing instance.
+     */
+    private class ICOIterator implements Iterator<Bitmap> {
+        private int mIndex = 0;
+
+        @Override
+        public boolean hasNext() {
+            return mIndex < mIconDirectory.length;
+        }
+
+        @Override
+        public Bitmap next() {
+            if (mIndex > mIconDirectory.length) {
+                throw new NoSuchElementException("No more elements in this ICO.");
+            }
+            return decodeBitmapAtIndex(mIndex++);
+        }
+
+        @Override
+        public void remove() {
+            if (mIconDirectory[mIndex] == null) {
+                throw new IllegalStateException("Remove already called for element " + mIndex);
+            }
+            mIconDirectory[mIndex] = null;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/favicons/decoders/IconDirectoryEntry.java
@@ -0,0 +1,201 @@
+/* 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.decoders;
+
+/**
+ * Representation of an ICO file ICONDIRENTRY structure.
+ */
+public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
+
+    public static int sMaxBPP;
+
+    int mWidth;
+    int mHeight;
+    int mPaletteSize;
+    int mBitsPerPixel;
+    int mPayloadSize;
+    int mPayloadOffset;
+    boolean mPayloadIsPNG;
+
+    // Tracks the index in the Icon Directory of this entry. Useful only for pruning.
+    int mIndex;
+    boolean mIsErroneous;
+
+    public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) {
+        mWidth = width;
+        mHeight = height;
+        mPaletteSize = paletteSize;
+        mBitsPerPixel = bitsPerPixel;
+        mPayloadSize = payloadSize;
+        mPayloadOffset = payloadOffset;
+        mPayloadIsPNG = payloadIsPNG;
+    }
+
+    /**
+     * Method to get a dummy Icon Directory Entry with the Erroneous bit set.
+     *
+     * @return An erroneous placeholder Icon Directory Entry.
+     */
+    public static IconDirectoryEntry getErroneousEntry() {
+        IconDirectoryEntry ret = new IconDirectoryEntry(-1, -1, -1, -1, -1, -1, false);
+        ret.mIsErroneous = true;
+
+        return ret;
+    }
+
+    /**
+     * Create an IconDirectoryEntry object from a byte[]. Interprets the buffer starting at the given
+     * offset as an IconDirectoryEntry and returns the result.
+     *
+     * @param buffer Byte array containing the icon directory entry to decode.
+     * @param regionOffset Offset into the byte array of the valid region of the buffer.
+     * @param regionLength Length of the valid region in the buffer.
+     * @param entryOffset Offset of the icon directory entry to decode within the buffer.
+     * @return An IconDirectoryEntry object representing the entry specified, or null if the entry
+     *         is obviously invalid.
+     */
+    public static IconDirectoryEntry createFromBuffer(byte[] buffer, int regionOffset, int regionLength, int entryOffset) {
+        // Verify that the reserved field is really zero.
+        if (buffer[entryOffset + 3] != 0) {
+            return getErroneousEntry();
+        }
+
+        // Verify that the entry points to a region that actually exists in the buffer, else bin it.
+        int fieldPtr = entryOffset + 8;
+        int entryLength = (buffer[fieldPtr] & 0xFF) |
+                          (buffer[fieldPtr + 1] & 0xFF) << 8 |
+                          (buffer[fieldPtr + 2] & 0xFF) << 16 |
+                          (buffer[fieldPtr + 3] & 0xFF) << 24;
+
+        // Advance to the offset field.
+        fieldPtr += 4;
+
+        int payloadOffset = (buffer[fieldPtr] & 0xFF) |
+                            (buffer[fieldPtr + 1] & 0xFF) << 8 |
+                            (buffer[fieldPtr + 2] & 0xFF) << 16 |
+                            (buffer[fieldPtr + 3] & 0xFF) << 24;
+
+        // Fail if the entry describes a region outside the buffer.
+        if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > regionOffset + regionLength) {
+            return getErroneousEntry();
+        }
+
+        // Extract the image dimensions.
+        int imageWidth = buffer[entryOffset] & 0xFF;
+        int imageHeight = buffer[entryOffset+1] & 0xFF;
+
+        // Because Microsoft, a size value of zero represents an image size of 256.
+        if (imageWidth == 0) {
+            imageWidth = 256;
+        }
+
+        if (imageHeight == 0) {
+            imageHeight = 256;
+        }
+
+        // If the image uses a colour palette, this is the number of colours, otherwise this is zero.
+        int paletteSize = buffer[entryOffset + 2] & 0xFF;
+
+        // The plane count - usually 0 or 1. When > 1, taken as multiplier on bitsPerPixel.
+        int colorPlanes = buffer[entryOffset + 4] & 0xFF;
+
+        int bitsPerPixel = (buffer[entryOffset + 6] & 0xFF) |
+                           (buffer[entryOffset + 7] & 0xFF) << 8;
+
+        if (colorPlanes > 1) {
+            bitsPerPixel *= colorPlanes;
+        }
+
+        // Look for PNG magic numbers at the start of the payload.
+        boolean payloadIsPNG = FaviconDecoder.bufferStartsWith(buffer, FaviconDecoder.ImageMagicNumbers.PNG.value, regionOffset + payloadOffset);
+
+        return new IconDirectoryEntry(imageWidth, imageHeight, paletteSize, bitsPerPixel, entryLength, payloadOffset, payloadIsPNG);
+    }
+
+    /**
+     * Get the number of bytes from the start of the ICO file to the beginning of this entry.
+     */
+    public int getOffset() {
+        return ICODecoder.ICO_HEADER_LENGTH_BYTES + (mIndex * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
+    }
+
+    @Override
+    public int compareTo(IconDirectoryEntry another) {
+        if (mWidth > another.mWidth) {
+            return 1;
+        }
+
+        if (mWidth < another.mWidth) {
+            return -1;
+        }
+
+        // Where both images exceed the max BPP, take the smaller of the two BPP values.
+        if (mBitsPerPixel >= sMaxBPP && another.mBitsPerPixel >= sMaxBPP) {
+            if (mBitsPerPixel < another.mBitsPerPixel) {
+                return 1;
+            }
+
+            if (mBitsPerPixel > another.mBitsPerPixel) {
+                return -1;
+            }
+        }
+
+        // Otherwise, take the larger of the BPP values.
+        if (mBitsPerPixel > another.mBitsPerPixel) {
+            return 1;
+        }
+
+        if (mBitsPerPixel < another.mBitsPerPixel) {
+            return -1;
+        }
+
+        // Prefer large palettes.
+        if (mPaletteSize > another.mPaletteSize) {
+            return 1;
+        }
+
+        if (mPaletteSize < another.mPaletteSize) {
+            return -1;
+        }
+
+        // Prefer smaller payloads.
+        if (mPayloadSize < another.mPayloadSize) {
+            return 1;
+        }
+
+        if (mPayloadSize > another.mPayloadSize) {
+            return -1;
+        }
+
+        // If all else fails, prefer PNGs over BMPs. They tend to be smaller.
+        if (mPayloadIsPNG && !another.mPayloadIsPNG) {
+            return 1;
+        }
+
+        if (!mPayloadIsPNG && another.mPayloadIsPNG) {
+            return -1;
+        }
+
+        return 0;
+    }
+
+    public static void setMaxBPP(int maxBPP) {
+        sMaxBPP = maxBPP;
+    }
+
+    @Override
+    public String toString() {
+        return "IconDirectoryEntry{" +
+                "\nmWidth=" + mWidth +
+                ", \nmHeight=" + mHeight +
+                ", \nmPaletteSize=" + mPaletteSize +
+                ", \nmBitsPerPixel=" + mBitsPerPixel +
+                ", \nmPayloadSize=" + mPayloadSize +
+                ", \nmPayloadOffset=" + mPayloadOffset +
+                ", \nmPayloadIsPNG=" + mPayloadIsPNG +
+                ", \nmIndex=" + mIndex +
+                '}';
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/favicons/decoders/LoadFaviconResult.java
@@ -0,0 +1,74 @@
+/* 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.decoders;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Iterator;
+
+/**
+ * Class representing the result of loading a favicon.
+ * This operation will produce either a collection of favicons, a single favicon, or no favicon.
+ * It is necessary to model single favicons differently to a collection of one favicon (An entity
+ * that may not exist with this scheme) since the in-database representation of these things differ.
+ * (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are
+ * stored as decoded bitmap blobs.)
+ */
+public class LoadFaviconResult {
+    private static final String LOGTAG = "LoadFaviconResult";
+
+    byte[] mFaviconBytes;
+    int mOffset;
+    int mLength;
+
+    boolean mHasMultipleBitmaps;
+    Iterator<Bitmap> mBitmapsDecoded;
+
+    public Iterator<Bitmap> getBitmaps() {
+        return mBitmapsDecoded;
+    }
+
+    /**
+     * Return a representation of this result suitable for storing in the database.
+     * For
+     *
+     * @return A byte array containing the bytes from which this result was decoded.
+     */
+    public byte[] getBytesForDatabaseStorage() {
+        // Begin by normalising the buffer.
+        if (mOffset != 0 || mLength != mFaviconBytes.length) {
+            final byte[] normalised = new byte[mLength];
+            System.arraycopy(mFaviconBytes, mOffset, normalised, 0, mLength);
+            mOffset = 0;
+            mFaviconBytes = normalised;
+        }
+
+        // For results containing a single image, we re-encode the result as a PNG in an effort to
+        // save space.
+        if (!mHasMultipleBitmaps) {
+            Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) mBitmapsDecoded).peek();
+            byte[] data = null;
+            ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+            if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
+                data = stream.toByteArray();
+            } else {
+                Log.w(LOGTAG, "Favicon compression failed.");
+            }
+
+            return data;
+        }
+
+        // For results containing multiple images, we store the result verbatim. (But cutting the
+        // buffer to size first).
+        // We may instead want to consider re-encoding the entire ICO as a collection of efficiently
+        // encoded PNGs. This may not be worth the CPU time (Indeed, the encoding of single-image
+        // favicons may also not be worth the time/space tradeoff.).
+        return mFaviconBytes;
+    }
+
+}
--- a/mobile/android/base/gfx/BitmapUtils.java
+++ b/mobile/android/base/gfx/BitmapUtils.java
@@ -113,24 +113,32 @@ public final class BitmapUtils {
         loader.onBitmapFound(null);
     }
 
     public static Bitmap decodeByteArray(byte[] bytes) {
         return decodeByteArray(bytes, null);
     }
 
     public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
+        return decodeByteArray(bytes, 0, bytes.length, options);
+    }
+
+    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
+        return decodeByteArray(bytes, offset, length, null);
+    }
+
+    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
         if (bytes.length <= 0) {
             throw new IllegalArgumentException("bytes.length " + bytes.length
                                                + " must be a positive number");
         }
 
         Bitmap bitmap = null;
         try {
-            bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+            bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
         } catch (OutOfMemoryError e) {
             Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
                           + ", options= " + options + ") OOM!", e);
             return null;
         }
 
         if (bitmap == null) {
             Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -123,16 +123,20 @@ gbjar.sources += [
     'db/SQLiteBridgeContentProvider.java',
     'db/TabsProvider.java',
     'Distribution.java',
     'DoorHangerPopup.java',
     'EditBookmarkDialog.java',
     'favicons/cache/FaviconCache.java',
     'favicons/cache/FaviconCacheElement.java',
     'favicons/cache/FaviconsForURL.java',
+    'favicons/decoders/FaviconDecoder.java',
+    'favicons/decoders/ICODecoder.java',
+    'favicons/decoders/IconDirectoryEntry.java',
+    'favicons/decoders/LoadFaviconResult.java',
     'favicons/Favicons.java',
     'favicons/LoadFaviconTask.java',
     'favicons/OnFaviconLoadedListener.java',
     'FilePickerResultHandler.java',
     'FilePickerResultHandlerSync.java',
     'FindInPageBar.java',
     'FirefoxAccountsHelper.java',
     'FormAssistPopup.java',