Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 07 Jan 2015 15:25:55 -0800
changeset 235559 ee72fef1c55494ac8a589e60e145bdca106b60ea
parent 235558 d55528833b3e521855a4fc8fc2c8ff65d9246e48 (current diff)
parent 235431 5b030e48e8b0835368d091936830a48df62f34a2 (diff)
child 235563 70de2960aa877d7755ee6f66bf2d4c4c46bfed2c
push id366
push usercmanchester@mozilla.com
push dateThu, 08 Jan 2015 16:40:24 +0000
reviewersmerge
milestone37.0a1
Merge fx-team to m-c a=merge
toolkit/components/reader/content/aboutReader.js
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -38,23 +38,33 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                          "nsIExternalProtocolService");
 this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
 
 /**
  * Trying to clone an Error object into a different container will yield an error.
  * We can work around this by copying the properties we care about onto a regular
  * object.
  *
- * @param {Error}        error        Error object to copy
- * @param {nsIDOMWindow} targetWindow The content window to attach the API
+ * @param {Error|nsIException} error        Error object to copy
+ * @param {nsIDOMWindow}       targetWindow The content window to clone into
  */
 const cloneErrorObject = function(error, targetWindow) {
   let obj = new targetWindow.Error();
-  for (let prop of Object.getOwnPropertyNames(error)) {
+  let props = Object.getOwnPropertyNames(error);
+  // nsIException properties are not enumerable, so we'll try to copy the most
+  // common and useful ones.
+  if (!props.length) {
+    props.push("message", "filename", "lineNumber", "columnNumber", "stack");
+  }
+  for (let prop of props) {
     let value = error[prop];
+    // for nsIException objects, the property may not be defined.
+    if (typeof value == "undefined") {
+      continue;
+    }
     if (typeof value != "string" && typeof value != "number") {
       value = String(value);
     }
 
     Object.defineProperty(Cu.waiveXrays(obj), prop, {
       configurable: false,
       enumerable: true,
       value: value,
@@ -73,26 +83,31 @@ const cloneErrorObject = function(error,
  * @param {any}          value        Value or object to copy
  * @param {nsIDOMWindow} targetWindow The content window to copy to
  */
 const cloneValueInto = function(value, targetWindow) {
   if (!value || typeof value != "object") {
     return value;
   }
 
+  // HAWK request errors contain an nsIException object inside `value`.
+  if (("error" in value) && (value.error instanceof Ci.nsIException)) {
+    value = value.error;
+  }
+
   // Strip Function properties, since they can not be cloned across boundaries
   // like this.
   for (let prop of Object.getOwnPropertyNames(value)) {
     if (typeof value[prop] == "function") {
       delete value[prop];
     }
   }
 
   // Inspect for an error this way, because the Error object is special.
-  if (value.constructor.name == "Error") {
+  if (value.constructor.name == "Error" || value instanceof Ci.nsIException) {
     return cloneErrorObject(value, targetWindow);
   }
 
   let clone;
   try {
     clone = Cu.cloneInto(value, targetWindow);
   } catch (ex) {
     MozLoopService.log.debug("Failed to clone value:", value);
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ b/browser/components/loop/content/shared/js/feedbackViews.js
@@ -71,17 +71,17 @@ loop.shared.views.FeedbackView = (functi
     },
 
     _getCategories: function() {
       return {
         audio_quality: l10n.get("feedback_category_audio_quality"),
         video_quality: l10n.get("feedback_category_video_quality"),
         disconnected : l10n.get("feedback_category_was_disconnected"),
         confusing:     l10n.get("feedback_category_confusing"),
-        other:         l10n.get("feedback_category_other")
+        other:         l10n.get("feedback_category_other2")
       };
     },
 
     _getCategoryFields: function() {
       var categories = this._getCategories();
       return Object.keys(categories).map(function(category, key) {
         return (
           React.createElement("label", {key: key, className: "feedback-category-label"}, 
@@ -114,56 +114,48 @@ loop.shared.views.FeedbackView = (functi
         return false;
       }
       return true;
     },
 
     handleCategoryChange: function(event) {
       var category = event.target.value;
       this.setState({
-        category: category,
-        description: category == "other" ? "" : this._getCategories()[category]
+        category: category
       });
       if (category == "other") {
         this.refs.description.getDOMNode().focus();
       }
     },
 
     handleDescriptionFieldChange: function(event) {
       this.setState({description: event.target.value});
     },
 
-    handleDescriptionFieldFocus: function(event) {
-      this.setState({category: "other", description: ""});
-    },
-
     handleFormSubmit: function(event) {
       event.preventDefault();
       // XXX this feels ugly, we really want a feedbackActions object here.
       this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
         happy: false,
         category: this.state.category,
         description: this.state.description
       }));
     },
 
     render: function() {
-      var descriptionDisplayValue = this.state.category === "other" ?
-                                    this.state.description : "";
       return (
         React.createElement(FeedbackLayout, {title: l10n.get("feedback_what_makes_you_sad"), 
                         reset: this.props.reset}, 
           React.createElement("form", {onSubmit: this.handleFormSubmit}, 
             this._getCategoryFields(), 
             React.createElement("p", null, 
               React.createElement("input", {type: "text", ref: "description", name: "description", 
                 className: "feedback-description", 
                 onChange: this.handleDescriptionFieldChange, 
-                onFocus: this.handleDescriptionFieldFocus, 
-                value: descriptionDisplayValue, 
+                value: this.state.description, 
                 placeholder: 
                   l10n.get("feedback_custom_category_text_placeholder")})
             ), 
             React.createElement("button", {type: "submit", className: "btn btn-success", 
                     disabled: !this._isFormReady()}, 
               l10n.get("feedback_submit_button")
             )
           )
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ b/browser/components/loop/content/shared/js/feedbackViews.jsx
@@ -71,17 +71,17 @@ loop.shared.views.FeedbackView = (functi
     },
 
     _getCategories: function() {
       return {
         audio_quality: l10n.get("feedback_category_audio_quality"),
         video_quality: l10n.get("feedback_category_video_quality"),
         disconnected : l10n.get("feedback_category_was_disconnected"),
         confusing:     l10n.get("feedback_category_confusing"),
-        other:         l10n.get("feedback_category_other")
+        other:         l10n.get("feedback_category_other2")
       };
     },
 
     _getCategoryFields: function() {
       var categories = this._getCategories();
       return Object.keys(categories).map(function(category, key) {
         return (
           <label key={key} className="feedback-category-label">
@@ -114,56 +114,48 @@ loop.shared.views.FeedbackView = (functi
         return false;
       }
       return true;
     },
 
     handleCategoryChange: function(event) {
       var category = event.target.value;
       this.setState({
-        category: category,
-        description: category == "other" ? "" : this._getCategories()[category]
+        category: category
       });
       if (category == "other") {
         this.refs.description.getDOMNode().focus();
       }
     },
 
     handleDescriptionFieldChange: function(event) {
       this.setState({description: event.target.value});
     },
 
-    handleDescriptionFieldFocus: function(event) {
-      this.setState({category: "other", description: ""});
-    },
-
     handleFormSubmit: function(event) {
       event.preventDefault();
       // XXX this feels ugly, we really want a feedbackActions object here.
       this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
         happy: false,
         category: this.state.category,
         description: this.state.description
       }));
     },
 
     render: function() {
-      var descriptionDisplayValue = this.state.category === "other" ?
-                                    this.state.description : "";
       return (
         <FeedbackLayout title={l10n.get("feedback_what_makes_you_sad")}
                         reset={this.props.reset}>
           <form onSubmit={this.handleFormSubmit}>
             {this._getCategoryFields()}
             <p>
               <input type="text" ref="description" name="description"
                 className="feedback-description"
                 onChange={this.handleDescriptionFieldChange}
-                onFocus={this.handleDescriptionFieldFocus}
-                value={descriptionDisplayValue}
+                value={this.state.description}
                 placeholder={
                   l10n.get("feedback_custom_category_text_placeholder")} />
             </p>
             <button type="submit" className="btn btn-success"
                     disabled={!this._isFormReady()}>
               {l10n.get("feedback_submit_button")}
             </button>
           </form>
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -69,17 +69,17 @@ fxos_app_needed=Please install the {{fxo
 
 feedback_call_experience_heading2=How was your conversation?
 feedback_what_makes_you_sad=What makes you sad?
 feedback_thank_you_heading=Thank you for your feedback!
 feedback_category_audio_quality=Audio quality
 feedback_category_video_quality=Video quality
 feedback_category_was_disconnected=Was disconnected
 feedback_category_confusing=Confusing
-feedback_category_other=Other:
+feedback_category_other2=Other
 feedback_custom_category_text_placeholder=What went wrong?
 feedback_submit_button=Submit
 feedback_back_button=Back
 ## LOCALIZATION NOTE (feedback_window_will_close_in2):
 ## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
 ## In this item, don't translate the part between {{..}}
 feedback_window_will_close_in2={[ plural(countdown) ]}
 feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
--- a/browser/components/loop/test/shared/feedbackViews_test.js
+++ b/browser/components/loop/test/shared/feedbackViews_test.js
@@ -128,26 +128,16 @@ describe("loop.shared.views.FeedbackView
        "chosen and a description is entered",
       function() {
         clickSadFace(comp);
         fillSadFeedbackForm(comp, "other", "fake");
 
         expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
       });
 
-    it("should empty the description field when a predefined category is " +
-       "chosen",
-      function() {
-        clickSadFace(comp);
-
-        fillSadFeedbackForm(comp, "confusing");
-
-        expect(comp.getDOMNode().querySelector(".feedback-description").value).eql("");
-      });
-
     it("should enable the form submit button once a predefined category is " +
        "chosen",
       function() {
         clickSadFace(comp);
 
         fillSadFeedbackForm(comp, "confusing");
 
         expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js
@@ -11,22 +11,16 @@
 const INIT_URI = "data:text/plain;charset=utf8,hello world";
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs";
 
 let loads = 0;
 function performTest(aRequest, aConsole)
 {
   let deferred = promise.defer();
 
-  loads++;
-  ok(aRequest, "page load was logged");
-  if (loads != 2) {
-    return;
-  }
-
   let headers = null;
 
   function readHeader(aName)
   {
     for (let header of headers) {
       if (header.name == aName) {
         return header.value;
       }
@@ -59,16 +53,21 @@ function performTest(aRequest, aConsole)
   HUDService.lastFinishedRequest.callback = null;
 
   return deferred.promise;
 }
 
 function waitForRequest() {
   let deferred = promise.defer();
   HUDService.lastFinishedRequest.callback = (req, console) => {
+    loads++;
+    ok(req, "page load was logged");
+    if (loads != 2) {
+      return;
+    }
     performTest(req, console).then(deferred.resolve);
   };
   return deferred.promise;
 }
 
 let test = asyncTest(function* () {
   let { browser } = yield loadTab(INIT_URI);
 
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -266,17 +266,17 @@ powered_by_afterLogo=
 
 feedback_call_experience_heading2=How was your conversation?
 feedback_what_makes_you_sad=What makes you sad?
 feedback_thank_you_heading=Thank you for your feedback!
 feedback_category_audio_quality=Audio quality
 feedback_category_video_quality=Video quality
 feedback_category_was_disconnected=Was disconnected
 feedback_category_confusing=Confusing
-feedback_category_other=Other:
+feedback_category_other2=Other
 feedback_custom_category_text_placeholder=What went wrong?
 feedback_submit_button=Submit
 feedback_back_button=Back
 ## LOCALIZATION NOTE (feedback_window_will_close_in2):
 ## Semicolon-separated list of plural forms. See:
 ## http://developer.mozilla.org/en/docs/Localization_and_Plurals
 ## In this item, don't translate the part between {{..}}
 feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -72,16 +72,18 @@ skip-if = processor == "x86"
 [testPromptGridInput]
 # bug 957185 for x86, bug 1001657 for 2.3
 skip-if = android_version == "10" || processor == "x86"
 # [testReaderMode] # see bug 913254, 936224
 [testReadingListCache]
 [testReadingListProvider]
 [testSearchHistoryProvider]
 [testSearchSuggestions]
+# disabled on 2.3; bug 907768
+skip-if = android_version == "10"
 [testSessionOOMSave]
 # disabled on x86 and 2.3; bug 945395
 skip-if = android_version == "10" || processor == "x86"
 [testSessionOOMRestore]
 # disabled on Android 2.3; bug 979600
 skip-if = android_version == "10"
 [testSettingsMenuItems]
 # disabled on Android 2.3; bug 979552
--- a/mobile/android/chrome/content/Reader.js
+++ b/mobile/android/chrome/content/Reader.js
@@ -1,34 +1,138 @@
 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 /* 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";
 
-const { utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
 
 let Reader = {
   // These values should match those defined in BrowserContract.java.
   STATUS_UNFETCHED: 0,
   STATUS_FETCH_FAILED_TEMPORARY: 1,
   STATUS_FETCH_FAILED_PERMANENT: 2,
   STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3,
   STATUS_FETCHED_ARTICLE: 4,
 
-  get isEnabledForParseOnLoad() {
-    delete this.isEnabledForParseOnLoad;
+  observe: function Reader_observe(aMessage, aTopic, aData) {
+    switch (aTopic) {
+      case "Reader:Added": {
+        let mm = window.getGroupMessageManager("browsers");
+        mm.broadcastAsyncMessage("Reader:Added", { url: aData });
+        break;
+      }
+      case "Reader:Removed": {
+        let uri = Services.io.newURI(aData, null, null);
+        ReaderMode.removeArticleFromCache(uri).catch(e => Cu.reportError("Error removing article from cache: " + e));
+
+        let mm = window.getGroupMessageManager("browsers");
+        mm.broadcastAsyncMessage("Reader:Removed", { url: aData });
+        break;
+      }
+      case "Gesture:DoubleTap": {
+        // Ideally, we would just do this all with web APIs in AboutReader.jsm (bug 1118487)
+        if (!BrowserApp.selectedBrowser.currentURI.spec.startsWith("about:reader")) {
+          return;
+        }
+
+        let win = BrowserApp.selectedBrowser.contentWindow;
+        let scrollBy;
+        // Arbitrary choice of innerHeight (50) to give some context after scroll.
+        if (JSON.parse(aData).y < (win.innerHeight / 2)) {
+          scrollBy = - win.innerHeight + 50;
+        } else {
+          scrollBy = win.innerHeight - 50;
+        }
+
+        let viewport = BrowserApp.selectedTab.getViewport();
+        let newY = Math.min(Math.max(viewport.cssY + scrollBy, viewport.cssPageTop), viewport.cssPageBottom);
+        let newRect = new Rect(viewport.cssX, newY, viewport.cssWidth, viewport.cssHeight);
+        ZoomHelper.zoomToRect(newRect, -1);
+        break;
+      }
+    }
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "Reader:AddToList":
+        this.addArticleToReadingList(message.data.article);
+        break;
+
+      case "Reader:ArticleGet":
+        this._getArticle(message.data.url, message.target).then((article) => {
+          message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
+        });
+        break;
 
-    // Listen for future pref changes.
-    Services.prefs.addObserver("reader.parse-on-load.", this, false);
+      case "Reader:FaviconRequest": {
+        let observer = (s, t, d) => {
+          Services.obs.removeObserver(observer, "Reader:FaviconReturn", false);
+          message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(d));
+        };
+        Services.obs.addObserver(observer, "Reader:FaviconReturn", false);
+        Messaging.sendRequest({
+          type: "Reader:FaviconRequest",
+          url: message.data.url
+        });
+        break;
+      }
+
+      case "Reader:ListStatusRequest":
+        Messaging.sendRequestForResult({
+          type: "Reader:ListStatusRequest",
+          url: message.data.url
+        }).then((data) => {
+          message.target.messageManager.sendAsyncMessage("Reader:ListStatusData", JSON.parse(data));
+        });
+        break;
+
+      case "Reader:RemoveFromList":
+        Messaging.sendRequest({
+          type: "Reader:RemoveFromList",
+          url: message.data.url
+        });
+        break;
 
-    return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
+      case "Reader:Share":
+        Messaging.sendRequest({
+          type: "Reader:Share",
+          url: message.data.url,
+          title: message.data.title
+        });
+        break;
+
+      case "Reader:ShowToast":
+        NativeWindow.toast.show(message.data.toast, "short");
+        break;
+
+      case "Reader:SystemUIVisibility":
+        Messaging.sendRequest({
+          type: "SystemUI:Visibility",
+          visible: message.data.visible
+        });
+        break;
+
+      case "Reader:ToolbarVisibility":
+        Messaging.sendRequest({
+          type: "BrowserToolbar:Visibility",
+          visible: message.data.visible
+        });
+        break;
+
+      case "Reader:UpdateIsArticle": {
+        let tab = BrowserApp.getTabForBrowser(message.target);
+        tab.isArticle = message.data.isArticle;
+        this.updatePageAction(tab);
+        break;
+      }
+    }
   },
 
   pageAction: {
     readerModeCallback: function(tabID) {
       Messaging.sendRequest({
         type: "Reader:Toggle",
         tabID: tabID
       });
@@ -62,53 +166,35 @@ let Reader = {
       // not track background reader viewers.
       UITelemetry.startSession("reader.1", null);
       return;
     }
 
     // Only stop a reader session if the foreground viewer is not visible.
     UITelemetry.stopSession("reader.1", "", null);
 
-    if (tab.savedArticle) {
+    if (tab.isArticle) {
       this.pageAction.id = PageActions.add({
         title: Strings.browser.GetStringFromName("readerMode.enter"),
         icon: "drawable://reader",
         clickCallback: () => this.pageAction.readerModeCallback(tab.id),
         longClickCallback: () => this.pageAction.readerModeActiveCallback(tab.id),
         important: true
       });
     }
   },
 
-  observe: function(aMessage, aTopic, aData) {
-    switch(aTopic) {
-      case "Reader:Removed": {
-        let uri = Services.io.newURI(aData, null, null);
-        ReaderMode.removeArticleFromCache(uri).catch(e => Cu.reportError("Error removing article from cache: " + e));
-        break;
-      }
-
-      case "nsPref:changed":
-        if (aData.startsWith("reader.parse-on-load.")) {
-          this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
-        }
-        break;
-    }
-  },
-
   _addTabToReadingList: Task.async(function* (tabID) {
     let tab = BrowserApp.getTabForId(tabID);
     if (!tab) {
       throw new Error("Can't add tab to reading list because no tab found for ID: " + tabID);
     }
 
-    let uri = tab.browser.currentURI;
-    let urlWithoutRef = uri.specIgnoringRef;
-
-    let article = yield this.getArticle(urlWithoutRef, tabID).catch(e => {
+    let urlWithoutRef = tab.browser.currentURI.specIgnoringRef;
+    let article = yield this._getArticle(urlWithoutRef, tab.browser).catch(e => {
       Cu.reportError("Error getting article for tab: " + e);
       return null;
     });
     if (!article) {
       // If there was a problem getting the article, just store the
       // URL and title from the tab.
       article = {
         url: urlWithoutRef,
@@ -135,55 +221,56 @@ let Reader = {
       length: article.length || 0,
       excerpt: article.excerpt || "",
       status: article.status,
     });
 
     ReaderMode.storeArticleInCache(article).catch(e => Cu.reportError("Error storing article in cache: " + e));
   },
 
-  _getStateForParseOnLoad: function () {
-    let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
-    let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
-    // For low-memory devices, don't allow reader mode since it takes up a lot of memory.
-    // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
-    return isForceEnabled || (isEnabled && !BrowserApp.isOnLowMemoryPlatform);
-  },
-
   /**
    * Gets an article for a given URL. This method will download and parse a document
    * if it does not find the article in the tab data or the cache.
    *
    * @param url The article URL.
-   * @param tabId (optional) The id of the tab where we can look for a saved article.
+   * @param browser The browser where the article is currently loaded.
    * @return {Promise}
    * @resolves JS object representing the article, or null if no article is found.
    */
-  getArticle: Task.async(function* (url, tabId) {
-    // First, look for an article object stored on the tab.
-    let tab = BrowserApp.getTabForId(tabId);
-    if (tab) {
-      let article = tab.savedArticle;
-      if (article && article.url == url) {
-        return article;
-      }
+  _getArticle: Task.async(function* (url, browser) {
+    // First, look for a saved article.
+    let article = yield this._getSavedArticle(browser);
+    if (article && article.url == url) {
+      return article;
     }
 
     // Next, try to find a parsed article in the cache.
     let uri = Services.io.newURI(url, null, null);
-    let article = yield ReaderMode.getArticleFromCache(uri);
+    article = yield ReaderMode.getArticleFromCache(uri);
     if (article) {
       return article;
     }
 
     // Article hasn't been found in the cache, we need to
     // download the page and parse the article out of it.
     return yield ReaderMode.downloadAndParseDocument(url);
   }),
 
+  _getSavedArticle: function(browser) {
+    return new Promise((resolve, reject) => {
+      let mm = browser.messageManager;
+      let listener = (message) => {
+        mm.removeMessageListener("Reader:SavedArticleData", listener);
+        resolve(message.data.article);
+      };
+      mm.addMessageListener("Reader:SavedArticleData", listener);
+      mm.sendAsyncMessage("Reader:SavedArticleGet");
+    });
+  },
+
   /**
    * Migrates old indexedDB reader mode cache to new JSON cache.
    */
   migrateCache: Task.async(function* () {
     let cacheDB = yield new Promise((resolve, reject) => {
       let request = window.indexedDB.open("about:reader", 1);
       request.onsuccess = event => resolve(event.target.result);
       request.onerror = event => reject(request.error);
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -104,27 +104,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/PermissionsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences",
                                   "resource://gre/modules/SharedPreferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
                                   "resource://gre/modules/Notifications.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
-                                  "resource://gre/modules/ReaderMode.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager",
                                   "resource://gre/modules/GMPInstallManager.jsm");
 
 // Lazily-loaded browser scripts:
 [
   ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
   ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
-  ["AboutReader", "chrome://global/content/reader/aboutReader.js"],
   ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
   ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
   ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
   ["Linkifier", "chrome://browser/content/Linkify.js"],
   ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"],
   ["CastingApps", "chrome://browser/content/CastingApps.js"],
 #ifdef NIGHTLY_BUILD
   ["WebcompatReporter", "chrome://browser/content/WebcompatReporter.js"],
@@ -145,17 +141,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
   ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
   ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
   ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
   ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
   ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
   ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
-  ["Reader", ["Reader:Removed"], "chrome://browser/content/Reader.js"],
+  ["Reader", ["Reader:Added", "Reader:Removed", "Gesture:DoubleTap"], "chrome://browser/content/Reader.js"],
 ].forEach(function (aScript) {
   let [name, notifications, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
   let observer = (s, t, d) => {
@@ -163,16 +159,49 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     Services.obs.addObserver(window[name], t, false);
     window[name].observe(s, t, d); // Explicitly notify new observer
   };
   notifications.forEach((notification) => {
     Services.obs.addObserver(observer, notification, false);
   });
 });
 
+// Lazily-loaded browser scripts that use message listeners.
+[
+  ["Reader", [
+    "Reader:AddToList",
+    "Reader:ArticleGet",
+    "Reader:FaviconRequest",
+    "Reader:ListStatusRequest",
+    "Reader:RemoveFromList",
+    "Reader:Share",
+    "Reader:ShowToast",
+    "Reader:ToolbarVisibility",
+    "Reader:SystemUIVisibility",
+    "Reader:UpdateIsArticle",
+  ], "chrome://browser/content/Reader.js"],
+].forEach(aScript => {
+  let [name, messages, script] = aScript;
+  XPCOMUtils.defineLazyGetter(window, name, function() {
+    let sandbox = {};
+    Services.scriptloader.loadSubScript(script, sandbox);
+    return sandbox[name];
+  });
+
+  let mm = window.getGroupMessageManager("browsers");
+  let listener = (message) => {
+    mm.removeMessageListener(message.name, listener);
+    mm.addMessageListener(message.name, window[name]);
+    window[name].receiveMessage(message);
+  };
+  messages.forEach((message) => {
+    mm.addMessageListener(message, listener);
+  });
+});
+
 // Lazily-loaded JS modules that use observer notifications
 [
   ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
             "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
 ].forEach(module => {
   let [name, notifications, resource] = module;
   XPCOMUtils.defineLazyModuleGetter(this, name, resource);
   let observer = (s, t, d) => {
@@ -501,16 +530,19 @@ var BrowserApp = {
       // Set the tiles click observer only if tiles reporting is enabled (that
       // is, a report URL is set in prefs).
       gTilesReportURL = Services.prefs.getCharPref("browser.tiles.reportURL");
       Services.obs.addObserver(this, "Tiles:Click", false);
     } catch (e) {
       // Tiles reporting is disabled.
     }
 
+    let mm = window.getGroupMessageManager("browsers");
+    mm.loadFrameScript("chrome://browser/content/content.js", true);
+
     // Notify Java that Gecko has loaded.
     Messaging.sendRequest({ type: "Gecko:Ready" });
   },
 
   get _startupStatus() {
     delete this._startupStatus;
 
     let savedMilestone = null;
@@ -3230,17 +3262,17 @@ function Tab(aURL, aParams) {
   this.viewportMeasureCallback = null;
   this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 };
   this.contentDocumentIsDisplayed = true;
   this.pluginDoorhangerTimeout = null;
   this.shouldShowPluginDoorhanger = true;
   this.clickToPlayPluginsActivated = false;
   this.desktopMode = false;
   this.originalURI = null;
-  this.savedArticle = null;
+  this.isArticle = false;
   this.hasTouchListener = false;
   this.browserWidth = 0;
   this.browserHeight = 0;
   this.tilesData = null;
 
   this.create(aURL, aParams);
 }
 
@@ -3281,16 +3313,17 @@ Tab.prototype = {
   create: function(aURL, aParams) {
     if (this.browser)
       return;
 
     aParams = aParams || {};
 
     this.browser = document.createElement("browser");
     this.browser.setAttribute("type", "content-targetable");
+    this.browser.setAttribute("messagemanagergroup", "browsers");
     this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
 
     // Make sure the previously selected panel remains selected. The selected panel of a deck is
     // not stable when panels are added.
     let selectedPanel = BrowserApp.deck.selectedPanel;
     BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
     BrowserApp.deck.selectedPanel = selectedPanel;
 
@@ -3575,17 +3608,16 @@ Tab.prototype = {
 
     // Make sure the previously selected panel remains selected. The selected panel of a deck is
     // not stable when panels are removed.
     let selectedPanel = BrowserApp.deck.selectedPanel;
     BrowserApp.deck.removeChild(this.browser);
     BrowserApp.deck.selectedPanel = selectedPanel;
 
     this.browser = null;
-    this.savedArticle = null;
   },
 
   // This should be called to update the browser when the tab gets selected/unselected
   setActive: function setActive(aActive) {
     if (!this.browser || !this.browser.docShell)
       return;
 
     this.lastTouchedAt = Date.now();
@@ -3971,24 +4003,16 @@ Tab.prototype = {
             this.browser.removeEventListener("click", ErrorPageEventHandler, true);
             this.browser.removeEventListener("pagehide", listener, true);
           }.bind(this);
 
           this.browser.addEventListener("pagehide", listener, true);
         }
 
         if (docURI.startsWith("about:reader")) {
-          // During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here
-          // For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded"
-          // Message can be received before the document body is available ... so we avoid instantiating an
-          // AboutReader object, expecting that an eventual valid message will follow.
-          let contentDocument = this.browser.contentDocument;
-          if (contentDocument.body) {
-            new AboutReader(contentDocument, this.browser.contentWindow);
-          }
           // Update the page action to show the "reader active" icon.
           Reader.updatePageAction(this);
         }
 
         break;
       }
 
       case "DOMFormHasPassword": {
@@ -4283,41 +4307,16 @@ Tab.prototype = {
                 type: "Robocop:TilesResponse",
                 response: this.response
               });
             }
           };
           xhr.send(this.tilesData);
           this.tilesData = null;
         }
-
-        // Don't try to parse the document if reader mode is disabled,
-        // or if the page is already in reader mode.
-        if (!Reader.isEnabledForParseOnLoad || this.readerActive) {
-          return;
-        }
-
-        // Reader mode is disabled until proven enabled.
-        this.savedArticle = null;
-        Reader.updatePageAction(this);
-
-        // Once document is fully loaded, parse it
-        ReaderMode.parseDocumentFromBrowser(this.browser).then(article => {
-          // The loaded page may have changed while we were parsing the document. 
-          // Make sure we've got the current one.
-          let currentURL = this.browser.currentURI.specIgnoringRef;
-
-          // Do nothing if there's no article or the page in this tab has changed.
-          if (article == null || (article.url != currentURL)) {
-            return;
-          }
-
-          this.savedArticle = article;
-          Reader.updatePageAction(this);
-        }).catch(e => Cu.reportError("Error parsing document from tab: " + e));
       }
     }
   },
 
   onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
     let contentWin = aWebProgress.DOMWindow;
     if (contentWin != contentWin.top)
         return;
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/content.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", "resource://gre/modules/AboutReader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+
+let dump = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "Content");
+
+let global = this;
+
+let AboutReaderListener = {
+  _savedArticle: null,
+
+  init: function() {
+    addEventListener("AboutReaderContentLoaded", this, false, true);
+    addEventListener("pageshow", this, false);
+    addMessageListener("Reader:SavedArticleGet", this);
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "Reader:SavedArticleGet":
+        sendAsyncMessage("Reader:SavedArticleData", { article: this._savedArticle });
+        break;
+    }
+  },
+
+  get isAboutReader() {
+    return content.document.documentURI.startsWith("about:reader");
+  },
+
+  handleEvent: function(event) {
+    if (event.originalTarget.defaultView != content) {
+      return;
+    }
+
+    switch (event.type) {
+      case "AboutReaderContentLoaded":
+        if (!this.isAboutReader) {
+          return;
+        }
+
+        // If we are restoring multiple reader mode tabs during session restore, duplicate "DOMContentLoaded"
+        // events may be fired for the visible tab. The inital "DOMContentLoaded" may be received before the
+        // document body is available, so we avoid instantiating an AboutReader object, expecting that a
+        // valid message will follow. See bug 925983.
+        if (content.document.body) {
+          new AboutReader(global, content);
+        }
+        break;
+
+      case "pageshow":
+        if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader) {
+          return;
+        }
+
+        // Reader mode is disabled until proven enabled.
+        this._savedArticle = null;
+        sendAsyncMessage("Reader:UpdateIsArticle", { isArticle: false });
+
+        ReaderMode.parseDocument(content.document).then(article => {
+          // The loaded page may have changed while we were parsing the document.
+          // Make sure we've got the current one.
+          let currentURL = Services.io.newURI(content.document.documentURI, null, null).specIgnoringRef;
+
+          // Do nothing if there's no article or the page in this tab has changed.
+          if (article == null || (article.url != currentURL)) {
+            return;
+          }
+
+          this._savedArticle = article;
+          sendAsyncMessage("Reader:UpdateIsArticle", { isArticle: true });
+
+        }).catch(e => Cu.reportError("Error parsing document: " + e));
+        break;
+    }
+  }
+};
+AboutReaderListener.init();
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -5,16 +5,17 @@
 
 
 chrome.jar:
 % content browser %content/ contentaccessible=yes
 
 * content/about.xhtml                  (content/about.xhtml)
   content/config.xhtml                 (content/config.xhtml)
   content/config.js                    (content/config.js)
+  content/content.js                   (content/content.js)
   content/aboutAddons.xhtml            (content/aboutAddons.xhtml)
   content/aboutAddons.js               (content/aboutAddons.js)
   content/aboutCertError.xhtml         (content/aboutCertError.xhtml)
   content/aboutDownloads.xhtml         (content/aboutDownloads.xhtml)
   content/aboutDownloads.js            (content/aboutDownloads.js)
   content/aboutFeedback.xhtml          (content/aboutFeedback.xhtml)
   content/aboutFeedback.js             (content/aboutFeedback.js)
   content/aboutPrivateBrowsing.xhtml   (content/aboutPrivateBrowsing.xhtml)
copy from toolkit/components/reader/content/aboutReader.js
copy to toolkit/components/reader/AboutReader.jsm
--- a/toolkit/components/reader/content/aboutReader.js
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -1,50 +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/. */
 
+"use strict";
+
 let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
 
-Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm")
+this.EXPORTED_SYMBOLS = [ "AboutReader" ];
+
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
-                                  "resource://gre/modules/UITelemetry.jsm");
-
-XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
-  window.QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIWebNavigation)
-    .QueryInterface(Ci.nsIDocShellTreeItem)
-    .rootTreeItem
-    .QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIDOMWindow)
-    .QueryInterface(Ci.nsIDOMChromeWindow));
+XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
 
 function dump(s) {
   Services.console.logStringMessage("AboutReader: " + s);
 }
 
 let gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
 
-let AboutReader = function(doc, win) {
-  dump("Init()");
+let AboutReader = function(mm, win) {
+  let doc = win.document;
+
+  this._mm = mm;
+  this._mm.addMessageListener("Reader:Added", this);
+  this._mm.addMessageListener("Reader:Removed", this);
 
   this._docRef = Cu.getWeakReference(doc);
   this._winRef = Cu.getWeakReference(win);
 
-  Services.obs.addObserver(this, "Reader:FaviconReturn", false);
-  Services.obs.addObserver(this, "Reader:Added", false);
-  Services.obs.addObserver(this, "Reader:Removed", false);
-  Services.obs.addObserver(this, "Gesture:DoubleTap", 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"));
   this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content"));
   this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
   this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
 
@@ -115,26 +108,23 @@ let AboutReader = function(doc, win) {
       value: 5,
       linkClass: "font-size5-sample" }
   ];
 
   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);
 
   // Track status of reader toolbar add/remove toggle button
   this._isReadingListItem = -1;
   this._updateToggleButton();
 
-  let url = queryArgs.url;
-  let tabId = queryArgs.tabId;
-  this._loadArticle(url, tabId);
+  this._loadArticle(queryArgs.url);
 }
 
 AboutReader.prototype = {
   _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " +
                           ".content p > a:only-child > img:only-child, " +
                           ".content .wp-caption img, " +
                           ".content figure img",
 
@@ -169,79 +159,53 @@ AboutReader.prototype = {
   get _toolbarElement() {
     return this._toolbarElementRef.get();
   },
 
   get _messageElement() {
     return this._messageElementRef.get();
   },
 
-  observe: function Reader_observe(aMessage, aTopic, aData) {
-    switch(aTopic) {
-      case "Reader:FaviconReturn": {
-        let args = JSON.parse(aData);
-        this._loadFavicon(args.url, args.faviconUrl);
-        Services.obs.removeObserver(this, "Reader:FaviconReturn");
-        break;
-      }
-
+  receiveMessage: function (message) {
+    switch (message.name) {
       case "Reader:Added": {
         // Page can be added by long-press pageAction, or by tap on banner icon.
-        if (aData == this._article.url) {
+        if (message.data.url == this._article.url) {
           if (this._isReadingListItem != 1) {
             this._isReadingListItem = 1;
             this._updateToggleButton();
           }
         }
         break;
       }
-
       case "Reader:Removed": {
-        if (aData == this._article.url) {
+        if (message.data.url == this._article.url) {
           if (this._isReadingListItem != 0) {
             this._isReadingListItem = 0;
             this._updateToggleButton();
           }
         }
         break;
       }
-
-      case "Gesture:DoubleTap": {
-        let args = JSON.parse(aData);
-        let scrollBy;
-        // Arbitary choice of innerHeight - 50 to give some context after scroll
-        if (args.y < (this._win.innerHeight / 2)) {
-          scrollBy = -this._win.innerHeight + 50;
-        } else {
-          scrollBy = this._win.innerHeight - 50;
-        }
-        this._scrollPage(scrollBy);
-        break;
-      }
     }
   },
 
   handleEvent: function Reader_handleEvent(aEvent) {
     if (!aEvent.isTrusted)
       return;
 
     switch (aEvent.type) {
-      case "touchstart":
-        this._scrolled = false;
-        break;
       case "click":
-        if (!this._scrolled)
-          this._toggleToolbarVisibility();
+        // XXX: Don't toggle the toolbar on double click. (See the "Gesture:DoubleTap" handler in Reader.js)
+        this._toggleToolbarVisibility();
         break;
       case "scroll":
-        if (!this._scrolled) {
-          let isScrollingUp = this._scrollOffset > aEvent.pageY;
-          this._setToolbarVisibility(isScrollingUp);
-          this._scrollOffset = aEvent.pageY;
-        }
+        let isScrollingUp = this._scrollOffset > aEvent.pageY;
+        this._setToolbarVisibility(isScrollingUp);
+        this._scrollOffset = aEvent.pageY;
         break;
       case "popstate":
         if (!aEvent.state)
           this._closeAllDropdowns();
         break;
       case "resize":
         this._updateImageMargins();
         break;
@@ -250,95 +214,76 @@ AboutReader.prototype = {
         this._handleDeviceLight(aEvent.value);
         break;
 
       case "visibilitychange":
         this._handleVisibilityChange();
         break;
 
       case "unload":
-        Services.obs.removeObserver(this, "Reader:Added");
-        Services.obs.removeObserver(this, "Reader:Removed");
-        Services.obs.removeObserver(this, "Gesture:DoubleTap");
+        this._mm.removeMessageListener("Reader:Added", this);
+        this._mm.removeMessageListener("Reader:Removed", this);
         break;
     }
   },
 
-  _scrollPage: function Reader_scrollPage(scrollByPixels) {
-    let viewport = BrowserApp.selectedTab.getViewport();
-    let newY = Math.min(Math.max(viewport.cssY + scrollByPixels, viewport.cssPageTop), viewport.cssPageBottom);
-    let newRect = new Rect(viewport.cssX, newY, viewport.cssWidth, viewport.cssHeight);
-
-    this._setToolbarVisibility(false);
-    this._setBrowserToolbarVisiblity(false);
-    this._scrolled  = true;
-    ZoomHelper.zoomToRect(newRect, -1);
-  },
-
   _updateToggleButton: function Reader_updateToggleButton() {
     let classes = this._doc.getElementById("toggle-button").classList;
 
     if (this._isReadingListItem == 1) {
       classes.add("on");
     } else {
       classes.remove("on");
     }
   },
 
   _requestReadingListStatus: function Reader_requestReadingListStatus() {
-    Messaging.sendRequestForResult({
-      type: "Reader:ListStatusRequest",
-      url: this._article.url
-    }).then((data) => {
-      let args = JSON.parse(data);
+    let handleListStatusData = (message) => {
+      this._mm.removeMessageListener("Reader:ListStatusData", handleListStatusData);
+
+      let args = message.data;
       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);
           }
         }
       }
-    });
+    };
+
+    this._mm.addMessageListener("Reader:ListStatusData", handleListStatusData);
+    this._mm.sendAsyncMessage("Reader:ListStatusRequest", { url: this._article.url });
   },
 
   _onReaderToggle: function Reader_onToggle() {
     if (!this._article)
       return;
 
     if (this._isReadingListItem == 0) {
-      // If we're in reader mode, we must have fetched the article.
-      this._article.status = gChromeWin.Reader.STATUS_FETCHED_ARTICLE;
-      gChromeWin.Reader.addArticleToReadingList(this._article);
-
+      this._mm.sendAsyncMessage("Reader:AddToList", { article: this._article });
       UITelemetry.addEvent("save.1", "button", null, "reader");
     } else {
-      Messaging.sendRequest({
-        type: "Reader:RemoveFromList",
-        url: this._article.url
-      });
-
+      this._mm.sendAsyncMessage("Reader:RemoveFromList", { url: this._article.url });
       UITelemetry.addEvent("unsave.1", "button", null, "reader");
     }
   },
 
   _onShare: function Reader_onShare() {
     if (!this._article)
       return;
 
-    Messaging.sendRequest({
-      type: "Reader:Share",
+    this._mm.sendAsyncMessage("Reader:Share", {
       url: this._article.url,
       title: this._article.title
     });
-
     UITelemetry.addEvent("share.1", "list", null);
   },
 
   _setFontSize: function Reader_setFontSize(newFontSize) {
     let bodyClasses = this._doc.body.classList;
 
     if (this._fontSize > 0)
       bodyClasses.remove("font-size" + this._fontSize);
@@ -475,61 +420,65 @@ AboutReader.prototype = {
       return;
 
     this._toolbarElement.classList.toggle("toolbar-hidden");
     this._setSystemUIVisibility(visible);
 
     if (!visible && !this._hasUsedToolbar) {
       this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar");
       if (!this._hasUsedToolbar) {
-        gChromeWin.NativeWindow.toast.show(gStrings.GetStringFromName("aboutReader.toolbarTip"), "short");
-
+        this._mm.sendAsyncMessage("Reader:ShowToast", { toast: gStrings.GetStringFromName("aboutReader.toolbarTip") });
         Services.prefs.setBoolPref("reader.has_used_toolbar", true);
         this._hasUsedToolbar = true;
       }
     }
   },
 
   _toggleToolbarVisibility: function Reader_toggleToolbarVisibility() {
     this._setToolbarVisibility(!this._getToolbarVisibility());
   },
 
   _setBrowserToolbarVisiblity: function Reader_setBrowserToolbarVisiblity(visible) {
-    Messaging.sendRequest({
-      type: "BrowserToolbar:Visibility",
-      visible: visible
-    });
+    this._mm.sendAsyncMessage("Reader:ToolbarVisibility", { visible: visible });
   },
 
   _setSystemUIVisibility: function Reader_setSystemUIVisibility(visible) {
-    Messaging.sendRequest({
-      type: "SystemUI:Visibility",
-      visible: visible
-    });
+    this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
   },
 
-  _loadArticle: Task.async(function* (url, tabId) {
+  _loadArticle: Task.async(function* (url) {
     this._showProgressDelayed();
 
-    let article = yield gChromeWin.Reader.getArticle(url, tabId).catch(e => {
-      Cu.reportError("Error loading article: " + e);
-      return null;
-    });
-    if (article) {
+    let article = yield this._getArticle(url);
+    if (article && article.url == url) {
       this._showContent(article);
     } else {
       this._win.location.href = url;
     }
   }),
 
+  _getArticle: function(url) {
+    return new Promise((resolve, reject) => {
+      let listener = (message) => {
+        this._mm.removeMessageListener("Reader:ArticleData", listener);
+        resolve(message.data.article);
+      };
+      this._mm.addMessageListener("Reader:ArticleData", listener);
+      this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url });
+    });
+  },
+
   _requestFavicon: function Reader_requestFavicon() {
-    Messaging.sendRequest({
-      type: "Reader:FaviconRequest",
-      url: this._article.url
-    });
+    let handleFaviconReturn = (message) => {
+      this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
+      this._loadFavicon(message.data.url, message.data.faviconUrl);
+    };
+
+    this._mm.addMessageListener("Reader:FaviconReturn", handleFaviconReturn);
+    this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
   },
 
   _loadFavicon: function Reader_loadFavicon(url, faviconUrl) {
     if (this._article.url !== url)
       return;
 
     let doc = this._doc;
 
@@ -781,17 +730,17 @@ AboutReader.prototype = {
         dropdownArrow.style.left = arrowLeft + "px";
       };
 
       win.addEventListener("resize", function(aEvent) {
         if (!aEvent.isTrusted)
           return;
 
         // Wait for reflow before calculating the new position of the popup.
-        setTimeout(updatePopupPosition, 0);
+        win.setTimeout(updatePopupPosition, 0);
       }, true);
 
       dropdownToggle.addEventListener("click", function(aEvent) {
         if (!aEvent.isTrusted)
           return;
 
         aEvent.stopPropagation();
 
--- a/toolkit/components/reader/ReaderMode.jsm
+++ b/toolkit/components/reader/ReaderMode.jsm
@@ -22,39 +22,71 @@ let ReaderMode = {
   CACHE_VERSION: 1,
 
   DEBUG: 0,
 
   // Don't try to parse the page if it has too many elements (for memory and
   // performance reasons)
   MAX_ELEMS_TO_PARSE: 3000,
 
+  get isEnabledForParseOnLoad() {
+    delete this.isEnabledForParseOnLoad;
+
+    // Listen for future pref changes.
+    Services.prefs.addObserver("reader.parse-on-load.", this, false);
+
+    return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
+  },
+
+  get isOnLowMemoryPlatform() {
+    let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
+    delete this.isOnLowMemoryPlatform;
+    return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
+  },
+
+  _getStateForParseOnLoad: function () {
+    let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
+    let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
+    // For low-memory devices, don't allow reader mode since it takes up a lot of memory.
+    // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
+    return isForceEnabled || (isEnabled && !this.isOnLowMemoryPlatform);
+  },
+
+  observe: function(aMessage, aTopic, aData) {
+    switch(aTopic) {
+      case "nsPref:changed":
+        if (aData.startsWith("reader.parse-on-load.")) {
+          this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
+        }
+        break;
+    }
+  },
+
   /**
    * Gets an article from a loaded browser's document. This method will parse the document
    * if it does not find the article in the cache.
    *
-   * @param browser A browser with a loaded page.
+   * @param doc A document to parse.
    * @return {Promise}
    * @resolves JS object representing the article, or null if no article is found.
    */
-  parseDocumentFromBrowser: Task.async(function* (browser) {
-    let uri = browser.currentURI;
+  parseDocument: Task.async(function* (doc) {
+    let uri = Services.io.newURI(doc.documentURI, null, null);
     if (!this._shouldCheckUri(uri)) {
       this.log("Reader mode disabled for URI");
       return null;
     }
 
     // First, try to find a parsed article in the cache.
     let article = yield this.getArticleFromCache(uri);
     if (article) {
       this.log("Page found in cache, return article immediately");
       return article;
     }
 
-    let doc = browser.contentWindow.document;
     return yield this._readerParse(uri, doc);
   }),
 
   /**
    * Downloads and parses a document from a URL.
    *
    * @param url URL to download and parse.
    * @return {Promise}
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -1,16 +1,18 @@
 <!DOCTYPE html>
 <html>
 
 <head>
   <meta content="text/html; charset=UTF-8" http-equiv="content-type">
   <meta name="viewport" content="width=device-width; user-scalable=0" />
 
   <link rel="stylesheet" href="chrome://global/skin/aboutReader.css" type="text/css"/>
+
+  <script type="text/javascript;version=1.8" src="chrome://global/content/reader/aboutReader.js"></script>
 </head>
 
 <body>
   <div id="reader-header" class="header">
     <a id="reader-domain" class="domain"></a>
     <div class="domain-border"></div>
     <h1 id="reader-title"></h1>
     <div id="reader-credits" class="credits"></div>
--- a/toolkit/components/reader/content/aboutReader.js
+++ b/toolkit/components/reader/content/aboutReader.js
@@ -1,843 +1,9 @@
 /* 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/. */
-
-let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
-
-Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm")
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
-                                  "resource://gre/modules/UITelemetry.jsm");
-
-XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
-  window.QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIWebNavigation)
-    .QueryInterface(Ci.nsIDocShellTreeItem)
-    .rootTreeItem
-    .QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIDOMWindow)
-    .QueryInterface(Ci.nsIDOMChromeWindow));
-
-function dump(s) {
-  Services.console.logStringMessage("AboutReader: " + s);
-}
-
-let gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
-
-let AboutReader = function(doc, win) {
-  dump("Init()");
-
-  this._docRef = Cu.getWeakReference(doc);
-  this._winRef = Cu.getWeakReference(win);
-
-  Services.obs.addObserver(this, "Reader:FaviconReturn", false);
-  Services.obs.addObserver(this, "Reader:Added", false);
-  Services.obs.addObserver(this, "Reader:Removed", false);
-  Services.obs.addObserver(this, "Gesture:DoubleTap", 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"));
-  this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content"));
-  this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
-  this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
-
-  this._toolbarEnabled = false;
-
-  this._scrollOffset = win.pageYOffset;
-
-  let body = doc.body;
-  body.addEventListener("touchstart", this, false);
-  body.addEventListener("click", this, false);
-
-  win.addEventListener("unload", this, false);
-  win.addEventListener("scroll", this, false);
-  win.addEventListener("popstate", this, false);
-  win.addEventListener("resize", this, false);
-
-  doc.addEventListener("visibilitychange", this, false);
-
-  this._setupAllDropdowns();
-  this._setupButton("toggle-button", this._onReaderToggle.bind(this));
-  this._setupButton("share-button", this._onShare.bind(this));
-
-  let colorSchemeOptions = [
-    { name: gStrings.GetStringFromName("aboutReader.colorSchemeDark"),
-      value: "dark"},
-    { name: gStrings.GetStringFromName("aboutReader.colorSchemeLight"),
-      value: "light"},
-    { name: gStrings.GetStringFromName("aboutReader.colorSchemeAuto"),
-      value: "auto"}
-  ];
-
-  let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
-  this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this));
-  this._setColorSchemePref(colorScheme);
-
-  let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample");
-  let fontTypeOptions = [
-    { name: fontTypeSample,
-      description: gStrings.GetStringFromName("aboutReader.fontTypeSerif"),
-      value: "serif",
-      linkClass: "serif" },
-    { name: fontTypeSample,
-      description: gStrings.GetStringFromName("aboutReader.fontTypeSansSerif"),
-      value: "sans-serif",
-      linkClass: "sans-serif"
-    },
-  ];
-
-  let fontType = Services.prefs.getCharPref("reader.font_type");
-  this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this));
-  this._setFontType(fontType);
-
-  let fontSizeSample = gStrings.GetStringFromName("aboutReader.fontSizeSample");
-  let fontSizeOptions = [
-    { name: fontSizeSample,
-      value: 1,
-      linkClass: "font-size1-sample" },
-    { name: fontSizeSample,
-      value: 2,
-      linkClass: "font-size2-sample" },
-    { name: fontSizeSample,
-      value: 3,
-      linkClass: "font-size3-sample" },
-    { name: fontSizeSample,
-      value: 4,
-      linkClass: "font-size4-sample" },
-    { name: fontSizeSample,
-      value: 5,
-      linkClass: "font-size5-sample" }
-  ];
-
-  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);
-
-  // Track status of reader toolbar add/remove toggle button
-  this._isReadingListItem = -1;
-  this._updateToggleButton();
-
-  let url = queryArgs.url;
-  let tabId = queryArgs.tabId;
-  this._loadArticle(url, tabId);
-}
-
-AboutReader.prototype = {
-  _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " +
-                          ".content p > a:only-child > img:only-child, " +
-                          ".content .wp-caption img, " +
-                          ".content figure img",
-
-  get _doc() {
-    return this._docRef.get();
-  },
-
-  get _win() {
-    return this._winRef.get();
-  },
-
-  get _headerElement() {
-    return this._headerElementRef.get();
-  },
-
-  get _domainElement() {
-    return this._domainElementRef.get();
-  },
-
-  get _titleElement() {
-    return this._titleElementRef.get();
-  },
-
-  get _creditsElement() {
-    return this._creditsElementRef.get();
-  },
-
-  get _contentElement() {
-    return this._contentElementRef.get();
-  },
-
-  get _toolbarElement() {
-    return this._toolbarElementRef.get();
-  },
-
-  get _messageElement() {
-    return this._messageElementRef.get();
-  },
-
-  observe: function Reader_observe(aMessage, aTopic, aData) {
-    switch(aTopic) {
-      case "Reader:FaviconReturn": {
-        let args = JSON.parse(aData);
-        this._loadFavicon(args.url, args.faviconUrl);
-        Services.obs.removeObserver(this, "Reader:FaviconReturn");
-        break;
-      }
-
-      case "Reader:Added": {
-        // Page can be added by long-press pageAction, or by tap on banner icon.
-        if (aData == this._article.url) {
-          if (this._isReadingListItem != 1) {
-            this._isReadingListItem = 1;
-            this._updateToggleButton();
-          }
-        }
-        break;
-      }
-
-      case "Reader:Removed": {
-        if (aData == this._article.url) {
-          if (this._isReadingListItem != 0) {
-            this._isReadingListItem = 0;
-            this._updateToggleButton();
-          }
-        }
-        break;
-      }
-
-      case "Gesture:DoubleTap": {
-        let args = JSON.parse(aData);
-        let scrollBy;
-        // Arbitary choice of innerHeight - 50 to give some context after scroll
-        if (args.y < (this._win.innerHeight / 2)) {
-          scrollBy = -this._win.innerHeight + 50;
-        } else {
-          scrollBy = this._win.innerHeight - 50;
-        }
-        this._scrollPage(scrollBy);
-        break;
-      }
-    }
-  },
-
-  handleEvent: function Reader_handleEvent(aEvent) {
-    if (!aEvent.isTrusted)
-      return;
-
-    switch (aEvent.type) {
-      case "touchstart":
-        this._scrolled = false;
-        break;
-      case "click":
-        if (!this._scrolled)
-          this._toggleToolbarVisibility();
-        break;
-      case "scroll":
-        if (!this._scrolled) {
-          let isScrollingUp = this._scrollOffset > aEvent.pageY;
-          this._setToolbarVisibility(isScrollingUp);
-          this._scrollOffset = aEvent.pageY;
-        }
-        break;
-      case "popstate":
-        if (!aEvent.state)
-          this._closeAllDropdowns();
-        break;
-      case "resize":
-        this._updateImageMargins();
-        break;
-
-      case "devicelight":
-        this._handleDeviceLight(aEvent.value);
-        break;
-
-      case "visibilitychange":
-        this._handleVisibilityChange();
-        break;
-
-      case "unload":
-        Services.obs.removeObserver(this, "Reader:Added");
-        Services.obs.removeObserver(this, "Reader:Removed");
-        Services.obs.removeObserver(this, "Gesture:DoubleTap");
-        break;
-    }
-  },
-
-  _scrollPage: function Reader_scrollPage(scrollByPixels) {
-    let viewport = BrowserApp.selectedTab.getViewport();
-    let newY = Math.min(Math.max(viewport.cssY + scrollByPixels, viewport.cssPageTop), viewport.cssPageBottom);
-    let newRect = new Rect(viewport.cssX, newY, viewport.cssWidth, viewport.cssHeight);
-
-    this._setToolbarVisibility(false);
-    this._setBrowserToolbarVisiblity(false);
-    this._scrolled  = true;
-    ZoomHelper.zoomToRect(newRect, -1);
-  },
-
-  _updateToggleButton: function Reader_updateToggleButton() {
-    let classes = this._doc.getElementById("toggle-button").classList;
-
-    if (this._isReadingListItem == 1) {
-      classes.add("on");
-    } else {
-      classes.remove("on");
-    }
-  },
-
-  _requestReadingListStatus: function Reader_requestReadingListStatus() {
-    Messaging.sendRequestForResult({
-      type: "Reader:ListStatusRequest",
-      url: this._article.url
-    }).then((data) => {
-      let args = JSON.parse(data);
-      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);
-          }
-        }
-      }
-    });
-  },
-
-  _onReaderToggle: function Reader_onToggle() {
-    if (!this._article)
-      return;
-
-    if (this._isReadingListItem == 0) {
-      // If we're in reader mode, we must have fetched the article.
-      this._article.status = gChromeWin.Reader.STATUS_FETCHED_ARTICLE;
-      gChromeWin.Reader.addArticleToReadingList(this._article);
-
-      UITelemetry.addEvent("save.1", "button", null, "reader");
-    } else {
-      Messaging.sendRequest({
-        type: "Reader:RemoveFromList",
-        url: this._article.url
-      });
-
-      UITelemetry.addEvent("unsave.1", "button", null, "reader");
-    }
-  },
-
-  _onShare: function Reader_onShare() {
-    if (!this._article)
-      return;
-
-    Messaging.sendRequest({
-      type: "Reader:Share",
-      url: this._article.url,
-      title: this._article.title
-    });
-
-    UITelemetry.addEvent("share.1", "list", null);
-  },
-
-  _setFontSize: function Reader_setFontSize(newFontSize) {
-    let bodyClasses = this._doc.body.classList;
-
-    if (this._fontSize > 0)
-      bodyClasses.remove("font-size" + this._fontSize);
-
-    this._fontSize = newFontSize;
-    bodyClasses.add("font-size" + this._fontSize);
-
-    Services.prefs.setIntPref("reader.font_size", this._fontSize);
-  },
-
-  _handleDeviceLight: function Reader_handleDeviceLight(newLux) {
-    // Desired size of the this._luxValues array.
-    let luxValuesSize = 10;
-    // Add new lux value at the front of the array.
-    this._luxValues.unshift(newLux);
-    // Add new lux value to this._totalLux for averaging later.
-    this._totalLux += newLux;
-
-    // Don't update when length of array is less than luxValuesSize except when it is 1.
-    if (this._luxValues.length < luxValuesSize) {
-      // Use the first lux value to set the color scheme until our array equals luxValuesSize.
-      if (this._luxValues.length == 1) {
-        this._updateColorScheme(newLux);
-      }
-      return;
-    }
-    // Holds the average of the lux values collected in this._luxValues.
-    let averageLuxValue = this._totalLux/luxValuesSize;
-
-    this._updateColorScheme(averageLuxValue);
-    // Pop the oldest value off the array.
-    let oldLux = this._luxValues.pop();
-    // Subtract oldLux since it has been discarded from the array.
-    this._totalLux -= oldLux;
-  },
-
-  _handleVisibilityChange: function Reader_handleVisibilityChange() {
-    let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
-    if (colorScheme != "auto") {
-      return;
-    }
-
-    // Turn off the ambient light sensor if the page is hidden
-    this._enableAmbientLighting(!this._doc.hidden);
-  },
-
-  // Setup or teardown the ambient light tracking system.
-  _enableAmbientLighting: function Reader_enableAmbientLighting(enable) {
-    if (enable) {
-      this._win.addEventListener("devicelight", this, false);
-      this._luxValues = [];
-      this._totalLux = 0;
-    } else {
-      this._win.removeEventListener("devicelight", this, false);
-      delete this._luxValues;
-      delete this._totalLux;
-    }
-  },
-
-  _updateColorScheme: function Reader_updateColorScheme(luxValue) {
-    // Upper bound value for "dark" color scheme beyond which it changes to "light".
-    let upperBoundDark = 50;
-    // Lower bound value for "light" color scheme beyond which it changes to "dark".
-    let lowerBoundLight = 10;
-    // Threshold for color scheme change.
-    let colorChangeThreshold = 20;
-
-    // Ignore changes that are within a certain threshold of previous lux values.
-    if ((this._colorScheme === "dark" && luxValue < upperBoundDark) ||
-        (this._colorScheme === "light" && luxValue > lowerBoundLight))
-      return;
-
-    if (luxValue < colorChangeThreshold)
-      this._setColorScheme("dark");
-    else
-      this._setColorScheme("light");
-  },
+ * 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/. */
 
-  _setColorScheme: function Reader_setColorScheme(newColorScheme) {
-    // "auto" is not a real color scheme
-    if (this._colorScheme === newColorScheme || newColorScheme === "auto")
-      return;
-
-    let bodyClasses = this._doc.body.classList;
-
-    if (this._colorScheme)
-      bodyClasses.remove(this._colorScheme);
-
-    this._colorScheme = newColorScheme;
-    bodyClasses.add(this._colorScheme);
-  },
-
-  // Pref values include "dark", "light", and "auto", which automatically switches
-  // between light and dark color schemes based on the ambient light level.
-  _setColorSchemePref: function Reader_setColorSchemePref(colorSchemePref) {
-    this._enableAmbientLighting(colorSchemePref === "auto");
-    this._setColorScheme(colorSchemePref);
-
-    Services.prefs.setCharPref("reader.color_scheme", colorSchemePref);
-  },
-
-  _setFontType: function Reader_setFontType(newFontType) {
-    if (this._fontType === newFontType)
-      return;
-
-    let bodyClasses = this._doc.body.classList;
-
-    if (this._fontType)
-      bodyClasses.remove(this._fontType);
-
-    this._fontType = newFontType;
-    bodyClasses.add(this._fontType);
-
-    Services.prefs.setCharPref("reader.font_type", this._fontType);
-  },
-
-  _getToolbarVisibility: function Reader_getToolbarVisibility() {
-    return !this._toolbarElement.classList.contains("toolbar-hidden");
-  },
-
-  _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._isReadingListItem == -1)
-      return;
-
-    if (this._getToolbarVisibility() === visible)
-      return;
-
-    this._toolbarElement.classList.toggle("toolbar-hidden");
-    this._setSystemUIVisibility(visible);
-
-    if (!visible && !this._hasUsedToolbar) {
-      this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar");
-      if (!this._hasUsedToolbar) {
-        gChromeWin.NativeWindow.toast.show(gStrings.GetStringFromName("aboutReader.toolbarTip"), "short");
-
-        Services.prefs.setBoolPref("reader.has_used_toolbar", true);
-        this._hasUsedToolbar = true;
-      }
-    }
-  },
-
-  _toggleToolbarVisibility: function Reader_toggleToolbarVisibility() {
-    this._setToolbarVisibility(!this._getToolbarVisibility());
-  },
-
-  _setBrowserToolbarVisiblity: function Reader_setBrowserToolbarVisiblity(visible) {
-    Messaging.sendRequest({
-      type: "BrowserToolbar:Visibility",
-      visible: visible
-    });
-  },
-
-  _setSystemUIVisibility: function Reader_setSystemUIVisibility(visible) {
-    Messaging.sendRequest({
-      type: "SystemUI:Visibility",
-      visible: visible
-    });
-  },
-
-  _loadArticle: Task.async(function* (url, tabId) {
-    this._showProgressDelayed();
-
-    let article = yield gChromeWin.Reader.getArticle(url, tabId).catch(e => {
-      Cu.reportError("Error loading article: " + e);
-      return null;
-    });
-    if (article) {
-      this._showContent(article);
-    } else {
-      this._win.location.href = url;
-    }
-  }),
-
-  _requestFavicon: function Reader_requestFavicon() {
-    Messaging.sendRequest({
-      type: "Reader:FaviconRequest",
-      url: this._article.url
-    });
-  },
-
-  _loadFavicon: function Reader_loadFavicon(url, faviconUrl) {
-    if (this._article.url !== url)
-      return;
-
-    let doc = this._doc;
-
-    let link = doc.createElement('link');
-    link.rel = 'shortcut icon';
-    link.href = faviconUrl;
-
-    doc.getElementsByTagName('head')[0].appendChild(link);
-  },
-
-  _updateImageMargins: function Reader_updateImageMargins() {
-    let windowWidth = this._win.innerWidth;
-    let contentWidth = this._contentElement.offsetWidth;
-    let maxWidthStyle = windowWidth + "px !important";
-
-    let setImageMargins = function(img) {
-      if (!img._originalWidth)
-        img._originalWidth = img.offsetWidth;
-
-      let imgWidth = img._originalWidth;
-
-      // If the image is taking more than half of the screen, just make
-      // it fill edge-to-edge.
-      if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55)
-        imgWidth = windowWidth;
-
-      let sideMargin = Math.max((contentWidth - windowWidth) / 2,
-                                (contentWidth - imgWidth) / 2);
-
-      let imageStyle = sideMargin + "px !important";
-      let widthStyle = imgWidth + "px !important";
-
-      let cssText = "max-width: " + maxWidthStyle + ";" +
-                    "width: " + widthStyle + ";" +
-                    "margin-left: " + imageStyle + ";" +
-                    "margin-right: " + imageStyle + ";";
-
-      img.style.cssText = cssText;
-    }
-
-    let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR);
-    for (let i = imgs.length; --i >= 0;) {
-      let img = imgs[i];
-
-      if (img.width > 0) {
-        setImageMargins(img);
-      } else {
-        img.onload = function() {
-          setImageMargins(img);
-        }
-      }
-    }
-  },
-
-  _maybeSetTextDirection: function Read_maybeSetTextDirection(article){
-    if(!article.dir)
-      return;
-
-    //Set "dir" attribute on content
-    this._contentElement.setAttribute("dir", article.dir);
-    this._headerElement.setAttribute("dir", article.dir);
-  },
-
-  _showError: function Reader_showError(error) {
-    this._headerElement.style.display = "none";
-    this._contentElement.style.display = "none";
-
-    this._messageElement.innerHTML = error;
-    this._messageElement.style.display = "block";
-
-    this._doc.title = error;
-  },
-
-  // This function is the JS version of Java's StringUtils.stripCommonSubdomains.
-  _stripHost: function Reader_stripHost(host) {
-    if (!host)
-      return host;
-
-    let start = 0;
-
-    if (host.startsWith("www."))
-      start = 4;
-    else if (host.startsWith("m."))
-      start = 2;
-    else if (host.startsWith("mobile."))
-      start = 7;
-
-    return host.substring(start);
-  },
-
-  _showContent: function Reader_showContent(article) {
-    this._messageElement.style.display = "none";
-
-    this._article = article;
-
-    this._domainElement.href = article.url;
-    let articleUri = Services.io.newURI(article.url, null, null);
-    this._domainElement.innerHTML = this._stripHost(articleUri.host);
+"use strict";
 
-    this._creditsElement.innerHTML = article.byline;
-
-    this._titleElement.textContent = article.title;
-    this._doc.title = article.title;
-
-    this._headerElement.style.display = "block";
-
-    let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
-    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() {
-    this._headerElement.style.display = "none";
-    this._contentElement.style.display = "none";
-  },
-
-  _showProgressDelayed: function Reader_showProgressDelayed() {
-    this._win.setTimeout(function() {
-      // Article has already been loaded, no need to show
-      // progress anymore.
-      if (this._article)
-        return;
-
-      this._headerElement.style.display = "none";
-      this._contentElement.style.display = "none";
-
-      this._messageElement.innerHTML = gStrings.GetStringFromName("aboutReader.loading");
-      this._messageElement.style.display = "block";
-    }.bind(this), 300);
-  },
-
-  _decodeQueryString: function Reader_decodeQueryString(url) {
-    let result = {};
-    let query = url.split("?")[1];
-    if (query) {
-      let pairs = query.split("&");
-      for (let i = 0; i < pairs.length; i++) {
-        let [name, value] = pairs[i].split("=");
-        result[name] = decodeURIComponent(value);
-      }
-    }
-
-    return result;
-  },
-
-  _setupSegmentedButton: function Reader_setupSegmentedButton(id, options, initialValue, callback) {
-    let doc = this._doc;
-    let segmentedButton = doc.getElementById(id);
-
-    for (let i = 0; i < options.length; i++) {
-      let option = options[i];
-
-      let item = doc.createElement("li");
-      let link = doc.createElement("a");
-      link.textContent = option.name;
-      item.appendChild(link);
-
-      if (option.linkClass !== undefined)
-        link.classList.add(option.linkClass);
-
-      if (option.description !== undefined) {
-        let description = doc.createElement("div");
-        description.textContent = option.description;
-        item.appendChild(description);
-      }
-
-      link.style.MozUserSelect = 'none';
-      segmentedButton.appendChild(item);
-
-      link.addEventListener("click", function(aEvent) {
-        if (!aEvent.isTrusted)
-          return;
-
-        aEvent.stopPropagation();
-
-        // Just pass the ID of the button as an extra and hope the ID doesn't change
-        // unless the context changes
-        UITelemetry.addEvent("action.1", "button", null, id);
-
-        let items = segmentedButton.children;
-        for (let j = items.length - 1; j >= 0; j--) {
-          items[j].classList.remove("selected");
-        }
-
-        item.classList.add("selected");
-        callback(option.value);
-      }.bind(this), true);
-
-      if (option.value === initialValue)
-        item.classList.add("selected");
-    }
-  },
-
-  _setupButton: function Reader_setupButton(id, callback) {
-    let button = this._doc.getElementById(id);
-
-    button.addEventListener("click", function(aEvent) {
-      if (!aEvent.isTrusted)
-        return;
-
-      aEvent.stopPropagation();
-      callback();
-    }, true);
-  },
-
-  _setupAllDropdowns: function Reader_setupAllDropdowns() {
-    let doc = this._doc;
-    let win = this._win;
-
-    let dropdowns = doc.getElementsByClassName("dropdown");
-
-    for (let i = dropdowns.length - 1; i >= 0; i--) {
-      let dropdown = dropdowns[i];
-
-      let dropdownToggle = dropdown.getElementsByClassName("dropdown-toggle")[0];
-      let dropdownPopup = dropdown.getElementsByClassName("dropdown-popup")[0];
-
-      if (!dropdownToggle || !dropdownPopup)
-        continue;
-
-      let dropdownArrow = doc.createElement("div");
-      dropdownArrow.className = "dropdown-arrow";
-      dropdownPopup.appendChild(dropdownArrow);
-
-      let updatePopupPosition = function() {
-        let popupWidth = dropdownPopup.offsetWidth + 30;
-        let arrowWidth = dropdownArrow.offsetWidth;
-        let toggleWidth = dropdownToggle.offsetWidth;
-        let toggleLeft = dropdownToggle.offsetLeft;
-
-        let popupShift = (toggleWidth - popupWidth) / 2;
-        let popupLeft = Math.max(0, Math.min(win.innerWidth - popupWidth, toggleLeft + popupShift));
-        dropdownPopup.style.left = popupLeft + "px";
-
-        let arrowShift = (toggleWidth - arrowWidth) / 2;
-        let arrowLeft = toggleLeft - popupLeft + arrowShift;
-        dropdownArrow.style.left = arrowLeft + "px";
-      };
-
-      win.addEventListener("resize", function(aEvent) {
-        if (!aEvent.isTrusted)
-          return;
-
-        // Wait for reflow before calculating the new position of the popup.
-        setTimeout(updatePopupPosition, 0);
-      }, true);
-
-      dropdownToggle.addEventListener("click", function(aEvent) {
-        if (!aEvent.isTrusted)
-          return;
-
-        aEvent.stopPropagation();
-
-        if (!this._getToolbarVisibility())
-          return;
-
-        let dropdownClasses = dropdown.classList;
-
-        if (dropdownClasses.contains("open")) {
-          win.history.back();
-        } else {
-          updatePopupPosition();
-          if (!this._closeAllDropdowns())
-            this._pushDropdownState();
-
-          dropdownClasses.add("open");
-        }
-      }.bind(this), true);
-    }
-  },
-
-  _pushDropdownState: function Reader_pushDropdownState() {
-    // FIXME: We're getting a NS_ERROR_UNEXPECTED error when we try
-    // to do win.history.pushState() here (see bug 682296). This is
-    // a workaround that allows us to push history state on the target
-    // content document.
-
-    let doc = this._doc;
-    let body = doc.body;
-
-    if (this._pushStateScript)
-      body.removeChild(this._pushStateScript);
-
-    this._pushStateScript = doc.createElement('script');
-    this._pushStateScript.type = "text/javascript";
-    this._pushStateScript.innerHTML = 'history.pushState({ dropdown: 1 }, document.title);';
-
-    body.appendChild(this._pushStateScript);
-  },
-
-  _closeAllDropdowns : function Reader_closeAllDropdowns() {
-    let dropdowns = this._doc.querySelectorAll(".dropdown.open");
-    for (let i = dropdowns.length - 1; i >= 0; i--) {
-      dropdowns[i].classList.remove("open");
-    }
-
-    return (dropdowns.length > 0)
-  }
-};
+window.addEventListener("DOMContentLoaded", function () {
+  document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
+});
--- a/toolkit/components/reader/moz.build
+++ b/toolkit/components/reader/moz.build
@@ -2,10 +2,11 @@
 # 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/.
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_JS_MODULES += [
+  'AboutReader.jsm',
   'ReaderMode.jsm'
 ]