Bug 1111142 - (Part 3) Replace fennec-specific logic in AboutReader.jsm with messages. r=mfinkle,bnicholson
☠☠ backed out by 156a06b49da8 ☠ ☠
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Tue, 23 Dec 2014 10:10:34 -0500
changeset 247683 9b62d0e8b412dd289edefdb45d2c8342dd0d8e5a
parent 247682 e66b9aa4b22c50b09f6c5cccdc22c056ea49d4f4
child 247684 918ab351ea214253a5b8c2289e275afbfd9f776c
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)
reviewersmfinkle, bnicholson
bugs1111142
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
Bug 1111142 - (Part 3) Replace fennec-specific logic in AboutReader.jsm with messages. r=mfinkle,bnicholson
mobile/android/chrome/content/Reader.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/content/content.js
toolkit/components/reader/AboutReader.jsm
toolkit/components/reader/ReaderMode.jsm
--- a/mobile/android/chrome/content/Reader.js
+++ b/mobile/android/chrome/content/Reader.js
@@ -1,34 +1,155 @@
 // -*- 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;
+  MESSAGES: [
+    "Reader:AddToList",
+    "Reader:ArticleGet",
+    "Reader:FaviconRequest",
+    "Reader:ListStatusRequest",
+    "Reader:RemoveFromList",
+    "Reader:Share",
+    "Reader:ShowToast",
+    "Reader:ToolbarVisibility",
+    "Reader:SystemUIVisibility",
+    "Reader:UpdateIsArticle",
+  ],
+
+  init: function() {
+    for (let msg of this.MESSAGES) {
+      window.messageManager.addMessageListener(msg, this);
+    }
+
+    Services.obs.addObserver(this, "Reader:Added", false);
+    Services.obs.addObserver(this, "Reader:Removed", false);
+    Services.obs.addObserver(this, "Gesture:DoubleTap", false);
+  },
+
+  observe: function Reader_observe(aMessage, aTopic, aData) {
+    switch (aTopic) {
+      case "Reader:Added": {
+        window.messageManager.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));
+
+        window.messageManager.broadcastAsyncMessage("Reader:Removed", { url: aData });
+        break;
+      }
+      case "Gesture:DoubleTap": {
+        // XXX: Ideally, we would just do this all with web APIs in AboutReader.jsm.
+        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;
 
-    // Listen for future pref changes.
-    Services.prefs.addObserver("reader.parse-on-load.", this, false);
+      case "Reader:ArticleGet":
+        this._getArticle(message.data.url, message.target).then((article) => {
+          message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
+        });
+        break;
+
+      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;
 
-    return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
+      case "Reader:RemoveFromList":
+        Messaging.sendRequest({
+          type: "Reader:RemoveFromList",
+          url: message.data.url
+        });
+        break;
+
+      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 +183,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 +238,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,18 +104,18 @@ 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");
+// XXX: Make this into a module?
+Services.scriptloader.loadSubScript("chrome://browser/content/Reader.js", this);
 
 // Lazily-loaded browser scripts:
 [
   ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
   ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
   ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
   ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
   ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
@@ -141,17 +141,16 @@ 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"],
 ].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) => {
@@ -443,16 +442,17 @@ var BrowserApp = {
     Distribution.init();
     Tabs.init();
 #ifdef ACCESSIBILITY
     AccessFu.attach(window);
 #endif
 #ifdef NIGHTLY_BUILD
     ShumwayUtils.init();
 #endif
+    Reader.init();
 
     let url = null;
     let pinned = false;
     if ("arguments" in window) {
       if (window.arguments[0])
         url = window.arguments[0];
       if (window.arguments[1])
         gScreenWidth = window.arguments[1];
@@ -3220,17 +3220,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);
 }
 
@@ -3561,17 +3561,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();
@@ -4261,41 +4260,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;
--- a/mobile/android/chrome/content/content.js
+++ b/mobile/android/chrome/content/content.js
@@ -4,35 +4,83 @@
  * 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 = {
-  init: function(chromeGlobal) {
-    chromeGlobal.addEventListener("AboutReaderContentLoaded", this, false, true);
+  _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.documentURI.startsWith("about:reader")) {
+    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(content.document, content);
+          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(this);
+AboutReaderListener.init();
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -3,46 +3,41 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
 
 this.EXPORTED_SYMBOLS = [ "AboutReader" ];
 
-Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm")
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+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");
 
-XPCOMUtils.defineLazyGetter(this, "gChromeWin", () => Services.wm.getMostRecentWindow("navigator:browser"));
-
 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"));
 
@@ -113,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",
 
@@ -167,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;
@@ -248,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);
@@ -473,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;
 
--- 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}