Merge fx-team to m-c
authorWes Kocher <wkocher@mozilla.com>
Wed, 18 Sep 2013 18:00:34 -0700
changeset 161596 803189f35921518c69410de472fb6a37038b2254
parent 161581 1907e00d462411cdb0faa96ebb3d2abee2f7b690 (current diff)
parent 161595 a0cad0d1d7fcf358ad41bf74752694242a9e95a0 (diff)
child 161667 8baf25d5ed30bbfb70e3a352fd88ac8747505492
child 161679 121cd7e739989e99707974a1cd11fffaf36213ad
child 161694 c40bf17bbc630bab9d26531de27e4c5a2010e95d
child 170331 c6d3d1418c78d501386f2673d23302de4e02f3ae
child 177444 ef5453969fc767e4680b9594597ac7df788e9595
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone27.0a1
first release with
nightly linux32
803189f35921 / 27.0a1 / 20130919030202 / files
nightly linux64
803189f35921 / 27.0a1 / 20130919030202 / files
nightly mac
803189f35921 / 27.0a1 / 20130919030202 / files
nightly win32
803189f35921 / 27.0a1 / 20130919030202 / files
nightly win64
803189f35921 / 27.0a1 / 20130919030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c
mobile/android/base/home/FaviconsLoader.java
mobile/android/base/home/HomeCursorLoaderCallbacks.java
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1114,28 +1114,35 @@ var gBrowserInit = {
     gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false);
 
     // Initialize the download manager some time after the app starts so that
     // auto-resume downloads begin (such as after crashing or quitting with
     // active downloads) and speeds up the first-load of the download manager UI.
     // If the user manually opens the download manager before the timeout, the
     // downloads will start right away, and getting the service again won't hurt.
     setTimeout(function() {
-      let DownloadsCommon =
-        Cu.import("resource:///modules/DownloadsCommon.jsm", {}).DownloadsCommon;
-      if (DownloadsCommon.useJSTransfer) {
-        // Open the data link without initalizing nsIDownloadManager.
-        DownloadsCommon.initializeAllDataLinks();
-      } else {
-        // Initalizing nsIDownloadManager will trigger the data link.
-        Services.downloads;
+      try {
+        let DownloadsCommon =
+          Cu.import("resource:///modules/DownloadsCommon.jsm", {}).DownloadsCommon;
+        if (DownloadsCommon.useJSTransfer) {
+          // Open the data link without initalizing nsIDownloadManager.
+          DownloadsCommon.initializeAllDataLinks();
+          let DownloadsTaskbar =
+            Cu.import("resource:///modules/DownloadsTaskbar.jsm", {}).DownloadsTaskbar;
+          DownloadsTaskbar.registerIndicator(window);
+        } else {
+          // Initalizing nsIDownloadManager will trigger the data link.
+          Services.downloads;
+          let DownloadTaskbarProgress =
+            Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
+          DownloadTaskbarProgress.onBrowserWindowLoad(window);
+        }
+      } catch (ex) {
+        Cu.reportError(ex);
       }
-      let DownloadTaskbarProgress =
-        Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
-      DownloadTaskbarProgress.onBrowserWindowLoad(window);
     }, 10000);
 
     // The object handling the downloads indicator is also initialized here in the
     // delayed startup function, but the actual indicator element is not loaded
     // unless there are downloads to be displayed.
     DownloadsButton.initializeIndicator();
 
 #ifndef XP_MACOSX
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/src/DownloadsTaskbar.jsm
@@ -0,0 +1,180 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * Handles the download progress indicator in the taskbar.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "DownloadsTaskbar",
+];
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+                                  "resource:///modules/RecentWindow.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gWinTaskbar", function () {
+  if (!("@mozilla.org/windows-taskbar;1" in Cc)) {
+    return null;
+  }
+  let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"]
+                     .getService(Ci.nsIWinTaskbar);
+  return winTaskbar.available && winTaskbar;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gMacTaskbarProgress", function () {
+  return ("@mozilla.org/widget/macdocksupport;1" in Cc) &&
+         Cc["@mozilla.org/widget/macdocksupport;1"]
+           .getService(Ci.nsITaskbarProgress);
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadsTaskbar
+
+/**
+ * Handles the download progress indicator in the taskbar.
+ */
+this.DownloadsTaskbar = {
+  /**
+   * Underlying DownloadSummary providing the aggregate download information, or
+   * null if the indicator has never been initialized.
+   */
+  _summary: null,
+
+  /**
+   * nsITaskbarProgress object to which download information is dispatched.
+   * This can be null if the indicator has never been initialized or if the
+   * indicator is currently hidden on Windows.
+   */
+  _taskbarProgress: null,
+
+  /**
+   * This method is called after a new browser window is opened, and ensures
+   * that the download progress indicator is displayed in the taskbar.
+   *
+   * On Windows, the indicator is attached to the first browser window that
+   * calls this method.  When the window is closed, the indicator is moved to
+   * another browser window, if available, in no particular order.  When there
+   * are no browser windows visible, the indicator is hidden.
+   *
+   * On Mac OS X, the indicator is initialized globally when this method is
+   * called for the first time.  Subsequent calls have no effect.
+   *
+   * @param aBrowserWindow
+   *        nsIDOMWindow object of the newly opened browser window to which the
+   *        indicator may be attached.
+   */
+  registerIndicator: function (aBrowserWindow)
+  {
+    if (!this._taskbarProgress) {
+      if (gMacTaskbarProgress) {
+        // On Mac OS X, we have to register the global indicator only once.
+        this._taskbarProgress = gMacTaskbarProgress;
+        // Free the XPCOM reference on shutdown, to prevent detecting a leak.
+        Services.obs.addObserver(() => {
+          this._taskbarProgress = null;
+          gMacTaskbarProgress = null;
+        }, "quit-application-granted", false);
+      } else if (gWinTaskbar) {
+        // On Windows, the indicator is currently hidden because we have no
+        // previous browser window, thus we should attach the indicator now.
+        this._attachIndicator(aBrowserWindow);
+      } else {
+        // The taskbar indicator is not available on this platform.
+        return;
+      }
+    }
+
+    // Ensure that the DownloadSummary object will be created asynchronously.
+    if (!this._summary) {
+      Downloads.getSummary(Downloads.ALL).then(summary => {
+        // In case the method is re-entered, we simply ignore redundant
+        // invocations of the callback, instead of keeping separate state.
+        if (this._summary) {
+          return;
+        }
+        this._summary = summary;
+        return this._summary.addView(this);
+      }).then(null, Cu.reportError);
+    }
+  },
+
+  /**
+   * On Windows, attaches the taskbar indicator to the specified browser window.
+   */
+  _attachIndicator: function (aWindow)
+  {
+    // Activate the indicator on the specified window.
+    let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIWebNavigation)
+                          .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
+                          .QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIXULWindow).docShell;
+    this._taskbarProgress = gWinTaskbar.getTaskbarProgress(docShell);
+
+    // If the DownloadSummary object has already been created, we should update
+    // the state of the new indicator, otherwise it will be updated as soon as
+    // the DownloadSummary view is registered.
+    if (this._summary) {
+      this.onSummaryChanged();
+    }
+
+    aWindow.addEventListener("unload", () => {
+      // Locate another browser window, excluding the one being closed.
+      let browserWindow = RecentWindow.getMostRecentBrowserWindow();
+      if (browserWindow) {
+        // Move the progress indicator to the other browser window.
+        this._attachIndicator(browserWindow);
+      } else {
+        // The last browser window has been closed.  We remove the reference to
+        // the taskbar progress object so that the indicator will be registered
+        // again on the next browser window that is opened.
+        this._taskbarProgress = null;
+      }
+    }, false);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// DownloadSummary view
+
+  onSummaryChanged: function ()
+  {
+    // If the last browser window has been closed, we have no indicator anymore.
+    if (!this._taskbarProgress) {
+      return;
+    }
+
+    if (this._summary.allHaveStopped || this._summary.progressTotalBytes == 0) {
+      this._taskbarProgress.setProgressState(
+                               Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0);
+    } else {
+      // For a brief moment before completion, some download components may
+      // report more transferred bytes than the total number of bytes.  Thus,
+      // ensure that we never break the expectations of the progress indicator.
+      let progressCurrentBytes = Math.min(this._summary.progressTotalBytes,
+                                          this._summary.progressCurrentBytes);
+      this._taskbarProgress.setProgressState(
+                               Ci.nsITaskbarProgress.STATE_NORMAL,
+                               progressCurrentBytes,
+                               this._summary.progressTotalBytes);
+    }
+  },
+};
--- a/browser/components/downloads/src/moz.build
+++ b/browser/components/downloads/src/moz.build
@@ -8,10 +8,11 @@ EXTRA_COMPONENTS += [
     'BrowserDownloads.manifest',
     'DownloadsStartup.js',
     'DownloadsUI.js',
 ]
 
 EXTRA_JS_MODULES += [
     'DownloadsCommon.jsm',
     'DownloadsLogger.jsm',
+    'DownloadsTaskbar.jsm',
 ]
 
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -210,39 +210,32 @@
 
 <!-- Tab preferences -->
 <groupbox data-category="paneGeneral" hidden="true">
     <caption label="&tabsGroup.label;"/>
     <checkbox id="linkTargeting" label="&newWindowsAsTabs.label;"
               accesskey="&newWindowsAsTabs.accesskey;"
               preference="browser.link.open_newwindow"
               onsyncfrompreference="return gMainPane.readLinkTarget();"
-              onsynctopreference="return gMainPane.writeLinkTarget();"
-              class="indent"/>
+              onsynctopreference="return gMainPane.writeLinkTarget();"/>
 
     <checkbox id="warnCloseMultiple" label="&warnCloseMultipleTabs.label;"
               accesskey="&warnCloseMultipleTabs.accesskey;"
-              preference="browser.tabs.warnOnClose"
-              class="indent"/>
+              preference="browser.tabs.warnOnClose"/>
 
     <checkbox id="warnOpenMany" label="&warnOpenManyTabs.label;"
               accesskey="&warnOpenManyTabs.accesskey;"
-              preference="browser.tabs.warnOnOpen"
-              class="indent"/>
+              preference="browser.tabs.warnOnOpen"/>
 
     <checkbox id="restoreOnDemand" label="&restoreTabsOnDemand.label;"
               accesskey="&restoreTabsOnDemand.accesskey;"
-              preference="browser.sessionstore.restore_on_demand"
-              class="indent"/>
+              preference="browser.sessionstore.restore_on_demand"/>
 
     <checkbox id="switchToNewTabs" label="&switchToNewTabs.label;"
               accesskey="&switchToNewTabs.accesskey;"
-              preference="browser.tabs.loadInBackground"
-              class="indent"/>
+              preference="browser.tabs.loadInBackground"/>
 
 #ifdef XP_WIN
     <checkbox id="showTabsInTaskbar" label="&showTabsInTaskbar.label;"
               accesskey="&showTabsInTaskbar.accesskey;"
-              preference="browser.taskbar.previews.enable"
-              class="indent"/>
+              preference="browser.taskbar.previews.enable"/>
 #endif
-
 </groupbox>
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -17,16 +17,17 @@ MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_breakpoints-contextmenu.js \
 	browser_dbg_breakpoints-disabled-reload.js \
 	browser_dbg_breakpoints-editor.js \
 	browser_dbg_breakpoints-highlight.js \
 	browser_dbg_breakpoints-new-script.js \
 	browser_dbg_breakpoints-pane.js \
 	browser_dbg_chrome-debugging.js \
 	browser_dbg_clean-exit.js \
+	browser_dbg_clean-exit-window.js \
 	browser_dbg_cmd-blackbox.js \
 	browser_dbg_cmd-break.js \
 	browser_dbg_cmd-dbg.js \
 	browser_dbg_conditional-breakpoints-01.js \
 	browser_dbg_conditional-breakpoints-02.js \
 	browser_dbg_debugger-statement.js \
 	browser_dbg_editor-contextmenu.js \
 	browser_dbg_editor-mode.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_clean-exit-window.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that closing a window with the debugger in a paused state exits cleanly.
+ */
+
+let gDebuggee, gPanel, gDebugger, gWindow;
+
+const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html";
+
+function test() {
+  addWindow(TAB_URL)
+    .then(win => initDebugger(TAB_URL, win))
+    .then(([aTab, aDebuggee, aPanel, aWindow]) => {
+      gDebuggee = aDebuggee;
+      gPanel = aPanel;
+      gDebugger = gPanel.panelWin;
+      gWindow = aWindow;
+
+      return testCleanExit(gWindow);
+    })
+    .then(null, aError => {
+      ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+    });
+}
+
+function testCleanExit(aWindow) {
+  let deferred = promise.defer();
+
+  gWindow = aWindow;
+  ok(!!gWindow, "Second window created.");
+
+  gWindow.focus();
+
+  let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  is(topWindow, gWindow,
+    "The second window is on top.");
+
+  let isActive = promise.defer();
+  let isLoaded = promise.defer();
+
+  promise.all([isActive.promise, isLoaded.promise]).then(() => {
+    gWindow.BrowserChromeTest.runWhenReady(() => {
+      waitForSourceAndCaretAndScopes(gPanel, ".html", 16).then(() => {
+        is(gDebugger.gThreadClient.paused, true,
+          "Should be paused after the debugger statement.");
+        gWindow.close();
+        deferred.resolve();
+        finish();
+      });
+
+      gDebuggee.runDebuggerStatement();
+    });
+  });
+
+  let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+  if (focusManager.activeWindow != gWindow) {
+    gWindow.addEventListener("activate", function onActivate(aEvent) {
+      if (aEvent.target != gWindow) {
+        return;
+      }
+      gWindow.removeEventListener("activate", onActivate, true);
+      isActive.resolve();
+    }, true);
+  } else {
+    isActive.resolve();
+  }
+
+  let contentLocation = gWindow.content.location.href;
+  if (contentLocation != TAB_URL) {
+    gWindow.document.addEventListener("load", function onLoad(aEvent) {
+      if (aEvent.target.documentURI != TAB_URL) {
+        return;
+      }
+      gWindow.document.removeEventListener("load", onLoad, true);
+      isLoaded.resolve();
+    }, true);
+  } else {
+    isLoaded.resolve();
+  }
+  return deferred.promise;
+}
+
+registerCleanupFunction(function() {
+  gWindow = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+});
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -411,28 +411,28 @@ function typeText(aElement, aText) {
 function backspaceText(aElement, aTimes) {
   info("Pressing backspace " + aTimes + " times.");
   for (let i = 0; i < aTimes; i++) {
     aElement.focus();
     EventUtils.sendKey("BACK_SPACE", aElement.ownerDocument.defaultView);
   }
 }
 
-function getTab(aTarget) {
+function getTab(aTarget, aWindow) {
   if (aTarget instanceof XULElement) {
     return promise.resolve(aTarget);
   } else {
-    return addTab(aTarget);
+    return addTab(aTarget, aWindow);
   }
 }
 
 function initDebugger(aTarget, aWindow) {
   info("Initializing a debugger panel.");
 
-  return getTab(aTarget).then(aTab => {
+  return getTab(aTarget, aWindow).then(aTab => {
     info("Debugee tab added successfully: " + aTarget);
 
     let deferred = promise.defer();
     let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject;
     let target = TargetFactory.forTab(aTab);
 
     gDevTools.showToolbox(target, "jsdebugger").then(aToolbox => {
       info("Debugger panel shown successfully.");
@@ -440,17 +440,17 @@ function initDebugger(aTarget, aWindow) 
       let debuggerPanel = aToolbox.getCurrentPanel();
       let panelWin = debuggerPanel.panelWin;
 
       // Wait for the initial resume...
       panelWin.gClient.addOneTimeListener("resumed", () => {
         info("Debugger client resumed successfully.");
 
         prepareDebugger(debuggerPanel);
-        deferred.resolve([aTab, debuggee, debuggerPanel]);
+        deferred.resolve([aTab, debuggee, debuggerPanel, aWindow]);
       });
     });
 
     return deferred.promise;
   });
 }
 
 function initChromeDebugger(aOnClose) {
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -663,81 +663,95 @@ PropertyView.prototype = {
       return false;
     }
 
     return true;
   },
 
   /**
    * Returns the className that should be assigned to the propertyView.
-   *
    * @return string
    */
   get propertyHeaderClassName()
   {
     if (this.visible) {
-      this.tree._darkStripe = !this.tree._darkStripe;
-      let darkValue = this.tree._darkStripe ?
-                      "property-view theme-bg-darker" : "property-view";
-      return darkValue;
+      let isDark = this.tree._darkStripe = !this.tree._darkStripe;
+      return isDark ? "property-view theme-bg-darker" : "property-view";
     }
     return "property-view-hidden";
   },
 
   /**
    * Returns the className that should be assigned to the propertyView content
    * container.
    * @return string
    */
   get propertyContentClassName()
   {
     if (this.visible) {
-      let darkValue = this.tree._darkStripe ?
-                      "property-content theme-bg-darker" : "property-content";
-      return darkValue;
+      let isDark = this.tree._darkStripe;
+      return isDark ? "property-content theme-bg-darker" : "property-content";
     }
     return "property-content-hidden";
   },
 
+  /**
+   * Build the markup for on computed style
+   * @return Element
+   */
   buildMain: function PropertyView_buildMain()
   {
     let doc = this.tree.styleDocument;
+    let onToggle = this.onStyleToggle.bind(this);
+
+    // Build the container element
     this.element = doc.createElementNS(HTML_NS, "div");
     this.element.setAttribute("class", this.propertyHeaderClassName);
 
-    this.matchedExpander = doc.createElementNS(HTML_NS, "div");
-    this.matchedExpander.className = "expander theme-twisty";
-    this.matchedExpander.setAttribute("tabindex", "0");
-    this.matchedExpander.addEventListener("click",
-      this.matchedExpanderClick.bind(this), false);
-    this.matchedExpander.addEventListener("keydown", function(aEvent) {
+    // Make it keyboard navigable
+    this.element.setAttribute("tabindex", "0");
+    this.element.addEventListener("keydown", function(aEvent) {
       let keyEvent = Ci.nsIDOMKeyEvent;
       if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
         this.mdnLinkClick();
       }
       if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
         aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
-        this.matchedExpanderClick(aEvent);
+        onToggle(aEvent);
       }
     }.bind(this), false);
+
+    // Build the twisty expand/collapse
+    this.matchedExpander = doc.createElementNS(HTML_NS, "div");
+    this.matchedExpander.className = "expander theme-twisty";
+    this.matchedExpander.addEventListener("click", onToggle, false);
     this.element.appendChild(this.matchedExpander);
 
+    // Build the style name element
     this.nameNode = doc.createElementNS(HTML_NS, "div");
-    this.element.appendChild(this.nameNode);
     this.nameNode.setAttribute("class", "property-name theme-fg-color5");
+    // Reset its tabindex attribute otherwise, if an ellipsis is applied
+    // it will be reachable via TABing
+    this.nameNode.setAttribute("tabindex", "");
     this.nameNode.textContent = this.nameNode.title = this.name;
-    this.nameNode.addEventListener("click", function(aEvent) {
-      this.matchedExpander.focus();
-    }.bind(this), false);
+    // Make it hand over the focus to the container
+    this.nameNode.addEventListener("click", () => this.element.focus(), false);
+    this.element.appendChild(this.nameNode);
 
+    // Build the style value element
     this.valueNode = doc.createElementNS(HTML_NS, "div");
-    this.element.appendChild(this.valueNode);
     this.valueNode.setAttribute("class", "property-value theme-fg-color1");
+    // Reset its tabindex attribute otherwise, if an ellipsis is applied
+    // it will be reachable via TABing
+    this.valueNode.setAttribute("tabindex", "");
     this.valueNode.setAttribute("dir", "ltr");
     this.valueNode.textContent = this.valueNode.title = this.value;
+    // Make it hand over the focus to the container
+    this.valueNode.addEventListener("click", () => this.element.focus(), false);
+    this.element.appendChild(this.valueNode);
 
     return this.element;
   },
 
   buildSelectorContainer: function PropertyView_buildSelectorContainer()
   {
     let doc = this.tree.styleDocument;
     let element = doc.createElementNS(HTML_NS, "div");
@@ -831,17 +845,17 @@ PropertyView.prototype = {
   },
 
   /**
    * The action when a user expands matched selectors.
    *
    * @param {Event} aEvent Used to determine the class name of the targets click
    * event.
    */
-  matchedExpanderClick: function PropertyView_matchedExpanderClick(aEvent)
+  onStyleToggle: function PropertyView_onStyleToggle(aEvent)
   {
     this.matchedExpanded = !this.matchedExpanded;
     this.refreshMatchedSelectors();
     aEvent.preventDefault();
   },
 
   /**
    * The action when a user clicks on the MDN help link for a property.
--- a/browser/devtools/styleinspector/test/Makefile.in
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -31,16 +31,17 @@ MOCHITEST_BROWSER_FILES = \
   browser_computedview_copy.js\
   browser_styleinspector_bug_677930_urls_clickable.js \
   browser_bug893965_css_property_completion_new_property.js \
   browser_bug893965_css_property_completion_existing_property.js \
   browser_bug894376_css_value_completion_new_property_value_pair.js \
   browser_bug894376_css_value_completion_existing_property_value_pair.js \
   browser_ruleview_bug_902966_revert_value_on_ESC.js \
   browser_ruleview_pseudoelement.js \
+  browser_computedview_bug835808_keyboard_nav.js \
   head.js \
   $(NULL)
 
 MOCHITEST_BROWSER_FILES += \
   browser_bug683672.html \
   browser_bug705707_is_content_stylesheet.html \
   browser_bug705707_is_content_stylesheet_imported.css \
   browser_bug705707_is_content_stylesheet_imported2.css \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_computedview_bug835808_keyboard_nav.js
@@ -0,0 +1,94 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the style inspector works properly
+
+let doc, computedView, inspector;
+
+function test()
+{
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad(evt) {
+    gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true);
+    doc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,computed view context menu test";
+}
+
+function createDocument()
+{
+  doc.body.innerHTML = '<style type="text/css"> ' +
+    'span { font-variant: small-caps; color: #000000; } ' +
+    '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ' +
+    'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">\n' +
+    '<h1>Some header text</h1>\n' +
+    '<p id="salutation" style="font-size: 12pt">hi.</p>\n' +
+    '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ' +
+    'solely to provide some things to <span style="color: yellow">' +
+    'highlight</span> and <span style="font-weight: bold">count</span> ' +
+    'style list-items in the box at right. If you are reading this, ' +
+    'you should go do something else instead. Maybe read a book. Or better ' +
+    'yet, write some test-cases for another bit of code. ' +
+    '<span style="font-style: italic">some text</span></p>\n' +
+    '<p id="closing">more text</p>\n' +
+    '<p>even more text</p>' +
+    '</div>';
+  doc.title = "Computed view keyboard navigation test";
+
+  openComputedView(startTests);
+}
+
+function startTests(aInspector, aComputedView)
+{
+  computedView = aComputedView;
+  inspector = aInspector;
+  testTabThrougStyles();
+}
+
+function endTests()
+{
+  computedView = inspector = doc = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function testTabThrougStyles()
+{
+  let span = doc.querySelector("span");
+
+  inspector.once("computed-view-refreshed", () => {
+    // Selecting the first computed style in the list
+    let firstStyle = computedView.styleDocument.querySelector(".property-view");
+    ok(firstStyle, "First computed style found in panel");
+    firstStyle.focus();
+
+    // Tab to select the 2nd style, press return
+    EventUtils.synthesizeKey("VK_TAB", {});
+    EventUtils.synthesizeKey("VK_RETURN", {});
+    inspector.once("computed-view-property-expanded", () => {
+      // Verify the 2nd style has been expanded
+      let secondStyleSelectors = computedView.styleDocument.querySelectorAll(
+        ".property-content .matchedselectors")[1];
+      ok(secondStyleSelectors.childNodes.length > 0, "Matched selectors expanded");
+
+      // Tab back up and test the same thing, with space
+      EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+      EventUtils.synthesizeKey("VK_SPACE", {});
+      inspector.once("computed-view-property-expanded", () => {
+        // Verify the 1st style has been expanded too
+        let firstStyleSelectors = computedView.styleDocument.querySelectorAll(
+          ".property-content .matchedselectors")[0];
+        ok(firstStyleSelectors.childNodes.length > 0, "Matched selectors expanded");
+
+        endTests();
+      });
+    });
+  });
+
+  inspector.selection.setNode(span);
+}
--- a/browser/themes/linux/devtools/computedview.css
+++ b/browser/themes/linux/devtools/computedview.css
@@ -40,29 +40,31 @@ body {
   vertical-align: middle;
 }
 
 .property-name {
   width: 50%;
   overflow-x: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
+  outline: 0;
 }
 
 .property-value {
   width: 50%;
   max-width: 100%;
   overflow-x: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   background-image: url(arrow-e.png);
   background-repeat: no-repeat;
   background-size: 5px 8px;
   background-position: 2px center;
   padding-left: 10px;
+  outline: 0;
 }
 
 .other-property-value {
   background-image: url(arrow-e.png);
   background-repeat: no-repeat;
   background-size: 5px 8px;
   background-position: left center;
   padding-left: 8px;
--- a/browser/themes/osx/devtools/computedview.css
+++ b/browser/themes/osx/devtools/computedview.css
@@ -58,29 +58,31 @@ body {
   vertical-align: middle;
 }
 
 .property-name {
   width: 50%;
   overflow-x: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
+  outline: 0;
 }
 
 .property-value {
   width: 50%;
   max-width: 100%;
   overflow-x: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   background-image: url(arrow-e.png);
   background-repeat: no-repeat;
   background-size: 5px 8px;
   background-position: 2px center;
   padding-left: 10px;
+  outline: 0;
 }
 
 .other-property-value {
   background-image: url(arrow-e.png);
   background-repeat: no-repeat;
   background-size: 5px 8px;
   background-position: left center;
   padding-left: 8px;
--- a/browser/themes/windows/devtools/computedview.css
+++ b/browser/themes/windows/devtools/computedview.css
@@ -58,29 +58,31 @@ body {
   vertical-align: middle;
 }
 
 .property-name {
   width: 50%;
   overflow-x: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
+  outline: 0;
 }
 
 .property-value {
   width: 50%;
   max-width: 100%;
   overflow-x: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   background-image: url(arrow-e.png);
   background-repeat: no-repeat;
   background-size: 5px 8px;
   background-position: 2px center;
   padding-left: 10px;
+  outline: 0;
 }
 
 .other-property-value {
   background-image: url(arrow-e.png);
   background-repeat: no-repeat;
   background-size: 5px 8px;
   background-position: left center;
   padding-left: 8px;
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -2252,17 +2252,17 @@ public class GeckoAppShell
      * will be invoked on future events of that type.
      *
      * This method is referenced by Robocop via reflection.
      */
     public static void registerEventListener(String event, GeckoEventListener listener) {
         sEventDispatcher.registerEventListener(event, listener);
     }
 
-    static EventDispatcher getEventDispatcher() {
+    public static EventDispatcher getEventDispatcher() {
         return sEventDispatcher;
     }
 
     /**
      * Remove a previously-registered listener for a gecko event.
      * This method is thread-safe and may be called at any time. In particular, calling it
      * with an event that is currently being processed has the properly-defined behaviour that
      * any removed listeners will still be invoked on the event currently being processed, but
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -215,24 +215,22 @@ FENNEC_JAVA_FILES = \
   gfx/VirtualLayer.java \
   home/BookmarksListAdapter.java \
   home/BookmarksListView.java \
   home/BookmarksPage.java \
   home/BookmarkFolderView.java \
   home/BookmarkThumbnailView.java \
   home/BrowserSearch.java \
   home/HistoryPage.java \
-  home/HomeCursorLoaderCallbacks.java \
   home/HomeFragment.java \
   home/HomeListView.java \
   home/HomePager.java \
   home/HomePagerTabStrip.java \
   home/HomeBanner.java \
   home/FadedTextView.java \
-  home/FaviconsLoader.java \
   home/LastTabsPage.java \
   home/MostRecentPage.java \
   home/MostVisitedPage.java \
   home/MultiTypeCursorAdapter.java \
   home/PinBookmarkDialog.java \
   home/ReadingListPage.java \
   home/SearchEngine.java \
   home/SearchEngineRow.java \
--- a/mobile/android/base/android-services-files.mk
+++ b/mobile/android/base/android-services-files.mk
@@ -20,16 +20,17 @@ SYNC_JAVA_FILES := \
   background/announcements/AnnouncementsFetchResourceDelegate.java \
   background/announcements/AnnouncementsService.java \
   background/announcements/AnnouncementsStartReceiver.java \
   background/BackgroundService.java \
   background/bagheera/BagheeraClient.java \
   background/bagheera/BagheeraRequestDelegate.java \
   background/bagheera/BoundedByteArrayEntity.java \
   background/bagheera/DeflateHelper.java \
+  background/common/DateUtils.java \
   background/common/log/Logger.java \
   background/common/log/writers/AndroidLevelCachingLogWriter.java \
   background/common/log/writers/AndroidLogWriter.java \
   background/common/log/writers/LevelFilteringLogWriter.java \
   background/common/log/writers/LogWriter.java \
   background/common/log/writers/PrintLogWriter.java \
   background/common/log/writers/SimpleTagLogWriter.java \
   background/common/log/writers/StringLogWriter.java \
--- a/mobile/android/base/animation/PropertyAnimator.java
+++ b/mobile/android/base/animation/PropertyAnimator.java
@@ -155,22 +155,27 @@ public class PropertyAnimator implements
         final ViewTreeObserver treeObserver;
         if (mElementsList.size() > 0) {
             treeObserver = mElementsList.get(0).view.getViewTreeObserver();
         } else {
             treeObserver = null;
         }
 
         // Try to start animation after any on-going layout round
-        // in the current view tree.
-        if (treeObserver != null && treeObserver.isAlive()) {
+        // in the current view tree. OnPreDrawListener seems broken
+        // on pre-Honeycomb devices, start animation immediatelly
+        // in this case.
+        if (Build.VERSION.SDK_INT >= 11 && treeObserver != null && treeObserver.isAlive()) {
             treeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                 @Override
                 public boolean onPreDraw() {
-                    treeObserver.removeOnPreDrawListener(this);
+                    if (treeObserver.isAlive()) {
+                        treeObserver.removeOnPreDrawListener(this);
+                    }
+
                     mFramePoster.postFirstAnimationFrame();
                     return true;
                 }
             });
         } else {
             mFramePoster.postFirstAnimationFrame();
         }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/common/DateUtils.java
@@ -0,0 +1,43 @@
+/* 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.background.common;
+
+import java.util.Calendar;
+import java.util.Formatter;
+import java.util.TimeZone;
+
+public class DateUtils {
+  private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
+
+  public static final class DateFormatter {
+    private final Calendar calendar;
+    private final Formatter formatter;
+    private final StringBuilder builder;
+
+    public DateFormatter() {
+      this.calendar = Calendar.getInstance(UTC);
+      this.builder = new StringBuilder();              // So we can reset it.
+      this.formatter = new Formatter(this.builder, null);
+    }
+
+    public String getDateString(long time) {
+      calendar.setTimeInMillis(time);
+      builder.setLength(0);
+      return formatter.format("%04d-%02d-%02d",
+                              calendar.get(Calendar.YEAR),
+                              calendar.get(Calendar.MONTH) + 1,      // 0-indexed.
+                              calendar.get(Calendar.DAY_OF_MONTH))
+                      .toString();
+    }
+
+    public String getDateStringForDay(long day) {
+      return getDateString(GlobalConstants.MILLISECONDS_PER_DAY * day);
+    }
+  }
+
+  public static int getDay(final long time) {
+    return (int) Math.floor(time / GlobalConstants.MILLISECONDS_PER_DAY);
+  }
+}
--- a/mobile/android/base/background/common/GlobalConstants.java.in
+++ b/mobile/android/base/background/common/GlobalConstants.java.in
@@ -42,9 +42,13 @@ public class GlobalConstants {
   // These are used to ask Fennec (via reflection) to send
   // us a pref notification. This avoids us having to guess
   // Fennec's prefs branch and pref name.
   // Eventually Fennec might listen to startup notifications and
   // do this automatically, but this will do for now. See Bug 800244.
   public static String GECKO_PREFERENCES_CLASS = "org.mozilla.gecko.GeckoPreferences";
   public static String GECKO_BROADCAST_ANNOUNCEMENTS_PREF_METHOD  = "broadcastAnnouncementsPref";
   public static String GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD  = "broadcastHealthReportUploadPref";
+
+  // Common time values.
+  public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+  public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;
 }
--- a/mobile/android/base/background/healthreport/EnvironmentBuilder.java
+++ b/mobile/android/base/background/healthreport/EnvironmentBuilder.java
@@ -4,16 +4,17 @@
 
 package org.mozilla.gecko.background.healthreport;
 
 import java.util.Iterator;
 
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.SysInfo;
+import org.mozilla.gecko.background.common.GlobalConstants;
 import org.mozilla.gecko.background.common.log.Logger;
 
 import android.content.ContentProvider;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 
 /**
@@ -72,17 +73,17 @@ public class EnvironmentBuilder {
     e.platformVersion = AppConstants.MOZILLA_VERSION;
     e.platformBuildID = AppConstants.MOZ_APP_BUILDID;
     e.xpcomabi = AppConstants.TARGET_XPCOM_ABI;
     e.os = "Android";
     e.architecture = SysInfo.getArchABI();       // Not just "arm".
     e.sysName = SysInfo.getName();
     e.sysVersion = SysInfo.getReleaseVersion();
 
-    e.profileCreation = (int) (info.getProfileCreationTime() / HealthReportConstants.MILLISECONDS_PER_DAY);
+    e.profileCreation = (int) (info.getProfileCreationTime() / GlobalConstants.MILLISECONDS_PER_DAY);
 
     // Corresponds to Gecko pref "extensions.blocklist.enabled".
     e.isBlocklistEnabled = (info.isBlocklistEnabled() ? 1 : 0);
 
     // Corresponds to one of two Gecko telemetry prefs. We reflect these into
     // GeckoPreferences as "datareporting.telemetry.enabled".
     e.isTelemetryEnabled = (info.isTelemetryEnabled() ? 1 : 0);
 
--- a/mobile/android/base/background/healthreport/HealthReportConstants.java.in
+++ b/mobile/android/base/background/healthreport/HealthReportConstants.java.in
@@ -1,48 +1,47 @@
 #filter substitution
 /* 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.background.healthreport;
 
+import org.mozilla.gecko.background.common.GlobalConstants;
+
 public class HealthReportConstants {
   public static final String HEALTH_AUTHORITY = "@ANDROID_PACKAGE_NAME@.health";
   public static final String GLOBAL_LOG_TAG = "GeckoHealth";
 
   /**
    * The earliest allowable value for the last ping time, corresponding to May 2nd 2013.
    * Used for sanity checks.
    */
   public static final long EARLIEST_LAST_PING = 1367500000000L;
 
-  public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
-  public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;
-
   // Not `final` so we have the option to turn this on at runtime with a magic addon.
   public static boolean UPLOAD_FEATURE_DISABLED = false;
 
   // Android SharedPreferences branch where global (not per-profile) uploader
   // settings are stored.
   public static final String PREFS_BRANCH = "background";
 
   // How frequently the submission policy is ticked over. This is how frequently our
   // intent is scheduled to be called by the Android Alarm Manager, not how
   // frequently we actually submit.
   public static final String PREF_SUBMISSION_INTENT_INTERVAL_MSEC = "healthreport_submission_intent_interval_msec";
-  public static final long DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC = MILLISECONDS_PER_DAY / 24;
+  public static final long DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY / 24;
 
   public static final String ACTION_HEALTHREPORT_UPLOAD_PREF = "@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF";
 
   public static final String PREF_MINIMUM_TIME_BETWEEN_UPLOADS = "healthreport_time_between_uploads";
-  public static final long DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS = MILLISECONDS_PER_DAY;
+  public static final long DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS = GlobalConstants.MILLISECONDS_PER_DAY;
 
   public static final String PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = "healthreport_time_before_first_submission";
-  public static final long DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = MILLISECONDS_PER_DAY;
+  public static final long DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = GlobalConstants.MILLISECONDS_PER_DAY;
 
   public static final String PREF_MINIMUM_TIME_AFTER_FAILURE = "healthreport_time_after_failure";
   public static final long DEFAULT_MINIMUM_TIME_AFTER_FAILURE = DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC;
 
   public static final String PREF_MAXIMUM_FAILURES_PER_DAY = "healthreport_maximum_failures_per_day";
   public static final long DEFAULT_MAXIMUM_FAILURES_PER_DAY = 2;
 
   // Authoritative.
--- a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
+++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.background.hea
 import java.io.File;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 import org.json.JSONObject;
+import org.mozilla.gecko.background.common.DateUtils;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
 
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.database.Cursor;
 import android.database.SQLException;
@@ -1024,17 +1025,17 @@ public class HealthReportDatabaseStorage
   }
 
   public void abortInitialization() {
     this.helper.getWritableDatabase().endTransaction();
   }
 
   @Override
   public int getDay(long time) {
-    return HealthReportUtils.getDay(time);
+    return DateUtils.getDay(time);
   }
 
   @Override
   public int getDay() {
     return this.getDay(System.currentTimeMillis());
   }
 
   private void recordDailyLast(int env, int day, int field, Object value, String table) {
--- a/mobile/android/base/background/healthreport/HealthReportGenerator.java
+++ b/mobile/android/base/background/healthreport/HealthReportGenerator.java
@@ -5,31 +5,34 @@
 package org.mozilla.gecko.background.healthreport;
 
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
 
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.background.common.DateUtils.DateFormatter;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
 
 import android.database.Cursor;
 import android.util.SparseArray;
 
 public class HealthReportGenerator {
   private static final int PAYLOAD_VERSION = 3;
 
   private static final String LOG_TAG = "GeckoHealthGen";
 
   private final HealthReportStorage storage;
+  private final DateFormatter dateFormatter;
 
   public HealthReportGenerator(HealthReportStorage storage) {
     this.storage = storage;
+    this.dateFormatter = new DateFormatter();
   }
 
   @SuppressWarnings("static-method")
   protected long now() {
     return System.currentTimeMillis();
   }
 
   /**
@@ -71,20 +74,20 @@ public class HealthReportGenerator {
     }
 
     // We want to map field IDs to some strings as we go.
     SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
 
     JSONObject document = new JSONObject();
 
     if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) {
-      document.put("lastPingDate", HealthReportUtils.getDateString(lastPingTime));
+      document.put("lastPingDate", dateFormatter.getDateString(lastPingTime));
     }
 
-    document.put("thisPingDate", HealthReportUtils.getDateString(now()));
+    document.put("thisPingDate", dateFormatter.getDateString(now()));
     document.put("version", PAYLOAD_VERSION);
 
     document.put("environments", getEnvironmentsJSON(currentEnvironment, envs));
     document.put("data", getDataJSON(currentEnvironment, envs, since));
 
     return document;
   }
 
@@ -142,17 +145,17 @@ public class HealthReportGenerator {
         int cField = cursor.getInt(2);
 
         Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField);
         boolean dateChanged = cDate != lastDate;
         boolean envChanged = cEnv != lastEnv;
 
         if (dateChanged) {
           if (dateObject != null) {
-            days.put(HealthReportUtils.getDateStringForDay(lastDate), dateObject);
+            days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
           }
           dateObject = new JSONObject();
           lastDate = cDate;
         }
 
         if (dateChanged || envChanged) {
           envObject = new JSONObject();
           // This is safe because we checked above that cEnv is valid.
@@ -174,17 +177,17 @@ public class HealthReportGenerator {
 
         // How we record depends on the type of the field, so we
         // break this out into a separate method for clarity.
         recordMeasurementFromCursor(field, measurement, cursor);
 
         cursor.moveToNext();
         continue;
       }
-      days.put(HealthReportUtils.getDateStringForDay(lastDate), dateObject);
+      days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
     } finally {
       cursor.close();
     }
     return days;
   }
 
   /**
    * Return the {@link JSONObject} parsed from the provided index of the given
--- a/mobile/android/base/background/healthreport/HealthReportUtils.java
+++ b/mobile/android/base/background/healthreport/HealthReportUtils.java
@@ -1,53 +1,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.background.healthreport;
 
-import java.text.SimpleDateFormat;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.Locale;
 import java.util.Set;
 import java.util.SortedSet;
-import java.util.TimeZone;
 import java.util.TreeSet;
 import java.util.UUID;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.apache.commons.codec.digest.DigestUtils;
 
 import android.content.ContentUris;
 import android.net.Uri;
 
 public class HealthReportUtils {
   public static final String LOG_TAG = HealthReportUtils.class.getSimpleName();
 
-  public static int getDay(final long time) {
-    return (int) Math.floor(time / HealthReportConstants.MILLISECONDS_PER_DAY);
-  }
-
   public static String getEnvironmentHash(final String input) {
     return DigestUtils.shaHex(input);
   }
 
-  public static String getDateStringForDay(long day) {
-    return getDateString(HealthReportConstants.MILLISECONDS_PER_DAY * day);
-  }
-
-  public static String getDateString(long time) {
-    final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
-    format.setTimeZone(TimeZone.getTimeZone("UTC"));
-    return format.format(time);
-  }
-
   /**
    * Take an environment URI (one that identifies an environment) and produce an
    * event URI.
    *
    * That this is needed is tragic.
    *
    * @param environmentURI
    *          the {@link Uri} returned by an environment operation.
--- a/mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java
+++ b/mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java
@@ -5,16 +5,17 @@
 package org.mozilla.gecko.background.healthreport.upload;
 
 import java.io.IOException;
 import java.util.Collection;
 
 import org.json.JSONObject;
 import org.mozilla.gecko.background.bagheera.BagheeraClient;
 import org.mozilla.gecko.background.bagheera.BagheeraRequestDelegate;
+import org.mozilla.gecko.background.common.GlobalConstants;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
 import org.mozilla.gecko.background.healthreport.HealthReportConstants;
 import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
 import org.mozilla.gecko.background.healthreport.HealthReportGenerator;
 import org.mozilla.gecko.sync.net.BaseResource;
 
 import android.content.ContentProviderClient;
@@ -98,17 +99,17 @@ public class AndroidSubmissionClient imp
       // close it. It's worth noting that this call will fail if called
       // out-of-process.
       HealthReportDatabaseStorage storage = EnvironmentBuilder.getStorage(client, profilePath);
       if (storage == null) {
         delegate.onHardFailure(localTime, null, "No storage when generating report.", null);
         return;
       }
 
-      long since = localTime - HealthReportConstants.MILLISECONDS_PER_SIX_MONTHS;
+      long since = localTime - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
       long last = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING);
 
       if (!storage.hasEventSince(last)) {
         delegate.onHardFailure(localTime, null, "No new events in storage.", null);
         return;
       }
 
       HealthReportGenerator generator = new HealthReportGenerator(storage);
--- a/mobile/android/base/health/BrowserHealthReporter.java
+++ b/mobile/android/base/health/BrowserHealthReporter.java
@@ -9,16 +9,17 @@ import android.content.ContentProviderCl
 import android.content.Context;
 import android.util.Log;
 
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 
 import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
+import org.mozilla.gecko.background.common.GlobalConstants;
 import org.mozilla.gecko.background.healthreport.HealthReportConstants;
 import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
 import org.mozilla.gecko.background.healthreport.HealthReportGenerator;
 
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONException;
@@ -119,17 +120,17 @@ public class BrowserHealthReporter imple
      * @throws JSONException if JSON generation fails.
      * @throws IllegalStateException if the environment does not allow to generate a report.
      * @return non-null Health Report.
      */
     public JSONObject generateReport() throws JSONException {
         GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
         String profilePath = profile.getDir().getAbsolutePath();
 
-        long since = System.currentTimeMillis() - HealthReportConstants.MILLISECONDS_PER_SIX_MONTHS;
+        long since = System.currentTimeMillis() - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
         long lastPingTime = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING);
 
         return generateReport(since, lastPingTime, profilePath);
     }
 
     @Override
     public void handleMessage(String event, JSONObject message) {
         try {
--- a/mobile/android/base/home/BookmarksPage.java
+++ b/mobile/android/base/home/BookmarksPage.java
@@ -174,17 +174,17 @@ public class BookmarksPage extends HomeF
         });
         mList.setAdapter(mListAdapter);
 
         // Invalidate the cached value that keeps track of whether or
         // not desktop bookmarks (or reading list items) exist.
         BrowserDB.invalidateCachedState();
 
         // Create callbacks before the initial loader is started.
-        mLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager());
+        mLoaderCallbacks = new CursorLoaderCallbacks();
         mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks();
         loadIfVisible();
     }
 
     @Override
     public void onDestroyView() {
         mList = null;
         mListAdapter = null;
@@ -448,49 +448,42 @@ public class BookmarksPage extends HomeF
             final int max = getContext().getResources().getInteger(R.integer.number_of_top_sites);
             return BrowserDB.getTopBookmarks(getContext().getContentResolver(), max);
         }
     }
 
     /**
      * Loader callbacks for the LoaderManager of this fragment.
      */
-    private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks {
-        public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) {
-            super(context, loaderManager);
-        }
-
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
             switch(id) {
                 case LOADER_ID_BOOKMARKS_LIST: {
                     if (args == null) {
                         return new BookmarksLoader(getActivity());
                     } else {
                         return new BookmarksLoader(getActivity(), args.getInt(BOOKMARKS_FOLDER_KEY));
                     }
                 }
 
                 case LOADER_ID_TOP_BOOKMARKS: {
                     return new TopBookmarksLoader(getActivity());
                 }
+            }
 
-                default: {
-                    return super.onCreateLoader(id, args);
-                }
-            }
+            return null;
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
             final int loaderId = loader.getId();
             switch(loaderId) {
                 case LOADER_ID_BOOKMARKS_LIST: {
                     mListAdapter.swapCursor(c);
-                    loadFavicons(c);
                     mList.setHeaderDividersEnabled(c != null && c.getCount() > 0);
                     break;
                 }
 
                 case LOADER_ID_TOP_BOOKMARKS: {
                     mTopBookmarksAdapter.swapCursor(c);
 
                     // Load the thumbnails.
@@ -504,21 +497,16 @@ public class BookmarksPage extends HomeF
                         if (urls.size() > 0) {
                             Bundle bundle = new Bundle();
                             bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls);
                             getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks);
                         }
                     }
                     break;
                 }
-
-                default: {
-                    super.onLoadFinished(loader, c);
-                    break;
-                }
             }
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
             final int loaderId = loader.getId();
             switch(loaderId) {
                 case LOADER_ID_BOOKMARKS_LIST: {
@@ -529,28 +517,18 @@ public class BookmarksPage extends HomeF
                 }
 
                 case LOADER_ID_TOP_BOOKMARKS: {
                     if (mTopBookmarks != null) {
                         mTopBookmarksAdapter.swapCursor(null);
                         break;
                     }
                 }
-
-                default: {
-                    super.onLoaderReset(loader);
-                    break;
-                }
             }
         }
-
-        @Override
-        public void onFaviconsLoaded() {
-            mListAdapter.notifyDataSetChanged();
-        }
     }
 
     /**
      * An AsyncTaskLoader to load the thumbnails from a cursor.
      */
     private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Thumbnail>> {
         private Map<String, Thumbnail> mThumbnails;
         private ArrayList<String> mUrls;
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -99,17 +99,17 @@ public class BrowserSearch extends HomeF
     private volatile SuggestClient mSuggestClient;
 
     // List of search engines from gecko
     private ArrayList<SearchEngine> mSearchEngines;
 
     // Whether search suggestions are enabled or not
     private boolean mSuggestionsEnabled;
 
-    // Callbacks used for the search and favicon cursor loaders
+    // Callbacks used for the search loader
     private CursorLoaderCallbacks mCursorLoaderCallbacks;
 
     // Callbacks used for the search suggestion loader
     private SuggestionLoaderCallbacks mSuggestionLoaderCallbacks;
 
     // Inflater used by the adapter
     private LayoutInflater mInflater;
 
@@ -281,27 +281,25 @@ public class BrowserSearch extends HomeF
 
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null));
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
-        final Activity activity = getActivity();
-
         // Intialize the search adapter
-        mAdapter = new SearchAdapter(activity);
+        mAdapter = new SearchAdapter(getActivity());
         mList.setAdapter(mAdapter);
 
         // Only create an instance when we need it
         mSuggestionLoaderCallbacks = null;
 
         // Create callbacks before the initial loader is started
-        mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager());
+        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
         loadIfVisible();
     }
 
     @Override
     public void handleMessage(String event, final JSONObject message) {
         if (event.equals("SearchEngines:Data")) {
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
@@ -767,59 +765,36 @@ public class BrowserSearch extends HomeF
                 return -1;
             }
 
             // Return search engine index
             return position - resultCount;
         }
     }
 
-    private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks {
-        public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) {
-            super(context, loaderManager);
-        }
-
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            if (id == LOADER_ID_SEARCH) {
-                return SearchLoader.createInstance(getActivity(), args);
-            } else {
-                return super.onCreateLoader(id, args);
-            }
+            return SearchLoader.createInstance(getActivity(), args);
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-            if (loader.getId() == LOADER_ID_SEARCH) {
-                mAdapter.swapCursor(c);
+            mAdapter.swapCursor(c);
 
-                // We should handle autocompletion based on the search term
-                // associated with the currently loader that has just provided
-                // the results.
-                SearchCursorLoader searchLoader = (SearchCursorLoader) loader;
-                handleAutocomplete(searchLoader.getSearchTerm(), c);
-
-                loadFavicons(c);
-            } else {
-                super.onLoadFinished(loader, c);
-            }
+            // We should handle autocompletion based on the search term
+            // associated with the currently loader that has just provided
+            // the results.
+            SearchCursorLoader searchLoader = (SearchCursorLoader) loader;
+            handleAutocomplete(searchLoader.getSearchTerm(), c);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            if (loader.getId() == LOADER_ID_SEARCH) {
-                mAdapter.swapCursor(null);
-            } else {
-                super.onLoaderReset(loader);
-            }
-        }
-
-        @Override
-        public void onFaviconsLoaded() {
-            mAdapter.notifyDataSetChanged();
+            mAdapter.swapCursor(null);
         }
     }
 
     private class SuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> {
         @Override
         public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) {
             // mSuggestClient is set to null in onDestroyView(), so using it
             // safely here relies on the fact that onCreateLoader() is called
deleted file mode 100644
--- a/mobile/android/base/home/FaviconsLoader.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/* -*- 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.favicons.Favicons;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.BrowserDB.URLColumns;
-import org.mozilla.gecko.gfx.BitmapUtils;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
-
-import java.util.ArrayList;
-
-/**
- * Encapsulates the implementation of the favicons cursorloader.
- */
-class FaviconsLoader {
-    // Argument containing list of urls for the favicons loader
-    private static final String FAVICONS_LOADER_URLS_ARG = "urls";
-
-    private FaviconsLoader() {
-    }
-
-    private static ArrayList<String> getUrlsWithoutFavicon(Cursor c) {
-        ArrayList<String> urls = new ArrayList<String>();
-
-        if (c == null || !c.moveToFirst()) {
-            return urls;
-        }
-
-        do {
-            final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
-
-            // We only want to load favicons from DB if they are not in the
-            // memory cache yet. The url is null for bookmark folders.
-            if (url == null || Favicons.getFaviconFromMemCache(url) != null) {
-                continue;
-            }
-
-            urls.add(url);
-        } while (c.moveToNext());
-
-        return urls;
-    }
-
-    private static void storeFaviconsInMemCache(Cursor c) {
-        if (c == null || !c.moveToFirst()) {
-            return;
-        }
-
-        do {
-            final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
-            final byte[] b = c.getBlob(c.getColumnIndexOrThrow(URLColumns.FAVICON));
-
-            if (b == null) {
-                continue;
-            }
-
-            Bitmap favicon = BitmapUtils.decodeByteArray(b);
-            if (favicon == null) {
-                continue;
-            }
-
-            favicon = Favicons.scaleImage(favicon);
-            Favicons.putFaviconInMemCache(url, favicon);
-        } while (c.moveToNext());
-    }
-
-    public static void restartFromCursor(LoaderManager manager, int loaderId,
-            LoaderCallbacks<Cursor> callbacks, Cursor c) {
-        // If there urls without in-memory favicons, trigger a new loader
-        // to load the images from disk to memory.
-        ArrayList<String> urls = getUrlsWithoutFavicon(c);
-        if (urls.size() > 0) {
-            Bundle args = new Bundle();
-            args.putStringArrayList(FAVICONS_LOADER_URLS_ARG, urls);
-
-            manager.restartLoader(loaderId, args, callbacks);
-        }
-    }
-
-    public static Loader<Cursor> createInstance(Context context, Bundle args) {
-        final ArrayList<String> urls = args.getStringArrayList(FAVICONS_LOADER_URLS_ARG);
-        return new FaviconsCursorLoader(context, urls);
-    }
-
-    private static class FaviconsCursorLoader extends SimpleCursorLoader {
-        private final ArrayList<String> mUrls;
-
-        public FaviconsCursorLoader(Context context, ArrayList<String> urls) {
-            super(context);
-            mUrls = urls;
-        }
-
-        @Override
-        public Cursor loadCursor() {
-            final ContentResolver cr = getContext().getContentResolver();
-
-            Cursor c = BrowserDB.getFaviconsForUrls(cr, mUrls);
-            storeFaviconsInMemCache(c);
-
-            return c;
-        }
-    }
-}
--- a/mobile/android/base/home/HomeBanner.java
+++ b/mobile/android/base/home/HomeBanner.java
@@ -1,25 +1,40 @@
 /* -*- 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.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
 
 import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.ImageButton;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 
-public class HomeBanner extends LinearLayout {
+public class HomeBanner extends LinearLayout
+                        implements GeckoEventListener {
+    private static final String LOGTAG = "GeckoHomeBanner";
 
     public HomeBanner(Context context) {
         this(context, null);
     }
 
     public HomeBanner(Context context, AttributeSet attrs) {
         super(context, attrs);
 
@@ -38,14 +53,83 @@ public class HomeBanner extends LinearLa
         closeButton.getDrawable().setAlpha(127);
 
         closeButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
                 HomeBanner.this.setVisibility(View.GONE);
             }
         });
+
+        setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Send the current message id back to JS.
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag()));
+            }
+        });
+
+        GeckoAppShell.getEventDispatcher().registerEventListener("HomeBanner:Data", this);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
     }
 
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
+     }
+
     public boolean isDismissed() {
         return (getVisibility() == View.GONE);
     }
+
+    @Override
+    public void handleMessage(String event, JSONObject message) {
+        try {
+            // Store the current message id to pass back to JS in the view's OnClickListener.
+            setTag(message.getString("id"));
+
+            final String text = message.getString("text");
+            final TextView textView = (TextView) findViewById(R.id.text);
+
+            // Update the banner message on the UI thread.
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    textView.setText(text);
+                    setVisibility(View.VISIBLE);
+                }
+            });
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Exception handling " + event + " message", e);
+            return;
+        }
+
+        final String iconURI = message.optString("iconURI");
+        final ImageView iconView = (ImageView) findViewById(R.id.icon);
+
+        if (TextUtils.isEmpty(iconURI)) {
+            // Hide the image view if we don't have an icon to show.
+            iconView.setVisibility(View.GONE);
+            return;
+        }
+
+        BitmapUtils.getDrawable(getContext(), iconURI, new BitmapUtils.BitmapLoader() {
+            @Override
+            public void onBitmapFound(final Drawable d) {
+                // Bail if getDrawable doesn't find anything.
+                if (d == null) {
+                    iconView.setVisibility(View.GONE);
+                    return;
+                }
+
+                // Update the banner icon on the UI thread.
+                ThreadUtils.postToUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        iconView.setImageDrawable(d);
+                    }
+                });
+            }
+        });
+    }
 }
deleted file mode 100644
--- a/mobile/android/base/home/HomeCursorLoaderCallbacks.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/* -*- 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 android.content.Context;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
-
-/**
- * Cursor loader callbacks that takes care loading favicons into memory.
- */
-abstract class HomeCursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
-
-    // Cursor loader ID for favicons query
-    private static final int LOADER_ID_FAVICONS = 100;
-
-    private final Context mContext;
-    private final LoaderManager mLoaderManager;
-
-    public HomeCursorLoaderCallbacks(Context context, LoaderManager loaderManager) {
-        mContext = context;
-        mLoaderManager = loaderManager;
-    }
-
-    public void loadFavicons(Cursor cursor) {
-        FaviconsLoader.restartFromCursor(mLoaderManager, LOADER_ID_FAVICONS, this, cursor);
-    }
-
-    @Override
-    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-        if (id == LOADER_ID_FAVICONS) {
-            return FaviconsLoader.createInstance(mContext, args);
-        }
-
-        return null;
-    }
-
-    @Override
-    public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-        if (loader.getId() == LOADER_ID_FAVICONS) {
-            onFaviconsLoaded();
-        }
-    }
-
-    @Override
-    public void onLoaderReset(Loader<Cursor> loader) {
-        // Do nothing by default.
-    }
-
-    // Callback for favicons loaded in memory.
-    public abstract void onFaviconsLoaded();
-}
--- a/mobile/android/base/home/LastTabsPage.java
+++ b/mobile/android/base/home/LastTabsPage.java
@@ -137,24 +137,22 @@ public class LastTabsPage extends HomeFr
         mEmptyView = null;
         mRestoreButton = null;
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
-        final Activity activity = getActivity();
-
         // Intialize adapter
-        mAdapter = new LastTabsAdapter(activity);
+        mAdapter = new LastTabsAdapter(getActivity());
         mList.setAdapter(mAdapter);
 
         // Create callbacks before the initial loader is started
-        mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager());
+        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
         loadIfVisible();
     }
 
     private void updateUiFromCursor(Cursor c) {
         if (c != null && c.getCount() > 0) {
             if (mTitle != null) {
                 mTitle.setVisibility(View.VISIBLE);
             }
@@ -257,48 +255,26 @@ public class LastTabsPage extends HomeFr
         }
 
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             return LayoutInflater.from(context).inflate(R.layout.home_item_row, parent, false);
         }
     }
 
-    private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks {
-        public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) {
-            super(context, loaderManager);
-        }
-
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            if (id == LOADER_ID_LAST_TABS) {
-                return new LastTabsCursorLoader(getActivity());
-            } else {
-                return super.onCreateLoader(id, args);
-            }
+            return new LastTabsCursorLoader(getActivity());
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-            if (loader.getId() == LOADER_ID_LAST_TABS) {
-                mAdapter.swapCursor(c);
-                updateUiFromCursor(c);
-                loadFavicons(c);
-            } else {
-                super.onLoadFinished(loader, c);
-            }
+            mAdapter.swapCursor(c);
+            updateUiFromCursor(c);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            if (loader.getId() == LOADER_ID_LAST_TABS) {
-                mAdapter.swapCursor(null);
-            } else {
-                super.onLoaderReset(loader);
-            }
-        }
-
-        @Override
-        public void onFaviconsLoaded() {
-            mAdapter.notifyDataSetChanged();
+            mAdapter.swapCursor(null);
         }
     }
 }
--- a/mobile/android/base/home/MostRecentPage.java
+++ b/mobile/android/base/home/MostRecentPage.java
@@ -12,16 +12,17 @@ import org.mozilla.gecko.home.HomePager.
 import org.mozilla.gecko.home.TwoLinePageRow;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewStub;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.ImageView;
@@ -122,24 +123,22 @@ public class MostRecentPage extends Home
         mTitle = null;
         mEmptyView = null;
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
-        final Activity activity = getActivity();
-
         // Intialize adapter
-        mAdapter = new MostRecentAdapter(activity);
+        mAdapter = new MostRecentAdapter(getActivity());
         mList.setAdapter(mAdapter);
 
         // Create callbacks before the initial loader is started
-        mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager());
+        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
         loadIfVisible();
     }
 
     @Override
     protected void load() {
         getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
     }
 
@@ -355,48 +354,26 @@ public class MostRecentPage extends Home
                 // Reached the last section, no need to continue
                 if (section == MostRecentSection.OLDER) {
                     break;
                 }
             } while (c.moveToNext());
         }
     }
 
-    private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks {
-        public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) {
-            super(context, loaderManager);
-        }
-
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            if (id == LOADER_ID_HISTORY) {
-                return new MostRecentCursorLoader(getActivity());
-            } else {
-                return super.onCreateLoader(id, args);
-            }
+            return new MostRecentCursorLoader(getActivity());
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-            if (loader.getId() == LOADER_ID_HISTORY) {
-                mAdapter.swapCursor(c);
-                updateUiFromCursor(c);
-                loadFavicons(c);
-            } else {
-                super.onLoadFinished(loader, c);
-            }
+            mAdapter.swapCursor(c);
+            updateUiFromCursor(c);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            if (loader.getId() == LOADER_ID_HISTORY) {
-                mAdapter.swapCursor(null);
-            } else {
-                super.onLoaderReset(loader);
-            }
-        }
-
-        @Override
-        public void onFaviconsLoaded() {
-            mAdapter.notifyDataSetChanged();
+            mAdapter.swapCursor(null);
         }
    }
 }
--- a/mobile/android/base/home/MostVisitedPage.java
+++ b/mobile/android/base/home/MostVisitedPage.java
@@ -11,16 +11,17 @@ import org.mozilla.gecko.db.BrowserDB.UR
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
 import android.support.v4.widget.CursorAdapter;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.widget.AdapterView;
 import android.widget.ImageView;
@@ -124,24 +125,22 @@ public class MostVisitedPage extends Hom
         mTitle = null;
         mEmptyView = null;
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
-        final Activity activity = getActivity();
-
         // Intialize the search adapter
-        mAdapter = new VisitedAdapter(activity, null);
+        mAdapter = new VisitedAdapter(getActivity(), null);
         mList.setAdapter(mAdapter);
 
         // Create callbacks before the initial loader is started
-        mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager());
+        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
         loadIfVisible();
     }
 
     @Override
     protected void load() {
         getLoaderManager().initLoader(LOADER_ID_FRECENCY, null, mCursorLoaderCallbacks);
     }
 
@@ -201,48 +200,26 @@ public class MostVisitedPage extends Hom
         }
 
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             return LayoutInflater.from(parent.getContext()).inflate(R.layout.home_item_row, parent, false);
         }
     }
 
-    private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks {
-        public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) {
-            super(context, loaderManager);
-        }
-
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            if (id == LOADER_ID_FRECENCY) {
-                return new FrecencyCursorLoader(getActivity());
-            } else {
-                return super.onCreateLoader(id, args);
-            }
+            return new FrecencyCursorLoader(getActivity());
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-            if (loader.getId() == LOADER_ID_FRECENCY) {
-                mAdapter.swapCursor(c);
-                updateUiFromCursor(c);
-                loadFavicons(c);
-            } else {
-                super.onLoadFinished(loader, c);
-            }
+            mAdapter.swapCursor(c);
+            updateUiFromCursor(c);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            if (loader.getId() == LOADER_ID_FRECENCY) {
-                mAdapter.swapCursor(null);
-            } else {
-                super.onLoaderReset(loader);
-            }
-        }
-
-        @Override
-        public void onFaviconsLoaded() {
-            mAdapter.notifyDataSetChanged();
+            mAdapter.swapCursor(null);
         }
     }
 }
--- a/mobile/android/base/home/PinBookmarkDialog.java
+++ b/mobile/android/base/home/PinBookmarkDialog.java
@@ -9,16 +9,17 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
 
 import android.app.Activity;
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.v4.app.DialogFragment;
 import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
 import android.support.v4.widget.CursorAdapter;
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -33,32 +34,29 @@ class PinBookmarkDialog extends DialogFr
     // Listener for url selection
     public static interface OnBookmarkSelectedListener {
         public void onBookmarkSelected(String url, String title);
     }
 
     // Cursor loader ID for search query
     private static final int LOADER_ID_SEARCH = 0;
 
-    // Cursor loader ID for favicons query
-    private static final int LOADER_ID_FAVICONS = 1;
-
     // Holds the current search term to use in the query
     private String mSearchTerm;
 
     // Adapter for the list of search results
     private SearchAdapter mAdapter;
 
     // Search entry
     private EditText mSearch;
 
     // Search results
     private ListView mList;
 
-    // Callbacks used for the search and favicon cursor loaders
+    // Callbacks used for the search loader
     private CursorLoaderCallbacks mLoaderCallbacks;
 
     // Bookmark selected listener
     private OnBookmarkSelectedListener mOnBookmarkSelectedListener;
 
     public static PinBookmarkDialog newInstance() {
         return new PinBookmarkDialog();
     }
@@ -120,25 +118,24 @@ class PinBookmarkDialog extends DialogFr
             }
         });
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
-        final Activity activity = getActivity();
         final LoaderManager manager = getLoaderManager();
 
         // Initialize the search adapter
-        mAdapter = new SearchAdapter(activity);
+        mAdapter = new SearchAdapter(getActivity());
         mList.setAdapter(mAdapter);
 
         // Create callbacks before the initial loader is started
-        mLoaderCallbacks = new CursorLoaderCallbacks(activity, manager);
+        mLoaderCallbacks = new CursorLoaderCallbacks();
 
         // Reconnect to the loader only if present
         manager.initLoader(LOADER_ID_SEARCH, null, mLoaderCallbacks);
 
         // Default filter.
         filter("");
     }
 
@@ -174,47 +171,25 @@ class PinBookmarkDialog extends DialogFr
         }
 
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             return (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, parent, false);
         }
     }
 
-    private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks {
-        public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) {
-            super(context, loaderManager);
-        }
-
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            if (id == LOADER_ID_SEARCH) {
-                return SearchLoader.createInstance(getActivity(), args);
-            } else {
-                return super.onCreateLoader(id, args);
-            }
+            return SearchLoader.createInstance(getActivity(), args);
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-            if (loader.getId() == LOADER_ID_SEARCH) {
-                mAdapter.swapCursor(c);
-                loadFavicons(c);
-            } else {
-                super.onLoadFinished(loader, c);
-            }
+            mAdapter.swapCursor(c);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            if (loader.getId() == LOADER_ID_SEARCH) {
-                mAdapter.swapCursor(null);
-            } else {
-                super.onLoaderReset(loader);
-            }
-        }
-
-        @Override
-        public void onFaviconsLoaded() {
-            mAdapter.notifyDataSetChanged();
+            mAdapter.swapCursor(null);
         }
     }
 }
--- a/mobile/android/base/home/ReadingListPage.java
+++ b/mobile/android/base/home/ReadingListPage.java
@@ -214,38 +214,23 @@ public class ReadingListPage extends Hom
     }
 
     /**
      * LoaderCallbacks implementation that interacts with the LoaderManager.
      */
     private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            switch(id) {
-                case LOADER_ID_READING_LIST:
-                    return new ReadingListLoader(getActivity());
-            }
-            return null;
+            return new ReadingListLoader(getActivity());
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-            final int loaderId = loader.getId();
-            switch(loaderId) {
-                case LOADER_ID_READING_LIST:
-                    mAdapter.swapCursor(c);
-                    break;
-           }
-
-           updateUiFromCursor(c);
+            mAdapter.swapCursor(c);
+            updateUiFromCursor(c);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            final int loaderId = loader.getId();
-            switch(loaderId) {
-                case LOADER_ID_READING_LIST:
-                    mAdapter.swapCursor(null);
-                    break;
-            }
+            mAdapter.swapCursor(null);
         }
     }
 }
--- a/mobile/android/base/home/TwoLinePageRow.java
+++ b/mobile/android/base/home/TwoLinePageRow.java
@@ -5,24 +5,29 @@
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Combined;
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UiAsyncTask;
 import org.mozilla.gecko.widget.FaviconView;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.os.Build;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class TwoLinePageRow extends LinearLayout
@@ -35,16 +40,18 @@ public class TwoLinePageRow extends Line
 
     private int mUrlIconId;
     private int mBookmarkIconId;
     private boolean mShowIcons;
 
     // The URL for the page corresponding to this view.
     private String mPageUrl;
 
+    private LoadFaviconTask mLoadFaviconTask;
+
     public TwoLinePageRow(Context context) {
         this(context, null);
     }
 
     public TwoLinePageRow(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         setGravity(Gravity.CENTER_VERTICAL);
@@ -69,16 +76,18 @@ public class TwoLinePageRow extends Line
         // Delay removing the listener to avoid modifying mTabsChangedListeners
         // while notifyListeners is iterating through the array.
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 Tabs.unregisterOnTabsChangedListener(TwoLinePageRow.this);
             }
         });
+
+        cancelLoadFaviconTask();
     }
 
     @Override
     public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
         switch(msg) {
             case ADDED:
             case CLOSED:
             case LOCATION_CHANGE:
@@ -126,16 +135,26 @@ public class TwoLinePageRow extends Line
      * tab changes or is closed.
      */
     private void updateDisplayedUrl(String url) {
         mPageUrl = url;
         updateDisplayedUrl();
     }
 
     /**
+     * Cancels any pending favicon loading task associated with this view.
+     */
+    private void cancelLoadFaviconTask() {
+        if (mLoadFaviconTask != null) {
+            mLoadFaviconTask.cancel(true);
+            mLoadFaviconTask = null;
+        }
+    }
+
+    /**
      * Replaces the page URL with "Switch to tab" if there is already a tab open with that URL.
      */
     private void updateDisplayedUrl() {
         int tabId = Tabs.getInstance().getTabIdForUrl(mPageUrl);
         if (!mShowIcons || tabId < 0) {
             setUrl(mPageUrl);
             setUrlIcon(NO_ICON);
         } else {
@@ -158,34 +177,43 @@ public class TwoLinePageRow extends Line
 
         int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL);
         final String url = cursor.getString(urlIndex);
 
         // Use the URL instead of an empty title for consistency with the normal URL
         // bar view - this is the equivalent of getDisplayTitle() in Tab.java
         setTitle(TextUtils.isEmpty(title) ? url : title);
 
-        updateDisplayedUrl(url);
-
-        int faviconIndex = cursor.getColumnIndex(URLColumns.FAVICON);
-        if (faviconIndex != -1) {
-            byte[] b = cursor.getBlob(faviconIndex);
+        // No need to do extra work if the URL associated with this view
+        // hasn't changed.
+        if (TextUtils.equals(mPageUrl, url)) {
+            return;
+        }
 
-            Bitmap favicon = null;
-            if (b != null) {
-                Bitmap bitmap = BitmapUtils.decodeByteArray(b);
-                if (bitmap != null) {
-                    favicon = Favicons.scaleImage(bitmap);
-                }
-            }
+        updateDisplayedUrl(url);
+        cancelLoadFaviconTask();
 
+        // First, try to find the favicon in the memory cache. If it's not
+        // cached yet, try to load it from the database, off main thread.
+        final Bitmap favicon = Favicons.getFaviconFromMemCache(url);
+        if (favicon != null) {
             setFaviconWithUrl(favicon, url);
         } else {
-            // If favicons is not on the cursor, try to fetch it from the memory cache
-            setFaviconWithUrl(Favicons.getFaviconFromMemCache(url), url);
+            // Show blank image until the new favicon finishes loading
+            mFavicon.clearImage();
+
+            mLoadFaviconTask = new LoadFaviconTask(url);
+
+            // Try to use a thread pool instead of serial execution of tasks
+            // to add more throughput to the favicon loading routines.
+            if (Build.VERSION.SDK_INT >= 11) {
+                mLoadFaviconTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+            } else {
+                mLoadFaviconTask.execute();
+            }
         }
 
         // Don't show bookmark/reading list icon, if not needed.
         if (!mShowIcons) {
             return;
         }
 
         final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID);
@@ -208,9 +236,42 @@ public class TwoLinePageRow extends Line
                 setBookmarkIcon(R.drawable.ic_url_bar_reader);
             } else {
                 setBookmarkIcon(R.drawable.ic_url_bar_star);
             }
         } else {
             setBookmarkIcon(NO_ICON);
         }
     }
+
+    private class LoadFaviconTask extends AsyncTask<Void, Void, Bitmap> {
+        private final String mUrl;
+
+        public LoadFaviconTask(String url) {
+            mUrl = url;
+        }
+
+        @Override
+        public Bitmap doInBackground(Void... params) {
+            Bitmap favicon = Favicons.getFaviconFromMemCache(mUrl);
+            if (favicon == null) {
+                final ContentResolver cr = getContext().getContentResolver();
+
+                final Bitmap faviconFromDb = BrowserDB.getFaviconForUrl(cr, mUrl);
+                if (faviconFromDb != null) {
+                    favicon = Favicons.scaleImage(faviconFromDb);
+                    Favicons.putFaviconInMemCache(mUrl, favicon);
+                }
+            }
+
+            return favicon;
+        }
+
+        @Override
+        public void onPostExecute(Bitmap favicon) {
+            if (TextUtils.equals(mPageUrl, mUrl)) {
+                setFaviconWithUrl(favicon, mUrl);
+            }
+
+            mLoadFaviconTask = null;
+        }
+    }
 }
--- a/mobile/android/base/resources/layout/home_banner.xml
+++ b/mobile/android/base/resources/layout/home_banner.xml
@@ -4,23 +4,23 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
      <ImageView android:id="@+id/icon"
                 android:layout_width="48dip"
                 android:layout_height="48dip"
                 android:layout_marginLeft="10dp"
-                android:layout_marginRight="10dp"
                 android:scaleType="centerInside"/>
 
      <TextView android:id="@+id/text"
                android:layout_width="0dip"
                android:layout_height="fill_parent"
                android:layout_weight="1"
+               android:layout_marginLeft="10dp"
                android:paddingTop="7dp"
                android:paddingBottom="7dp"
                android:textAppearance="@style/TextAppearance.Widget.HomeBanner"
                android:layout_gravity="bottom"
                android:singleLine="false"
                android:maxLines="3"
                android:ellipsize="end"
                android:gravity="center_vertical"/>
--- a/mobile/android/base/widget/FaviconView.java
+++ b/mobile/android/base/widget/FaviconView.java
@@ -59,24 +59,25 @@ public class FaviconView extends ImageVi
     /**
      * Formats the image for display, if the prerequisite data are available. Upscales tiny Favicons to
      * normal sized ones, replaces null bitmaps with the default Favicon, and fills all remaining space
      * in this view with the coloured background.
      */
     private void formatImage() {
         // If we're called before bitmap is set, just show the default.
         if (mIconBitmap == null) {
-            setImageResource(0);
+            setImageResource(R.drawable.favicon);
             hideBackground();
             return;
         }
 
         // If we're called before size set, abort.
         if (mActualWidth == 0 || mActualHeight == 0) {
             hideBackground();
+            setImageResource(R.drawable.favicon);
             return;
         }
 
         if (mScalingExpected && mActualWidth != mIconBitmap.getWidth()) {
             scaleBitmap();
             // Don't scale the image every time something changes.
             mScalingExpected = false;
         }
@@ -150,16 +151,24 @@ public class FaviconView extends ImageVi
         mIconKey = key;
         mScalingExpected = allowScaling;
 
         // Possibly update the display.
         formatImage();
     }
 
     /**
+     * Clear image and background shown by this view.
+     */
+    public void clearImage() {
+        setImageResource(0);
+        hideBackground();
+    }
+
+    /**
      * 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
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/Home.jsm
@@ -0,0 +1,132 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Home"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// See bug 915424
+function resolveGeckoURI(aURI) {
+  if (!aURI)
+    throw "Can't resolve an empty uri";
+
+  if (aURI.startsWith("chrome://")) {
+    let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+    return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
+  } else if (aURI.startsWith("resource://")) {
+    let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+    return handler.resolveURI(Services.io.newURI(aURI, null, null));
+  }
+  return aURI;
+}
+
+function sendMessageToJava(message) {
+  return Services.androidBridge.handleGeckoMessage(JSON.stringify(message));
+}
+
+function BannerMessage(options) {
+  let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+  this.id = uuidgen.generateUUID().toString();
+
+  if ("text" in options && options.text != null)
+    this.text = options.text;
+
+  if ("icon" in options && options.icon != null)
+    this.iconURI = resolveGeckoURI(options.icon);
+
+  if ("onclick" in options && typeof options.onclick === "function")
+    this.onclick = options.onclick;
+}
+
+let HomeBanner = {
+  // Holds the messages that will rotate through the banner.
+  _messages: {},
+
+  // A queue used to keep track of which message to show next.
+  _queue: [],
+
+  observe: function(subject, topic, data) {
+    switch(topic) {
+      case "HomeBanner:Get":
+        this._handleGet();
+        break;
+
+      case "HomeBanner:Click":
+        this._handleClick(data);
+        break;
+    }
+  },
+
+  _handleGet: function() {
+    // Get the message from the front of the queue, then add it back
+    // to the end of the queue to show it again later.
+    let id = this._queue.shift();
+    this._queue.push(id);
+
+    let message = this._messages[id];
+    sendMessageToJava({
+      type: "HomeBanner:Data",
+      id: message.id,
+      text: message.text,
+      iconURI: message.iconURI
+    });
+  },
+
+  _handleClick: function(id) {
+    let message = this._messages[id];
+    if (message.onclick)
+      message.onclick();
+  },
+
+  /**
+   * Adds a new banner message to the rotation.
+   *
+   * @return id Unique identifer for the message.
+   */
+  add: function(options) {
+    let message = new BannerMessage(options);
+    this._messages[message.id] = message;
+
+    // Add the new message to the end of the queue.
+    this._queue.push(message.id);
+
+    // If this is the first message we're adding, add
+    // observers to listen for requests from the Java UI.
+    if (Object.keys(this._messages).length == 1) {
+      Services.obs.addObserver(this, "HomeBanner:Get", false);
+      Services.obs.addObserver(this, "HomeBanner:Click", false);
+    }
+
+    return message.id;
+  },
+
+  /**
+   * Removes a banner message from the rotation.
+   *
+   * @param id The id of the message to remove.
+   */
+  remove: function(id) {
+    delete this._messages[id];
+
+    // Remove the message from the queue.
+    let index = this._queue.indexOf(id);
+    this._queue.splice(index, 1);
+
+    // If there are no more messages, remove the observers.
+    if (Object.keys(this._messages).length == 0) {
+      Services.obs.removeObserver(this, "HomeBanner:Get");
+      Services.obs.removeObserver(this, "HomeBanner:Click");
+    }
+  }
+};
+
+// Public API
+this.Home = {
+  banner: HomeBanner
+}
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_JS_MODULES += [
     'ContactService.jsm',
+    'Home.jsm',
     'JNI.jsm',
     'LightweightThemeConsumer.jsm',
     'OrderedBroadcast.jsm',
     'Prompt.jsm',
     'Sanitizer.jsm',
     'SharedPreferences.jsm',
 ]
--- a/mobile/android/services/java-sources.mn
+++ b/mobile/android/services/java-sources.mn
@@ -7,16 +7,17 @@ background/announcements/AnnouncementsFe
 background/announcements/AnnouncementsFetchResourceDelegate.java
 background/announcements/AnnouncementsService.java
 background/announcements/AnnouncementsStartReceiver.java
 background/BackgroundService.java
 background/bagheera/BagheeraClient.java
 background/bagheera/BagheeraRequestDelegate.java
 background/bagheera/BoundedByteArrayEntity.java
 background/bagheera/DeflateHelper.java
+background/common/DateUtils.java
 background/common/log/Logger.java
 background/common/log/writers/AndroidLevelCachingLogWriter.java
 background/common/log/writers/AndroidLogWriter.java
 background/common/log/writers/LevelFilteringLogWriter.java
 background/common/log/writers/LogWriter.java
 background/common/log/writers/PrintLogWriter.java
 background/common/log/writers/SimpleTagLogWriter.java
 background/common/log/writers/StringLogWriter.java
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -283,19 +283,27 @@ EventLoopStack.prototype = {
   get size() {
     return this._inspector.eventLoopNestLevel;
   },
 
   /**
    * The URL of the debuggee who pushed the event loop on top of the stack.
    */
   get lastPausedUrl() {
-    return this.size > 0
-      ? this._inspector.lastNestRequestor.url
-      : null;
+    let url = null;
+    if (this.size > 0) {
+      try {
+        url = this._inspector.lastNestRequestor.url
+      } catch (e) {
+        // The tab's URL getter may throw if the tab is destroyed by the time
+        // this code runs, but we don't really care at this point.
+        dumpn(e);
+      }
+    }
+    return url;
   },
 
   /**
    * Push a new nested event loop onto the stack.
    *
    * @returns EventLoop
    */
   push: function () {
@@ -931,17 +939,17 @@ ThreadActor.prototype = {
         message: "Can't resume when debuggee isn't paused. Current state is '"
           + this._state + "'"
       };
     }
 
     // In case of multiple nested event loops (due to multiple debuggers open in
     // different tabs or multiple debugger clients connected to the same tab)
     // only allow resumption in a LIFO order.
-    if (this._nestedEventLoops.size
+    if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl
         && this._nestedEventLoops.lastPausedUrl !== this._hooks.url) {
       return {
         error: "wrongOrder",
         message: "trying to resume in the wrong order.",
         lastPausedUrl: this._nestedEventLoops.lastPausedUrl
       };
     }
 
--- a/widget/cocoa/nsLookAndFeel.mm
+++ b/widget/cocoa/nsLookAndFeel.mm
@@ -244,17 +244,17 @@ nsLookAndFeel::NativeGetColor(ColorID aI
       break;          
     case eColorID__moz_mac_menutextdisable:
       aColor = NS_RGB(0x88,0x88,0x88);
       break;      
     case eColorID__moz_mac_menutextselect:
       aColor = GetColorFromNSColor([NSColor selectedMenuItemTextColor]);
       break;      
     case eColorID__moz_mac_disabledtoolbartext:
-      aColor = NS_RGB(0x3F,0x3F,0x3F);
+      aColor = GetColorFromNSColor([NSColor disabledControlTextColor]);
       break;
     case eColorID__moz_mac_menuselect:
       aColor = GetColorFromNSColor([NSColor alternateSelectedControlColor]);
       break;
     case eColorID__moz_buttondefault:
       aColor = NS_RGB(0xDC,0xDC,0xDC);
       break;
     case eColorID__moz_cellhighlight:
--- a/widget/tests/test_platform_colors.xul
+++ b/widget/tests/test_platform_colors.xul
@@ -70,17 +70,17 @@ var colors = {
   "-moz-html-cellhighlighttext": ["rgb(0, 0, 0)"],
   "-moz-mac-chrome-active": ["rgb(150, 150, 150)", "rgb(167, 167, 167)", "rgb(178, 178, 178)"],
   "-moz-mac-chrome-inactive": ["rgb(202, 202, 202)", "rgb(216, 216, 216)", "rgb(225, 225, 225)"],
   //"-moz-mac-focusring": ["rgb(83, 144, 210)", "rgb(95, 112, 130)", "rgb(63, 152, 221)", "rgb(108, 126, 141)"],
   "-moz-mac-menuselect": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"],
   "-moz-mac-menushadow": ["rgb(163, 163, 163)"],
   "-moz-mac-menutextdisable": ["rgb(152, 152, 152)", "rgb(136, 136, 136)"],
   "-moz-mac-menutextselect": ["rgb(255, 255, 255)"],
-  "-moz-mac-disabledtoolbartext": ["rgb(63, 63, 63)"],
+  "-moz-mac-disabledtoolbartext": ["rgb(127, 127, 127)"],
   "-moz-mac-secondaryhighlight": ["rgb(212, 212, 212)"],
   "-moz-menuhover": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"],
   "-moz-menuhovertext": ["rgb(255, 255, 255)", "rgb(255, 254, 254)", "rgb(254, 255, 254)"],
   "-moz-menubartext": ["rgb(0, 0, 0)"],
   //"-moz-menubarhovertext": ["rgb(255, 255, 255)"],
   "-moz-oddtreerow": ["rgb(236, 242, 254)", "rgb(240, 240, 240)", "rgb(243, 245, 250)", "rgb(243, 246, 250)"],
   "-moz-visitedhyperlinktext": ["rgb(85, 26, 139)"],
   "currentcolor": ["rgb(0, 0, 0)"],