Bug 1591469, move reader mode to use JSWindowActor instead of message managers, r=Gijs
☠☠ backed out by 03a111855a06 ☠ ☠
authorNeil Deakin <neil@mozilla.com>
Tue, 12 May 2020 01:06:51 +0000
changeset 529272 3a50d857d0979b25bf7f9fe24234bc6b3d9fb464
parent 529271 de6d1c1375eebf27d9e52aafba5b4138581e35c0
child 529273 9923799dd260cb125fb149b92298034ca3beb112
push id37406
push userdluca@mozilla.com
push dateTue, 12 May 2020 09:34:21 +0000
treeherdermozilla-central@1706d4d54ec6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1591469
milestone78.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 1591469, move reader mode to use JSWindowActor instead of message managers, r=Gijs Differential Revision: https://phabricator.services.mozilla.com/D71452
browser/actors/AboutReaderChild.jsm
browser/actors/AboutReaderParent.jsm
browser/actors/moz.build
browser/base/content/browser-fullZoom.js
browser/base/content/browser-sets.inc
browser/base/content/browser.js
browser/base/content/browser.xhtml
browser/base/content/test/performance/browser_startup_content.js
browser/components/BrowserGlue.jsm
browser/components/extensions/parent/ext-browser.js
browser/components/extensions/parent/ext-tabs.js
browser/components/newtab/lib/ASRouterTriggerListeners.jsm
browser/components/newtab/test/browser/browser_asrouter_trigger_listeners.js
browser/components/pocket/content/SaveToPocket.jsm
browser/components/uitour/UITour.jsm
browser/modules/ReaderParent.jsm
browser/modules/moz.build
toolkit/components/narrate/NarrateControls.jsm
toolkit/components/reader/AboutReader.jsm
toolkit/components/reader/content/aboutReader.html
toolkit/components/reader/content/aboutReader.js
toolkit/components/reader/jar.mn
toolkit/components/reader/test/browser.ini
toolkit/components/reader/test/browser_readerMode_pocket.js
--- a/browser/actors/AboutReaderChild.jsm
+++ b/browser/actors/AboutReaderChild.jsm
@@ -1,182 +1,209 @@
 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
 /* 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";
 
 var EXPORTED_SYMBOLS = ["AboutReaderChild"];
 
-const { ActorChild } = ChromeUtils.import(
-  "resource://gre/modules/ActorChild.jsm"
-);
-
 ChromeUtils.defineModuleGetter(
   this,
   "AboutReader",
   "resource://gre/modules/AboutReader.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ReaderMode",
   "resource://gre/modules/ReaderMode.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "Readerable",
   "resource://gre/modules/Readerable.jsm"
 );
 
-class AboutReaderChild extends ActorChild {
-  constructor(dispatcher) {
-    super(dispatcher);
+class AboutReaderChild extends JSWindowActorChild {
+  constructor() {
+    super();
 
+    this._reader = null;
     this._articlePromise = null;
     this._isLeavingReaderableReaderMode = false;
   }
 
+  willDestroy() {
+    this.cancelPotentialPendingReadabilityCheck();
+    this.readerModeHidden();
+  }
+
+  readerModeHidden() {
+    if (this._reader) {
+      this._reader.clearActor();
+    }
+    this._reader = null;
+  }
+
   receiveMessage(message) {
     switch (message.name) {
       case "Reader:ToggleReaderMode":
         if (!this.isAboutReader) {
-          this._articlePromise = ReaderMode.parseDocument(
-            this.content.document
-          ).catch(Cu.reportError);
-          ReaderMode.enterReaderMode(this.mm.docShell, this.content);
+          this._articlePromise = ReaderMode.parseDocument(this.document).catch(
+            Cu.reportError
+          );
+          ReaderMode.enterReaderMode(this.docShell, this.contentWindow);
         } else {
           this._isLeavingReaderableReaderMode = this.isReaderableAboutReader;
-          ReaderMode.leaveReaderMode(this.mm.docShell, this.content);
+          ReaderMode.leaveReaderMode(this.docShell, this.contentWindow);
         }
         break;
 
       case "Reader:PushState":
         this.updateReaderButton(!!(message.data && message.data.isArticle));
         break;
     }
+
+    // Forward the message to the reader if it has been created.
+    if (this._reader) {
+      this._reader.receiveMessage(message);
+    }
   }
 
   get isAboutReader() {
-    if (!this.content) {
+    if (!this.document) {
       return false;
     }
-    return this.content.document.documentURI.startsWith("about:reader");
+    return this.document.documentURI.startsWith("about:reader");
   }
 
   get isReaderableAboutReader() {
-    return (
-      this.isAboutReader &&
-      !this.content.document.documentElement.dataset.isError
-    );
+    return this.isAboutReader && !this.document.documentElement.dataset.isError;
   }
 
   handleEvent(aEvent) {
-    if (aEvent.originalTarget.defaultView != this.content) {
+    if (aEvent.originalTarget.defaultView != this.contentWindow) {
       return;
     }
 
     switch (aEvent.type) {
-      case "AboutReaderContentLoaded":
+      case "DOMContentLoaded":
         if (!this.isAboutReader) {
+          this.updateReaderButton();
           return;
         }
 
-        if (this.content.document.body) {
+        if (this.document.body) {
           // Update the toolbar icon to show the "reader active" icon.
-          this.mm.sendAsyncMessage("Reader:UpdateReaderButton");
-          new AboutReader(this.mm, this.content, this._articlePromise);
+          this.sendAsyncMessage("Reader:UpdateReaderButton");
+          this._reader = new AboutReader(this, this._articlePromise);
           this._articlePromise = null;
         }
         break;
 
       case "pagehide":
         this.cancelPotentialPendingReadabilityCheck();
         // this._isLeavingReaderableReaderMode is used here to keep the Reader Mode icon
         // visible in the location bar when transitioning from reader-mode page
         // back to the readable source page.
-        this.mm.sendAsyncMessage("Reader:UpdateReaderButton", {
+        this.sendAsyncMessage("Reader:UpdateReaderButton", {
           isArticle: this._isLeavingReaderableReaderMode,
         });
         if (this._isLeavingReaderableReaderMode) {
           this._isLeavingReaderableReaderMode = false;
         }
         break;
 
       case "pageshow":
         // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
         // event, so we need to rely on "pageshow" in this case.
         if (aEvent.persisted) {
           this.updateReaderButton();
         }
         break;
-      case "DOMContentLoaded":
-        this.updateReaderButton();
-        break;
     }
   }
 
   /**
    * NB: this function will update the state of the reader button asynchronously
    * after the next mozAfterPaint call (assuming reader mode is enabled and
    * this is a suitable document). Calling it on things which won't be
    * painted is not going to work.
    */
   updateReaderButton(forceNonArticle) {
     if (
       !Readerable.isEnabledForParseOnLoad ||
       this.isAboutReader ||
-      !this.content ||
-      !(this.content.document instanceof this.content.HTMLDocument) ||
-      this.content.document.mozSyntheticDocument
+      !this.contentWindow ||
+      !(this.document instanceof this.contentWindow.HTMLDocument) ||
+      this.document.mozSyntheticDocument
     ) {
       return;
     }
 
     this.scheduleReadabilityCheckPostPaint(forceNonArticle);
   }
 
   cancelPotentialPendingReadabilityCheck() {
     if (this._pendingReadabilityCheck) {
-      this.mm.removeEventListener(
-        "MozAfterPaint",
-        this._pendingReadabilityCheck
-      );
+      if (this._listenerWindow) {
+        this._listenerWindow.removeEventListener(
+          "MozAfterPaint",
+          this._pendingReadabilityCheck
+        );
+      }
       delete this._pendingReadabilityCheck;
+      delete this._listenerWindow;
     }
   }
 
   scheduleReadabilityCheckPostPaint(forceNonArticle) {
     if (this._pendingReadabilityCheck) {
       // We need to stop this check before we re-add one because we don't know
       // if forceNonArticle was true or false last time.
       this.cancelPotentialPendingReadabilityCheck();
     }
     this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(
       this,
       forceNonArticle
     );
-    this.mm.addEventListener("MozAfterPaint", this._pendingReadabilityCheck);
+
+    this._listenerWindow = this.contentWindow.windowRoot;
+    this.contentWindow.windowRoot.addEventListener(
+      "MozAfterPaint",
+      this._pendingReadabilityCheck
+    );
   }
 
   onPaintWhenWaitedFor(forceNonArticle, event) {
     // In non-e10s, we'll get called for paints other than ours, and so it's
     // possible that this page hasn't been laid out yet, in which case we
     // should wait until we get an event that does relate to our layout. We
-    // determine whether any of our this.content got painted by checking if there
-    // are any painted rects.
+    // determine whether any of our this.contentWindow got painted by checking
+    // if there are any painted rects.
     if (!event.clientRects.length) {
       return;
     }
 
     this.cancelPotentialPendingReadabilityCheck();
+
+    // Ignore errors from actors that have been unloaded before the
+    // paint event timer fires.
+    let document;
+    try {
+      document = this.document;
+    } catch (ex) {
+      return;
+    }
+
     // Only send updates when there are articles; there's no point updating with
     // |false| all the time.
-    if (Readerable.isProbablyReaderable(this.content.document)) {
-      this.mm.sendAsyncMessage("Reader:UpdateReaderButton", {
+    if (Readerable.isProbablyReaderable(document)) {
+      this.sendAsyncMessage("Reader:UpdateReaderButton", {
         isArticle: true,
       });
     } else if (forceNonArticle) {
-      this.mm.sendAsyncMessage("Reader:UpdateReaderButton", {
+      this.sendAsyncMessage("Reader:UpdateReaderButton", {
         isArticle: false,
       });
     }
   }
 }
rename from browser/modules/ReaderParent.jsm
rename to browser/actors/AboutReaderParent.jsm
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/actors/AboutReaderParent.jsm
@@ -1,16 +1,16 @@
 // -*- 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";
 
-var EXPORTED_SYMBOLS = ["ReaderParent"];
+var EXPORTED_SYMBOLS = ["AboutReaderParent"];
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(
   this,
   "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm"
 );
@@ -19,71 +19,156 @@ ChromeUtils.defineModuleGetter(
   "ReaderMode",
   "resource://gre/modules/ReaderMode.jsm"
 );
 
 const gStringBundle = Services.strings.createBundle(
   "chrome://global/locale/aboutReader.properties"
 );
 
-var ReaderParent = {
-  // Listeners are added in BrowserGlue.jsm
-  receiveMessage(message) {
+// A set of all of the AboutReaderParent actors that exist.
+// See bug 1631146 for a request for a less manual way of doing this.
+let gAllActors = new Set();
+
+// A map of message names to listeners that listen to messages
+// received by the AboutReaderParent actors.
+let gListeners = new Map();
+
+class AboutReaderParent extends JSWindowActorParent {
+  didDestroy() {
+    gAllActors.delete(this);
+  }
+
+  isReaderMode(browser) {
+    return browser.currentURI.spec.startsWith("about:reader");
+  }
+
+  static addMessageListener(name, listener) {
+    if (!gListeners.has(name)) {
+      gListeners.set(name, new Set([listener]));
+    } else {
+      gListeners.get(name).add(listener);
+    }
+  }
+
+  static removeMessageListener(name, listener) {
+    if (!gListeners.has(name)) {
+      return;
+    }
+
+    gListeners.get(name).delete(listener);
+  }
+
+  static broadcastAsyncMessage(name, data) {
+    for (let actor of gAllActors) {
+      // Ignore errors for actors that might not be valid yet or anymore.
+      try {
+        actor.sendAsyncMessage(name, data);
+      } catch (ex) {}
+    }
+  }
+
+  callListeners(message) {
+    let listeners = gListeners.get(message.name);
+    if (!listeners) {
+      return;
+    }
+
+    message.target = this.browsingContext.embedderElement;
+    for (let listener of listeners.values()) {
+      try {
+        listener.receiveMessage(message);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  }
+
+  async receiveMessage(message) {
     switch (message.name) {
       case "Reader:FaviconRequest": {
-        if (message.target.messageManager) {
-          try {
-            let preferredWidth = message.data.preferredWidth || 0;
-            let uri = Services.io.newURI(message.data.url);
+        try {
+          let preferredWidth = message.data.preferredWidth || 0;
+          let uri = Services.io.newURI(message.data.url);
+
+          let result = await new Promise(resolve => {
             PlacesUtils.favicons.getFaviconURLForPage(
               uri,
               iconUri => {
                 if (iconUri) {
                   iconUri = PlacesUtils.favicons.getFaviconLinkForIcon(iconUri);
-                  message.target.messageManager.sendAsyncMessage(
-                    "Reader:FaviconReturn",
-                    {
-                      url: message.data.url,
-                      faviconUrl: iconUri.pathQueryRef.replace(/^favicon:/, ""),
-                    }
-                  );
+                  resolve({
+                    url: message.data.url,
+                    faviconUrl: iconUri.pathQueryRef.replace(/^favicon:/, ""),
+                  });
+                } else {
+                  resolve(null);
                 }
               },
               preferredWidth
             );
-          } catch (ex) {
-            Cu.reportError(
-              "Error requesting favicon URL for about:reader content: " + ex
-            );
-          }
+          });
+
+          this.callListeners(message);
+          return result;
+        } catch (ex) {
+          Cu.reportError(
+            "Error requesting favicon URL for about:reader content: " + ex
+          );
         }
+
         break;
       }
 
       case "Reader:UpdateReaderButton": {
-        let browser = message.target;
+        let browser = this.browsingContext.embedderElement;
+        if (!browser) {
+          return undefined;
+        }
+
+        if (browser.outerBrowser) {
+          browser = browser.outerBrowser; // handle RDM mode
+        }
+
         if (message.data && message.data.isArticle !== undefined) {
           browser.isArticle = message.data.isArticle;
         }
         this.updateReaderButton(browser);
+        this.callListeners(message);
         break;
       }
+
+      default:
+        this.callListeners(message);
+        break;
     }
-  },
+
+    return undefined;
+  }
+
+  static updateReaderButton(browser) {
+    let windowGlobal = browser.browsingContext.currentWindowGlobal;
+    let actor = windowGlobal.getActor("AboutReader");
+    actor.updateReaderButton(browser);
+  }
 
   updateReaderButton(browser) {
-    let win = browser.ownerGlobal;
-    if (browser != win.gBrowser.selectedBrowser) {
+    let tabBrowser = browser.getTabBrowser();
+    if (!tabBrowser || browser != tabBrowser.selectedBrowser) {
       return;
     }
 
+    let win = browser.ownerGlobal;
+
     let button = win.document.getElementById("reader-mode-button");
     let menuitem = win.document.getElementById("menu_readerModeItem");
     let key = win.document.getElementById("key_toggleReaderMode");
-    if (browser.currentURI.spec.startsWith("about:reader")) {
+    if (this.isReaderMode(browser)) {
+      gAllActors.add(this);
+
       let closeText = gStringBundle.GetStringFromName("readerView.close");
 
       button.setAttribute("readeractive", true);
       button.hidden = false;
       button.setAttribute("aria-label", closeText);
 
       menuitem.setAttribute("label", closeText);
       menuitem.setAttribute("hidden", false);
@@ -110,35 +195,45 @@ var ReaderParent = {
       );
 
       key.setAttribute("disabled", !browser.isArticle);
 
       if (browser.isArticle) {
         Services.obs.notifyObservers(null, "reader-mode-available");
       }
     }
-  },
+  }
 
-  forceShowReaderIcon(browser) {
+  static forceShowReaderIcon(browser) {
     browser.isArticle = true;
-    this.updateReaderButton(browser);
-  },
+    AboutReaderParent.updateReaderButton(browser);
+  }
 
-  buttonClick(event) {
+  static buttonClick(event) {
     if (event.button != 0) {
       return;
     }
-    this.toggleReaderMode(event);
-  },
+    AboutReaderParent.toggleReaderMode(event);
+  }
+
+  static toggleReaderMode(event) {
+    let win = event.target.ownerGlobal;
+    if (win.gBrowser) {
+      let browser = win.gBrowser.selectedBrowser;
 
-  toggleReaderMode(event) {
-    let win = event.target.ownerGlobal;
-    let browser = win.gBrowser.selectedBrowser;
-    browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
-  },
+      let windowGlobal = browser.browsingContext.currentWindowGlobal;
+      let actor = windowGlobal.getActor("AboutReader");
+      if (actor) {
+        if (actor.isReaderMode(browser)) {
+          gAllActors.delete(this);
+        }
+        actor.sendAsyncMessage("Reader:ToggleReaderMode", {});
+      }
+    }
+  }
 
   /**
    * Gets an article for a given URL. This method will download and parse a document.
    *
    * @param url The article URL.
    * @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.
@@ -147,10 +242,10 @@ var ReaderParent = {
     return ReaderMode.downloadAndParseDocument(url).catch(e => {
       if (e && e.newURL) {
         // Pass up the error so we can navigate the browser in question to the new URL:
         throw e;
       }
       Cu.reportError("Error downloading and parsing document: " + e);
       return null;
     });
-  },
-};
+  }
+}
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -5,16 +5,19 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "General")
 
 with Files("ContentSearch*.jsm"):
     BUG_COMPONENT = ("Firefox", "Search")
 
+with Files("AboutReaderParent.jsm"):
+    BUG_COMPONENT = ("Toolkit", "Reader Mode")
+
 with Files("LightweightThemeChild.jsm"):
     BUG_COMPONENT = ("WebExtensions", "Themes")
 
 with Files("PageInfoChild.jsm"):
     BUG_COMPONENT = ("Firefox", "Page Info Window")
 
 with Files("PageStyleChild.jsm"):
     BUG_COMPONENT = ("Firefox", "Menus")
@@ -29,16 +32,17 @@ FINAL_TARGET_FILES.actors += [
     'AboutNewTabChild.jsm',
     'AboutPluginsChild.jsm',
     'AboutPluginsParent.jsm',
     'AboutPrivateBrowsingChild.jsm',
     'AboutPrivateBrowsingParent.jsm',
     'AboutProtectionsChild.jsm',
     'AboutProtectionsParent.jsm',
     'AboutReaderChild.jsm',
+    'AboutReaderParent.jsm',
     'AboutTabCrashedChild.jsm',
     'AboutTabCrashedParent.jsm',
     'BlockedSiteChild.jsm',
     'BlockedSiteParent.jsm',
     'BrowserTabChild.jsm',
     'BrowserTabParent.jsm',
     'ClickHandlerChild.jsm',
     'ClickHandlerParent.jsm',
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -329,34 +329,34 @@ var FullZoom = {
 
   /**
    * If browser in reader mode sends message to reader in order to decrease font size,
    * Otherwise reduces the zoom level of the page in the current browser.
    */
   async reduce() {
     let browser = gBrowser.selectedBrowser;
     if (browser.currentURI.spec.startsWith("about:reader")) {
-      browser.messageManager.sendAsyncMessage("Reader:ZoomOut");
+      browser.sendMessageToActor("Reader:ZoomOut", {}, "AboutReader");
     } else if (this._isPDFViewer(browser)) {
       browser.messageManager.sendAsyncMessage("PDFJS:ZoomOut");
     } else {
       ZoomManager.reduce();
       this._ignorePendingZoomAccesses(browser);
       await this._applyZoomToPref(browser);
     }
   },
 
   /**
    * If browser in reader mode sends message to reader in order to increase font size,
    * Otherwise enlarges the zoom level of the page in the current browser.
    */
   async enlarge() {
     let browser = gBrowser.selectedBrowser;
     if (browser.currentURI.spec.startsWith("about:reader")) {
-      browser.messageManager.sendAsyncMessage("Reader:ZoomIn");
+      browser.sendMessageToActor("Reader:ZoomIn", {}, "AboutReader");
     } else if (this._isPDFViewer(browser)) {
       browser.messageManager.sendAsyncMessage("PDFJS:ZoomIn");
     } else {
       ZoomManager.enlarge();
       this._ignorePendingZoomAccesses(browser);
       await this._applyZoomToPref(browser);
     }
   },
@@ -366,17 +366,17 @@ var FullZoom = {
    * Otherwise enlarges the zoom level of the page in the current browser.
    * This function is not async like reduce/enlarge, because it is invoked by our
    * event handler. This means that the call to _applyZoomToPref is not awaited and
    * will happen asynchronously.
    */
   changeZoomBy(aBrowser, aValue) {
     if (aBrowser.currentURI.spec.startsWith("about:reader")) {
       const message = aValue > 0 ? "Reader::ZoomIn" : "Reader:ZoomOut";
-      aBrowser.messageManager.sendAsyncMessage(message);
+      aBrowser.sendMessageToActor(message, {}, "AboutReader");
       return;
     } else if (this._isPDFViewer(aBrowser)) {
       const message = aValue > 0 ? "PDFJS::ZoomIn" : "PDFJS:ZoomOut";
       aBrowser.messageManager.sendAsyncMessage(message);
       return;
     }
     let zoom = ZoomManager.getZoomForBrowser(aBrowser);
     zoom += aValue;
@@ -407,17 +407,17 @@ var FullZoom = {
    * Sets the zoom level of the page in the given browser to the global zoom
    * level.
    *
    * @return A promise which resolves when the zoom reset has been applied.
    */
   reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) {
     let forceValue;
     if (browser.currentURI.spec.startsWith("about:reader")) {
-      browser.messageManager.sendAsyncMessage("Reader:ResetZoom");
+      browser.sendMessageToActor("Reader:ResetZoom", {}, "AboutReader");
     } else if (this._isPDFViewer(browser)) {
       browser.messageManager.sendAsyncMessage("PDFJS:ZoomReset");
       // Ensure that the UI elements of the PDF viewer won't be zoomed in/out
       // on reset, even if/when browser default zoom value is not set to 100%.
       forceValue = 1;
     }
     let token = this._getBrowserToken(browser);
     let result = ZoomUI.getGlobalValue().then(value => {
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -37,17 +37,17 @@
     <command id="cmd_toggleMute" oncommand="gBrowser.toggleMuteAudioOnMultiSelectedTabs(gBrowser.selectedTab)"/>
     <command id="cmd_CustomizeToolbars" oncommand="gCustomizeMode.enter()"/>
     <command id="cmd_toggleOfflineStatus" oncommand="BrowserOffline.toggleOfflineStatus();"/>
     <command id="cmd_quitApplication" oncommand="goQuitApplication()"/>
 
     <command id="View:PageSource" oncommand="BrowserViewSource(window.gBrowser.selectedBrowser);"/>
     <command id="View:PageInfo" oncommand="BrowserPageInfo();"/>
     <command id="View:FullScreen" oncommand="BrowserFullScreen();"/>
-    <command id="View:ReaderView" oncommand="ReaderParent.toggleReaderMode(event);"/>
+    <command id="View:ReaderView" oncommand="AboutReaderParent.toggleReaderMode(event);"/>
     <command id="View:PictureInPicture" oncommand="PictureInPicture.onCommand(event);"/>
     <command id="cmd_find" oncommand="gLazyFindCommand('onFindCommand')"/>
     <command id="cmd_findAgain" oncommand="gLazyFindCommand('onFindAgainCommand', false)"/>
     <command id="cmd_findPrevious" oncommand="gLazyFindCommand('onFindAgainCommand', true)"/>
 #ifdef XP_MACOSX
     <command id="cmd_findSelection" oncommand="gLazyFindCommand('onFindSelectionCommand')"/>
 #endif
     <!-- work-around bug 392512 -->
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -11,16 +11,17 @@ var { AppConstants } = ChromeUtils.impor
   "resource://gre/modules/AppConstants.jsm"
 );
 ChromeUtils.import("resource://gre/modules/NotificationDB.jsm");
 
 // lazy module getters
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+  AboutReaderParent: "resource:///actors/AboutReaderParent.jsm",
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AMTelemetry: "resource://gre/modules/AddonManager.jsm",
   NewTabPagePreloading: "resource:///modules/NewTabPagePreloading.jsm",
   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   CFRPageActions: "resource://activity-stream/lib/CFRPageActions.jsm",
   CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
@@ -55,17 +56,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   Pocket: "chrome://pocket/content/Pocket.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   // TODO (Bug 1529552): Remove once old urlbar code goes away.
   ReaderMode: "resource://gre/modules/ReaderMode.jsm",
-  ReaderParent: "resource:///modules/ReaderParent.jsm",
   RFPHelper: "resource://gre/modules/RFPHelper.jsm",
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SessionStartup: "resource:///modules/sessionstore/SessionStartup.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
   SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
   SiteDataManager: "resource:///modules/SiteDataManager.jsm",
@@ -5278,17 +5278,17 @@ var XULBrowserWindow = {
       ) {
         gCustomizeMode.exit();
       }
 
       CFRPageActions.updatePageActions(gBrowser.selectedBrowser);
     }
     Services.obs.notifyObservers(null, "touchbar-location-change", location);
     UpdateBackForwardCommands(gBrowser.webNavigation);
-    ReaderParent.updateReaderButton(gBrowser.selectedBrowser);
+    AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser);
 
     if (!gMultiProcessBrowser) {
       // Bug 1108553 - Cannot rotate images with e10s
       gGestureSupport.restoreRotationState();
     }
 
     // See bug 358202, when tabs are switched during a drag operation,
     // timers don't fire on windows (bug 203573)
@@ -5881,19 +5881,23 @@ var TabsProgressListener = {
   },
 
   onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
     // Filter out location changes caused by anchor navigation
     // or history.push/pop/replaceState.
     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
       // Reader mode cares about history.pushState and friends.
       // FIXME: The content process should manage this directly (bug 1445351).
-      aBrowser.messageManager.sendAsyncMessage("Reader:PushState", {
-        isArticle: aBrowser.isArticle,
-      });
+      aBrowser.sendMessageToActor(
+        "Reader:PushState",
+        {
+          isArticle: aBrowser.isArticle,
+        },
+        "AboutReader"
+      );
       return;
     }
 
     // Filter out location changes in sub documents.
     if (!aWebProgress.isTopLevel) {
       return;
     }
 
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -1022,17 +1022,17 @@
                   <label id="userContext-label"/>
                   <image id="userContext-indicator"/>
                 </hbox>
                 <image id="reader-mode-button"
                        class="urlbar-icon urlbar-page-action"
                        tooltip="dynamic-shortcut-tooltip"
                        role="button"
                        hidden="true"
-                       onclick="ReaderParent.buttonClick(event);"/>
+                       onclick="AboutReaderParent.buttonClick(event);"/>
                 <toolbarbutton id="urlbar-zoom-button"
                        onclick="FullZoom.reset();"
                        tooltip="dynamic-shortcut-tooltip"
                        hidden="true"/>
                 <box id="pageActionSeparator" class="urlbar-page-action"/>
                 <image id="pageActionButton"
                        class="urlbar-icon urlbar-page-action"
                        role="button"
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -40,17 +40,16 @@ const whitelist = {
 
     // Browser front-end
     "resource:///actors/AboutReaderChild.jsm",
     "resource:///actors/BrowserTabChild.jsm",
     "resource:///actors/LinkHandlerChild.jsm",
     "resource:///actors/SearchTelemetryChild.jsm",
     "resource:///actors/PromptChild.jsm",
     "resource://gre/actors/AutoCompleteChild.jsm",
-    "resource://gre/modules/ActorChild.jsm",
     "resource://gre/modules/ActorManagerChild.jsm",
     "resource://gre/modules/E10SUtils.jsm",
     "resource://gre/modules/Readerable.jsm",
 
     // Telemetry
     "resource://gre/modules/TelemetryController.jsm", // bug 1470339
     "resource://gre/modules/TelemetryUtils.jsm", // bug 1470339
 
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -152,16 +152,31 @@ let ACTORS = {
       events: {
         DOMWindowCreated: { capture: true },
       },
     },
 
     matches: ["about:protections"],
   },
 
+  AboutReader: {
+    parent: {
+      moduleURI: "resource:///actors/AboutReaderParent.jsm",
+    },
+    child: {
+      moduleURI: "resource:///actors/AboutReaderChild.jsm",
+      events: {
+        DOMContentLoaded: {},
+        pageshow: { mozSystemGroup: true },
+        pagehide: { mozSystemGroup: true },
+      },
+    },
+    messageManagerGroups: ["browsers"],
+  },
+
   AboutTabCrashed: {
     parent: {
       moduleURI: "resource:///actors/AboutTabCrashedParent.jsm",
     },
     child: {
       moduleURI: "resource:///actors/AboutTabCrashedChild.jsm",
 
       events: {
@@ -516,30 +531,16 @@ let ACTORS = {
       moduleURI: "resource:///actors/WebRTCChild.jsm",
     },
 
     allFrames: true,
   },
 };
 
 let LEGACY_ACTORS = {
-  AboutReader: {
-    child: {
-      module: "resource:///actors/AboutReaderChild.jsm",
-      group: "browsers",
-      events: {
-        AboutReaderContentLoaded: { wantUntrusted: true },
-        DOMContentLoaded: {},
-        pageshow: { mozSystemGroup: true },
-        pagehide: { mozSystemGroup: true },
-      },
-      messages: ["Reader:ToggleReaderMode", "Reader:PushState"],
-    },
-  },
-
   URIFixup: {
     child: {
       module: "resource:///actors/URIFixupChild.jsm",
       group: "browsers",
       observers: ["keyword-uri-fixup"],
     },
   },
 };
@@ -695,17 +696,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
 });
 
 // eslint-disable-next-line no-unused-vars
 XPCOMUtils.defineLazyModuleGetters(this, {
   AboutLoginsParent: "resource:///modules/AboutLoginsParent.jsm",
   AsyncPrefs: "resource://gre/modules/AsyncPrefs.jsm",
   PluginManager: "resource:///actors/PluginParent.jsm",
-  ReaderParent: "resource:///modules/ReaderParent.jsm",
 });
 
 /**
  * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
  * XXX Bug 1325373 is for making eslint detect these automatically.
  */
 
 let initializedModules = {};
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -20,16 +20,21 @@ ChromeUtils.defineModuleGetter(
   "BrowserWindowTracker",
   "resource:///modules/BrowserWindowTracker.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "PromiseUtils",
   "resource://gre/modules/PromiseUtils.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AboutReaderParent",
+  "resource:///actors/AboutReaderParent.jsm"
+);
 
 var { ExtensionError } = ExtensionUtils;
 
 var { defineLazyGetter } = ExtensionCommon;
 
 const READER_MODE_PREFIX = "about:reader";
 
 let tabTracker;
@@ -347,17 +352,17 @@ class TabTracker extends TabTrackerBase 
 
     windowTracker.addListener("TabClose", this);
     windowTracker.addListener("TabOpen", this);
     windowTracker.addListener("TabSelect", this);
     windowTracker.addListener("TabMultiSelect", this);
     windowTracker.addOpenListener(this._handleWindowOpen);
     windowTracker.addCloseListener(this._handleWindowClose);
 
-    Services.mm.addMessageListener("Reader:UpdateReaderButton", this);
+    AboutReaderParent.addMessageListener("Reader:UpdateReaderButton", this);
 
     /* eslint-disable mozilla/balanced-listeners */
     this.on("tab-detached", this._handleTabDestroyed);
     this.on("tab-removed", this._handleTabDestroyed);
     /* eslint-enable mozilla/balanced-listeners */
   }
 
   getId(nativeTab) {
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -1484,18 +1484,21 @@ this.tabs = class extends ExtensionAPI {
         async toggleReaderMode(tabId) {
           let tab = await promiseTabWhenReady(tabId);
           if (!tab.isInReaderMode && !tab.isArticle) {
             throw new ExtensionError(
               "The specified tab cannot be placed into reader mode."
             );
           }
           let nativeTab = getTabOrActive(tabId);
-          nativeTab.linkedBrowser.messageManager.sendAsyncMessage(
-            "Reader:ToggleReaderMode"
+
+          nativeTab.linkedBrowser.sendMessageToActor(
+            "Reader:ToggleReaderMode",
+            {},
+            "AboutReader"
           );
         },
 
         moveInSuccession(tabIds, tabId, options) {
           const { insert, append } = options || {};
           const tabIdSet = new Set(tabIds);
           if (tabIdSet.size !== tabIds.length) {
             throw new ExtensionError(
--- a/browser/components/newtab/lib/ASRouterTriggerListeners.jsm
+++ b/browser/components/newtab/lib/ASRouterTriggerListeners.jsm
@@ -6,16 +6,17 @@
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   EveryWindow: "resource:///modules/EveryWindow.jsm",
+  AboutReaderParent: "resource:///actors/AboutReaderParent.jsm",
 });
 
 const FEW_MINUTES = 15 * 60 * 1000; // 15 mins
 const MATCH_PATTERN_OPTIONS = { ignorePath: true };
 
 function isPrivateWindow(win) {
   return (
     !(win instanceof Ci.nsIDOMWindow) ||
@@ -94,17 +95,17 @@ this.ASRouterTriggerListeners = new Map(
       _triggerHandler: null,
       _hosts: new Set(),
       _matchPatternSet: null,
       readerModeEvent: "Reader:UpdateReaderButton",
 
       init(triggerHandler, hosts, patterns) {
         if (!this._initialized) {
           this.receiveMessage = this.receiveMessage.bind(this);
-          Services.mm.addMessageListener(this.readerModeEvent, this);
+          AboutReaderParent.addMessageListener(this.readerModeEvent, this);
           this._triggerHandler = triggerHandler;
           this._initialized = true;
         }
         if (patterns) {
           this._matchPatternSet = createMatchPatternSet([
             ...(this._matchPatternSet ? this._matchPatternSet.patterns : []),
             ...patterns,
           ]);
@@ -123,17 +124,17 @@ this.ASRouterTriggerListeners = new Map(
           if (match) {
             this._triggerHandler(target, { id: this.id, param: match });
           }
         }
       },
 
       uninit() {
         if (this._initialized) {
-          Services.mm.removeMessageListener(this.readerModeEvent, this);
+          AboutReaderParent.removeMessageListener(this.readerModeEvent, this);
           this._initialized = false;
           this._triggerHandler = null;
           this._hosts = new Set();
           this._matchPatternSet = null;
         }
       },
     },
   ],
--- a/browser/components/newtab/test/browser/browser_asrouter_trigger_listeners.js
+++ b/browser/components/newtab/test/browser/browser_asrouter_trigger_listeners.js
@@ -58,17 +58,20 @@ add_task(async function check_openArticl
     articleTrigger.init((browser, match) => resolve(match), ["example.com"])
   );
 
   const win = await BrowserTestUtils.openNewBrowserWindow();
   await openURLInWindow(win, TEST_URL);
   // Send a message from the content page (the TEST_URL) to the parent
   // This should trigger the `receiveMessage` cb in the articleTrigger
   await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => {
-    sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
+    let readerActor = content.windowGlobalChild.getActor("AboutReader");
+    readerActor.sendAsyncMessage("Reader:UpdateReaderButton", {
+      isArticle: true,
+    });
   });
 
   await listenerTriggered.then(data =>
     is(
       data.param.url,
       TEST_URL,
       "We should match on the TEST_URL as a website article"
     )
--- a/browser/components/pocket/content/SaveToPocket.jsm
+++ b/browser/components/pocket/content/SaveToPocket.jsm
@@ -24,16 +24,21 @@ ChromeUtils.defineModuleGetter(
   "Pocket",
   "chrome://pocket/content/Pocket.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ReaderMode",
   "resource://gre/modules/ReaderMode.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AboutReaderParent",
+  "resource:///actors/AboutReaderParent.jsm"
+);
 
 var EXPORTED_SYMBOLS = ["SaveToPocket"];
 
 var PocketPageAction = {
   pageAction: null,
   urlbarNode: null,
 
   init() {
@@ -239,18 +244,18 @@ var SaveToPocket = {
     if (this.prefEnabled) {
       PocketOverlay.startup();
     } else {
       // We avoid calling onPrefChange or similar here, because we don't want to
       // shut down things that haven't started up, or broadcast unnecessary messages.
       this.updateElements(false);
       Services.obs.addObserver(this, "browser-delayed-startup-finished");
     }
-    Services.mm.addMessageListener("Reader:OnSetup", this);
-    Services.mm.addMessageListener("Reader:Clicked-pocket-button", this);
+    AboutReaderParent.addMessageListener("Reader:OnSetup", this);
+    AboutReaderParent.addMessageListener("Reader:Clicked-pocket-button", this);
   },
 
   observe(subject, topic, data) {
     if (topic == "browser-delayed-startup-finished") {
       // We only get here if pocket is disabled; the observer is removed when
       // we're enabled.
       this.updateElementsInWindow(subject, false);
     }
@@ -260,29 +265,29 @@ var SaveToPocket = {
     id: "pocket-button",
     image: "chrome://pocket/content/panels/img/pocket-outline.svg",
     width: 20,
     height: 20,
   },
 
   onPrefChange(pref, oldValue, newValue) {
     if (!newValue) {
-      Services.mm.broadcastAsyncMessage("Reader:RemoveButton", {
+      AboutReaderParent.broadcastAsyncMessage("Reader:RemoveButton", {
         id: "pocket-button",
       });
       PocketOverlay.shutdown();
       Services.obs.addObserver(this, "browser-delayed-startup-finished");
     } else {
       Services.obs.removeObserver(this, "browser-delayed-startup-finished");
       PocketOverlay.startup();
       // The title for the button is extracted from browser.xhtml where it comes from a DTD.
       // If we don't have this, there's also no possibility of there being a reader
       // mode tab already loaded. We'll get an Reader:OnSetup message when that happens.
       if (this._readerButtonData.title) {
-        Services.mm.broadcastAsyncMessage(
+        AboutReaderParent.broadcastAsyncMessage(
           "Reader:AddButton",
           this._readerButtonData
         );
       }
     }
     this.updateElements(newValue);
   },
 
@@ -314,19 +319,20 @@ var SaveToPocket = {
         // We must have a browser window; get the tooltip for the button if we don't
         // have it already.
         if (!this._readerButtonData.title) {
           let doc = message.target.ownerDocument;
           let button = doc.getElementById("pocket-button");
           this._readerButtonData.title = button.getAttribute("tooltiptext");
         }
         // Tell the reader about our button.
-        message.target.messageManager.sendAsyncMessage(
+        message.target.sendMessageToActor(
           "Reader:AddButton",
-          this._readerButtonData
+          this._readerButtonData,
+          "AboutReader"
         );
         break;
       }
       case "Reader:Clicked-pocket-button": {
         PocketPageAction.pageAction.doCommand(message.target.ownerGlobal);
         break;
       }
     }
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -41,18 +41,18 @@ ChromeUtils.defineModuleGetter(
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ProfileAge",
   "resource://gre/modules/ProfileAge.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
-  "ReaderParent",
-  "resource:///modules/ReaderParent.jsm"
+  "AboutReaderParent",
+  "resource:///actors/AboutReaderParent.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ResetProfile",
   "resource://gre/modules/ResetProfile.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
@@ -657,24 +657,24 @@ var UITour = {
       case "ping": {
         if (typeof data.callbackID == "string") {
           this.sendPageCallback(browser, data.callbackID);
         }
         break;
       }
 
       case "forceShowReaderIcon": {
-        ReaderParent.forceShowReaderIcon(browser);
+        AboutReaderParent.forceShowReaderIcon(browser);
         break;
       }
 
       case "toggleReaderMode": {
         let targetPromise = this.getTarget(window, "readerMode-urlBar");
         targetPromise.then(target => {
-          ReaderParent.toggleReaderMode({ target: target.node });
+          AboutReaderParent.toggleReaderMode({ target: target.node });
         });
         break;
       }
 
       case "closeTab": {
         // Find the <tabbrowser> element of the <browser> for which the event
         // was generated originally. If the browser where the UI tour is loaded
         // is windowless, just ignore the request to close the tab. The request
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -74,19 +74,16 @@ with Files("OpenInTabsUtils.jsm"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
 
 with Files("PermissionUI.jsm"):
    BUG_COMPONENT = ("Firefox", "Site Permissions")
 
 with Files("ProcessHangMonitor.jsm"):
     BUG_COMPONENT = ("Core", "DOM: Content Processes")
 
-with Files("ReaderParent.jsm"):
-    BUG_COMPONENT = ("Toolkit", "Reader Mode")
-
 with Files("Sanitizer.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("SelectionChangedMenulist.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("SiteDataManager.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
@@ -140,17 +137,16 @@ EXTRA_JS_MODULES += [
     'LaterRun.jsm',
     'LiveBookmarkMigrator.jsm',
     'NewTabPagePreloading.jsm',
     'OpenInTabsUtils.jsm',
     'PageActions.jsm',
     'PermissionUI.jsm',
     'PingCentre.jsm',
     'ProcessHangMonitor.jsm',
-    'ReaderParent.jsm',
     'Sanitizer.jsm',
     'SelectionChangedMenulist.jsm',
     'SiteDataManager.jsm',
     'SitePermissions.jsm',
     'TabsList.jsm',
     'TabUnloader.jsm',
     'ThemeVariableMap.jsm',
     'TransientPrefs.jsm',
--- a/toolkit/components/narrate/NarrateControls.jsm
+++ b/toolkit/components/narrate/NarrateControls.jsm
@@ -16,18 +16,17 @@ const { AsyncPrefs } = ChromeUtils.impor
 );
 
 var EXPORTED_SYMBOLS = ["NarrateControls"];
 
 var gStrings = Services.strings.createBundle(
   "chrome://global/locale/narrate.properties"
 );
 
-function NarrateControls(mm, win, languagePromise) {
-  this._mm = mm;
+function NarrateControls(win, languagePromise) {
   this._winRef = Cu.getWeakReference(win);
   this._languagePromise = languagePromise;
 
   win.addEventListener("unload", this);
 
   // Append content style sheet in document head
   let style = win.document.createElement("link");
   style.rel = "stylesheet";
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -42,39 +42,33 @@ var gStrings = Services.strings.createBu
 const zoomOnCtrl =
   Services.prefs.getIntPref("mousewheel.with_control.action", 3) == 3;
 const zoomOnMeta =
   Services.prefs.getIntPref("mousewheel.with_meta.action", 1) == 3;
 
 const gIsFirefoxDesktop =
   Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
 
-var AboutReader = function(mm, win, articlePromise) {
+var AboutReader = function(actor, articlePromise) {
+  let win = actor.contentWindow;
   let url = this._getOriginalUrl(win);
   if (!(url.startsWith("http://") || url.startsWith("https://"))) {
     let errorMsg =
       "Only http:// and https:// URLs can be loaded in about:reader.";
     if (Services.prefs.getBoolPref("reader.errors.includeURLs")) {
       errorMsg += " Tried to load: " + url + ".";
     }
     Cu.reportError(errorMsg);
     win.location.href = "about:blank";
     return;
   }
 
   let doc = win.document;
 
-  this._mm = mm;
-  this._mm.addMessageListener("Reader:CloseDropdown", this);
-  this._mm.addMessageListener("Reader:AddButton", this);
-  this._mm.addMessageListener("Reader:RemoveButton", this);
-  this._mm.addMessageListener("Reader:GetStoredArticleData", this);
-  this._mm.addMessageListener("Reader:ZoomIn", this);
-  this._mm.addMessageListener("Reader:ZoomOut", this);
-  this._mm.addMessageListener("Reader:ResetZoom", this);
+  this._actor = actor;
 
   this._docRef = Cu.getWeakReference(doc);
   this._winRef = Cu.getWeakReference(win);
   this._innerWindowId = win.windowUtils.currentInnerWindowID;
 
   this._article = null;
   this._languagePromise = new Promise(resolve => {
     this._foundLanguage = resolve;
@@ -131,17 +125,17 @@ var AboutReader = function(mm, win, arti
   this._setupButton(
     "close-button",
     this._onReaderClose.bind(this),
     "aboutReader.toolbar.close"
   );
 
   if (gIsFirefoxDesktop) {
     // we're ready for any external setup, send a signal for that.
-    this._mm.sendAsyncMessage("Reader:OnSetup");
+    this._actor.sendAsyncMessage("Reader:OnSetup");
   }
 
   let colorSchemeValues = JSON.parse(
     Services.prefs.getCharPref("reader.color_scheme.values")
   );
   let colorSchemeOptions = colorSchemeValues.map(value => {
     return {
       name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
@@ -188,17 +182,17 @@ var AboutReader = function(mm, win, arti
 
   this._setupFontSizeButtons();
 
   this._setupContentWidthButtons();
 
   this._setupLineHeightButtons();
 
   if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
-    new NarrateControls(mm, win, this._languagePromise);
+    new NarrateControls(win, this._languagePromise);
   }
 
   this._loadArticle();
 
   let dropdown = this._toolbarElement;
 
   let elemL10nMap = {
     ".minus-button": "minus",
@@ -287,36 +281,18 @@ AboutReader.prototype = {
     if (this._toolbarVertical !== undefined) {
       return this._toolbarVertical;
     }
     return (this._toolbarVertical = Services.prefs.getBoolPref(
       "reader.toolbar.vertical"
     ));
   },
 
-  // Provides unique view Id.
-  get viewId() {
-    let _viewId = Cc["@mozilla.org/uuid-generator;1"]
-      .getService(Ci.nsIUUIDGenerator)
-      .generateUUID()
-      .toString();
-    Object.defineProperty(this, "viewId", { value: _viewId });
-
-    return _viewId;
-  },
-
   receiveMessage(message) {
     switch (message.name) {
-      // Triggered by Android user pressing BACK while the banner font-dropdown is open.
-      case "Reader:CloseDropdown": {
-        // Just close it.
-        this._closeDropdowns();
-        break;
-      }
-
       case "Reader:AddButton": {
         if (
           message.data.id &&
           message.data.image &&
           !this._doc.getElementsByClassName(message.data.id)[0]
         ) {
           let btn = this._doc.createElement("button");
           btn.dataset.buttonid = message.data.id;
@@ -329,39 +305,33 @@ AboutReader.prototype = {
             btn.textContent = message.data.text;
           }
           if (message.data.width && message.data.height) {
             btn.style.backgroundSize = `${message.data.width}px ${message.data.height}px`;
           }
           let tb = this._toolbarElement;
           tb.appendChild(btn);
           this._setupButton(message.data.id, button => {
-            this._mm.sendAsyncMessage(
+            this._actor.sendAsyncMessage(
               "Reader:Clicked-" + button.dataset.buttonid,
               { article: this._article }
             );
           });
         }
         break;
       }
       case "Reader:RemoveButton": {
         if (message.data.id) {
           let btn = this._doc.getElementsByClassName(message.data.id)[0];
           if (btn) {
             btn.remove();
           }
         }
         break;
       }
-      case "Reader:GetStoredArticleData": {
-        this._mm.sendAsyncMessage("Reader:StoredArticleData", {
-          article: this._article,
-        });
-        break;
-      }
       case "Reader:ZoomIn": {
         this._changeFontSize(+1);
         break;
       }
       case "Reader:ZoomOut": {
         this._changeFontSize(-1);
         break;
       }
@@ -400,17 +370,16 @@ AboutReader.prototype = {
           // hide the system UI and the "reader-toolbar" only if the dropdown is not opened
           let selector = ".dropdown.open";
           let openDropdowns = this._doc.querySelectorAll(selector);
           if (openDropdowns.length) {
             break;
           }
 
           let isScrollingUp = this._scrollOffset > vv.pageTop;
-          this._setSystemUIVisibility(isScrollingUp);
           this._setToolbarVisibility(isScrollingUp);
         }
 
         this._scrollOffset = vv.pageTop;
         break;
       case "resize":
         this._updateImageMargins();
         if (this._isToolbarVertical) {
@@ -459,45 +428,28 @@ AboutReader.prototype = {
       case "visibilitychange":
         this._handleVisibilityChange();
         break;
 
       case "pagehide":
         // Close the Banners Font-dropdown, cleanup Android BackPressListener.
         this._closeDropdowns();
 
-        this._mm.removeMessageListener("Reader:CloseDropdown", this);
-        this._mm.removeMessageListener("Reader:AddButton", this);
-        this._mm.removeMessageListener("Reader:RemoveButton", this);
-        this._mm.removeMessageListener("Reader:GetStoredArticleData", this);
-        this._mm.removeMessageListener("Reader:ZoomIn", this);
-        this._mm.removeMessageListener("Reader:ZoomOut", this);
-        this._mm.removeMessageListener("Reader:ResetZoom", this);
-        this._windowUnloaded = true;
+        this._actor.readerModeHidden();
+        this.clearActor();
         break;
     }
   },
 
-  observe(subject, topic, data) {
-    if (
-      subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId
-    ) {
-      return;
-    }
-
-    Services.obs.removeObserver(this, "inner-window-destroyed");
-
-    this._mm.removeMessageListener("Reader:CloseDropdown", this);
-    this._mm.removeMessageListener("Reader:AddButton", this);
-    this._mm.removeMessageListener("Reader:RemoveButton", this);
-    this._windowUnloaded = true;
+  clearActor() {
+    this._actor = null;
   },
 
   _onReaderClose() {
-    ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
+    ReaderMode.leaveReaderMode(this._actor.docShell, this._win);
   },
 
   async _resetFontSize() {
     await AsyncPrefs.reset("reader.font_size");
     let currentSize = Services.prefs.getIntPref("reader.font_size");
     this._setFontSize(currentSize);
   },
 
@@ -853,20 +805,16 @@ AboutReader.prototype = {
     }
 
     this._fontType = newFontType;
     bodyClasses.add(this._fontType);
 
     AsyncPrefs.set("reader.font_type", this._fontType);
   },
 
-  _setSystemUIVisibility(visible) {
-    this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible });
-  },
-
   _setToolbarVisibility(visible) {
     let tb = this._toolbarElement;
 
     if (visible) {
       if (tb.style.opacity != "1") {
         tb.removeAttribute("hidden");
         tb.style.opacity = "1";
       }
@@ -888,73 +836,50 @@ AboutReader.prototype = {
     let url = this._getOriginalUrl();
     this._showProgressDelayed();
 
     let article;
     if (this._articlePromise) {
       article = await this._articlePromise;
     } else {
       try {
-        article = await this._getArticle(url);
+        article = await ReaderMode.downloadAndParseDocument(url);
       } catch (e) {
         if (e && e.newURL) {
           let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
           this._win.location.replace(readerURL);
           return;
         }
       }
     }
 
-    if (this._windowUnloaded) {
+    if (!this._actor) {
       return;
     }
 
     // Replace the loading message with an error message if there's a failure.
     // Users are supposed to navigate away by themselves (because we cannot
     // remove ourselves from session history.)
     if (!article) {
       this._showError();
       return;
     }
 
     this._showContent(article);
   },
 
-  _getArticle(url) {
-    if (this.PLATFORM_HAS_CACHE) {
-      return new Promise((resolve, reject) => {
-        let listener = message => {
-          this._mm.removeMessageListener("Reader:ArticleData", listener);
-          if (message.data.newURL) {
-            reject({ newURL: message.data.newURL });
-            return;
-          }
-          resolve(message.data.article);
-        };
-        this._mm.addMessageListener("Reader:ArticleData", listener);
-        this._mm.sendAsyncMessage("Reader:ArticleGet", { url });
-      });
-    }
-    return ReaderMode.downloadAndParseDocument(url);
-  },
-
-  _requestFavicon() {
-    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", {
+  async _requestFavicon() {
+    let iconDetails = await this._actor.sendQuery("Reader:FaviconRequest", {
       url: this._article.url,
       preferredWidth: 16 * this._win.devicePixelRatio,
     });
+
+    if (iconDetails) {
+      this._loadFavicon(iconDetails.url, iconDetails.faviconUrl);
+    }
   },
 
   _loadFavicon(url, faviconUrl) {
     if (this._article.url !== url) {
       return;
     }
 
     let doc = this._doc;
@@ -1129,17 +1054,17 @@ AboutReader.prototype = {
     this._contentElement.classList.remove("reader-show-element");
   },
 
   _showProgressDelayed() {
     this._win.setTimeout(() => {
       // No need to show progress if the article has been loaded,
       // if the window has been unloaded, or if there was an error
       // trying to load the article.
-      if (this._article || this._windowUnloaded || this._error) {
+      if (this._article || !this._actor || this._error) {
         return;
       }
 
       this._headerElement.classList.remove("reader-show-element");
       this._contentElement.classList.remove("reader-show-element");
 
       this._messageElement.textContent = gStrings.GetStringFromName(
         "aboutReader.loading2"
@@ -1295,17 +1220,16 @@ AboutReader.prototype = {
     if (dropdown.classList.contains("open")) {
       return;
     }
 
     this._closeDropdowns();
 
     // Trigger BackPressListener initialization in Android.
     dropdown.classList.add("open");
-    this._mm.sendAsyncMessage("Reader:DropdownOpened", this.viewId);
   },
 
   /*
    * If the ReaderView has open dropdowns, close them. If we are closing the
    * dropdowns because the page is scrolling, allow popups to stay open with
    * the keep-open class.
    */
   _closeDropdowns(scrolling) {
@@ -1313,21 +1237,16 @@ AboutReader.prototype = {
     if (scrolling) {
       selector += ":not(.keep-open)";
     }
 
     let openDropdowns = this._doc.querySelectorAll(selector);
     for (let dropdown of openDropdowns) {
       dropdown.classList.remove("open");
     }
-
-    // Trigger BackPressListener cleanup in Android.
-    if (openDropdowns.length) {
-      this._mm.sendAsyncMessage("Reader:DropdownClosed", this.viewId);
-    }
   },
 
   /*
    * Scroll reader view to a reference
    */
   _goToReference(ref) {
     if (ref) {
       this._win.location.hash = ref;
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -5,17 +5,16 @@
 <!DOCTYPE html>
 <html>
 
 <head>
   <meta http-equiv="Content-Security-Policy" content="default-src chrome:; img-src data: *; media-src *; object-src 'none'" />
   <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 src="chrome://global/content/reader/aboutReader.js"></script>
 </head>
 
 <body>
   <div class="container">
     <div class="header reader-header">
       <a class="domain reader-domain"></a>
       <div class="domain-border"></div>
       <h1 class="reader-title"></h1>
deleted file mode 100644
--- a/toolkit/components/reader/content/aboutReader.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* 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";
-
-window.addEventListener("DOMContentLoaded", function() {
-  document.dispatchEvent(
-    new CustomEvent("AboutReaderContentLoaded", { bubbles: true })
-  );
-});
--- a/toolkit/components/reader/jar.mn
+++ b/toolkit/components/reader/jar.mn
@@ -1,7 +1,6 @@
 # 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/.
 
 toolkit.jar:
   content/global/reader/aboutReader.html             (content/aboutReader.html)
-  content/global/reader/aboutReader.js               (content/aboutReader.js)
--- a/toolkit/components/reader/test/browser.ini
+++ b/toolkit/components/reader/test/browser.ini
@@ -3,16 +3,20 @@ support-files = head.js
 [browser_readerMode.js]
 support-files =
   readerModeNonArticle.html
   readerModeArticle.html
   readerModeArticleHiddenNodes.html
 [browser_readerMode_hidden_nodes.js]
 support-files =
   readerModeArticleHiddenNodes.html
+[browser_readerMode_pocket.js]
+support-files =
+  readerModeArticleShort.html
+  readerModeArticleMedium.html
 [browser_readerMode_with_anchor.js]
 skip-if = fission # Bug 1616805
 support-files =
   readerModeArticle.html
 [browser_bug1124271_readerModePinnedTab.js]
 support-files =
   readerModeArticle.html
 [browser_bug1453818_samesite_cookie.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/reader/test/browser_readerMode_pocket.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// This test verifies that the Save To Pocket button appears in reader mode,
+// and is toggled hidden and visible when pocket is disabled and enabled.
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+  "chrome://mochitests/content",
+  "http://example.com"
+);
+
+async function getPocketButtonsCount(browser) {
+  return SpecialPowers.spawn(browser, [], () => {
+    return content.document.getElementsByClassName("pocket-button").length;
+  });
+}
+
+add_task(async function() {
+  // set the pocket preference before beginning.
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.pocket.enabled", true]],
+  });
+
+  var readerButton = document.getElementById("reader-mode-button");
+
+  let tab1 = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    TEST_PATH + "readerModeArticleShort.html"
+  );
+
+  let promiseTabLoad = promiseTabLoadEvent(tab1);
+  readerButton.click();
+  await promiseTabLoad;
+
+  let tab2 = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    TEST_PATH + "readerModeArticleMedium.html"
+  );
+
+  promiseTabLoad = promiseTabLoadEvent(tab2);
+  readerButton.click();
+  await promiseTabLoad;
+
+  is(
+    await getPocketButtonsCount(tab1.linkedBrowser),
+    1,
+    "tab 1 has a pocket button"
+  );
+  is(
+    await getPocketButtonsCount(tab1.linkedBrowser),
+    1,
+    "tab 2 has a pocket button"
+  );
+
+  // Turn off the pocket preference. The Save To Pocket buttons should disappear.
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.pocket.enabled", false]],
+  });
+
+  is(
+    await getPocketButtonsCount(tab1.linkedBrowser),
+    0,
+    "tab 1 has no pocket button"
+  );
+  is(
+    await getPocketButtonsCount(tab1.linkedBrowser),
+    0,
+    "tab 2 has no pocket button"
+  );
+
+  // Turn on the pocket preference. The Save To Pocket buttons should reappear again.
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.pocket.enabled", true]],
+  });
+
+  is(
+    await getPocketButtonsCount(tab1.linkedBrowser),
+    1,
+    "tab 1 has a pocket button again"
+  );
+  is(
+    await getPocketButtonsCount(tab1.linkedBrowser),
+    1,
+    "tab 2 has a pocket button again"
+  );
+
+  BrowserTestUtils.removeTab(tab1);
+  BrowserTestUtils.removeTab(tab2);
+});