Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 07 Jan 2015 15:25:55 -0800
changeset 248302 ee72fef1c55494ac8a589e60e145bdca106b60ea
parent 248291 d55528833b3e521855a4fc8fc2c8ff65d9246e48 (current diff)
parent 248301 5b030e48e8b0835368d091936830a48df62f34a2 (diff)
child 248365 70de2960aa877d7755ee6f66bf2d4c4c46bfed2c
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.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 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'
 ]