Merge fxteam to m-c
authorWes Kocher <wkocher@mozilla.com>
Mon, 14 Oct 2013 19:05:06 -0700
changeset 164549 23bd0deec3597094677bf4865fa129e178c31013
parent 164536 ddd03c32fab13099b81e21cdd38f1a2a4565fb3b (current diff)
parent 164548 8c2d93bcb585fb7ccdfca3373315f4524d36f2f4 (diff)
child 164558 ffa4e404f6f71fcfc80856ac85d8e0751d2bce45
child 164571 06974acff642cf1e26cd6248eb3fd0267c3390dc
child 164645 c5cc4233220fc27b0660fdf3c125482be0f06291
child 170424 4eba3ca48a1b8f9f4b1b469bf72a98ffcbfdd094
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
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fxteam to m-c
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4132,17 +4132,17 @@
          * In both cases, it is most likely that the close button area has
          * been accidentally clicked, therefore we do not close the tab.
          *
          * We don't want to ignore processing of more than one click event,
          * though, since the user might actually be repeatedly clicking to
          * close many tabs at once.
          */
         let target = event.originalTarget;
-        if (target.className == 'tab-close-button') {
+        if (target.classList.contains('tab-close-button')) {
           // We preemptively set this to allow the closing-multiple-tabs-
           // in-a-row case.
           if (this._blockDblClick) {
             target._ignoredCloseButtonClicks = true;
           } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
             target._ignoredCloseButtonClicks = true;
             event.stopPropagation();
             return;
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -1138,37 +1138,47 @@ SourceScripts.prototype = {
   /**
    * Handler for the debugger client's 'blackboxchange' notification.
    */
   _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
     const item = DebuggerView.Sources.getItemByValue(url);
     if (item) {
       DebuggerView.Sources.callMethod("checkItem", item.target, !isBlackBoxed);
     }
-    DebuggerView.Sources.maybeShowBlackBoxMessage();
+    DebuggerView.maybeShowBlackBoxMessage();
   },
 
   /**
    * Set the black boxed status of the given source.
    *
    * @param Object aSource
    *        The source form.
    * @param bool aBlackBoxFlag
    *        True to black box the source, false to un-black box it.
+   * @returns Promise
+   *          A promize that resolves to [aSource, isBlackBoxed] or rejects to
+   *          [aSource, error].
    */
   blackBox: function(aSource, aBlackBoxFlag) {
     const sourceClient = this.activeThread.source(aSource);
-    sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](({ error, message }) => {
+    const deferred = promise.defer();
+
+    sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](aPacket => {
+      const { error, message } = aPacket;
       if (error) {
         let msg = "Couldn't toggle black boxing for " + aSource.url + ": " + message;
         dumpn(msg);
         Cu.reportError(msg);
-        return;
+        deferred.reject([aSource, msg]);
+      } else {
+        deferred.resolve([aSource, sourceClient.isBlackBoxed]);
       }
     });
+
+    return deferred.promise;
   },
 
   /**
    * Pretty print a source's text. All subsequent calls to |getText| will return
    * the pretty text. Nothing will happen for non-javascript files.
    *
    * @param Object aSource
    *        The source form from the RDP.
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -45,17 +45,16 @@ SourcesView.prototype = Heritage.extend(
     this.emptyText = L10N.getStr("noSourcesText");
     this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
 
     this._commandset = document.getElementById("debuggerCommands");
     this._popupset = document.getElementById("debuggerPopupset");
     this._cmPopup = document.getElementById("sourceEditorContextMenu");
     this._cbPanel = document.getElementById("conditional-breakpoint-panel");
     this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox");
-    this._editorDeck = document.getElementById("editor-deck");
     this._stopBlackBoxButton = document.getElementById("black-boxed-message-button");
     this._prettyPrintButton = document.getElementById("pretty-print");
 
     if (Prefs.prettyPrintEnabled) {
       this._prettyPrintButton.removeAttribute("hidden");
     }
 
     window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false);
@@ -378,33 +377,39 @@ SourcesView.prototype = Heritage.extend(
     this._unselectBreakpoint();
     this._hideConditionalPopup();
   },
 
   /**
    * Pretty print the selected source.
    */
   prettyPrint: function() {
+    if (this._prettyPrintButton.hasAttribute("disabled")) {
+      return;
+    }
+
     const resetEditor = ([{ url }]) => {
       // Only set the text when the source is still selected.
       if (url == this.selectedValue) {
         DebuggerView.setEditorLocation(url, 0, { force: true });
       }
     };
     const printError = ([{ url }, error]) => {
       let err = DevToolsUtils.safeErrorString(error);
       let msg = "Couldn't prettify source: " + url + "\n" + err;
       Cu.reportError(msg);
       dumpn(msg);
       return;
     }
 
-    let { source } = this.selectedItem.attachment;
-    let prettyPrinted = DebuggerController.SourceScripts.prettyPrint(source);
-    prettyPrinted.then(resetEditor, printError);
+    DebuggerView.showProgressBar();
+    const { source } = this.selectedItem.attachment;
+    DebuggerController.SourceScripts.prettyPrint(source)
+      .then(resetEditor, printError)
+      .then(DebuggerView.showEditor);
   },
 
   /**
    * Marks a breakpoint as selected in this sources container.
    *
    * @param object aItem
    *        The breakpoint item to select.
    */
@@ -685,43 +690,62 @@ SourcesView.prototype = Heritage.extend(
     }
     // The container is not empty and an actual item was selected.
     DebuggerView.setEditorLocation(sourceItem.value);
 
     // Set window title.
     let script = sourceItem.value.split(" -> ").pop();
     document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", script);
 
-    this.maybeShowBlackBoxMessage();
+    DebuggerView.maybeShowBlackBoxMessage();
+    this._updatePrettyPrintButtonState();
   },
 
   /**
-   * Show or hide the black box message vs. source editor depending on if the
-   * selected source is black boxed or not.
+   * Enable or disable the pretty print button depending on whether the selected
+   * source is black boxed or not.
    */
-  maybeShowBlackBoxMessage: function() {
-    let sourceForm = this.selectedItem.attachment.source;
-    let sourceClient = DebuggerController.activeThread.source(sourceForm);
-    this._editorDeck.selectedIndex = sourceClient.isBlackBoxed ? 1 : 0;
+  _updatePrettyPrintButtonState: function() {
+    const { source } = this.selectedItem.attachment;
+    if (gThreadClient.source(source).isBlackBoxed) {
+      this._prettyPrintButton.setAttribute("disabled", true);
+    } else {
+      this._prettyPrintButton.removeAttribute("disabled");
+    }
   },
 
   /**
    * The click listener for the sources container.
    */
   _onSourceClick: function() {
     // Use this container as a filtering target.
     DebuggerView.Filtering.target = this;
   },
 
   /**
    * The check listener for the sources container.
    */
   _onSourceCheck: function({ detail: { checked }, target }) {
-    let item = this.getItemForElement(target);
-    DebuggerController.SourceScripts.blackBox(item.attachment.source, !checked);
+    const shouldBlackBox = !checked;
+
+    // Be optimistic that the (un-)black boxing will succeed and enable/disable
+    // the pretty print button immediately. Then, once we actually get the
+    // results from the server, make sure that it is in the correct state again
+    // by calling `_updatePrettyPrintButtonState`.
+
+    if (shouldBlackBox) {
+      this._prettyPrintButton.setAttribute("disabled", true);
+    } else {
+      this._prettyPrintButton.removeAttribute("disabled");
+    }
+
+    const { source } = this.getItemForElement(target).attachment;
+    DebuggerController.SourceScripts.blackBox(source, shouldBlackBox)
+      .then(this._updatePrettyPrintButtonState,
+            this._updatePrettyPrintButtonState);
   },
 
   /**
    * The click listener for the "stop black boxing" button.
    */
   _onStopBlackBoxing: function() {
     let sourceForm = this.selectedItem.attachment.source;
     DebuggerController.SourceScripts.blackBox(sourceForm, false);
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -109,16 +109,22 @@ let DebuggerView = {
    */
   _initializePanes: function() {
     dumpn("Initializing the DebuggerView panes");
 
     this._sourcesPane = document.getElementById("sources-pane");
     this._instrumentsPane = document.getElementById("instruments-pane");
     this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
 
+    this.showEditor = this.showEditor.bind(this);
+    this.showBlackBoxMessage = this.showBlackBoxMessage.bind(this);
+    this.showProgressBar = this.showProgressBar.bind(this);
+    this.maybeShowBlackBoxMessage = this.maybeShowBlackBoxMessage.bind(this);
+    this._editorDeck = document.getElementById("editor-deck");
+
     this._onTabSelect = this._onInstrumentsPaneTabSelect.bind(this);
     this._instrumentsPane.tabpanels.addEventListener("select", this._onTabSelect);
 
     this._collapsePaneString = L10N.getStr("collapsePanes");
     this._expandPaneString = L10N.getStr("expandPanes");
 
     this._sourcesPane.setAttribute("width", Prefs.sourcesWidth);
     this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
@@ -217,16 +223,50 @@ let DebuggerView = {
 
     DebuggerController.Breakpoints.destroy().then(() => {
       window.emit(EVENTS.EDITOR_UNLOADED, this.editor);
       aCallback();
     });
   },
 
   /**
+   * Display the source editor.
+   */
+  showEditor: function() {
+    this._editorDeck.selectedIndex = 0;
+  },
+
+  /**
+   * Display the black box message.
+   */
+  showBlackBoxMessage: function() {
+    this._editorDeck.selectedIndex = 1;
+  },
+
+  /**
+   * Display the progress bar.
+   */
+  showProgressBar: function() {
+    this._editorDeck.selectedIndex = 2;
+  },
+
+  /**
+   * Show or hide the black box message vs. source editor depending on if the
+   * selected source is black boxed or not.
+   */
+  maybeShowBlackBoxMessage: function() {
+    let { source } = DebuggerView.Sources.selectedItem.attachment;
+    if (gThreadClient.source(source).isBlackBoxed) {
+      this.showBlackBoxMessage();
+    } else {
+      this.showEditor();
+    }
+  },
+
+  /**
    * Sets the currently displayed text contents in the source editor.
    * This resets the mode and undo stack.
    *
    * @param string aTextContent
    *        The source text content.
    */
   _setEditorText: function(aTextContent = "") {
     this.editor.setMode(SourceEditor.MODES.TEXT);
@@ -517,16 +557,17 @@ let DebuggerView = {
   editor: null,
   _editorSource: {},
   _loadingText: "",
   _sourcesPane: null,
   _instrumentsPane: null,
   _instrumentsPaneToggleButton: null,
   _collapsePaneString: "",
   _expandPaneString: "",
+  _editorDeck: null,
 };
 
 /**
  * A stacked list of items, compatible with WidgetMethods instances, used for
  * displaying views like the watch expressions, filtering or search results etc.
  *
  * You should never need to access these methods directly, use the wrapped
  * WidgetMethods instead.
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -340,16 +340,20 @@
               &debuggerUI.blackBoxMessage.label;
             </label>
             <button id="black-boxed-message-button"
                     class="devtools-toolbarbutton"
                     label="&debuggerUI.blackBoxMessage.unBlackBoxButton;"
                     image="chrome://browser/skin/devtools/blackBoxMessageEye.png"
                     command="unBlackBoxCommand"/>
           </vbox>
+          <vbox id="source-progress-container" align="center" pack="center">
+            <progressmeter id="source-progress"
+                           mode="undetermined"/>
+          </vbox>
         </deck>
         <splitter class="devtools-side-splitter"/>
         <tabbox id="instruments-pane"
                 class="devtools-sidebar-tabs"
                 hidden="true">
           <tabs>
             <tab id="variables-tab" label="&debuggerUI.tabs.variables;"/>
             <tab id="events-tab" label="&debuggerUI.tabs.events;"/>
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -117,16 +117,17 @@ support-files =
 [browser_dbg_pretty-print-02.js]
 [browser_dbg_pretty-print-03.js]
 [browser_dbg_pretty-print-04.js]
 [browser_dbg_pretty-print-05.js]
 [browser_dbg_pretty-print-06.js]
 [browser_dbg_pretty-print-07.js]
 [browser_dbg_pretty-print-08.js]
 [browser_dbg_pretty-print-09.js]
+[browser_dbg_pretty-print-10.js]
 [browser_dbg_progress-listener-bug.js]
 [browser_dbg_reload-preferred-script-01.js]
 [browser_dbg_reload-preferred-script-02.js]
 [browser_dbg_reload-preferred-script-03.js]
 [browser_dbg_reload-same-script.js]
 [browser_dbg_scripts-switching-01.js]
 [browser_dbg_scripts-switching-02.js]
 [browser_dbg_scripts-switching-03.js]
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-01.js
@@ -19,19 +19,21 @@ function test() {
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
 
     waitForSourceShown(gPanel, "code_ugly.js")
       .then(testSourceIsUgly)
       .then(() => {
         const finished = waitForSourceShown(gPanel, "code_ugly.js");
         clickPrettyPrintButton();
+        testProgressBarShown();
         return finished;
       })
       .then(testSourceIsPretty)
+      .then(testEditorShown)
       .then(testSourceIsStillPretty)
       .then(() => closeDebuggerAndFinish(gPanel))
       .then(null, aError => {
         ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
       });
   });
 }
 
@@ -41,21 +43,31 @@ function testSourceIsUgly() {
 }
 
 function clickPrettyPrintButton() {
   EventUtils.sendMouseEvent({ type: "click" },
                             gDebugger.document.getElementById("pretty-print"),
                             gDebugger);
 }
 
+function testProgressBarShown() {
+  const deck = gDebugger.document.getElementById("editor-deck");
+  is(deck.selectedIndex, 2, "The progress bar should be shown");
+}
+
 function testSourceIsPretty() {
   ok(gEditor.getText().contains("\n    "),
      "The source should be pretty printed.")
 }
 
+function testEditorShown() {
+  const deck = gDebugger.document.getElementById("editor-deck");
+  is(deck.selectedIndex, 0, "The editor should be shown");
+}
+
 function testSourceIsStillPretty() {
   const deferred = promise.defer();
 
   const { source } = gSources.selectedItem.attachment;
   gDebugger.DebuggerController.SourceScripts.getText(source).then(([, text]) => {
     ok(text.contains("\n    "),
        "Subsequent calls to getText return the pretty printed source.");
     deferred.resolve();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-10.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that we disable the pretty print button for black boxed sources,
+ * and that clicking it doesn't do anything.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+let gTab, gDebuggee, gPanel, gDebugger;
+let gEditor, gSources;
+
+function test() {
+  initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPanel = aPanel;
+    gDebugger = gPanel.panelWin;
+    gEditor = gDebugger.DebuggerView.editor;
+    gSources = gDebugger.DebuggerView.Sources;
+
+    waitForSourceShown(gPanel, "code_ugly.js")
+      .then(testSourceIsUgly)
+      .then(blackBoxSource)
+      .then(waitForThreadEvents.bind(null, gPanel, "blackboxchange"))
+      .then(clickPrettyPrintButton)
+      .then(testSourceIsStillUgly)
+      .then(() => closeDebuggerAndFinish(gPanel))
+      .then(null, aError => {
+        ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
+      });
+  });
+}
+
+function testSourceIsUgly() {
+  ok(!gEditor.getText().contains("\n    "),
+     "The source shouldn't be pretty printed yet.");
+}
+
+function blackBoxSource() {
+  const checkbox = gDebugger.document.querySelector(
+    ".selected .side-menu-widget-item-checkbox");
+  checkbox.click();
+}
+
+function clickPrettyPrintButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebugger.document.getElementById("pretty-print"),
+                            gDebugger);
+}
+
+function testSourceIsStillUgly() {
+  const { source } = gSources.selectedItem.attachment;
+  return gDebugger.DebuggerController.SourceScripts.getText(source).then(([, text]) => {
+    ok(!text.contains("\n    "));
+  });
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+  gEditor = null;
+  gSources = null;
+});
--- a/browser/devtools/sourceeditor/test/browser.ini
+++ b/browser/devtools/sourceeditor/test/browser.ini
@@ -25,9 +25,10 @@ support-files =
 [browser_bug725392_mouse_coords_char_offset.js]
 [browser_bug725430_comment_uncomment.js]
 [browser_bug725618_moveLines_shortcut.js]
 [browser_bug729480_line_vertical_align.js]
 [browser_bug729960_block_bracket_jump.js]
 [browser_bug731721_debugger_stepping.js]
 [browser_bug744021_next_prev_bracket_jump.js]
 [browser_codemirror.js]
+skip-if = os == "linux"
 [browser_sourceeditor_initialization.js]
--- a/browser/metro/base/content/bindings/grid.xml
+++ b/browser/metro/base/content/bindings/grid.xml
@@ -21,16 +21,20 @@
     <implementation implements="nsIDOMXULSelectControlElement">
       <property name="_grid" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'grid');"/>
 
       <property name="isBound" readonly="true" onget="return !!this._grid"/>
       <property name="isArranging" readonly="true" onget="return !!this._scheduledArrangeItemsTimerId"/>
 
       <field name="controller">null</field>
 
+      <!-- collection of child items excluding empty tiles -->
+      <property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem');"/>
+      <property name="itemCount" readonly="true" onget="return this.items.length;"/>
+
       <!-- nsIDOMXULMultiSelectControlElement (not fully implemented) -->
 
       <method name="clearSelection">
         <body>
           <![CDATA[
             // 'selection' and 'selected' are confusingly overloaded here
             // as richgrid is adopting multi-select behavior, but select/selected are already being
             // used to describe triggering the default action of a tile
@@ -153,18 +157,16 @@
             // returns Set
             return verbSet;
           ]]>
         </getter>
       </property>
 
     <!-- nsIDOMXULSelectControlElement -->
 
-      <property name="itemCount" readonly="true" onget="return this.children.length;"/>
-
       <field name="_selectedItem">null</field>
       <property name="selectedItem" onget="return this._selectedItem;">
         <setter>
           <![CDATA[
             this.selectItem(val);
           ]]>
         </setter>
       </property>
@@ -197,17 +199,17 @@
       </property>
 
       <method name="appendItem">
         <parameter name="aLabel"/>
         <parameter name="aValue"/>
         <parameter name="aSkipArrange"/>
         <body>
           <![CDATA[
-            let addition = this.createItemElement(aLabel, aValue);
+            let addition = this._createItemElement(aLabel, aValue);
             this.appendChild(addition);
             if (!aSkipArrange)
               this.arrangeItems();
             return addition;
           ]]>
         </body>
       </method>
 
@@ -227,17 +229,17 @@
       <method name="insertItemAt">
         <parameter name="anIndex"/>
         <parameter name="aLabel"/>
         <parameter name="aValue"/>
         <parameter name="aSkipArrange"/>
         <body>
           <![CDATA[
             let existing = this.getItemAtIndex(anIndex);
-            let addition = this.createItemElement(aLabel, aValue);
+            let addition = this._createItemElement(aLabel, aValue);
             if (existing) {
               this.insertBefore(addition, existing);
             } else {
               this.appendChild(addition);
             }
             if (!aSkipArrange)
               this.arrangeItems();
             return addition;
@@ -245,57 +247,60 @@
         </body>
       </method>
       <method name="removeItemAt">
         <parameter name="anIndex"/>
         <parameter name="aSkipArrange"/>
         <body>
           <![CDATA[
             let item = this.getItemAtIndex(anIndex);
-            return item ? this.removeItem(item, aSkipArrange) : null;
+            if (!item)
+              return null;
+            return this.removeItem(item, aSkipArrange);
           ]]>
         </body>
       </method>
 
       <method name="removeItem">
         <parameter name="aItem"/>
         <parameter name="aSkipArrange"/>
         <body>
           <![CDATA[
-            let removal = aItem.parentNode == this && this.removeChild(aItem);
+            if (!aItem || Array.indexOf(this.items, aItem) < 0)
+              return null;
+            let removal = this.removeChild(aItem);
             if (removal && !aSkipArrange)
                 this.arrangeItems();
 
             // note that after removal the node is unbound
             // so none of the richgriditem binding methods & properties are available
             return removal;
           ]]>
         </body>
       </method>
 
-
       <method name="getIndexOfItem">
         <parameter name="anItem"/>
         <body>
           <![CDATA[
             if (!anItem)
               return -1;
 
-            return Array.indexOf(this.children, anItem);
+            return Array.indexOf(this.items, anItem);
           ]]>
         </body>
       </method>
 
       <method name="getItemAtIndex">
         <parameter name="anIndex"/>
         <body>
           <![CDATA[
             if (!this._isIndexInBounds(anIndex))
               return null;
-            return this.children.item(anIndex);
+            return this.items.item(anIndex);
           ]]>
         </body>
       </method>
 
       <method name="getItemsByUrl">
         <parameter name="aUrl"/>
         <body>
           <![CDATA[
@@ -604,26 +609,28 @@
         <parameter name="anIndex"/>
         <body>
           <![CDATA[
             return anIndex >= 0 && anIndex < this.itemCount;
           ]]>
         </body>
       </method>
 
-      <method name="createItemElement">
+      <method name="_createItemElement">
         <parameter name="aLabel"/>
         <parameter name="aValue"/>
         <body>
           <![CDATA[
             let item = this.ownerDocument.createElement("richgriditem");
-            item.setAttribute("label", aLabel);
             if (aValue) {
               item.setAttribute("value", aValue);
             }
+            if (aLabel) {
+              item.setAttribute("label", aLabel);
+            }
             if(this.hasAttribute("tiletype")) {
               item.setAttribute("tiletype", this.getAttribute("tiletype"));
             }
             return item;
           ]]>
         </body>
       </method>
 
@@ -855,17 +862,17 @@
             this.refreshBackgroundImage();
           ]]>
         </body>
       </method>
 
       <property name="control">
         <getter><![CDATA[
           let parent = this.parentNode;
-          while (parent) {
+          while (parent && parent != this.ownerDocument.documentElement) {
             if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
               return parent;
             parent = parent.parentNode;
           }
           return null;
         ]]></getter>
       </property>
 
--- a/browser/metro/base/content/startui/TopSitesView.js
+++ b/browser/metro/base/content/startui/TopSitesView.js
@@ -95,17 +95,17 @@ TopSitesView.prototype = Util.extend(Obj
           // stop the appbar from dismissing,
           // the selectionchange event will trigger re-population of the context appbar
           aEvent.preventDefault();
         }
         break;
       case "pin":
         let pinIndices = [];
         Array.forEach(selectedTiles, function(aNode) {
-          pinIndices.push( Array.indexOf(aNode.control.children, aNode) );
+          pinIndices.push( Array.indexOf(aNode.control.items, aNode) );
           aNode.contextActions.delete('pin');
           aNode.contextActions.add('unpin');
         });
         TopSites.pinSites(sites, pinIndices);
         break;
       case "unpin":
         Array.forEach(selectedTiles, function(aNode) {
           aNode.contextActions.delete('unpin');
@@ -148,63 +148,61 @@ TopSitesView.prototype = Util.extend(Obj
         if (tileNode) {
           this.updateTile(tileNode, new Site(site));
         }
       }
     } else {
         // flush, recreate all
       this.isUpdating = true;
       // destroy and recreate all item nodes, skip calling arrangeItems
-      grid.clearAll(true);
       this.populateGrid();
     }
   },
 
   updateTile: function(aTileNode, aSite, aArrangeGrid) {
     this._updateFavicon(aTileNode, Util.makeURI(aSite.url));
 
     Task.spawn(function() {
       let filepath = PageThumbsStorage.getFilePathForURL(aSite.url);
       if (yield OS.File.exists(filepath)) {
         aSite.backgroundImage = 'url("'+PageThumbs.getThumbnailURL(aSite.url)+'")';
-        aTileNode.setAttribute("customImage", aSite.backgroundImage);
-        if (aTileNode.refresh) {
-          aTileNode.refresh()
+        if ('backgroundImage' in aTileNode) {
+          aTileNode.backgroundImage = aSite.backgroundImage;
+        } else {
+          aTileNode.setAttribute("customImage", aSite.backgroundImage);
         }
       }
     });
 
     aSite.applyToTileNode(aTileNode);
+    if (aTileNode.refresh) {
+      aTileNode.refresh();
+    }
     if (aArrangeGrid) {
       this._set.arrangeItems();
     }
   },
 
   populateGrid: function populateGrid() {
     this.isUpdating = true;
 
     let sites = TopSites.getSites();
-    let length = Math.min(sites.length, this._topSitesMax || Infinity);
+    if (this._topSitesMax) {
+      sites = sites.slice(0, this._topSitesMax);
+    }
     let tileset = this._set;
+    tileset.clearAll(true);
 
-    // if we're updating with a collection that is smaller than previous
-    // remove any extra tiles
-    while (tileset.children.length > length) {
-      tileset.removeChild(tileset.children[tileset.children.length -1]);
-    }
-
-    for (let idx=0; idx < length; idx++) {
-      let isNew = !tileset.children[idx],
-          site = sites[idx];
-      let item = isNew ? tileset.createItemElement(site.title, site.url) : tileset.children[idx];
+    for (let site of sites) {
+      // call to private _createItemElement is a temp measure
+      // we'll eventually just request the next slot
+      let item = tileset._createItemElement(site.title, site.url);
 
       this.updateTile(item, site);
-      if (isNew) {
-        tileset.appendChild(item);
-      }
+      tileset.appendChild(item);
     }
     tileset.arrangeItems();
     this.isUpdating = false;
   },
 
   forceReloadOfThumbnail: function forceReloadOfThumbnail(url) {
     let nodes = this._set.querySelectorAll('richgriditem[value="'+url+'"]');
     for (let item of nodes) {
--- a/browser/metro/base/tests/mochitest/browser_tiles.js
+++ b/browser/metro/base/tests/mochitest/browser_tiles.js
@@ -13,28 +13,28 @@ gTests.push({
   desc: "richgrid binding is applied",
   run: function() {
     ok(doc, "doc got defined");
 
     let grid = doc.querySelector("#grid1");
     ok(grid, "#grid1 is found");
     is(typeof grid.clearSelection, "function", "#grid1 has the binding applied");
 
-    is(grid.children.length, 2, "#grid1 has a 2 items");
-    is(grid.children[0].control, grid, "#grid1 item's control points back at #grid1'");
+    is(grid.items.length, 2, "#grid1 has a 2 items");
+    is(grid.items[0].control, grid, "#grid1 item's control points back at #grid1'");
   }
 });
 
 gTests.push({
   desc: "item clicks are handled",
   run: function() {
     let grid = doc.querySelector("#grid1");
     is(typeof grid.handleItemClick, "function", "grid.handleItemClick is a function");
     let handleStub = stubMethod(grid, 'handleItemClick');
-    let itemId = "grid1_item1"; // grid.children[0].getAttribute("id");
+    let itemId = "grid1_item1"; // grid.items[0].getAttribute("id");
 
     // send click to item and wait for next tick;
     EventUtils.sendMouseEvent({type: 'click'}, itemId, doc.defaultView);
     yield waitForMs(0);
 
     is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item");
     handleStub.restore();
 
@@ -109,19 +109,18 @@ gTests.push({
     is(grid.itemCount, 3, "grid has 3 items initially");
     is(grid.rowCount, 2, "grid has 2 rows initially");
     is(grid.columnCount, 2, "grid has 2 cols initially");
 
     let arrangeSpy = spyOnMethod(grid, "arrangeItems");
     grid.clearAll();
 
     is(grid.itemCount, 0, "grid has 0 itemCount after clearAll");
-    is(grid.children.length, 0, "grid has 0 children after clearAll");
-    is(grid.rowCount, 0, "grid has 0 rows when empty");
-    is(grid.columnCount, 0, "grid has 0 cols when empty");
+    is(grid.items.length, 0, "grid has 0 items after clearAll");
+    // now that we use slots, an empty grid may still have non-zero rows & columns
 
     is(arrangeSpy.callCount, 1, "arrangeItems is called once when we clearAll");
     arrangeSpy.restore();
   }
 });
 
 gTests.push({
   desc: "empty grid",
@@ -145,23 +144,23 @@ gTests.push({
 gTests.push({
   desc: "appendItem",
   run: function() {
      // implements an appendItem with signature title, uri, returns item element
      // appendItem triggers arrangeItems
     let grid = doc.querySelector("#emptygrid");
 
     is(grid.itemCount, 0, "0 itemCount when empty");
-    is(grid.children.length, 0, "0 children when empty");
+    is(grid.items.length, 0, "0 items when empty");
     is(typeof grid.appendItem, "function", "appendItem is a function on the grid");
 
     let arrangeStub = stubMethod(grid, "arrangeItems");
     let newItem = grid.appendItem("test title", "about:blank");
 
-    ok(newItem && grid.children[0]==newItem, "appendItem gives back the item");
+    ok(newItem && grid.items[0]==newItem, "appendItem gives back the item");
     is(grid.itemCount, 1, "itemCount is incremented when we appendItem");
     is(newItem.getAttribute("label"), "test title", "title ends up on label attribute");
     is(newItem.getAttribute("value"), "about:blank", "url ends up on value attribute");
 
     is(arrangeStub.callCount, 1, "arrangeItems is called when we appendItem");
     arrangeStub.restore();
   }
 });
@@ -188,17 +187,17 @@ gTests.push({
     is(grid.itemCount, 2, "2 items initially");
     is(typeof grid.removeItemAt, "function", "removeItemAt is a function on the grid");
 
     let arrangeStub = stubMethod(grid, "arrangeItems");
     let removedItem = grid.removeItemAt(0);
 
     ok(removedItem, "removeItemAt gives back an item");
     is(removedItem.getAttribute("id"), "grid2_item1", "removeItemAt gives back the correct item");
-    is(grid.children[0].getAttribute("id"), "grid2_item2", "2nd item becomes the first item");
+    is(grid.items[0].getAttribute("id"), "grid2_item2", "2nd item becomes the first item");
     is(grid.itemCount, 1, "itemCount is decremented when we removeItemAt");
 
     is(arrangeStub.callCount, 1, "arrangeItems is called when we removeItemAt");
     arrangeStub.restore();
   }
 });
 
 gTests.push({
@@ -210,20 +209,20 @@ gTests.push({
 
     is(grid.itemCount, 2, "2 items initially");
     is(typeof grid.insertItemAt, "function", "insertItemAt is a function on the grid");
 
     let arrangeStub = stubMethod(grid, "arrangeItems");
     let insertedItem = grid.insertItemAt(1, "inserted item", "http://example.com/inserted");
 
     ok(insertedItem, "insertItemAt gives back an item");
-    is(grid.children[1], insertedItem, "item is inserted at the correct index");
+    is(grid.items[1], insertedItem, "item is inserted at the correct index");
     is(insertedItem.getAttribute("label"), "inserted item", "insertItemAt creates item with the correct label");
     is(insertedItem.getAttribute("value"), "http://example.com/inserted", "insertItemAt creates item with the correct url value");
-    is(grid.children[2].getAttribute("id"), "grid3_item2", "following item ends up at the correct index");
+    is(grid.items[2].getAttribute("id"), "grid3_item2", "following item ends up at the correct index");
     is(grid.itemCount, 3, "itemCount is incremented when we insertItemAt");
 
     is(arrangeStub.callCount, 1, "arrangeItems is called when we insertItemAt");
     arrangeStub.restore();
   }
 });
 
 gTests.push({
@@ -270,21 +269,21 @@ gTests.push({
   desc: "removeItem",
   run: function() {
     let grid = doc.querySelector("#grid5");
 
     is(grid.itemCount, 4, "4 items total");
     is(typeof grid.removeItem, "function", "removeItem is a function on the grid");
 
     let arrangeStub = stubMethod(grid, "arrangeItems");
-    let removedFirst = grid.removeItem( grid.children[0] );
+    let removedFirst = grid.removeItem( grid.items[0] );
 
     is(arrangeStub.callCount, 1, "arrangeItems is called when we removeItem");
 
-    let removed2nd = grid.removeItem( grid.children[0], true);
+    let removed2nd = grid.removeItem( grid.items[0], true);
     is(removed2nd.getAttribute("label"), "2nd item", "the next item was returned");
     is(grid.itemCount, 2, "2 items remain");
 
     // callCount should still be at 1
     is(arrangeStub.callCount, 1, "arrangeItems is not called when we pass the truthy skipArrange param");
 
     let otherItem = grid.ownerDocument.querySelector("#grid6_item1");
     let removedFail = grid.removeItem(otherItem);
@@ -311,51 +310,51 @@ gTests.push({
     is(typeof grid.clearSelection, "function", "clearSelection is a function on the grid");
     is(typeof grid.selectedItems, "object", "selectedItems is a property on the grid");
     is(typeof grid.toggleItemSelection, "function", "toggleItemSelection is function on the grid");
     is(typeof grid.selectItem, "function", "selectItem is a function on the grid");
 
     is(grid.itemCount, 2, "2 items initially");
     is(grid.selectedItems.length, 0, "nothing selected initially");
 
-    grid.toggleItemSelection(grid.children[1]);
-    ok(grid.children[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item");
+    grid.toggleItemSelection(grid.items[1]);
+    ok(grid.items[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item");
     is(grid.selectedIndex, 1, "selectedIndex is correct");
 
-    grid.toggleItemSelection(grid.children[1]);
-    ok(!grid.children[1].selected, "toggleItemSelection sets falsy selected prop on previously-selected item");
+    grid.toggleItemSelection(grid.items[1]);
+    ok(!grid.items[1].selected, "toggleItemSelection sets falsy selected prop on previously-selected item");
     is(grid.selectedIndex, -1, "selectedIndex reports correctly with nothing selected");
 
     // item selection
-    grid.selectItem(grid.children[1]);
-    ok(grid.children[1].selected, "Item selected property is truthy after grid.selectItem");
-    ok(grid.children[1].getAttribute("selected"), "Item selected attribute is truthy after grid.selectItem");
+    grid.selectItem(grid.items[1]);
+    ok(grid.items[1].selected, "Item selected property is truthy after grid.selectItem");
+    ok(grid.items[1].getAttribute("selected"), "Item selected attribute is truthy after grid.selectItem");
     ok(grid.selectedItems.length, "There are selectedItems after grid.selectItem");
 
     // clearSelection
-    grid.selectItem(grid.children[0]);
-    grid.selectItem(grid.children[1]);
+    grid.selectItem(grid.items[0]);
+    grid.selectItem(grid.items[1]);
     grid.clearSelection();
     is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
     is(grid.selectedIndex, -1, "selectedIndex resets after clearSelection");
 
     // select events
     // in seltype=single mode, select is like the default action for the tile
     // (think <a>, not <select multiple>)
     let handler = {
       handleEvent: function(aEvent) {}
     };
     let handlerStub = stubMethod(handler, "handleEvent");
     doc.defaultView.addEventListener("select", handler, false);
     info("select listener added");
 
-    info("calling selectItem, currently it is:" + grid.children[0].selected);
+    info("calling selectItem, currently it is:" + grid.items[0].selected);
     // Note: A richgrid in seltype=single mode fires "select" events from selectItem
-    grid.selectItem(grid.children[0]);
-    info("calling selectItem, now it is:" + grid.children[0].selected);
+    grid.selectItem(grid.items[0]);
+    info("calling selectItem, now it is:" + grid.items[0].selected);
     yield waitForMs(0);
 
     is(handlerStub.callCount, 1, "select event handler was called when we selected an item");
     is(handlerStub.calledWith[0].type, "select", "handler got a select event");
     is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target");
     handlerStub.restore();
     doc.defaultView.removeEventListener("select", handler, false);
   }
@@ -373,51 +372,48 @@ gTests.push({
     is(typeof grid.clearSelection, "function", "clearSelection is a function on the grid");
     is(typeof grid.selectedItems, "object", "selectedItems is a property on the grid");
     is(typeof grid.toggleItemSelection, "function", "toggleItemSelection is function on the grid");
     is(typeof grid.selectItem, "function", "selectItem is a function on the grid");
 
     is(grid.itemCount, 2, "2 items initially");
     is(grid.selectedItems.length, 0, "nothing selected initially");
 
-    grid.toggleItemSelection(grid.children[1]);
-    ok(grid.children[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item");
+    grid.toggleItemSelection(grid.items[1]);
+    ok(grid.items[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item");
     is(grid.selectedItems.length, 1, "1 item selected when we first toggleItemSelection");
-    is(grid.selectedItems[0], grid.children[1], "the right item is selected");
+    is(grid.selectedItems[0], grid.items[1], "the right item is selected");
     is(grid.selectedIndex, 1, "selectedIndex is correct");
 
-    grid.toggleItemSelection(grid.children[1]);
+    grid.toggleItemSelection(grid.items[1]);
     is(grid.selectedItems.length, 0, "Nothing selected when we toggleItemSelection again");
 
     // clearSelection
-    grid.children[0].selected=true;
-    grid.children[1].selected=true;
+    grid.items[0].selected=true;
+    grid.items[1].selected=true;
     is(grid.selectedItems.length, 2, "Both items are selected before calling clearSelection");
     grid.clearSelection();
     is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
-    ok(!(grid.children[0].selected || grid.children[1].selected), "selected properties all falsy when we clearSelection");
+    ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we clearSelection");
 
     // selectionchange events
     // in seltype=multiple mode, we track selected state on all items
     // (think <select multiple> not <a>)
     let handler = {
       handleEvent: function(aEvent) {}
     };
     let handlerStub = stubMethod(handler, "handleEvent");
     doc.defaultView.addEventListener("selectionchange", handler, false);
     info("selectionchange listener added");
 
-    info("calling toggleItemSelection, currently it is:" + grid.children[0].selected);
+    info("calling toggleItemSelection, currently it is:" + grid.items[0].selected);
     // Note: A richgrid in seltype=single mode fires "select" events from selectItem
-    grid.toggleItemSelection(grid.children[0]);
-    info("/calling toggleItemSelection, now it is:" + grid.children[0].selected);
+    grid.toggleItemSelection(grid.items[0]);
+    info("/calling toggleItemSelection, now it is:" + grid.items[0].selected);
     yield waitForMs(0);
 
     is(handlerStub.callCount, 1, "selectionchange event handler was called when we selected an item");
     is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event");
     is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target");
     handlerStub.restore();
     doc.defaultView.removeEventListener("selectionchange", handler, false);
   }
 });
-
-     // implements a getItemAtIndex method (or grid.children[idx] ?)
-
--- a/browser/metro/base/tests/mochitest/browser_topsites.js
+++ b/browser/metro/base/tests/mochitest/browser_topsites.js
@@ -189,17 +189,17 @@ gTests.push({
 
     let arrangedPromise = waitForEvent(grid, "arranged");
     yield TopSitesTestHelper.updatePagesAndWait();
     // pause until the update has fired and the view is finishd updating
     yield arrangedPromise;
   },
   run: function() {
     let grid = TopSitesTestHelper.grid;
-    let items = grid.children;
+    let items = grid.items;
     is(items.length, 8, "should be 8 topsites"); // i.e. not 10
     if(items.length) {
       let firstitem = items[0];
       is(
         firstitem.getAttribute("label"),
         "brian",
         "first item label should be 'brian': " + firstitem.getAttribute("label")
       );
@@ -225,17 +225,17 @@ gTests.push({
     // pause until the update has fired and the view is finishd updating
     let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
     yield TopSitesTestHelper.updatePagesAndWait();
     yield arrangedPromise;
   },
   run: function() {
     // test that pinned state of each site as rendered matches our expectations
     let pins = this.pins.split(",");
-    let items = TopSitesTestHelper.grid.children;
+    let items = TopSitesTestHelper.grid.items;
     is(items.length, 8, "should be 8 topsites in the grid");
 
     is(TopSitesTestHelper.document.querySelectorAll("#start-topsites-grid > [pinned]").length, 3, "should be 3 children with 'pinned' attribute");
     try {
       Array.forEach(items, function(aItem, aIndex){
         // pinned state should agree with the pins array
         is(
             aItem.hasAttribute("pinned"), !!pins[aIndex],
@@ -268,35 +268,35 @@ gTests.push({
     yield TopSitesTestHelper.updatePagesAndWait();
     yield arrangedPromise;
   },
   run: function() {
     // pin a site
     // test that site is pinned as expected
     // and that sites fill positions around it
     let grid = TopSitesTestHelper.grid,
-        items = grid.children;
+        items = grid.items;
     is(items.length, 4, this.desc + ": should be 4 topsites");
 
-    let tile = grid.children[2],
+    let tile = grid.items[2],
         url = tile.getAttribute("value"),
         title = tile.getAttribute("label");
 
     info(this.desc + ": pinning site at index 2");
     TopSites.pinSites([{
           url: url,
           title: title
         }], [2]);
 
     // pinning shouldn't require re-arranging - just wait for isUpdating flag to flip
     yield waitForCondition(function(){
       return !grid.controller.isUpdating;
     });
 
-    let thirdTile = grid.children[2];
+    let thirdTile = grid.items[2];
     ok( thirdTile.hasAttribute("pinned"), thirdTile.getAttribute("value")+ " should look pinned" );
 
     // visit some more sites
     yield TopSitesTestHelper.fillHistory( TopSitesTestHelper.mockLinks("brian,dougal,dylan,ermintrude,florence,moose") );
 
     // force flush and repopulation of links cache
     yield TopSites.prepareCache(true);
     // pause until the update has fired and the view is finishd updating
@@ -324,34 +324,34 @@ gTests.push({
     let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
     yield TopSitesTestHelper.updatePagesAndWait();
     yield arrangedPromise;
   },
   run: function() {
     // unpin a pinned site
     // test that sites are unpinned as expected
     let grid = TopSitesTestHelper.grid,
-        items = grid.children;
+        items = grid.items;
     is(items.length, 8, this.desc + ": should be 8 topsites");
     let site = {
       url: items[1].getAttribute("value"),
       title: items[1].getAttribute("label")
     };
     // verify assumptions before unpinning this site
     ok( NewTabUtils.pinnedLinks.isPinned(site), "2nd item is pinned" );
     ok( items[1].hasAttribute("pinned"), "2nd item has pinned attribute" );
 
     // unpinning shouldn't require re-arranging - just wait for isUpdating flag to flip
     TopSites.unpinSites([site]);
 
     yield waitForCondition(function(){
       return !grid.controller.isUpdating;
     });
 
-    let secondTile = grid.children[1];
+    let secondTile = grid.items[1];
     ok( !secondTile.hasAttribute("pinned"), "2nd item should no longer be marked as pinned" );
     ok( !NewTabUtils.pinnedLinks.isPinned(site), "2nd item should no longer be pinned" );
   }
 });
 
 gTests.push({
   desc: "block/unblock sites",
   setUp: function() {
@@ -366,17 +366,17 @@ gTests.push({
     yield TopSitesTestHelper.updatePagesAndWait();
     yield arrangedPromise;
   },
   run: function() {
     try {
       // block a site
       // test that sites are removed from the grid as expected
       let grid = TopSitesTestHelper.grid,
-          items = grid.children;
+          items = grid.items;
       is(items.length, 8, this.desc + ": should be 8 topsites");
 
       let brianSite = TopSitesTestHelper.siteFromNode(items[0]);
       let dougalSite = TopSitesTestHelper.siteFromNode(items[1]);
       let dylanSite = TopSitesTestHelper.siteFromNode(items[2]);
 
       let arrangedPromise = waitForEvent(grid, "arranged");
       // we'll block brian (he's not pinned)
@@ -421,17 +421,17 @@ gTests.push({
       // verify brian, dougal and dyland are unblocked and back in the grid
       ok( !NewTabUtils.blockedLinks.isBlocked(brianSite), "site was unblocked" );
       is( grid.querySelectorAll("[value='"+brianSite.url+"']").length, 1, "Unblocked site is back in the grid");
 
       ok( !NewTabUtils.blockedLinks.isBlocked(dougalSite), "site was unblocked" );
       is( grid.querySelectorAll("[value='"+dougalSite.url+"']").length, 1, "Unblocked site is back in the grid");
       // ..and that a previously pinned site is re-pinned after being blocked, then restored
       ok( NewTabUtils.pinnedLinks.isPinned(dougalSite), "Restoring previously pinned site makes it pinned again" );
-      is( grid.children[1].getAttribute("value"), dougalSite.url, "Blocked Site restored to pinned index" );
+      is( grid.items[1].getAttribute("value"), dougalSite.url, "Blocked Site restored to pinned index" );
 
       ok( !NewTabUtils.blockedLinks.isBlocked(dylanSite), "site was unblocked" );
       is( grid.querySelectorAll("[value='"+dylanSite.url+"']").length, 1, "Unblocked site is back in the grid");
 
     } catch(ex) {
 
       ok(false, this.desc+": Caught exception in test: " + ex);
       info("trace: " + ex.stack);
@@ -453,17 +453,17 @@ gTests.push({
     let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
     yield TopSitesTestHelper.updatePagesAndWait();
     yield arrangedPromise;
   },
   run: function() {
     // delete a both pinned and unpinned sites
     // test that sites are removed from the grid
     let grid = TopSitesTestHelper.grid,
-        items = grid.children;
+        items = grid.items;
     is(items.length, 4, this.desc + ": should be 4 topsites");
 
     let brianTile = grid.querySelector('richgriditem[value$="brian"]');
     let dougalTile = grid.querySelector('richgriditem[value$="dougal"]')
 
     // verify assumptions before deleting sites
     ok( brianTile, "Tile for Brian was created");
     ok( dougalTile, "Tile for Dougal was created");
--- a/browser/themes/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -61,26 +61,32 @@
 #sources .side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents {
   color: #888;
 }
 
 #sources .side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents > .dbg-breakpoint {
   display: none;
 }
 
-/* Black box message */
+/* Black box message and source progress meter */
 
-#black-boxed-message {
+#black-boxed-message,
+#source-progress-container {
   background: url(background-noise-toolbar.png) rgb(61,69,76);
   /* Prevent the container deck from aquiring the height from this message. */
   min-height: 1px;
   padding: 25vh 0;
   color: white;
 }
 
+#source-progress {
+  min-height: 2em;
+  min-width: 40em;
+}
+
 #black-boxed-message-label,
 #black-boxed-message-button {
   text-align: center;
   font-size: 120%;
 }
 
 #black-boxed-message-button {
   margin-top: 1em;
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -59,26 +59,32 @@
 #sources .side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents {
   color: #888;
 }
 
 #sources .side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents > .dbg-breakpoint {
   display: none;
 }
 
-/* Black box message */
+/* Black box message and source progress meter */
 
-#black-boxed-message {
+#black-boxed-message,
+#source-progress-container {
   background: url(background-noise-toolbar.png) rgb(61,69,76);
   /* Prevent the container deck from aquiring the height from this message. */
   min-height: 1px;
   padding: 25vh 0;
   color: white;
 }
 
+#source-progress {
+  min-height: 2em;
+  min-width: 40em;
+}
+
 #black-boxed-message-label,
 #black-boxed-message-button {
   text-align: center;
   font-size: 120%;
 }
 
 #black-boxed-message-button {
   margin-top: 1em;
--- a/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -59,26 +59,32 @@
 #sources .side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents {
   color: #888;
 }
 
 #sources .side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents > .dbg-breakpoint {
   display: none;
 }
 
-/* Black box message */
+/* Black box message and source progress meter */
 
-#black-boxed-message {
+#black-boxed-message,
+#source-progress-container {
   background: url(background-noise-toolbar.png) rgb(61,69,76);
   /* Prevent the container deck from aquiring the height from this message. */
   min-height: 1px;
   padding: 25vh 0;
   color: white;
 }
 
+#source-progress {
+  min-height: 2em;
+  min-width: 40em;
+}
+
 #black-boxed-message-label,
 #black-boxed-message-button {
   text-align: center;
   font-size: 120%;
 }
 
 #black-boxed-message-button {
   margin-top: 1em;
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -335,16 +335,36 @@ abstract public class BrowserApp extends
             @Override
             public void run() {
                 final int count = BrowserDB.getReadingListCount(getContentResolver());
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListCountReturn", Integer.toString(count)));
             }
         });
     }
 
+    void handleReaderListStatusRequest(final String url) {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                final int inReadingList = BrowserDB.isReadingListItem(getContentResolver(), url) ? 1 : 0;
+
+                final JSONObject json = new JSONObject();
+                try {
+                    json.put("url", url);
+                    json.put("inReadingList", inReadingList);
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "JSON error - failed to return inReadingList status", e);
+                    return;
+                }
+
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListStatusReturn", json.toString()));
+            }
+        });
+    }
+
     void handleReaderAdded(int result, final String title, final String url) {
         if (result != READER_ADD_SUCCESS) {
             if (result == READER_ADD_FAILED) {
                 showToast(R.string.reading_list_failed, Toast.LENGTH_SHORT);
             } else if (result == READER_ADD_DUPLICATE) {
                 showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
             }
 
@@ -1099,16 +1119,18 @@ abstract public class BrowserApp extends
 
             } else if (event.equals("Telemetry:Gather")) {
                 Telemetry.HistogramAdd("PLACES_PAGES_COUNT", BrowserDB.getCount(getContentResolver(), "history"));
                 Telemetry.HistogramAdd("PLACES_BOOKMARKS_COUNT", BrowserDB.getCount(getContentResolver(), "bookmarks"));
                 Telemetry.HistogramAdd("FENNEC_FAVICONS_COUNT", BrowserDB.getCount(getContentResolver(), "favicons"));
                 Telemetry.HistogramAdd("FENNEC_THUMBNAILS_COUNT", BrowserDB.getCount(getContentResolver(), "thumbnails"));
             } else if (event.equals("Reader:ListCountRequest")) {
                 handleReaderListCountRequest();
+            } else if (event.equals("Reader:ListStatusRequest")) {
+                handleReaderListStatusRequest(message.getString("url"));
             } else if (event.equals("Reader:Added")) {
                 final int result = message.getInt("result");
                 final String title = message.getString("title");
                 final String url = message.getString("url");
                 handleReaderAdded(result, title, url);
             } else if (event.equals("Reader:Removed")) {
                 final String url = message.getString("url");
                 handleReaderRemoved(url);
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1456,16 +1456,17 @@ abstract public class GeckoApp
         }
 
         //app state callbacks
         mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
 
         //register for events
         registerEventListener("log");
         registerEventListener("Reader:ListCountRequest");
+        registerEventListener("Reader:ListStatusRequest");
         registerEventListener("Reader:Added");
         registerEventListener("Reader:Removed");
         registerEventListener("Reader:Share");
         registerEventListener("Reader:FaviconRequest");
         registerEventListener("onCameraCapture");
         registerEventListener("Menu:Add");
         registerEventListener("Menu:Remove");
         registerEventListener("Menu:Update");
@@ -1991,16 +1992,17 @@ abstract public class GeckoApp
         super.onRestart();
     }
 
     @Override
     public void onDestroy()
     {
         unregisterEventListener("log");
         unregisterEventListener("Reader:ListCountRequest");
+        unregisterEventListener("Reader:ListStatusRequest");
         unregisterEventListener("Reader:Added");
         unregisterEventListener("Reader:Removed");
         unregisterEventListener("Reader:Share");
         unregisterEventListener("Reader:FaviconRequest");
         unregisterEventListener("onCameraCapture");
         unregisterEventListener("Menu:Add");
         unregisterEventListener("Menu:Remove");
         unregisterEventListener("Menu:Update");
--- a/mobile/android/base/NotificationHandler.java
+++ b/mobile/android/base/NotificationHandler.java
@@ -28,16 +28,17 @@ public class NotificationHandler {
      * {@link android.app.Service#startForeground(int, android.app.Notification)}
      * associates the foreground with exactly one notification from the service.
      * To keep Fennec alive during downloads (and to make sure it can be killed
      * once downloads are complete), we make sure that the foreground is always
      * associated with an active progress notification if and only if at least
      * one download is in progress.
      */
     private Notification mForegroundNotification;
+    private int mForegroundNotificationId;
 
     public NotificationHandler(Context context) {
         mContext = context;
         mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
     }
 
     /**
      * Adds a notification.
@@ -163,28 +164,31 @@ public class NotificationHandler {
     private boolean isProgressStyle(Notification notification) {
         if (notification != null && notification instanceof AlertNotification) {
             return ((AlertNotification)notification).isProgressStyle();
         }
         return false;
     }
 
     protected void setForegroundNotification(int id, Notification notification) {
+        mForegroundNotificationId = id;
         mForegroundNotification = notification;
     }
 
-    private void updateForegroundNotification(int id, Notification oldNotification) {
-        if (mForegroundNotification == oldNotification) {
+    private void updateForegroundNotification(int oldId, Notification oldNotification) {
+        if (mForegroundNotificationId == oldId) {
             // If we're removing the notification associated with the
             // foreground, we need to pick another active notification to act
             // as the foreground notification.
             Notification foregroundNotification = null;
-            for (final Notification notification : mNotifications.values()) {
+            int foregroundId = 0;
+            for (final Integer id : mNotifications.keySet()) {
+                final Notification notification = mNotifications.get(id);
                 if (isOngoing(notification)) {
                     foregroundNotification = notification;
+                    foregroundId = id;
                     break;
                 }
             }
-
-            setForegroundNotification(id, foregroundNotification);
+            setForegroundNotification(foregroundId, foregroundNotification);
         }
     }
 }
--- a/mobile/android/base/ReaderModeUtils.java
+++ b/mobile/android/base/ReaderModeUtils.java
@@ -57,22 +57,21 @@ public class ReaderModeUtils {
 
         String urlFromAboutReader = getUrlFromAboutReader(newUrl);
         if (urlFromAboutReader == null)
             return false;
 
         return urlFromAboutReader.equals(currentUrl);
     }
 
-    public static String getAboutReaderForUrl(String url, boolean inReadingList) {
-        return getAboutReaderForUrl(url, -1, inReadingList);
+    public static String getAboutReaderForUrl(String url) {
+        return getAboutReaderForUrl(url, -1);
     }
 
-    public static String getAboutReaderForUrl(String url, int tabId, boolean inReadingList) {
-        String aboutReaderUrl = "about:reader?url=" + Uri.encode(url) +
-                                "&readingList=" + (inReadingList ? 1 : 0);
+    public static String getAboutReaderForUrl(String url, int tabId) {
+        String aboutReaderUrl = "about:reader?url=" + Uri.encode(url);
 
         if (tabId >= 0)
             aboutReaderUrl += "&tabId=" + tabId;
 
         return aboutReaderUrl;
     }
 }
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -459,17 +459,17 @@ public class Tab {
         GeckoAppShell.sendEventToGecko(e);
     }
 
     public void toggleReaderMode() {
         if (ReaderModeUtils.isAboutReader(mUrl)) {
             Tabs.getInstance().loadUrl(ReaderModeUtils.getUrlFromAboutReader(mUrl));
         } else if (mReaderEnabled) {
             mEnteringReaderMode = true;
-            Tabs.getInstance().loadUrl(ReaderModeUtils.getAboutReaderForUrl(mUrl, mId, mReadingListItem));
+            Tabs.getInstance().loadUrl(ReaderModeUtils.getAboutReaderForUrl(mUrl, mId));
         }
     }
 
     public boolean isEnteringReaderMode() {
         return mEnteringReaderMode;
     }
 
     public void doReload() {
--- a/mobile/android/base/favicons/Favicons.java
+++ b/mobile/android/base/favicons/Favicons.java
@@ -346,16 +346,17 @@ public class Favicons {
         try {
             // Fall back to trying "someScheme:someDomain.someExtension/favicon.ico".
             URI u = new URI(pageURL);
             return new URI(u.getScheme(),
                            u.getAuthority(),
                            "/favicon.ico", null,
                            null).toString();
         } catch (URISyntaxException e) {
+            Log.e(LOGTAG, "URISyntaxException getting default favicon URL", e);
             return null;
         }
     }
 
     public static void removeLoadTask(long taskId) {
         sLoadTasks.remove(taskId);
     }
 
--- a/mobile/android/base/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/favicons/LoadFaviconTask.java
@@ -229,16 +229,20 @@ public class LoadFaviconTask extends UiA
             }
 
             // If we found a faviconURL - use it.
             if (storedFaviconUrl != null) {
                 mFaviconUrl = storedFaviconUrl;
             } else {
                 // If we don't have a stored one, fall back to the default.
                 mFaviconUrl = Favicons.guessDefaultFaviconURL(mPageUrl);
+
+                if (TextUtils.isEmpty(mFaviconUrl)) {
+                    return null;
+                }
                 isUsingDefaultURL = true;
             }
         }
 
         // Check if favicon has failed - if so, give up. We need this check because, sometimes, we
         // didn't know the real Favicon URL until we asked the database.
         if (Favicons.isFailedFavicon(mFaviconUrl)) {
             return null;
--- a/mobile/android/base/favicons/cache/FaviconCache.java
+++ b/mobile/android/base/favicons/cache/FaviconCache.java
@@ -200,16 +200,20 @@ public class FaviconCache {
     /**
      * Determine if the provided favicon URL is marked as a failure (Has failed to load before -
      * such icons get blacklisted for a time to prevent us endlessly retrying.)
      *
      * @param faviconURL Favicon URL to check if failed in memcache.
      * @return true if this favicon is blacklisted, false otherwise.
      */
     public boolean isFailedFavicon(String faviconURL) {
+        if (faviconURL == null) {
+            return true;
+        }
+
         startRead();
 
         boolean isExpired = false;
         boolean isAborting = false;
 
         try {
             // If we don't have it in the cache, it certainly isn't a known failure.
             if (!mBackingMap.containsKey(faviconURL)) {
@@ -236,17 +240,17 @@ public class FaviconCache {
             }
         } catch (Exception unhandled) {
             // Handle any exception thrown and return the locks to a sensible state.
             finishRead();
 
             // Flag to prevent finally from doubly-unlocking.
             isAborting = true;
             Log.e(LOGTAG, "FaviconCache exception!", unhandled);
-            return false;
+            return true;
         }  finally {
             if (!isAborting) {
                 if (isExpired) {
                     // No longer expired.
                     upgradeReadToWrite();
                 } else {
                     finishRead();
                 }
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -148,30 +148,30 @@ abstract class HomeFragment extends Frag
                 Log.e(LOGTAG, "Can't open in new tab because URL is null");
                 return false;
             }
 
             int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
             if (item.getItemId() == R.id.home_open_private_tab)
                 flags |= Tabs.LOADURL_PRIVATE;
 
-            final String url = (info.inReadingList ? ReaderModeUtils.getAboutReaderForUrl(info.url, true) : info.url);
+            final String url = (info.inReadingList ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
             Tabs.getInstance().loadUrl(url, flags);
             Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show();
             return true;
         }
 
         if (itemId == R.id.home_edit_bookmark) {
             // UI Dialog associates to the activity context, not the applications'.
             new EditBookmarkDialog(getActivity()).show(info.url);
             return true;
         }
 
         if (itemId == R.id.home_open_in_reader) {
-            final String url = ReaderModeUtils.getAboutReaderForUrl(info.url, true);
+            final String url = ReaderModeUtils.getAboutReaderForUrl(info.url);
             Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NONE);
             return true;
         }
 
         if (itemId == R.id.home_remove) {
             // Prioritize removing a history entry over a bookmark in the case of a combined item.
             final int historyId = info.historyId;
             if (historyId > -1) {
--- a/mobile/android/base/home/ReadingListPage.java
+++ b/mobile/android/base/home/ReadingListPage.java
@@ -96,17 +96,17 @@ public class ReadingListPage extends Hom
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 final Cursor c = mAdapter.getCursor();
                 if (c == null || !c.moveToPosition(position)) {
                     return;
                 }
 
                 String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
-                url = ReaderModeUtils.getAboutReaderForUrl(url, true);
+                url = ReaderModeUtils.getAboutReaderForUrl(url);
 
                 // This item is a TwoLinePageRow, so we allow switch-to-tab.
                 mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
             }
         });
 
         registerForContextMenu(mList);
     }
--- a/mobile/android/base/tests/testBookmark.java.in
+++ b/mobile/android/base/tests/testBookmark.java.in
@@ -53,20 +53,22 @@ public class testBookmark extends AboutH
         mDatabaseHelper.deleteBookmark(BOOKMARK_URL);
         waitForBookmarked(false);
     }
 
     private void waitForBookmarked(final boolean isBookmarked) {
         boolean bookmarked = waitForTest(new BooleanTest() {
             @Override
             public boolean test() {
-              return mDatabaseHelper.isBookmark(BOOKMARK_URL);
+                return (isBookmarked) ?
+                    mDatabaseHelper.isBookmark(BOOKMARK_URL) :
+                    !mDatabaseHelper.isBookmark(BOOKMARK_URL);
             }
         }, WAIT_FOR_BOOKMARKED_TIMEOUT);
-        mAsserter.is(bookmarked, isBookmarked, BOOKMARK_URL + " was " + (bookmarked ? "added as a bookmark" : "removed from bookmarks"));
+        mAsserter.is(bookmarked, true, BOOKMARK_URL + " was " + (isBookmarked ? "added as a bookmark" : "removed from bookmarks"));
     }
 
     private void setUpBookmark() {
         // Bookmark a page for the test
         loadUrl(BOOKMARK_URL);
         waitForText(StringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
         nav.bookmark();
         mAsserter.is(waitForText(StringHelper.BOOKMARK_ADDED_LABEL), true, "bookmark added successfully");
--- a/mobile/android/chrome/content/aboutReader.js
+++ b/mobile/android/chrome/content/aboutReader.js
@@ -28,16 +28,17 @@ let AboutReader = function(doc, win) {
   this._docRef = Cu.getWeakReference(doc);
   this._winRef = Cu.getWeakReference(win);
 
   Services.obs.addObserver(this, "Reader:FaviconReturn", false);
   Services.obs.addObserver(this, "Reader:Add", false);
   Services.obs.addObserver(this, "Reader:Remove", false);
   Services.obs.addObserver(this, "Reader:ListCountReturn", false);
   Services.obs.addObserver(this, "Reader:ListCountUpdated", false);
+  Services.obs.addObserver(this, "Reader:ListStatusReturn", false);
 
   this._article = null;
 
   dump("Feching toolbar, header and content notes from about:reader");
   this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header"));
   this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
   this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
   this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
@@ -114,21 +115,22 @@ let AboutReader = function(doc, win) {
 
   let fontSize = Services.prefs.getIntPref("reader.font_size");
   this._setupSegmentedButton("font-size-buttons", fontSizeOptions, fontSize, this._setFontSize.bind(this));
   this._setFontSize(fontSize);
 
   dump("Decoding query arguments");
   let queryArgs = this._decodeQueryString(win.location.href);
 
-  this._isReadingListItem = (queryArgs.readingList == "1");
+  // Track status of reader toolbar add/remove toggle button
+  this._isReadingListItem = -1;
   this._updateToggleButton();
 
   // Track status of reader toolbar list button
-  this._readingListCount = 0;
+  this._readingListCount = -1;
   this._updateListButton();
   this._requestReadingListCount();
 
   let url = queryArgs.url;
   let tabId = queryArgs.tabId;
   if (tabId) {
     dump("Loading from tab with ID: " + tabId + ", URL: " + url);
     this._loadFromTab(tabId, url);
@@ -187,40 +189,68 @@ AboutReader.prototype = {
         this._loadFavicon(args.url, args.faviconUrl);
         Services.obs.removeObserver(this, "Reader:FaviconReturn");
         break;
       }
 
       case "Reader:Add": {
         let args = JSON.parse(aData);
         if (args.url == this._article.url) {
-          if (!this._isReadingListItem) {
-            this._isReadingListItem = true;
+          if (this._isReadingListItem != 1) {
+            this._isReadingListItem = 1;
             this._updateToggleButton();
           }
         }
         break;
       }
 
       case "Reader:Remove": {
         if (aData == this._article.url) {
-          if (this._isReadingListItem) {
-            this._isReadingListItem = false;
+          if (this._isReadingListItem != 0) {
+            this._isReadingListItem = 0;
             this._updateToggleButton();
           }
         }
         break;
       }
 
       case "Reader:ListCountReturn":
-      case "Reader:ListCountUpdated":  {
+      case "Reader:ListCountUpdated": {
         let count = parseInt(aData);
         if (this._readingListCount != count) {
+          let isInitialStateChange = (this._readingListCount == -1);
           this._readingListCount = count;
           this._updateListButton();
+
+          // Display the toolbar when all its initial component states are known
+          if (isInitialStateChange) {
+            this._setToolbarVisibility(true);
+          }
+
+          // Initial readinglist count is requested before any page is displayed
+          if (this._article) {
+            this._requestReadingListStatus();
+          }
+        }
+        break;
+      }
+
+      case "Reader:ListStatusReturn": {
+        let args = JSON.parse(aData);
+        if (args.url == this._article.url) {
+          if (this._isReadingListItem != args.inReadingList) {
+            let isInitialStateChange = (this._isReadingListItem == -1);
+            this._isReadingListItem = args.inReadingList;
+            this._updateToggleButton();
+
+            // Display the toolbar when all its initial component states are known
+            if (isInitialStateChange) {
+              this._setToolbarVisibility(true);
+            }
+          }
         }
         break;
       }
     }
   },
 
   handleEvent: function Reader_handleEvent(aEvent) {
     if (!aEvent.isTrusted)
@@ -252,24 +282,25 @@ AboutReader.prototype = {
         this._handleDeviceLight(aEvent.value);
         break;
 
       case "unload":
         Services.obs.removeObserver(this, "Reader:Add");
         Services.obs.removeObserver(this, "Reader:Remove");
         Services.obs.removeObserver(this, "Reader:ListCountReturn");
         Services.obs.removeObserver(this, "Reader:ListCountUpdated");
+        Services.obs.removeObserver(this, "Reader:ListStatusReturn");
         break;
     }
   },
 
   _updateToggleButton: function Reader_updateToggleButton() {
     let classes = this._doc.getElementById("toggle-button").classList;
 
-    if (this._isReadingListItem) {
+    if (this._isReadingListItem == 1) {
       classes.add("on");
     } else {
       classes.remove("on");
     }
   },
 
   _updateListButton: function Reader_updateListButton() {
     let classes = this._doc.getElementById("list-button").classList;
@@ -280,24 +311,31 @@ AboutReader.prototype = {
       classes.remove("on");
     }
   },
 
   _requestReadingListCount: function Reader_requestReadingListCount() {
     gChromeWin.sendMessageToJava({ type: "Reader:ListCountRequest" });
   },
 
+  _requestReadingListStatus: function Reader_requestReadingListStatus() {
+    gChromeWin.sendMessageToJava({
+      type: "Reader:ListStatusRequest",
+      url: this._article.url
+    });
+  },
+
   _onReaderToggle: function Reader_onToggle() {
     if (!this._article)
       return;
 
-    this._isReadingListItem = !this._isReadingListItem;
+    this._isReadingListItem = (this._isReadingListItem == 1) ? 0 : 1;
     this._updateToggleButton();
 
-    if (this._isReadingListItem) {
+    if (this._isReadingListItem == 1) {
       gChromeWin.Reader.storeArticleInCache(this._article, function(success) {
         dump("Reader:Add (in reader) success=" + success);
 
         let result = (success ? gChromeWin.Reader.READER_ADD_SUCCESS :
             gChromeWin.Reader.READER_ADD_FAILED);
 
         let json = JSON.stringify({ fromAboutReader: true, url: this._article.url });
         Services.obs.notifyObservers(null, "Reader:Add", json);
@@ -319,17 +357,17 @@ AboutReader.prototype = {
           type: "Reader:Removed",
           url: this._article.url
         });
       }.bind(this));
     }
   },
 
   _onList: function Reader_onList() {
-    if (!this._article || this._readingListCount == 0)
+    if (!this._article || this._readingListCount < 1)
       return;
 
     gChromeWin.sendMessageToJava({ type: "Reader:GoToReadingList" });
   },
 
   _onShare: function Reader_onShare() {
     if (!this._article)
       return;
@@ -450,16 +488,20 @@ AboutReader.prototype = {
   _setToolbarVisibility: function Reader_setToolbarVisibility(visible) {
     let win = this._win;
     if (win.history.state)
       win.history.back();
 
     if (!this._toolbarEnabled)
       return;
 
+    // Don't allow visible toolbar until banner state is known
+    if (this._readingListCount == -1 || this._isReadingListItem == -1)
+      return;
+
     if (this._getToolbarVisibility() === visible)
       return;
 
     this._toolbarElement.classList.toggle("toolbar-hidden");
 
     if (!visible && !this._hasUsedToolbar) {
       this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar");
       if (!this._hasUsedToolbar) {
@@ -617,16 +659,17 @@ AboutReader.prototype = {
     let contentFragment = parserUtils.parseFragment(article.content, Ci.nsIParserUtils.SanitizerDropForms,
                                                     false, articleUri, this._contentElement);
     this._contentElement.innerHTML = "";
     this._contentElement.appendChild(contentFragment);
     this._updateImageMargins();
     this._maybeSetTextDirection(article);
 
     this._contentElement.style.display = "block";
+    this._requestReadingListStatus();
 
     this._toolbarEnabled = true;
     this._setToolbarVisibility(true);
 
     this._requestFavicon();
   },
 
   _hideContent: function Reader_hideContent() {
--- a/mobile/android/chrome/content/downloads.js
+++ b/mobile/android/chrome/content/downloads.js
@@ -90,17 +90,18 @@ var Downloads = {
   showNotification: function dl_showNotification(aDownload, aMessage, aTitle, aOptions) {
     let msg = {
         type: "Notification:Show",
         id: this.getNotificationIdFromDownload(aDownload),
         title: aTitle,
         smallIcon: URI_GENERIC_ICON_DOWNLOAD,
         text: aMessage,
         ongoing: false,
-        cookie: aDownload.guid
+        cookie: aDownload.guid,
+        when: aDownload.startTime
     };
 
     if (aOptions && aOptions.icon) {
       msg.smallIcon = aOptions.icon;
     }
 
     if (aOptions && aOptions.percentComplete) {
       msg.progress_value = aOptions.percentComplete;
--- a/mobile/android/components/FilePicker.js
+++ b/mobile/android/components/FilePicker.js
@@ -158,29 +158,35 @@ FilePicker.prototype = {
   },
 
   get mode() {
     return this._mode;
   },
 
   show: function() {
     if (this._domWin) {
-      PromptUtils.fireDialogEvent(this._domWin, "DOMWillOpenModalDialog");
+      this.fireDialogEvent(this._domWin, "DOMWillOpenModalDialog");
       let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
       winUtils.enterModalState();
     }
 
     this._promptActive = true;
     this._sendMessage();
 
     let thread = Services.tm.currentThread;
     while (this._promptActive)
       thread.processNextEvent(true);
     delete this._promptActive;
 
+    if (this._domWin) {
+      let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      winUtils.leaveModalState();
+      this.fireDialogEvent(this._domWin, "DOMModalDialogClosed");
+    }
+
     if (this._filePath)
       return Ci.nsIFilePicker.returnOK;
 
     return Ci.nsIFilePicker.returnCancel;
   },
 
   open: function(callback) {
     this._callback = callback;
@@ -240,13 +246,27 @@ FilePicker.prototype = {
         if (this.mIndex >= this.mFiles.length) {
           throw Components.results.NS_ERROR_FAILURE;
         }
         return mapFunction(this.mFiles[this.mIndex++]);
       }
     };
   },
 
+  fireDialogEvent: function(aDomWin, aEventName) {
+    // accessing the document object can throw if this window no longer exists. See bug 789888.
+    try {
+      if (!aDomWin.document)
+        return;
+      let event = aDomWin.document.createEvent("Events");
+      event.initEvent(aEventName, true, true);
+      let winUtils = aDomWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+      winUtils.dispatchEventToChromeOnly(aDomWin, event);
+    } catch(ex) {
+    }
+  },
+
   classID: Components.ID("{18a4e042-7c7c-424b-a583-354e68553a7f}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFilePicker, Ci.nsIObserver])
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FilePicker]);