Bug 1290014 - Use new icon framework in UI code. r=ahunt,Grisha
authorSebastian Kaspari <s.kaspari@gmail.com>
Tue, 16 Aug 2016 11:44:21 +0200
changeset 312468 a803f062653e7554b50fc1d3b145fe71ec5447e3
parent 312467 7d65390d95e755b5cfd6896e0f564b41e6f47e7f
child 312469 a73dcd57f417301ee649a6777c3ccdc027d7eb01
push id20447
push userkwierso@gmail.com
push dateFri, 02 Sep 2016 20:36:44 +0000
treeherderfx-team@969397f22187 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersahunt, Grisha
bugs1290014
milestone51.0a1
Bug 1290014 - Use new icon framework in UI code. r=ahunt,Grisha This patch replaces the old favicon code with the new icon code in the UI code. MozReview-Commit-ID: 2YdDpYUhb0M
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
mobile/android/base/java/org/mozilla/gecko/Tab.java
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java
mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java
mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java
mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java
mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -29,18 +29,16 @@ import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.delegates.BrowserAppDelegate;
 import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
 import org.mozilla.gecko.delegates.ScreenshotDelegate;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.distribution.PartnerBrowserCustomizationsClient;
 import org.mozilla.gecko.dlc.DownloadContentService;
-import org.mozilla.gecko.favicons.Favicons;
-import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.feeds.ContentNotificationsDelegate;
 import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
@@ -50,16 +48,19 @@ import org.mozilla.gecko.home.HomeConfig
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
 import org.mozilla.gecko.home.HomeFragment;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.home.HomeScreen;
 import org.mozilla.gecko.home.SearchEngine;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.javaaddons.JavaAddonManager;
 import org.mozilla.gecko.media.VideoPlayer;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.NotificationClient;
 import org.mozilla.gecko.notifications.ServiceNotificationClient;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
@@ -97,16 +98,17 @@ import org.mozilla.gecko.updater.PostUpd
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.PrefUtils;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.AnchoredPopup;
@@ -1815,60 +1817,50 @@ public class BrowserApp extends GeckoApp
             }
 
         } else {
             super.handleMessage(event, message, callback);
         }
     }
 
     private void getFaviconFromCache(final EventCallback callback, final String url) {
-        final OnFaviconLoadedListener listener = new OnFaviconLoadedListener() {
-            @Override
-            public void onFaviconLoaded(final String url, final String faviconURL, final Bitmap favicon) {
-                ThreadUtils.assertOnUiThread();
-                // Convert Bitmap to Base64 data URI in background.
-                ThreadUtils.postToBackgroundThread(new Runnable() {
+        Icons.with(this)
+                .pageUrl(url)
+                .skipNetwork()
+                .executeCallbackOnBackgroundThread()
+                .build()
+                .execute(new IconCallback() {
                     @Override
-                    public void run() {
+                    public void onIconResponse(IconResponse response) {
                         ByteArrayOutputStream out = null;
                         Base64OutputStream b64 = null;
 
-                        // Failed to load favicon from local.
-                        if (favicon == null) {
-                            callback.sendError("Failed to get favicon from cache");
-                        } else {
+                        try {
+                            out = new ByteArrayOutputStream();
+                            out.write("data:image/png;base64,".getBytes());
+                            b64 = new Base64OutputStream(out, Base64.NO_WRAP);
+                            response.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, b64);
+                            callback.sendSuccess(new String(out.toByteArray()));
+                        } catch (IOException e) {
+                            Log.w(LOGTAG, "Failed to convert to base64 data URI");
+                            callback.sendError("Failed to convert favicon to a base64 data URI");
+                        } finally {
                             try {
-                                out = new ByteArrayOutputStream();
-                                out.write("data:image/png;base64,".getBytes());
-                                b64 = new Base64OutputStream(out, Base64.NO_WRAP);
-                                favicon.compress(Bitmap.CompressFormat.PNG, 100, b64);
-                                callback.sendSuccess(new String(out.toByteArray()));
+                                if (out != null) {
+                                    out.close();
+                                }
+                                if (b64 != null) {
+                                    b64.close();
+                                }
                             } catch (IOException e) {
-                                Log.w(LOGTAG, "Failed to convert to base64 data URI");
-                                callback.sendError("Failed to convert favicon to a base64 data URI");
-                            } finally {
-                                try {
-                                    if (out != null) {
-                                        out.close();
-                                    }
-                                    if (b64 != null) {
-                                        b64.close();
-                                    }
-                                } catch (IOException e) {
-                                    Log.w(LOGTAG, "Failed to close the streams");
-                                }
+                                Log.w(LOGTAG, "Failed to close the streams");
                             }
                         }
                     }
                 });
-            }
-        };
-        Favicons.getSizedFaviconForPageFromLocal(getContext(),
-                url,
-                listener);
     }
 
     /**
      * Use a dummy Intent to do a default browser check.
      *
      * @return true if this package is the default browser on this device, false otherwise.
      */
     private boolean isDefaultBrowser(String action) {
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -4,25 +4,27 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.FullScreenState;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.health.HealthRecorder;
 import org.mozilla.gecko.health.SessionInformation;
 import org.mozilla.gecko.health.StubbedHealthRecorder;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuInflater;
 import org.mozilla.gecko.menu.MenuPanel;
 import org.mozilla.gecko.notifications.AppNotificationClient;
 import org.mozilla.gecko.notifications.NotificationClient;
 import org.mozilla.gecko.notifications.NotificationHelper;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.mozglue.SafeIntent;
@@ -1149,21 +1151,16 @@ public abstract class GeckoApp
         GeckoAppShell.setContextGetter(this);
         GeckoAppShell.setApplicationContext(getApplicationContext());
         GeckoAppShell.setGeckoInterface(this);
         // We need to set the notification client before launching Gecko, since Gecko could start
         // sending notifications immediately after startup, which we don't want to lose/crash on.
         GeckoAppShell.setNotificationClient(makeNotificationClient());
 
         Tabs.getInstance().attachToContext(this);
-        try {
-            Favicons.initializeWithContext(this);
-        } catch (Exception e) {
-            Log.e(LOGTAG, "Exception starting favicon cache. Corrupt resources?", e);
-        }
 
         // Tell Stumbler to register a local broadcast listener to listen for preference intents.
         // We do this via intents since we can't easily access Stumbler directly,
         // as it might be compiled outside of Fennec.
         getApplicationContext().sendBroadcast(
                 new Intent(INTENT_REGISTER_STUMBLER_LISTENER)
         );
 
@@ -1911,22 +1908,28 @@ public abstract class GeckoApp
     @Override
     public String getDefaultUAString() {
         return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
                                           AppConstants.USER_AGENT_FENNEC_MOBILE;
     }
 
     @Override
     public void createShortcut(final String title, final String url) {
-        Favicons.getPreferredIconForHomeScreenShortcut(this, url, new OnFaviconLoadedListener() {
-            @Override
-            public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
-                doCreateShortcut(title, url, favicon);
-            }
-        });
+        Icons.with(this)
+                .pageUrl(url)
+                .skipNetwork()
+                .skipMemory()
+                .forLauncherIcon()
+                .build()
+                .execute(new IconCallback() {
+                    @Override
+                    public void onIconResponse(IconResponse response) {
+                        doCreateShortcut(title, url, response.getBitmap());
+                    }
+                });
     }
 
     private void doCreateShortcut(final String aTitle, final String aURI, final Bitmap aIcon) {
         // The intent to be launched by the shortcut.
         Intent shortcutIntent = new Intent();
         shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT);
         shortcutIntent.setData(Uri.parse(aURI));
         shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
@@ -2262,18 +2265,16 @@ public abstract class GeckoApp
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
                     rec.close(GeckoApp.this);
                 }
             });
         }
 
-        Favicons.close();
-
         super.onDestroy();
 
         Tabs.unregisterOnTabsChangedListener(this);
 
         if (!isFinishing()) {
             // GeckoApp was not intentionally destroyed, so keep our process alive.
             return;
         }
--- a/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
@@ -4,18 +4,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserProvider;
-import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.home.ImageLoader;
+import org.mozilla.gecko.icons.storage.MemoryStorage;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentCallbacks2;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -178,17 +178,17 @@ class MemoryMonitor extends BroadcastRec
 
         // TODO hook in memory-reduction stuff for different levels here
         if (level >= MEMORY_PRESSURE_MEDIUM) {
             //Only send medium or higher events because that's all that is used right now
             if (GeckoThread.isRunning()) {
                 dispatchMemoryPressure();
             }
 
-            Favicons.clearMemCache();
+            MemoryStorage.get().evictAll();
             ImageLoader.clearLruCache();
             LocalBroadcastManager.getInstance(mAppContext)
                     .sendBroadcast(new Intent(BrowserProvider.ACTION_SHRINK_MEMORY));
         }
         return true;
     }
 
     /**
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -4,65 +4,66 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.TreeSet;
+import java.util.concurrent.Future;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadata;
-import org.mozilla.gecko.favicons.FaviconGenerator;
-import org.mozilla.gecko.favicons.Favicons;
-import org.mozilla.gecko.favicons.LoadFaviconTask;
-import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
-import org.mozilla.gecko.favicons.RemoteFavicon;
 import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequestBuilder;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.reader.ReadingListHelper;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.SiteLogins;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.os.Build;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
-import org.mozilla.gecko.widget.SiteLogins;
 
 public class Tab {
     private static final String LOGTAG = "GeckoTab";
 
     private static Pattern sColorPattern;
     private final int mId;
     private final BrowserDB mDB;
     private long mLastUsed;
     private String mUrl;
     private String mBaseDomain;
     private String mUserRequested; // The original url requested. May be typed by the user or sent by an extneral app for example.
     private String mTitle;
     private Bitmap mFavicon;
     private String mFaviconUrl;
     private String mApplicationId; // Intended to be null after explicit user action.
 
-    // The set of all available Favicons for this tab, sorted by attractiveness.
-    final TreeSet<RemoteFavicon> mAvailableFavicons = new TreeSet<>();
+    private IconRequestBuilder mIconRequestBuilder;
+    private Future<IconResponse> mRunningIconRequest;
+
     private boolean mHasFeeds;
     private boolean mHasOpenSearch;
     private final SiteIdentity mSiteIdentity;
     private SiteLogins mSiteLogins;
     private BitmapDrawable mThumbnail;
     private final int mParentId;
     // Indicates the url was loaded from a source external to the app. This will be cleared
     // when the user explicitly loads a new url (e.g. clicking a link is not explicit).
@@ -429,104 +430,65 @@ public class Tab {
         mHasTouchListeners = aValue;
     }
 
     public boolean getHasTouchListeners() {
         return mHasTouchListeners;
     }
 
     public synchronized void addFavicon(String faviconURL, int faviconSize, String mimeType) {
-        RemoteFavicon favicon = new RemoteFavicon(faviconURL, faviconSize, mimeType);
+        mIconRequestBuilder
+                .icon(IconDescriptor.createFavicon(faviconURL, faviconSize, mimeType))
+                .deferBuild();
+    }
 
-        // Add this Favicon to the set of available Favicons.
-        synchronized (mAvailableFavicons) {
-            mAvailableFavicons.add(favicon);
-        }
+    public synchronized void addTouchicon(String iconUrl, int faviconSize, String mimeType) {
+        mIconRequestBuilder
+                .icon(IconDescriptor.createTouchicon(iconUrl, faviconSize, mimeType))
+                .deferBuild();
     }
 
     public void loadFavicon() {
         // Static Favicons never change
         if (AboutPages.isBuiltinIconPage(mUrl) && mFavicon != null) {
             return;
         }
 
-        // If we have a Favicon explicitly set, load it.
-        if (!mAvailableFavicons.isEmpty()) {
-            RemoteFavicon newFavicon = mAvailableFavicons.first();
-
-            // If the new Favicon is different, cancel the old load. Else, abort.
-            if (newFavicon.faviconUrl.equals(mFaviconUrl)) {
-                return;
-            }
-
-            Favicons.cancelFaviconLoad(mFaviconLoadId);
-            mFaviconUrl = newFavicon.faviconUrl;
-        } else {
-            // Otherwise, fallback to the default Favicon.
-            mFaviconUrl = null;
-        }
-
-        final Favicons.LoadType loadType;
-        if (mSiteIdentity.getSecurityMode() == SiteIdentity.SecurityMode.CHROMEUI) {
-            loadType = Favicons.LoadType.PRIVILEGED;
-        } else {
-            loadType = Favicons.LoadType.UNPRIVILEGED;
+        if (mIconRequestBuilder == null) {
+            // For the first internal homepage we might want to load a favicon without ever receiving
+            // a location change event first. In this case we didn't start to build a request yet.
+            // Let's do that now.
+            mIconRequestBuilder = Icons.with(mAppContext).pageUrl(mUrl);
         }
 
-        int flags = (isPrivate() || mErrorType != ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST;
-        mFaviconLoadId = Favicons.getSizedFavicon(mAppContext, mUrl, mFaviconUrl,
-                loadType, Favicons.browserToolbarFaviconSize, flags,
-                new OnFaviconLoadedListener() {
+        mRunningIconRequest = mIconRequestBuilder
+                .build()
+                .execute(new IconCallback() {
                     @Override
-                    public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap favicon) {
-                        // The tab might be pointing to another URL by the time the
-                        // favicon is finally loaded, in which case we simply ignore it.
-                        if (!pageUrl.equals(mUrl)) {
-                            return;
-                        }
+                    public void onIconResponse(IconResponse response) {
+                        mFavicon = response.getBitmap();
 
-                        // That one failed. Try the next one.
-                        if (favicon == null) {
-                            // If what we just tried to load originated from the set of declared icons..
-                            if (!mAvailableFavicons.isEmpty()) {
-                                // Discard it.
-                                mAvailableFavicons.remove(mAvailableFavicons.first());
-
-                                // Load the next best, if we have one. If not, it'll fall back to the
-                                // default Favicon URL, before giving up.
-                                loadFavicon();
-
-                                return;
-                            }
-
-                            // Total failure: generate a default favicon.
-                            FaviconGenerator.generate(mAppContext, mUrl, this);
-                            return;
-                        }
-
-                        mFavicon = favicon;
-                        mFaviconLoadId = Favicons.NOT_LOADING;
                         Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.FAVICON);
                     }
-                }
-        );
+                });
     }
 
     public synchronized void clearFavicon() {
         // Cancel any ongoing favicon load (if we never finished downloading the old favicon before
         // we changed page).
-        Favicons.cancelFaviconLoad(mFaviconLoadId);
+        if (mRunningIconRequest != null) {
+            mRunningIconRequest.cancel(true);
+        }
 
         // Keep the favicon unchanged while entering reader mode
         if (mEnteringReaderMode)
             return;
 
         mFavicon = null;
         mFaviconUrl = null;
-        mAvailableFavicons.clear();
     }
 
     public void setHasFeeds(boolean hasFeeds) {
         mHasFeeds = hasFeeds;
     }
 
     public void setHasOpenSearch(boolean hasOpenSearch) {
         mHasOpenSearch = hasOpenSearch;
@@ -659,16 +621,20 @@ public class Tab {
             updateBookmark();
             if (!sameDocument) {
                 // We can unconditionally clear the favicon and title here: we
                 // already filtered both cases in which this was a (pseudo-)
                 // spurious location change, so we're definitely loading a new
                 // page.
                 clearFavicon();
 
+                // Start to build a new request to load a favicon.
+                mIconRequestBuilder = Icons.with(mAppContext)
+                        .pageUrl(uri);
+
                 // Load local static Favicons immediately
                 if (AboutPages.isBuiltinIconPage(uri)) {
                     loadFavicon();
                 }
 
                 updateTitle(null);
             }
         }
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -14,17 +14,16 @@ import java.util.concurrent.atomic.Atomi
 import android.support.annotation.Nullable;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.WhatsNewReceiver;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
@@ -108,16 +107,17 @@ public class Tabs implements GeckoEventL
             "Tab:LoadedFromCache",
             "Content:LocationChange",
             "Content:SecurityChange",
             "Content:StateChange",
             "Content:LoadError",
             "Content:PageShow",
             "DOMTitleChanged",
             "Link:Favicon",
+            "Link:Touchicon",
             "Link:Feed",
             "Link:OpenSearch",
             "DesktopMode:Changed",
             "Tab:ViewportMetadata",
             "Tab:StreamStart",
             "Tab:StreamStop",
             "Tab:AudioPlayingChange");
 
@@ -515,30 +515,28 @@ public class Tabs implements GeckoEventL
                 notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR);
             } else if (event.equals("Content:PageShow")) {
                 tab.setLoadedFromCache(message.getBoolean("fromCache"));
                 tab.updateUserRequested(message.getString("userRequested"));
                 notifyListeners(tab, TabEvents.PAGE_SHOW);
             } else if (event.equals("DOMTitleChanged")) {
                 tab.updateTitle(message.getString("title"));
             } else if (event.equals("Link:Favicon")) {
-                // Don't bother if the type isn't one we can decode.
-                if (!Favicons.canDecodeType(message.getString("mime"))) {
-                    return;
-                }
+                // Add the favicon to the set of available icons for this tab.
 
-                // Add the favicon to the set of available icons for this tab.
                 tab.addFavicon(message.getString("href"), message.getInt("size"), message.getString("mime"));
 
                 // Load the favicon. If the tab is still loading, we actually do the load once the
                 // page has loaded, in an attempt to prevent the favicon load from having a
                 // detrimental effect on page load time.
                 if (tab.getState() != Tab.STATE_LOADING) {
                     tab.loadFavicon();
                 }
+            } else if (event.equals("Link:Touchicon")) {
+                tab.addTouchicon(message.getString("href"), message.getInt("size"), message.getString("mime"));
             } else if (event.equals("Link:Feed")) {
                 tab.setHasFeeds(true);
                 notifyListeners(tab, TabEvents.LINK_FEED);
             } else if (event.equals("Link:OpenSearch")) {
                 boolean visible = message.getBoolean("visible");
                 tab.setHasOpenSearch(visible);
             } else if (event.equals("DesktopMode:Changed")) {
                 tab.setDesktopMode(message.getBoolean("desktopMode"));
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -22,16 +22,17 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Visits;
 import org.mozilla.gecko.db.BrowserContract.Schema;
 import org.mozilla.gecko.db.BrowserContract.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserContract.TopSites;
 import org.mozilla.gecko.db.BrowserContract.UrlAnnotations;
 import org.mozilla.gecko.db.DBUtils.UpdateOperation;
+import org.mozilla.gecko.icons.IconsHelper;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.BroadcastReceiver;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -1687,17 +1688,17 @@ public class BrowserProvider extends Sha
         // Extract the page URL from the ContentValues
         if (values.containsKey(Favicons.PAGE_URL)) {
             pageUrl = values.getAsString(Favicons.PAGE_URL);
             values.remove(Favicons.PAGE_URL);
         }
 
         // If no URL is provided, insert using the default one.
         if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) {
-            values.put(Favicons.URL, org.mozilla.gecko.favicons.Favicons.guessDefaultFaviconURL(pageUrl));
+            values.put(Favicons.URL, IconsHelper.guessDefaultFaviconURL(pageUrl));
         }
 
         final long now = System.currentTimeMillis();
         values.put(Favicons.DATE_CREATED, now);
         values.put(Favicons.DATE_MODIFIED, now);
 
         beginWrite(db);
         final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener;
 import org.mozilla.gecko.home.BrowserSearch.OnSearchListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
+import org.mozilla.gecko.icons.IconResponse;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.widget.AnimatedHeightLayout;
 import org.mozilla.gecko.widget.FaviconView;
 import org.mozilla.gecko.widget.FlowLayout;
 
@@ -378,17 +379,18 @@ class SearchEngineRow extends AnimatedHe
      * @param searchSuggestionsEnabled whether or not suggestions from the default search engine are enabled
      * @param searchEngine the search engine to use throughout the SearchEngineRow class
      * @param rawSearchHistorySuggestions search history suggestions
      * @param animate whether or not to use animations
      **/
     public void updateSuggestions(boolean searchSuggestionsEnabled, SearchEngine searchEngine, @Nullable List<String> rawSearchHistorySuggestions, boolean animate) {
         mSearchEngine = searchEngine;
         // Set the search engine icon (e.g., Google) for the row.
-        mIconView.updateAndScaleImage(mSearchEngine.getIcon(), mSearchEngine.getEngineIdentifier());
+
+        mIconView.updateAndScaleImage(IconResponse.create(mSearchEngine.getIcon()));
         // Set the initial content description.
         setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString());
 
         final int recycledSuggestionCount = mSuggestionView.getChildCount();
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
         final boolean savedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true);
 
         // Remove duplicates of search engine suggestions from saved searches.
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java
@@ -1,38 +1,39 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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.home;
 
-import org.mozilla.gecko.db.BrowserContract.TopSites;
-import org.mozilla.gecko.favicons.FaviconGenerator;
-import org.mozilla.gecko.favicons.Favicons;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.widget.ImageView.ScaleType;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserContract.TopSites;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+
+import java.util.concurrent.Future;
+
 /**
  * A view that displays the thumbnail and the title/url for a top/pinned site.
  * If the title/url is longer than the width of the view, they are faded out.
  * If there is no valid url, a default string is shown at 50% opacity.
  * This is denoted by the empty state.
  */
-public class TopSitesGridItemView extends RelativeLayout {
+public class TopSitesGridItemView extends RelativeLayout implements IconCallback {
     private static final String LOGTAG = "GeckoTopSitesGridItemView";
 
     // Empty state, to denote there is no valid url.
     private static final int[] STATE_EMPTY = { android.R.attr.state_empty };
 
     private static final ScaleType SCALE_TYPE_FAVICON   = ScaleType.CENTER;
     private static final ScaleType SCALE_TYPE_RESOURCE  = ScaleType.CENTER;
     private static final ScaleType SCALE_TYPE_THUMBNAIL = ScaleType.CENTER_CROP;
@@ -40,28 +41,26 @@ public class TopSitesGridItemView extend
 
     // Child views.
     private final TextView mTitleView;
     private final TopSitesThumbnailView mThumbnailView;
 
     // Data backing this view.
     private String mTitle;
     private String mUrl;
-    private String mFaviconURL;
 
     private boolean mThumbnailSet;
 
     // Matches BrowserContract.TopSites row types
     private int mType = -1;
 
     // Dirty state.
     private boolean mIsDirty;
 
-    // Empty state.
-    private int mLoadId = Favicons.NOT_LOADING;
+    private Future<IconResponse> mOngoingIconRequest;
 
     public TopSitesGridItemView(Context context) {
         this(context, null);
     }
 
     public TopSitesGridItemView(Context context, AttributeSet attrs) {
         this(context, attrs, R.attr.topSitesGridItemViewStyle);
     }
@@ -134,17 +133,17 @@ public class TopSitesGridItemView extend
         updateTitleView();
     }
 
     public void blankOut() {
         mUrl = "";
         mTitle = "";
         updateType(TopSites.TYPE_BLANK);
         updateTitleView();
-        setLoadId(Favicons.NOT_LOADING);
+        cancelIconLoading();
         ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
         displayThumbnail(R.drawable.top_site_add);
 
     }
 
     public void markAsDirty() {
         mIsDirty = true;
     }
@@ -177,17 +176,17 @@ public class TopSitesGridItemView extend
         } else if (changed) {
             // Because we'll have a new favicon or thumbnail arriving shortly, and
             // we need to not reject it because we already had a thumbnail.
             mThumbnailSet = false;
         }
 
         if (changed) {
             updateTitleView();
-            setLoadId(Favicons.NOT_LOADING);
+            cancelIconLoading();
             ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
         }
 
         if (updateType(type)) {
             changed = true;
         }
 
         // The dirty state forces the state update to return true
@@ -195,62 +194,57 @@ public class TopSitesGridItemView extend
         // are loaded in TopSitesPanel/TopSitesGridAdapter.
         changed = (changed || mIsDirty);
         mIsDirty = false;
 
         return changed;
     }
 
     /**
+     * Try to load an icon for the given page URL.
+     */
+    public void loadFavicon(String pageUrl) {
+        mOngoingIconRequest = Icons.with(getContext())
+                .pageUrl(pageUrl)
+                .skipNetwork()
+                .build()
+                .execute(this);
+    }
+
+    private void cancelIconLoading() {
+        if (mOngoingIconRequest != null) {
+            mOngoingIconRequest.cancel(true);
+        }
+    }
+
+    /**
      * Display the thumbnail from a resource.
      *
      * @param resId Resource ID of the drawable to show.
      */
     public void displayThumbnail(int resId) {
         mThumbnailView.setScaleType(SCALE_TYPE_RESOURCE);
         mThumbnailView.setImageResource(resId);
         mThumbnailView.setBackgroundColor(0x0);
         mThumbnailSet = false;
     }
 
-    private void generateDefaultIcon() {
-        ThreadUtils.assertOnBackgroundThread();
-
-        final Bitmap bitmap = FaviconGenerator.generate(getContext(), mUrl).bitmap;
-        final int dominantColor = BitmapUtils.getDominantColor(bitmap);
-
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mThumbnailView.setScaleType(SCALE_TYPE_FAVICON);
-                mThumbnailView.setImageBitmap(bitmap);
-                mThumbnailView.setBackgroundColor(0x7FFFFFFF & dominantColor); // 50% dominant color
-                mThumbnailSet = false;
-            }
-        });
-    }
-
     /**
      * Display the thumbnail from a bitmap.
      *
      * @param thumbnail The bitmap to show as thumbnail.
      */
     public void displayThumbnail(Bitmap thumbnail) {
         if (thumbnail == null) {
-            ThreadUtils.postToBackgroundThread(new Runnable() {
-                @Override
-                public void run() {
-                    generateDefaultIcon();
-                }
-            });
             return;
         }
 
         mThumbnailSet = true;
-        Favicons.cancelFaviconLoad(mLoadId);
+
+        cancelIconLoading();
         ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
 
         mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL);
         mThumbnailView.setImageBitmap(thumbnail, true);
         mThumbnailView.setBackgroundDrawable(null);
     }
 
     /**
@@ -265,61 +259,16 @@ public class TopSitesGridItemView extend
         mThumbnailSet = true;
 
         ImageLoader.with(getContext())
                    .load(imageUrl)
                    .noFade()
                    .into(mThumbnailView);
     }
 
-    public void displayFavicon(Bitmap favicon, String faviconURL, int expectedLoadId) {
-        if (mLoadId != Favicons.NOT_LOADING &&
-            mLoadId != expectedLoadId) {
-            // View recycled.
-            return;
-        }
-
-        // Yes, there's a chance of a race here.
-        displayFavicon(favicon, faviconURL);
-    }
-
-    /**
-     * Display the thumbnail from a favicon.
-     *
-     * @param favicon The favicon to show as thumbnail.
-     */
-    public void displayFavicon(Bitmap favicon, String faviconURL) {
-        if (mThumbnailSet) {
-            // Already showing a thumbnail; do nothing.
-            return;
-        }
-
-        if (favicon == null) {
-            ThreadUtils.postToBackgroundThread(new Runnable() {
-                @Override
-                public void run() {
-                    generateDefaultIcon();
-                }
-            });
-            return;
-        }
-
-        if (faviconURL != null) {
-            mFaviconURL = faviconURL;
-        }
-
-        mThumbnailView.setScaleType(SCALE_TYPE_FAVICON);
-        mThumbnailView.setImageBitmap(favicon, false);
-
-        if (mFaviconURL != null) {
-            final int bgColor = Favicons.getFaviconColor(mFaviconURL);
-            mThumbnailView.setBackgroundColorWithOpacityFilter(bgColor);
-        }
-    }
-
     /**
      * Update the item type associated with this view. Returns true if
      * the type has changed, false otherwise.
      */
     private boolean updateType(int type) {
         if (mType == type) {
             return false;
         }
@@ -341,13 +290,23 @@ public class TopSitesGridItemView extend
         String title = getTitle();
         if (!TextUtils.isEmpty(title)) {
             mTitleView.setText(title);
         } else {
             mTitleView.setText(R.string.home_top_sites_add);
         }
     }
 
-    public void setLoadId(int aLoadId) {
-        Favicons.cancelFaviconLoad(mLoadId);
-        mLoadId = aLoadId;
+    /**
+     * Display the loaded icon (if no thumbnail is set).
+     */
+    @Override
+    public void onIconResponse(IconResponse response) {
+        if (mThumbnailSet) {
+            // Already showing a thumbnail; do nothing.
+            return;
+        }
+
+        mThumbnailView.setScaleType(SCALE_TYPE_FAVICON);
+        mThumbnailView.setImageBitmap(response.getBitmap(), false);
+        mThumbnailView.setBackgroundColorWithOpacityFilter(response.getColor());
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
@@ -9,32 +9,34 @@ import static org.mozilla.gecko.db.URLMe
 import static org.mozilla.gecko.db.URLMetadataTable.TILE_IMAGE_URL_COLUMN;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Future;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserContract.TopSites;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.favicons.Favicons;
-import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
 import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -652,54 +654,25 @@ public class TopSitesPanel extends HomeF
             }
 
             // If thumbnails are still being loaded, don't try to load favicons
             // just yet. If we sent in a thumbnail, we're done now.
             if (mThumbnailInfos == null || thumbnail != null) {
                 return;
             }
 
-            // If we have no thumbnail, attempt to show a Favicon instead.
-            LoadIDAwareFaviconLoadedListener listener = new LoadIDAwareFaviconLoadedListener(view);
-            final int loadId = Favicons.getSizedFaviconForPageFromLocal(context, url, listener);
-            if (loadId == Favicons.LOADED) {
-                // Great!
-                return;
-            }
-
-            // Give each side enough information to shake hands later.
-            listener.setLoadId(loadId);
-            view.setLoadId(loadId);
+            view.loadFavicon(url);
         }
 
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             return new TopSitesGridItemView(context);
         }
     }
 
-    private static class LoadIDAwareFaviconLoadedListener implements OnFaviconLoadedListener {
-        private volatile int loadId = Favicons.NOT_LOADING;
-        private final TopSitesGridItemView view;
-        public LoadIDAwareFaviconLoadedListener(TopSitesGridItemView view) {
-            this.view = view;
-        }
-
-        public void setLoadId(int id) {
-            this.loadId = id;
-        }
-
-        @Override
-        public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
-            if (TextUtils.equals(this.view.getUrl(), url)) {
-                this.view.displayFavicon(favicon, faviconURL, this.loadId);
-            }
-        }
-    }
-
     private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
             trace("Creating TopSitesLoader: " + id);
             return new TopSitesLoader(getActivity());
         }
 
         /**
@@ -718,17 +691,17 @@ public class TopSitesPanel extends HomeF
             mListAdapter.swapCursor(c);
             mGridAdapter.swapCursor(c);
             updateUiFromCursor(c);
 
             final int col = c.getColumnIndexOrThrow(TopSites.URL);
 
             // Load the thumbnails.
             // Even though the cursor we're given is supposed to be fresh,
-            // we get a bad first value unless we reset its position.
+            // we getIcon a bad first value unless we reset its position.
             // Using move(-1) and moveToNext() doesn't work correctly under
             // rotation, so we use moveToFirst.
             if (!c.moveToFirst()) {
                 return;
             }
 
             final ArrayList<String> urls = new ArrayList<String>();
             int i = 1;
@@ -882,17 +855,17 @@ public class TopSitesPanel extends HomeF
                     // This should never be null, but if it is...
                     final byte[] b = cursor.getBlob(dataIndex);
                     if (b == null) {
                         continue;
                     }
 
                     final Bitmap bitmap = BitmapUtils.decodeByteArray(b);
 
-                    // Our thumbnails are never null, so if we get a null decoded
+                    // Our thumbnails are never null, so if we getIcon a null decoded
                     // bitmap, it's because we hit an OOM or some other disaster.
                     // Give up immediately rather than hammering on.
                     if (bitmap == null) {
                         Log.w(LOGTAG, "Aborting thumbnail load; decode failed.");
                         break;
                     }
 
                     thumbnails.put(url, new ThumbnailInfo(bitmap));
--- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
@@ -1,27 +1,32 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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.home;
 
+import java.lang.ref.WeakReference;
+import java.util.concurrent.Future;
+
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
-import org.mozilla.gecko.favicons.LoadFaviconTask;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
-import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.widget.FaviconView;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.Gravity;
@@ -38,22 +43,19 @@ public class TwoLinePageRow extends Line
 
     private final TextView mTitle;
     private final TextView mUrl;
     private final ImageView mStatusIcon;
 
     private int mSwitchToTabIconId;
 
     private final FaviconView mFavicon;
+    private Future<IconResponse> mOngoingIconLoad;
 
     private boolean mShowIcons;
-    private int mLoadFaviconJobId = Favicons.NOT_LOADING;
-
-    // Listener for handling Favicon loads.
-    private final OnFaviconLoadedListener mFaviconListener;
 
     // The URL for the page corresponding to this view.
     private String mPageUrl;
 
     private boolean mHasReaderCacheItem;
 
     public TwoLinePageRow(Context context) {
         this(context, null);
@@ -71,17 +73,16 @@ public class TwoLinePageRow extends Line
         mTitle = (TextView) findViewById(R.id.title);
         mUrl = (TextView) findViewById(R.id.url);
         mStatusIcon = (ImageView) findViewById(R.id.status_icon_bookmark);
 
         mSwitchToTabIconId = NO_ICON;
         mShowIcons = true;
 
         mFavicon = (FaviconView) findViewById(R.id.icon);
-        mFaviconListener = new UpdateViewFaviconLoadedListener(mFavicon);
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
 
         Tabs.registerOnTabsChangedListener(this);
     }
@@ -253,36 +254,41 @@ public class TwoLinePageRow extends Line
 
         // No point updating the below things if URL has not changed. Prevents evil Favicon flicker.
         if (url.equals(mPageUrl)) {
             return;
         }
 
         // Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
         mFavicon.clearImage();
-        Favicons.cancelFaviconLoad(mLoadFaviconJobId);
+
+        if (mOngoingIconLoad != null) {
+            mOngoingIconLoad.cancel(true);
+        }
 
         // Displayed RecentTabsPanel URLs may refer to pages opened in reader mode, so we
         // remove the about:reader prefix to ensure the Favicon loads properly.
         final String pageURL = ReaderModeUtils.stripAboutReaderUrl(url);
 
         if (bookmarkId < BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
-            mLoadFaviconJobId = Favicons.getSizedFavicon(
-                    getContext(),
-                    pageURL,
-                    PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString(),
-                    Favicons.LoadType.PRIVILEGED,
-                    Favicons.defaultFaviconSize,
-                    // We want to load the favicon from the content provider but we do not want the
-                    // favicon loader to fallback to loading a favicon from the web using a guessed
-                    // default URL.
-                    LoadFaviconTask.FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL,
-                    mFaviconListener);
+            mOngoingIconLoad = Icons.with(getContext())
+                    .pageUrl(pageURL)
+                    .skipNetwork()
+                    .privileged(true)
+                    .icon(IconDescriptor.createGenericIcon(
+                            PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString()))
+                    .build()
+                    .execute(mFavicon.createIconCallback());
         } else {
-            mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), pageURL, mFaviconListener);
+            mOngoingIconLoad = Icons.with(getContext())
+                    .pageUrl(pageURL)
+                    .skipNetwork()
+                    .build()
+                    .execute(mFavicon.createIconCallback());
+
         }
 
         updateDisplayedUrl(url, hasReaderCacheItem);
     }
 
     /**
      * Update the data displayed by this row.
      * <p>
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java
@@ -23,29 +23,31 @@ public class IconRequest {
     // Those values are written by the IconRequestBuilder class.
     /* package-private */ String pageUrl;
     /* package-private */ boolean privileged;
     /* package-private */ TreeSet<IconDescriptor> icons;
     /* package-private */ boolean skipNetwork;
     /* package-private */ boolean backgroundThread;
     /* package-private */ boolean skipDisk;
     /* package-private */ boolean skipMemory;
+    /* package-private */ int targetSize;
+    /* package-private */ boolean prepareOnly;
     private IconCallback callback;
-    private int targetSize;
 
     /* package-private */ IconRequest(Context context) {
         this.context = context.getApplicationContext();
         this.icons = new TreeSet<>(new IconDescriptorComparator());
 
         // Setting some sensible defaults.
         this.privileged = false;
         this.skipMemory = false;
         this.skipDisk = false;
         this.skipNetwork = false;
         this.targetSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
+        this.prepareOnly = false;
     }
 
     /**
      * Execute this request and try to load an icon. Once an icon has been loaded successfully the
      * callback will be executed.
      *
      * The returned Future can be used to cancel the job.
      */
@@ -160,9 +162,15 @@ public class IconRequest {
      * Move to the next icon. This method is called after all loaders for the current best icon
      * have failed. After calling this method getBestIcon() will return the next icon to try.
      * hasIconDescriptors() should be called before requesting the next icon.
      */
     /* package-private */ void moveToNextIcon() {
         icons.remove(getBestIcon());
     }
 
+    /**
+     * Should this request be prepared but not actually load an icon?
+     */
+    /* package-private */ boolean shouldPrepareOnly() {
+        return prepareOnly;
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java
@@ -3,16 +3,18 @@
  * 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.icons;
 
 import android.content.Context;
 import android.support.annotation.CheckResult;
 
+import org.mozilla.gecko.GeckoAppShell;
+
 import ch.boye.httpclientandroidlib.util.TextUtils;
 
 /**
  * Builder for creating a request to load an icon.
  */
 public class IconRequestBuilder {
     private final IconRequest request;
 
@@ -79,26 +81,45 @@ public class IconRequestBuilder {
      */
     @CheckResult
     public IconRequestBuilder skipMemory() {
         request.skipMemory = true;
         return this;
     }
 
     /**
+     * The icon will be used as (Android) launcher icon. The loaded icon will be scaled to the
+     * preferred Android launcher icon size.
+     */
+    public IconRequestBuilder forLauncherIcon() {
+        request.targetSize = GeckoAppShell.getPreferredIconSize();
+        return this;
+    }
+
+    /**
      * Execute the callback on the background thread. By default the callback is always executed on
      * the UI thread in order to add the loaded icon to a view easily.
      */
     @CheckResult
     public IconRequestBuilder executeCallbackOnBackgroundThread() {
         request.backgroundThread = true;
         return this;
     }
 
     /**
+     * When executing the request then only prepare executing it but do not actually load an icon.
+     * This mode is only used for some legacy code that uses the icon URL and therefore needs to
+     * perform a lookup of the URL but doesn't want to load the icon yet.
+     */
+    public IconRequestBuilder prepareOnly() {
+        request.prepareOnly = true;
+        return this;
+    }
+
+    /**
      * Return the request built with this builder.
      */
     @CheckResult
     public IconRequest build() {
         if (TextUtils.isEmpty(request.pageUrl)) {
             throw new IllegalStateException("Page URL is required");
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
@@ -46,16 +46,21 @@ import java.util.concurrent.Callable;
 
     @Override
     public IconResponse call() {
         try {
             logRequest(request);
 
             prepareRequest(request);
 
+            if (request.shouldPrepareOnly()) {
+                // This request should only be prepared but not load an actual icon.
+                return null;
+            }
+
             final IconResponse response = loadIcon(request);
 
             if (response != null) {
                 processIcon(request, response);
                 executeCallback(request, response);
 
                 logResponse(response);
 
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java
@@ -1,30 +1,36 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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.preferences;
 
 import android.content.ContentValues;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.os.Build;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.LocalBrowserDB;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.IconsHelper;
+import org.mozilla.gecko.icons.storage.DiskStorage;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.OperationApplicationException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
 import android.provider.BaseColumns;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.util.ArrayList;
 
 public class AndroidImport implements Runnable {
     /**
      * The Android M SDK removed several fields and methods from android.provider.Browser. This class is used as a
      * replacement to support building with the new SDK but at the same time still use these fields on lower Android
@@ -97,17 +103,17 @@ public class AndroidImport implements Ru
                     long modified = System.currentTimeMillis();
                     byte[] data = cursor.getBlob(faviconCol);
                     mDB.updateBookmarkInBatch(mCr, mOperations,
                                               url, title, null, -1,
                                               created, modified,
                                               BrowserContract.Bookmarks.DEFAULT_POSITION,
                                               null, Bookmarks.TYPE_BOOKMARK);
                     if (data != null) {
-                        mDB.updateFaviconInBatch(mCr, mOperations, url, null, null, data);
+                        storeBitmap(data, url);
                     }
                     cursor.moveToNext();
                 }
             }
         } finally {
             if (cursor != null)
                 cursor.close();
         }
@@ -135,17 +141,17 @@ public class AndroidImport implements Ru
                 while (!cursor.isAfterLast()) {
                     String url = cursor.getString(urlCol);
                     String title = cursor.getString(titleCol);
                     long date = cursor.getLong(dateCol);
                     int visits = cursor.getInt(visitsCol);
                     byte[] data = cursor.getBlob(faviconCol);
                     mDB.updateHistoryInBatch(mCr, mOperations, url, title, date, visits);
                     if (data != null) {
-                        mDB.updateFaviconInBatch(mCr, mOperations, url, null, null, data);
+                        storeBitmap(data, url);
                     }
                     ContentValues visitData = new ContentValues();
                     visitData.put(LocalBrowserDB.HISTORY_VISITS_DATE, date);
                     visitData.put(LocalBrowserDB.HISTORY_VISITS_URL, url);
                     visitData.put(LocalBrowserDB.HISTORY_VISITS_COUNT, visits);
                     visitsToSynthesize.add(visitData);
                     cursor.moveToNext();
                 }
@@ -159,16 +165,37 @@ public class AndroidImport implements Ru
 
         // Now that we have flushed history records, we need to synthesize individual visits. We have
         // gathered information about all of the visits we need to synthesize into visitsForSynthesis.
         mDB.insertVisitsFromImportHistoryInBatch(mCr, mOperations, visitsToSynthesize);
 
         flushBatchOperations();
     }
 
+    private void storeBitmap(byte[] data, String url) {
+        if (TextUtils.isEmpty(url) || data == null) {
+            return;
+        }
+
+        final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+        if (bitmap == null) {
+            return;
+        }
+
+        final String iconUrl = IconsHelper.guessDefaultFaviconURL(url);
+        if (iconUrl == null) {
+            return;
+        }
+
+        final DiskStorage storage = DiskStorage.get(mContext);
+
+        storage.putIcon(url, bitmap);
+        storage.putMapping(url, iconUrl);
+    }
+
     protected Cursor query(Uri mainUri, Uri fallbackUri, String condition) {
         final Cursor cursor = mCr.query(mainUri, null, condition, null, null);
         if (Build.MANUFACTURER.equals(SAMSUNG_MANUFACTURER) && (cursor == null || cursor.getCount() == 0)) {
             if (cursor != null) {
                 cursor.close();
             }
             return mCr.query(fallbackUri, null, null, null, null);
         }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
@@ -6,17 +6,20 @@
 package org.mozilla.gecko.preferences;
 
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.icons.storage.DiskStorage;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Set;
 
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.Log;
 
 class PrivateDataPreference extends MultiPrefMultiChoicePreference {
     private static final String LOGTAG = "GeckoPrivateDataPreference";
@@ -48,12 +51,17 @@ class PrivateDataPreference extends Mult
             final String key = value.substring(PREF_KEY_PREFIX.length());
             try {
                 json.put(key, true);
             } catch (JSONException e) {
                 Log.e(LOGTAG, "JSON error", e);
             }
         }
 
+        if (values.contains("private.data.offlineApps")) {
+            // Remove all icons from storage if removing "Offline website data" was selected.
+            DiskStorage.get(getContext()).evictAll();
+        }
+
         // clear private data in gecko
         GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
@@ -3,20 +3,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.preferences;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.favicons.Favicons;
-import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
-import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
-import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.widget.FaviconView;
 
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.support.design.widget.Snackbar;
@@ -61,17 +61,17 @@ public class SearchEnginePreference exte
 
         // We synchronise to avoid a race condition between this and the favicon loading callback in
         // setSearchEngineFromJSON.
         synchronized (bitmapLock) {
             // Set the icon in the FaviconView.
             mFaviconView = ((FaviconView) view.findViewById(R.id.search_engine_icon));
 
             if (mIconBitmap != null) {
-                mFaviconView.updateAndScaleImage(mIconBitmap, getTitle().toString());
+                mFaviconView.updateAndScaleImage(IconResponse.create(mIconBitmap));
             }
         }
     }
 
     @Override
     protected int getPreferenceLayoutResource() {
         return R.layout.preference_search_engine;
     }
@@ -154,45 +154,30 @@ public class SearchEnginePreference exte
         final String engineName = geckoEngineJSON.getString("name");
         final SpannableString titleSpannable = new SpannableString(engineName);
 
         setTitle(titleSpannable);
 
         final String iconURI = geckoEngineJSON.getString("iconURI");
         // Keep a reference to the bitmap - we'll need it later in onBindView.
         try {
-            final int desiredWidth;
-            if (mFaviconView != null) {
-                desiredWidth = mFaviconView.getWidth();
-            } else {
-                // largestFaviconSize is initialized when Favicons is attached to a
-                // context, which occurs during GeckoApp.onCreate. That might not
-                // ever happen (leaving it at 0), so we fall back.
-                if (Favicons.largestFaviconSize == 0) {
-                    desiredWidth = 128;
-                } else {
-                    desiredWidth = Favicons.largestFaviconSize;
-                }
-            }
-
-            Favicons.getSizedFavicon(getContext(), mIdentifier, iconURI,
-                Favicons.LoadType.PRIVILEGED, // We have an internal store of search engine icons, hence we're always loading PRIVILEGED icons here
-                desiredWidth, 0,
-                new OnFaviconLoadedListener() {
-                    @Override
-                    public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
-                        synchronized (bitmapLock) {
-                            mIconBitmap = favicon;
+            Icons.with(getContext())
+                    .pageUrl(mIdentifier)
+                    .icon(IconDescriptor.createGenericIcon(iconURI))
+                    .privileged(true)
+                    .build()
+                    .execute(new IconCallback() {
+                        @Override
+                        public void onIconResponse(IconResponse response) {
+                            mIconBitmap = response.getBitmap();
 
                             if (mFaviconView != null) {
-                                mFaviconView.updateAndScaleImage(mIconBitmap, getTitle().toString());
+                                mFaviconView.updateAndScaleImage(response);
                             }
                         }
-                    }
-                }
-            );
+                    });
         } catch (IllegalArgumentException e) {
             Log.e(LOGTAG, "IllegalArgumentException creating Bitmap. Most likely a zero-length bitmap.", e);
         } catch (NullPointerException e) {
             Log.e(LOGTAG, "NullPointerException creating Bitmap. Most likely a zero-length bitmap.", e);
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
@@ -21,25 +21,27 @@ import android.widget.TextView;
 
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.util.ThreadUtils;
 
 /**
  * Prompt to promote adding the current website to the home screen.
  */
-public class HomeScreenPrompt extends Locales.LocaleAwareActivity implements OnFaviconLoadedListener {
+public class HomeScreenPrompt extends Locales.LocaleAwareActivity implements IconCallback {
     private static final String EXTRA_TITLE = "title";
     private static final String EXTRA_URL = "url";
 
     private static final String TELEMETRY_EXTRA = "home_screen_promotion";
 
     private View containerView;
     private ImageView iconView;
     private String title;
@@ -136,22 +138,23 @@ public class HomeScreenPrompt extends Lo
         intent.addCategory(Intent.CATEGORY_HOME);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         startActivity(intent);
 
         finish();
     }
 
     private void loadShortcutIcon() {
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                Favicons.getPreferredIconForHomeScreenShortcut(HomeScreenPrompt.this, url, HomeScreenPrompt.this);
-            }
-        });
+        Icons.with(this)
+                .pageUrl(url)
+                .skipNetwork()
+                .skipMemory()
+                .forLauncherIcon()
+                .build()
+                .execute(this);
     }
 
     private void slideIn() {
         containerView.setTranslationY(500);
         containerView.setAlpha(0);
 
         final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
         translateAnimator.setDuration(400);
@@ -232,21 +235,12 @@ public class HomeScreenPrompt extends Lo
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         onDecline();
 
         return true;
     }
 
     @Override
-    public void onFaviconLoaded(String url, String faviconURL, final Bitmap favicon) {
-        if (favicon == null) {
-            return;
-        }
-
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                iconView.setImageBitmap(favicon);
-            }
-        });
+    public void onIconResponse(IconResponse response) {
+        iconView.setImageBitmap(response.getBitmap());
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
@@ -7,26 +7,29 @@ package org.mozilla.gecko.reader;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.Context;
 import android.util.Log;
 
+import java.util.concurrent.ExecutionException;
+
 public final class ReadingListHelper implements NativeEventListener {
     private static final String LOGTAG = "GeckoReadingListHelper";
 
     protected final Context context;
     private final BrowserDB db;
 
     public ReadingListHelper(Context context, GeckoProfile profile) {
         this.context = context;
@@ -61,17 +64,45 @@ public final class ReadingListHelper imp
     /**
      * Gecko (ReaderMode) requests the page favicon to append to the
      * document head for display.
      */
     private void handleReaderModeFaviconRequest(final EventCallback callback, final String url) {
         (new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) {
             @Override
             public String doInBackground() {
-                return Favicons.getFaviconURLForPageURL(db, context.getContentResolver(), url);
+                // This is a bit ridiculous if you look at the bigger picture: Reader mode extracts
+                // the article content. We insert the content into a new document (about:reader).
+                // Some events are exchanged to lookup the icon URL for the actual website. This
+                // URL is then added to the markup which will then trigger our icon loading code in
+                // the Tab class.
+                //
+                // The Tab class could just lookup and load the icon itself. All it needs to do is
+                // to strip the about:reader URL and perform a normal icon load from cache.
+                //
+                // A more global solution (looking at desktop and iOS) would be to copy the <link>
+                // markup from the original page to the about:reader page and then rely on our normal
+                // icon loading code. This would work even if we do not have anything in the cache
+                // for some kind of reason.
+
+                final IconRequest request = Icons.with(context)
+                        .pageUrl(url)
+                        .prepareOnly()
+                        .build();
+
+                try {
+                    request.execute(null).get();
+                    if (request.getIconCount() > 0) {
+                        return request.getBestIcon().getUrl();
+                    }
+                } catch (InterruptedException | ExecutionException e) {
+                    // Ignore
+                }
+
+                return null;
             }
 
             @Override
             public void onPostExecute(String faviconUrl) {
                 JSONObject args = new JSONObject();
                 if (faviconUrl != null) {
                     try {
                         args.put("url", url);
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
@@ -1,50 +1,45 @@
 /* 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.tabs;
 
-import java.lang.ref.WeakReference;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.favicons.Favicons;
-import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.FaviconView;
-
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.graphics.Typeface;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.FaviconView;
+
+import java.util.concurrent.Future;
+
 public class TabHistoryItemRow extends RelativeLayout {
     private final FaviconView favicon;
     private final TextView title;
     private final ImageView timeLineTop;
     private final ImageView timeLineBottom;
-    // Listener for handling Favicon loads.
-    private final OnFaviconLoadedListener faviconListener;
-
-    private int loadFaviconJobId = Favicons.NOT_LOADING;
+    private Future<IconResponse> ongoingIconLoad;
 
     public TabHistoryItemRow(Context context, AttributeSet attrs) {
         super(context, attrs);
         LayoutInflater.from(context).inflate(R.layout.tab_history_item_row, this);
         favicon = (FaviconView) findViewById(R.id.tab_history_icon);
         title = (TextView) findViewById(R.id.tab_history_title);
         timeLineTop = (ImageView) findViewById(R.id.tab_history_timeline_top);
         timeLineBottom = (ImageView) findViewById(R.id.tab_history_timeline_bottom);
-        faviconListener = new UpdateViewFaviconLoadedListener(favicon);
     }
 
     // Update the views with historic page detail.
     public void update(final TabHistoryPage historyPage, boolean isFirstElement, boolean isLastElement) {
         ThreadUtils.assertOnUiThread();
 
         timeLineTop.setVisibility(isFirstElement ? View.INVISIBLE : View.VISIBLE);
         timeLineBottom.setVisibility(isLastElement ? View.INVISIBLE : View.VISIBLE);
@@ -55,41 +50,20 @@ public class TabHistoryItemRow extends R
             title.setTypeface(null, Typeface.BOLD);
         } else {
             // Clear previously set bold font.
             title.setTypeface(null, Typeface.NORMAL);
         }
 
         favicon.setEnabled(historyPage.isSelected());
         favicon.clearImage();
-        Favicons.cancelFaviconLoad(loadFaviconJobId);
-        loadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), historyPage.getUrl(), faviconListener);
-    }
 
-    // Only holds a reference to the FaviconView itself, so if the row gets
-    // discarded while a task is outstanding, we'll leak less memory.
-    private static class UpdateViewFaviconLoadedListener implements OnFaviconLoadedListener {
-        private final WeakReference<FaviconView> view;
-        public UpdateViewFaviconLoadedListener(FaviconView view) {
-            this.view = new WeakReference<FaviconView>(view);
+        if (ongoingIconLoad != null) {
+            ongoingIconLoad.cancel(true);
         }
 
-        /**
-         * Update this row's favicon.
-         * <p>
-         * This method is always invoked on the UI thread.
-         */
-        @Override
-        public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
-            FaviconView v = view.get();
-            if (v == null) {
-                return;
-            }
-
-            if (favicon == null) {
-                v.showDefaultFavicon(url);
-                return;
-            }
-
-            v.updateImage(favicon, faviconURL);
-        }
+        ongoingIconLoad = Icons.with(getContext())
+                .pageUrl(historyPage.getUrl())
+                .skipNetwork()
+                .build()
+                .execute(favicon.createIconCallback());
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
@@ -2,31 +2,36 @@
 /* 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.widget;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.favicons.FaviconGenerator;
-import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.icons.IconCallback;
+import org.mozilla.gecko.icons.IconResponse;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.RectF;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.TypedValue;
+import android.view.View;
 import android.widget.ImageView;
+
+import java.lang.ref.WeakReference;
+
 /**
  * Special version of ImageView for favicons.
  * Displays solid colour background around Favicon to fill space not occupied by the icon. Colour
  * selected is the dominant colour of the provided Favicon.
  */
 public class FaviconView extends ImageView {
     private static final String LOGTAG = "GeckoFaviconView";
 
@@ -36,21 +41,16 @@ public class FaviconView extends ImageVi
     private static final int DEFAULT_CORNER_RADIUS_DP = 4;
 
     private Bitmap mIconBitmap;
 
     // Reference to the unscaled bitmap, if any, to prevent repeated assignments of the same bitmap
     // to the view from causing repeated rescalings (Some of the callers do this)
     private Bitmap mUnscaledBitmap;
 
-    // Key into the Favicon dominant colour cache. Should be the Favicon URL if the image displayed
-    // here is a Favicon managed by the caching system. If not, any appropriately unique-to-this-image
-    // string is acceptable.
-    private String mIconKey;
-
     private int mActualWidth;
     private int mActualHeight;
 
     // Flag indicating if the most recently assigned image is considered likely to need scaling.
     private boolean mScalingExpected;
 
     // Dominant color of the favicon.
     private int mDominantColor;
@@ -145,22 +145,17 @@ public class FaviconView extends ImageVi
         }
 
         setImageBitmap(mIconBitmap);
 
         // After scaling, determine if we have empty space around the scaled image which we need to
         // fill with the coloured background. If applicable, show it.
         // We assume Favicons are still squares and only bother with the background if more than 3px
         // of it would be displayed.
-        if (Math.abs(mIconBitmap.getWidth() - mActualWidth) > 3) {
-            mDominantColor = Favicons.getFaviconColor(mIconKey);
-            if (mDominantColor == -1) {
-                mDominantColor = 0;
-            }
-        } else {
+        if (Math.abs(mIconBitmap.getWidth() - mActualWidth) < 3) {
             mDominantColor = 0;
         }
     }
 
     private void scaleBitmap() {
         // If the Favicon can be resized to fill the view exactly without an enlargment of more than
         // a factor of two, do so.
         int doubledSize = mIconBitmap.getWidth() * 2;
@@ -176,105 +171,95 @@ public class FaviconView extends ImageVi
 
     /**
      * Sets the icon displayed in this Favicon view to the bitmap provided. If the size of the view
      * has been set, the display will be updated right away, otherwise the update will be deferred
      * until then. The key provided is used to cache the result of the calculation of the dominant
      * colour of the provided image - this value is used to draw the coloured background in this view
      * if the icon is not large enough to fill it.
      *
-     * @param bitmap favicon image
-     * @param key string used as a key to cache the dominant color of this image
      * @param allowScaling If true, allows the provided bitmap to be scaled by this FaviconView.
      *                     Typically, you should prefer using Favicons obtained via the caching system
      *                     (Favicons class), so as to exploit caching.
      */
-    private void updateImageInternal(Bitmap bitmap, String key, boolean allowScaling) {
-        if (bitmap == null) {
-            Log.w(LOGTAG, "updateImageInternal() called without bitmap");
-
-            // At this point we do not have a page URL anymore.
-            showDefaultFavicon(null);
+    private void updateImageInternal(IconResponse response, boolean allowScaling) {
+        // Reassigning the same bitmap? Don't bother.
+        if (mUnscaledBitmap == response.getBitmap()) {
             return;
         }
-
-        // Reassigning the same bitmap? Don't bother.
-        if (mUnscaledBitmap == bitmap) {
-            return;
-        }
-        mUnscaledBitmap = bitmap;
-        mIconBitmap = bitmap;
-        mIconKey = key;
+        mUnscaledBitmap = response.getBitmap();
+        mIconBitmap = response.getBitmap();
+        mDominantColor = response.getColor();
         mScalingExpected = allowScaling;
 
         // Possibly update the display.
         formatImage();
     }
 
-    public void showDefaultFavicon(final String pageURL) {
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                final Bitmap favicon = FaviconGenerator.generate(getContext(), pageURL).bitmap;
-
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        // We handle the default favicon as any other favicon to avoid the complications of special
-                        // casing it. This means that the icon can be scaled both up and down, and the dominant
-                        // color box can used if it is enabled in XML attrs.
-                        updateAndScaleImage(favicon, DEFAULT_FAVICON_KEY);
-                    }
-                });
-            }
-        });
-    }
-
     private void showNoImage() {
         setImageDrawable(null);
         mDominantColor = 0;
     }
 
     /**
      * Clear image and background shown by this view.
      */
     public void clearImage() {
         showNoImage();
         mUnscaledBitmap = null;
         mIconBitmap = null;
-        mIconKey = null;
+        mDominantColor = 0;
         mScalingExpected = false;
     }
 
     /**
      * Update the displayed image and apply the scaling logic.
      * The scaling logic will attempt to resize the image to fit correctly inside the view in a way
      * that avoids unreasonable levels of loss of quality.
      * Scaling is necessary only when the icon being provided is not drawn from the Favicon cache
      * introduced in Bug 914296.
      *
      * Due to Bug 913746, icons bundled for search engines are not available to the cache, so must
      * always have the scaling logic applied here. At the time of writing, this is the only case in
      * which the scaling logic here is applied.
-     *
-     * @param bitmap The bitmap to display in this favicon view.
-     * @param key The key to use into the dominant colours cache when selecting a background colour.
      */
-    public void updateAndScaleImage(Bitmap bitmap, String key) {
-        updateImageInternal(bitmap, key, true);
+    public void updateAndScaleImage(IconResponse response) {
+        updateImageInternal(response, true);
     }
 
     /**
      * Update the image displayed in the Favicon view without scaling. Images larger than the view
      * will be centrally cropped. Images smaller than the view will be placed centrally and the
      * extra space filled with the dominant colour of the provided image.
-     *
-     * @param bitmap The bitmap to display in this favicon view.
-     * @param key The key to use into the dominant colours cache when selecting a background colour.
      */
-    public void updateImage(Bitmap bitmap, String key) {
-        updateImageInternal(bitmap, key, false);
+    public void updateImage(IconResponse response) {
+        updateImageInternal(response, false);
     }
 
     public Bitmap getBitmap() {
         return mIconBitmap;
     }
+
+    /**
+     * Create an IconCallback implementation that will update this view after an icon has been loaded.
+     */
+    public IconCallback createIconCallback() {
+        return new Callback(this);
+    }
+
+    private static class Callback implements IconCallback {
+        private final WeakReference<FaviconView> viewReference;
+
+        private Callback(FaviconView view) {
+            this.viewReference = new WeakReference<FaviconView>(view);
+        }
+
+        @Override
+        public void onIconResponse(IconResponse response) {
+            final FaviconView view = viewReference.get();
+            if (view == null) {
+                return;
+            }
+
+            view.updateImage(response);
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java
@@ -4,39 +4,36 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
 import android.text.Html;
 import android.text.Spanned;
+import android.text.TextUtils;
 import android.text.method.PasswordTransformationMethod;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.CompoundButton;
 import android.widget.EditText;
 import android.widget.TextView;
 import android.widget.Toast;
-import ch.boye.httpclientandroidlib.util.TextUtils;
+
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
-import org.json.JSONArray;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.favicons.Favicons;
-import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 
 import java.util.Locale;
 
 public class LoginDoorHanger extends DoorHanger {
     private static final String LOGTAG = "LoginDoorHanger";
     private enum ActionType { EDIT, SELECT }
 
     private final TextView mMessage;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -3994,16 +3994,17 @@ Tab.prototype = {
 
         // Sanitize rel link
         let list = this.sanitizeRelString(target.rel);
         if (list.indexOf("[icon]") != -1) {
           jsonMessage = this.makeFaviconMessage(target);
         } else if (list.indexOf("[apple-touch-icon]") != -1 ||
             list.indexOf("[apple-touch-icon-precomposed]") != -1) {
           jsonMessage = this.makeFaviconMessage(target);
+          jsonMessage['type'] = 'Link:Touchicon';
           this.addMetadata("touchIconList", jsonMessage.href, jsonMessage.size);
         } else if (list.indexOf("[alternate]") != -1 && aEvent.type == "DOMLinkAdded") {
           let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
           let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
 
           if (!isFeed)
             return;