Bug 1111142 - (Part 3) Replace fennec-specific logic in AboutReader.jsm with messages. r=mfinkle,bnicholson
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Tue, 23 Dec 2014 10:10:34 -0500
changeset 239211 93bca334a998098266ce0c2c82381885c5c30ac7
parent 239210 931fc7ae771e0f7c57fb524e3d7abdccdc450ae6
child 239212 0e6ad992ae071ee21b575814d693218eb1b761ec
push id7472
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 20:36:27 +0000
treeherdermozilla-aurora@300ca104f8fb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle, bnicholson
bugs1111142
milestone37.0a1
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,159 @@
 // -*- 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": {
+        // 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;
 
-    // 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 +187,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 +242,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);
 
 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"],
@@ -144,17 +144,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) => {
@@ -449,16 +448,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];
@@ -3231,17 +3231,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);
 }
 
@@ -3576,17 +3576,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();
@@ -4276,41 +4275,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}