Backed out 13 changesets (bug 1530402, bug 1533156) for failing test_ext_tabs_query.html on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Fri, 15 Nov 2019 05:48:23 +0200
changeset 502110 17db3abeba1a0ee39f3887279ff770c5f04f6313
parent 502109 0ba862a839d190ce4c443e4db2f5788aeeea97ad
child 502111 409ebe45687765c849936626fbc7d95fcfc17f6d
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1530402, 1533156
milestone72.0a1
backs outaa9499a196e3aca565d76ad4a65f16ef9377f3d0
ce12e494a189350a349aa98af22a561992fec846
1b053a203af963985d4d10492f6d9a11ccc56056
f57bc0ee284b453b2f619cb6e735f0c69512ea12
701de7bcb4840b7bdd70dbf3ecb44ffc46e0d2d8
ab7b21969769b3296eb6de6e67d988d6792cf71a
c7b8cc91f2454e9e5fec2355274031afd87a5c80
e2be10dbad5e70ac4c2cc993fac38ee1d921c7ba
f52a8fedcded6e24a7e04bd5873b9f85db62f920
4fcca36d5b3e81bdd232540daa12987200dae5ee
82b28001164b902bab92f653f875f2487a6e06c3
1dc6a4ebc9e1373385e3b2d703b9319ca4694a10
ae5ead19921d90b992876fc1ef06c4ef81adf8ac
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
Backed out 13 changesets (bug 1530402, bug 1533156) for failing test_ext_tabs_query.html on a CLOSED TREE Backed out changeset aa9499a196e3 (bug 1530402) Backed out changeset ce12e494a189 (bug 1530402) Backed out changeset 1b053a203af9 (bug 1530402) Backed out changeset f57bc0ee284b (bug 1530402) Backed out changeset 701de7bcb484 (bug 1530402) Backed out changeset ab7b21969769 (bug 1530402) Backed out changeset c7b8cc91f245 (bug 1530402) Backed out changeset e2be10dbad5e (bug 1530402) Backed out changeset f52a8fedcded (bug 1530402) Backed out changeset 4fcca36d5b3e (bug 1530402) Backed out changeset 82b28001164b (bug 1530402) Backed out changeset 1dc6a4ebc9e1 (bug 1533156) Backed out changeset ae5ead19921d (bug 1530402)
browser/components/extensions/ext-browser.json
browser/components/extensions/parent/ext-browserAction.js
browser/components/extensions/parent/ext-pageAction.js
browser/components/extensions/schemas/browser_action.json
browser/components/extensions/schemas/jar.mn
browser/components/extensions/schemas/page_action.json
browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js
image/imgITools.idl
image/imgTools.cpp
mobile/android/chrome/geckoview/geckoview.js
mobile/android/components/extensions/ext-android.js
mobile/android/components/extensions/ext-browserAction.js
mobile/android/components/extensions/ext-pageAction.js
mobile/android/components/extensions/ext-utils.js
mobile/android/components/extensions/schemas/browser_action.json
mobile/android/components/extensions/schemas/jar.mn
mobile/android/components/extensions/schemas/page_action.json
mobile/android/components/geckoview/GeckoViewStartup.js
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/background.js
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/expected.png
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/geo-19.png
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/geo-38.png
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/icon.svg
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/content.js
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/manifest.json
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-browser-action.html
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-browser-action.js
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-page-action.html
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-page-action.js
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-popup.html
mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify/icons/icon.svg
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ExtensionActionTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionSettings.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ImageDecoder.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionEventDispatcher.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml
mobile/android/geckoview_example/src/main/res/layout/browser_action.xml
mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml
mobile/android/geckoview_example/src/main/res/values/colors.xml
mobile/android/geckoview_example/src/main/res/values/ids.xml
mobile/android/modules/BrowserActions.jsm
mobile/android/modules/PageActions.jsm
mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
mobile/android/modules/moz.build
toolkit/components/extensions/ExtensionActions.jsm
toolkit/components/extensions/moz.build
toolkit/components/extensions/schemas/browser_action.json
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/page_action.json
toolkit/components/moz.build
widget/android/ImageDecoderSupport.cpp
widget/android/ImageDecoderSupport.h
widget/android/bindings/JavaExceptions-classes.txt
widget/android/moz.build
widget/android/nsAppShell.cpp
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -4,17 +4,17 @@
     "schema": "chrome://browser/content/schemas/bookmarks.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["bookmarks"]
     ]
   },
   "browserAction": {
     "url": "chrome://browser/content/parent/ext-browserAction.js",
-    "schema": "chrome://extensions/content/schemas/browser_action.json",
+    "schema": "chrome://browser/content/schemas/browser_action.json",
     "scopes": ["addon_parent"],
     "manifest": ["browser_action"],
     "paths": [
       ["browserAction"]
     ]
   },
   "browsingData": {
     "url": "chrome://browser/content/parent/ext-browsingData.js",
@@ -135,17 +135,17 @@
     "scopes": ["addon_parent"],
     "manifest": ["omnibox"],
     "paths": [
       ["omnibox"]
     ]
   },
   "pageAction": {
     "url": "chrome://browser/content/parent/ext-pageAction.js",
-    "schema": "chrome://extensions/content/schemas/page_action.json",
+    "schema": "chrome://browser/content/schemas/page_action.json",
     "scopes": ["addon_parent"],
     "manifest": ["page_action"],
     "paths": [
       ["pageAction"]
     ]
   },
   "pkcs11": {
     "url": "chrome://browser/content/parent/ext-pkcs11.js",
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -27,140 +27,141 @@ ChromeUtils.defineModuleGetter(
   "resource://gre/modules/Timer.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ViewPopup",
   "resource:///modules/ExtensionPopups.jsm"
 );
 
-var { DefaultWeakMap } = ExtensionUtils;
+var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
 
 var { ExtensionParent } = ChromeUtils.import(
   "resource://gre/modules/ExtensionParent.jsm"
 );
-var { BrowserActionBase } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionActions.jsm"
-);
 
 var { IconDetails, StartupCache } = ExtensionParent;
 
+XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
+
 const POPUP_PRELOAD_TIMEOUT_MS = 200;
 
 // WeakMap[Extension -> BrowserAction]
 const browserActionMap = new WeakMap();
 
 XPCOMUtils.defineLazyGetter(this, "browserAreas", () => {
   return {
     navbar: CustomizableUI.AREA_NAVBAR,
     menupanel: CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
     tabstrip: CustomizableUI.AREA_TABSTRIP,
     personaltoolbar: CustomizableUI.AREA_BOOKMARKS,
   };
 });
 
-class BrowserAction extends BrowserActionBase {
-  constructor(extension, buttonDelegate) {
-    let tabContext = new TabContext(target => {
-      let window = target.ownerGlobal;
-      if (target === window) {
-        return this.getContextData(null);
-      }
-      return tabContext.get(window);
-    });
-    super(tabContext, extension);
-    this.buttonDelegate = buttonDelegate;
-  }
-
-  updateOnChange(target) {
-    if (target) {
-      let window = target.ownerGlobal;
-      if (target === window || target.selected) {
-        this.buttonDelegate.updateWindow(window);
-      }
-    } else {
-      for (let window of windowTracker.browserWindows()) {
-        this.buttonDelegate.updateWindow(window);
-      }
-    }
-  }
-
-  getTab(tabId) {
-    if (tabId !== null) {
-      return tabTracker.getTab(tabId);
-    }
-    return null;
-  }
-
-  getWindow(windowId) {
-    if (windowId !== null) {
-      return windowTracker.getWindow(windowId);
-    }
-    return null;
-  }
-}
-
 this.browserAction = class extends ExtensionAPI {
   static for(extension) {
     return browserActionMap.get(extension);
   }
 
   async onManifestEntry(entryName) {
     let { extension } = this;
 
     let options = extension.manifest.browser_action;
 
-    this.action = new BrowserAction(extension, this);
-    await this.action.loadIconData();
-
     this.iconData = new DefaultWeakMap(icons => this.getIconData(icons));
-    this.iconData.set(
-      this.action.getIcon(),
-      await StartupCache.get(
-        extension,
-        ["browserAction", "default_icon_data"],
-        () => this.getIconData(this.action.getIcon())
-      )
-    );
 
     let widgetId = makeWidgetId(extension.id);
     this.id = `${widgetId}-browser-action`;
     this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
     this.widget = null;
 
     this.pendingPopup = null;
     this.pendingPopupTimeout = null;
     this.eventQueue = [];
 
     this.tabManager = extension.tabManager;
+
+    this.defaults = {
+      enabled: true,
+      title: options.default_title || extension.name,
+      badgeText: "",
+      badgeBackgroundColor: [0xd9, 0, 0, 255],
+      badgeDefaultColor: [255, 255, 255, 255],
+      badgeTextColor: null,
+      popup: options.default_popup || "",
+      area: browserAreas[options.default_area || "navbar"],
+    };
+    this.globals = Object.create(this.defaults);
+
     this.browserStyle = options.browser_style;
 
     browserActionMap.set(extension, this);
 
+    this.defaults.icon = await StartupCache.get(
+      extension,
+      ["browserAction", "default_icon"],
+      () =>
+        IconDetails.normalize(
+          {
+            path: options.default_icon || extension.manifest.icons,
+            iconType: "browserAction",
+            themeIcons: options.theme_icons,
+          },
+          extension
+        )
+    );
+
+    this.iconData.set(
+      this.defaults.icon,
+      await StartupCache.get(
+        extension,
+        ["browserAction", "default_icon_data"],
+        () => this.getIconData(this.defaults.icon)
+      )
+    );
+
+    this.tabContext = new TabContext(target => {
+      let window = target.ownerGlobal;
+      if (target === window) {
+        return this.globals;
+      }
+      return this.tabContext.get(window);
+    });
+
+    // eslint-disable-next-line mozilla/balanced-listeners
+    this.tabContext.on("location-change", this.handleLocationChange.bind(this));
+
     this.build();
   }
 
+  handleLocationChange(eventType, tab, fromBrowse) {
+    if (fromBrowse) {
+      this.tabContext.clear(tab);
+      this.updateOnChange(tab);
+    }
+  }
+
   onShutdown() {
     browserActionMap.delete(this.extension);
-    this.action.onShutdown();
 
+    this.tabContext.shutdown();
     CustomizableUI.destroyWidget(this.id);
 
     this.clearPopup();
   }
 
   build() {
     let widget = CustomizableUI.createWidget({
       id: this.id,
       viewId: this.viewId,
       type: "view",
       removable: true,
-      label: this.action.getProperty(null, "title"),
-      tooltiptext: this.action.getProperty(null, "title"),
-      defaultArea: browserAreas[this.action.getDefaultArea()],
+      label: this.defaults.title || this.extension.name,
+      tooltiptext: this.defaults.title || "",
+      defaultArea: this.defaults.area,
       showInPrivateBrowsing: this.extension.privateBrowsingAllowed,
 
       // Don't attempt to load properties from the built-in widget string
       // bundle.
       localized: false,
 
       onBeforeCreated: document => {
         let view = document.createXULElement("panelview");
@@ -196,17 +197,17 @@ this.browserAction = class extends Exten
         node.setAttribute("constrain-size", "true");
         node.setAttribute("data-extensionid", this.extension.id);
 
         node.onmousedown = event => this.handleEvent(event);
         node.onmouseover = event => this.handleEvent(event);
         node.onmouseout = event => this.handleEvent(event);
         node.onauxclick = event => this.handleEvent(event);
 
-        this.updateButton(node, this.action.getContextData(null), true);
+        this.updateButton(node, this.globals, true);
       },
 
       onBeforeCommand: event => {
         this.lastClickInfo = {
           button: event.button || 0,
           modifiers: clickModifiersFromEvent(event),
         };
       },
@@ -217,17 +218,17 @@ this.browserAction = class extends Exten
         ExtensionTelemetry.browserActionPopupOpen.stopwatchStart(
           extension,
           this
         );
         let document = event.target.ownerDocument;
         let tabbrowser = document.defaultView.gBrowser;
 
         let tab = tabbrowser.selectedTab;
-        let popupURL = this.action.getProperty(tab, "popup");
+        let popupURL = this.getProperty(tab, "popup");
         this.tabManager.addActiveTabPermission(tab);
 
         // Popups are shown only if a popup URL is defined; otherwise
         // a "click" event is dispatched. This is done for compatibility with the
         // Google Chrome onClicked extension API.
         if (popupURL) {
           try {
             let popup = this.getPopup(document.defaultView, popupURL);
@@ -263,16 +264,21 @@ this.browserAction = class extends Exten
           event.preventDefault();
           this.emit("click", tabbrowser.selectedBrowser);
           // Ensure we close any popups this node was in:
           CustomizableUI.hidePanelForNode(event.target);
         }
       },
     });
 
+    // eslint-disable-next-line mozilla/balanced-listeners
+    this.tabContext.on("tab-select", (evt, tab) => {
+      this.updateWindow(tab.ownerGlobal);
+    });
+
     this.widget = widget;
   }
 
   /**
    * Triggers this browser action for the given window, with the same effects as
    * if it were clicked by a user.
    *
    * This has no effect if the browser action is disabled for, or not
@@ -285,24 +291,24 @@ this.browserAction = class extends Exten
     if (!this.pendingPopup && popup) {
       popup.closePopup();
       return;
     }
 
     let widget = this.widget.forWindow(window);
     let tab = window.gBrowser.selectedTab;
 
-    if (!widget.node || !this.action.getProperty(tab, "enabled")) {
+    if (!widget.node || !this.getProperty(tab, "enabled")) {
       return;
     }
 
     // Popups are shown only if a popup URL is defined; otherwise
     // a "click" event is dispatched. This is done for compatibility with the
     // Google Chrome onClicked extension API.
-    if (this.action.getProperty(tab, "popup")) {
+    if (this.getProperty(tab, "popup")) {
       if (this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
         await window.document.getElementById("nav-bar").overflowable.show();
       }
 
       let event = new window.CustomEvent("command", {
         bubbles: true,
         cancelable: true,
       });
@@ -319,18 +325,18 @@ this.browserAction = class extends Exten
     let window = button.ownerGlobal;
 
     switch (event.type) {
       case "mousedown":
         if (event.button == 0) {
           // Begin pre-loading the browser for the popup, so it's more likely to
           // be ready by the time we get a complete click.
           let tab = window.gBrowser.selectedTab;
-          let popupURL = this.action.getProperty(tab, "popup");
-          let enabled = this.action.getProperty(tab, "enabled");
+          let popupURL = this.getProperty(tab, "popup");
+          let enabled = this.getProperty(tab, "enabled");
 
           if (
             popupURL &&
             enabled &&
             (this.pendingPopup || !ViewPopup.for(this.extension, window))
           ) {
             this.eventQueue.push("Mousedown");
             // Add permission for the active tab so it will exist for the popup.
@@ -366,18 +372,18 @@ this.browserAction = class extends Exten
           }
         }
         break;
 
       case "mouseover": {
         // Begin pre-loading the browser for the popup, so it's more likely to
         // be ready by the time we get a complete click.
         let tab = window.gBrowser.selectedTab;
-        let popupURL = this.action.getProperty(tab, "popup");
-        let enabled = this.action.getProperty(tab, "enabled");
+        let popupURL = this.getProperty(tab, "popup");
+        let enabled = this.getProperty(tab, "enabled");
 
         if (
           popupURL &&
           enabled &&
           (this.pendingPopup || !ViewPopup.for(this.extension, window))
         ) {
           this.eventQueue.push("Hover");
           this.pendingPopup = this.getPopup(window, popupURL, true);
@@ -417,17 +423,17 @@ this.browserAction = class extends Exten
         break;
 
       case "auxclick":
         if (event.button !== 1) {
           return;
         }
 
         let { gBrowser } = window;
-        if (this.action.getProperty(gBrowser.selectedTab, "enabled")) {
+        if (this.getProperty(gBrowser.selectedTab, "enabled")) {
           this.lastClickInfo = {
             button: 1,
             modifiers: clickModifiersFromEvent(event),
           };
 
           this.emit("click", gBrowser.selectedBrowser);
           // Ensure we close any popups this node was in:
           CustomizableUI.hidePanelForNode(event.target);
@@ -536,17 +542,17 @@ this.browserAction = class extends Exten
       }
 
       let serializeColor = ([r, g, b, a]) =>
         `rgba(${r}, ${g}, ${b}, ${a / 255})`;
       node.setAttribute(
         "badgeStyle",
         [
           `background-color: ${serializeColor(tabData.badgeBackgroundColor)}`,
-          `color: ${serializeColor(this.action.getTextColor(tabData))}`,
+          `color: ${serializeColor(this.getTextColor(tabData))}`,
         ].join("; ")
       );
 
       let style = this.iconData.get(tabData.icon);
       node.setAttribute("style", style);
     };
     if (sync) {
       callback();
@@ -586,51 +592,318 @@ this.browserAction = class extends Exten
    *
    * @param {ChromeWindow} window
    *        Browser chrome window.
    */
   updateWindow(window) {
     let node = this.widget.forWindow(window).node;
     if (node) {
       let tab = window.gBrowser.selectedTab;
-      this.updateButton(node, this.action.getContextData(tab));
+      this.updateButton(node, this.tabContext.get(tab));
+    }
+  }
+
+  /**
+   * Update the toolbar button when the extension changes the icon, title, url, etc.
+   * If it only changes a parameter for a single tab, `target` will be that tab.
+   * If it only changes a parameter for a single window, `target` will be that window.
+   * Otherwise `target` will be null.
+   *
+   * @param {XULElement|ChromeWindow|null} target
+   *        Browser tab or browser chrome window, may be null.
+   */
+  updateOnChange(target) {
+    if (target) {
+      let window = target.ownerGlobal;
+      if (target === window || target.selected) {
+        this.updateWindow(window);
+      }
+    } else {
+      for (let window of windowTracker.browserWindows()) {
+        this.updateWindow(window);
+      }
+    }
+  }
+
+  /**
+   * Gets the target object corresponding to the `details` parameter of the various
+   * get* and set* API methods.
+   *
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
+   * @throws if both `tabId` and `windowId` are specified, or if they are invalid.
+   * @returns {XULElement|ChromeWindow|null}
+   *        If a `tabId` was specified, the corresponding XULElement tab.
+   *        If a `windowId` was specified, the corresponding ChromeWindow.
+   *        Otherwise, `null`.
+   */
+  getTargetFromDetails({ tabId, windowId }) {
+    if (tabId != null && windowId != null) {
+      throw new ExtensionError(
+        "Only one of tabId and windowId can be specified."
+      );
+    }
+    if (tabId != null) {
+      return tabTracker.getTab(tabId);
+    } else if (windowId != null) {
+      return windowTracker.getWindow(windowId);
+    }
+    return null;
+  }
+
+  /**
+   * Gets the data associated with a tab, window, or the global one.
+   *
+   * @param {XULElement|ChromeWindow|null} target
+   *        A XULElement tab, a ChromeWindow, or null for the global data.
+   * @returns {Object}
+   *        The icon, title, badge, etc. associated with the target.
+   */
+  getContextData(target) {
+    if (target) {
+      return this.tabContext.get(target);
     }
+    return this.globals;
+  }
+
+  /**
+   * Set a global, window specific or tab specific property.
+   *
+   * @param {XULElement|ChromeWindow|null} target
+   *        A XULElement tab, a ChromeWindow, or null for the global data.
+   * @param {string} prop
+   *        String property to set. Should should be one of "icon", "title", "badgeText",
+   *        "popup", "badgeBackgroundColor", "badgeTextColor" or "enabled".
+   * @param {string} value
+   *        Value for prop.
+   * @returns {Object}
+   *        The object to which the property has been set.
+   */
+  setProperty(target, prop, value) {
+    let values = this.getContextData(target);
+    if (value === null) {
+      delete values[prop];
+    } else {
+      values[prop] = value;
+    }
+
+    this.updateOnChange(target);
+    return values;
+  }
+
+  /**
+   * Retrieve the value of a global, window specific or tab specific property.
+   *
+   * @param {XULElement|ChromeWindow|null} target
+   *        A XULElement tab, a ChromeWindow, or null for the global data.
+   * @param {string} prop
+   *        String property to retrieve. Should should be one of "icon", "title",
+   *        "badgeText", "popup", "badgeBackgroundColor" or "enabled".
+   * @returns {string} value
+   *          Value of prop.
+   */
+  getProperty(target, prop) {
+    return this.getContextData(target)[prop];
+  }
+
+  setPropertyFromDetails(details, prop, value) {
+    return this.setProperty(this.getTargetFromDetails(details), prop, value);
+  }
+
+  getPropertyFromDetails(details, prop) {
+    return this.getProperty(this.getTargetFromDetails(details), prop);
+  }
+
+  /**
+   * Determines the text badge color to be used in a tab, window, or globally.
+   *
+   * @param {Object} values
+   *        The values associated with the tab or window, or global values.
+   * @returns {ColorArray}
+   */
+  getTextColor(values) {
+    // If a text color has been explicitly provided, use it.
+    let { badgeTextColor } = values;
+    if (badgeTextColor) {
+      return badgeTextColor;
+    }
+
+    // Otherwise, check if the default color to be used has been cached previously.
+    let { badgeDefaultColor } = values;
+    if (badgeDefaultColor) {
+      return badgeDefaultColor;
+    }
+
+    // Choose a color among white and black, maximizing contrast with background
+    // according to https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-procedure
+    let [r, g, b] = values.badgeBackgroundColor
+      .slice(0, 3)
+      .map(function(channel) {
+        channel /= 255;
+        if (channel <= 0.03928) {
+          return channel / 12.92;
+        }
+        return ((channel + 0.055) / 1.055) ** 2.4;
+      });
+    let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+
+    // The luminance is 0 for black, 1 for white, and `lum` for the background color.
+    // Since `0 <= lum`, the contrast ratio for black is `c0 = (lum + 0.05) / 0.05`.
+    // Since `lum <= 1`, the contrast ratio for white is `c1 = 1.05 / (lum + 0.05)`.
+    // We want to maximize contrast, so black is chosen if `c1 < c0`, that is, if
+    // `1.05 * 0.05 < (L + 0.05) ** 2`. Otherwise white is chosen.
+    let channel = 1.05 * 0.05 < (lum + 0.05) ** 2 ? 0 : 255;
+    let result = [channel, channel, channel, 255];
+
+    // Cache the result as high as possible in the prototype chain
+    while (!Object.getOwnPropertyDescriptor(values, "badgeDefaultColor")) {
+      values = Object.getPrototypeOf(values);
+    }
+    values.badgeDefaultColor = result;
+    return result;
   }
 
   getAPI(context) {
     let { extension } = context;
     let { tabManager } = extension;
-    let { action } = this;
+
+    let browserAction = this;
+
+    function parseColor(color, kind) {
+      if (typeof color == "string") {
+        let rgba = InspectorUtils.colorToRGBA(color);
+        if (!rgba) {
+          throw new ExtensionError(`Invalid badge ${kind} color: "${color}"`);
+        }
+        color = [rgba.r, rgba.g, rgba.b, Math.round(rgba.a * 255)];
+      }
+      return color;
+    }
 
     return {
       browserAction: {
-        ...action.api(context),
-
         onClicked: new EventManager({
           context,
           name: "browserAction.onClicked",
           inputHandling: true,
           register: fire => {
             let listener = (event, browser) => {
               context.withPendingBrowser(browser, () =>
                 fire.sync(
                   tabManager.convert(tabTracker.activeTab),
-                  this.lastClickInfo
+                  browserAction.lastClickInfo
                 )
               );
             };
-            this.on("click", listener);
+            browserAction.on("click", listener);
             return () => {
-              this.off("click", listener);
+              browserAction.off("click", listener);
             };
           },
         }).api(),
 
-        openPopup: () => {
+        enable: function(tabId) {
+          browserAction.setPropertyFromDetails({ tabId }, "enabled", true);
+        },
+
+        disable: function(tabId) {
+          browserAction.setPropertyFromDetails({ tabId }, "enabled", false);
+        },
+
+        isEnabled: function(details) {
+          return browserAction.getPropertyFromDetails(details, "enabled");
+        },
+
+        setTitle: function(details) {
+          browserAction.setPropertyFromDetails(details, "title", details.title);
+        },
+
+        getTitle: function(details) {
+          return browserAction.getPropertyFromDetails(details, "title");
+        },
+
+        setIcon: function(details) {
+          details.iconType = "browserAction";
+
+          let icon = IconDetails.normalize(details, extension, context);
+          if (!Object.keys(icon).length) {
+            icon = null;
+          }
+          browserAction.setPropertyFromDetails(details, "icon", icon);
+        },
+
+        setBadgeText: function(details) {
+          browserAction.setPropertyFromDetails(
+            details,
+            "badgeText",
+            details.text
+          );
+        },
+
+        getBadgeText: function(details) {
+          return browserAction.getPropertyFromDetails(details, "badgeText");
+        },
+
+        setPopup: function(details) {
+          // Note: Chrome resolves arguments to setIcon relative to the calling
+          // context, but resolves arguments to setPopup relative to the extension
+          // root.
+          // For internal consistency, we currently resolve both relative to the
+          // calling context.
+          let url = details.popup && context.uri.resolve(details.popup);
+          if (url && !context.checkLoadURL(url)) {
+            return Promise.reject({ message: `Access denied for URL ${url}` });
+          }
+          browserAction.setPropertyFromDetails(details, "popup", url);
+        },
+
+        getPopup: function(details) {
+          return browserAction.getPropertyFromDetails(details, "popup");
+        },
+
+        setBadgeBackgroundColor: function(details) {
+          let color = parseColor(details.color, "background");
+          let values = browserAction.setPropertyFromDetails(
+            details,
+            "badgeBackgroundColor",
+            color
+          );
+          if (color === null) {
+            // Let the default text color inherit after removing background color
+            delete values.badgeDefaultColor;
+          } else {
+            // Invalidate a cached default color calculated with the old background
+            values.badgeDefaultColor = null;
+          }
+        },
+
+        getBadgeBackgroundColor: function(details, callback) {
+          return browserAction.getPropertyFromDetails(
+            details,
+            "badgeBackgroundColor"
+          );
+        },
+
+        setBadgeTextColor: function(details) {
+          let color = parseColor(details.color, "text");
+          browserAction.setPropertyFromDetails(
+            details,
+            "badgeTextColor",
+            color
+          );
+        },
+
+        getBadgeTextColor: function(details) {
+          let target = browserAction.getTargetFromDetails(details);
+          let values = browserAction.getContextData(target);
+          return browserAction.getTextColor(values);
+        },
+
+        openPopup: function() {
           let window = windowTracker.topWindow;
-          this.triggerAction(window);
+          browserAction.triggerAction(window);
         },
       },
     };
   }
 };
 
 global.browserActionFor = this.browserAction.for;
--- a/browser/components/extensions/parent/ext-pageAction.js
+++ b/browser/components/extensions/parent/ext-pageAction.js
@@ -17,65 +17,87 @@ ChromeUtils.defineModuleGetter(
   "resource:///modules/PageActions.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "PanelPopup",
   "resource:///modules/ExtensionPopups.jsm"
 );
 
-var { DefaultWeakMap } = ExtensionUtils;
+var { ExtensionParent } = ChromeUtils.import(
+  "resource://gre/modules/ExtensionParent.jsm"
+);
 
-var { PageActionBase } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionActions.jsm"
-);
+var { IconDetails, StartupCache } = ExtensionParent;
+
+var { DefaultWeakMap } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
 let pageActionMap = new WeakMap();
 
-class PageAction extends PageActionBase {
-  constructor(extension, buttonDelegate) {
-    let tabContext = new TabContext(tab => this.getContextData(null));
-    super(tabContext, extension);
-    this.buttonDelegate = buttonDelegate;
-  }
-
-  updateOnChange(target) {
-    this.buttonDelegate.updateButton(target.ownerGlobal);
-  }
-
-  getTab(tabId) {
-    if (tabId !== null) {
-      return tabTracker.getTab(tabId);
-    }
-    return null;
-  }
-}
-
 this.pageAction = class extends ExtensionAPI {
   static for(extension) {
     return pageActionMap.get(extension);
   }
 
   async onManifestEntry(entryName) {
     let { extension } = this;
     let options = extension.manifest.page_action;
 
-    this.action = new PageAction(extension, this);
-    await this.action.loadIconData();
-
     let widgetId = makeWidgetId(extension.id);
     this.id = widgetId + "-page-action";
 
     this.tabManager = extension.tabManager;
 
+    // `show` can have three different values:
+    // - `false`. This means the page action is not shown.
+    //   It's set as default if show_matches is empty. Can also be set in a tab via
+    //   `pageAction.hide(tabId)`, e.g. in order to override show_matches.
+    // - `true`. This means the page action is shown.
+    //   It's never set as default because <all_urls> doesn't really match all URLs
+    //   (e.g. "about:" URLs). But can be set in a tab via `pageAction.show(tabId)`.
+    // - `undefined`.
+    //   This is the default value when there are some patterns in show_matches.
+    //   Can't be set as a tab-specific value.
+    let show, showMatches, hideMatches;
+    let show_matches = options.show_matches || [];
+    let hide_matches = options.hide_matches || [];
+    if (!show_matches.length) {
+      // Always hide by default. No need to do any pattern matching.
+      show = false;
+    } else {
+      // Might show or hide depending on the URL. Enable pattern matching.
+      const { restrictSchemes } = extension;
+      showMatches = new MatchPatternSet(show_matches, { restrictSchemes });
+      hideMatches = new MatchPatternSet(hide_matches, { restrictSchemes });
+    }
+
+    this.defaults = {
+      show,
+      showMatches,
+      hideMatches,
+      title: options.default_title || extension.name,
+      popup: options.default_popup || "",
+      pinned: options.pinned,
+    };
+
     this.browserStyle = options.browser_style;
 
+    this.tabContext = new TabContext(tab => this.defaults);
+
+    this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
+
     pageActionMap.set(extension, this);
 
+    this.defaults.icon = await StartupCache.get(
+      extension,
+      ["pageAction", "default_icon"],
+      () => this.normalize({ path: options.default_icon || "" })
+    );
+
     this.lastValues = new DefaultWeakMap(() => ({}));
 
     if (!this.browserPageAction) {
       let onPlacedHandler = (buttonNode, isPanel) => {
         // eslint-disable-next-line mozilla/balanced-listeners
         buttonNode.addEventListener("auxclick", event => {
           if (event.button !== 1 || event.target.disabled) {
             return;
@@ -95,20 +117,20 @@ this.pageAction = class extends Extensio
           this.emit("click", tab);
         });
       };
 
       this.browserPageAction = PageActions.addAction(
         new PageActions.Action({
           id: widgetId,
           extensionID: extension.id,
-          title: this.action.getProperty(null, "title"),
-          iconURL: this.action.getProperty(null, "title"),
-          pinnedToUrlbar: this.action.getPinned(),
-          disabled: !this.action.getProperty(null, "enabled"),
+          title: this.defaults.title,
+          iconURL: this.defaults.icon,
+          pinnedToUrlbar: this.defaults.pinned,
+          disabled: !this.defaults.show,
           onCommand: (event, buttonNode) => {
             this.lastClickInfo = {
               button: event.button || 0,
               modifiers: clickModifiersFromEvent(event),
             };
             this.handleClick(event.target.ownerGlobal);
           },
           onBeforePlacedInWindow: browserWindow => {
@@ -124,90 +146,143 @@ this.pageAction = class extends Extensio
           onRemovedFromWindow: browserWindow => {
             browserWindow.document.removeEventListener("popupshowing", this);
           },
         })
       );
 
       // If the page action is only enabled in some URLs, do pattern matching in
       // the active tabs and update the button if necessary.
-      if (this.action.getProperty(null, "enabled") === undefined) {
+      if (show === undefined) {
         for (let window of windowTracker.browserWindows()) {
           let tab = window.gBrowser.selectedTab;
-          if (this.action.isShownForTab(tab)) {
+          if (this.isShown(tab)) {
             this.updateButton(window);
           }
         }
       }
     }
   }
 
   onShutdown(isAppShutdown) {
     pageActionMap.delete(this.extension);
-    this.action.onShutdown();
+
+    this.tabContext.shutdown();
 
     // Removing the browser page action causes PageActions to forget about it
     // across app restarts, so don't remove it on app shutdown, but do remove
     // it on all other shutdowns since there's no guarantee the action will be
     // coming back.
     if (!isAppShutdown && this.browserPageAction) {
       this.browserPageAction.remove();
       this.browserPageAction = null;
     }
   }
 
+  // Returns the value of the property |prop| for the given tab, where
+  // |prop| is one of "show", "title", "icon", "popup".
+  getProperty(tab, prop) {
+    return this.tabContext.get(tab)[prop];
+  }
+
+  // Sets the value of the property |prop| for the given tab to the
+  // given value, symmetrically to |getProperty|.
+  //
+  // If |tab| is currently selected, updates the page action button to
+  // reflect the new value.
+  setProperty(tab, prop, value) {
+    if (value != null) {
+      this.tabContext.get(tab)[prop] = value;
+    } else {
+      delete this.tabContext.get(tab)[prop];
+    }
+
+    if (tab.selected) {
+      this.updateButton(tab.ownerGlobal);
+    }
+  }
+
+  normalize(details, context = null) {
+    let icon = IconDetails.normalize(details, this.extension, context);
+    if (!Object.keys(icon).length) {
+      icon = null;
+    }
+    return icon;
+  }
+
   // Updates the page action button in the given window to reflect the
   // properties of the currently selected tab:
   //
   // Updates "tooltiptext" and "aria-label" to match "title" property.
   // Updates "image" to match the "icon" property.
-  // Enables or disables the icon, based on the "enabled" and "patternMatching" properties.
+  // Enables or disables the icon, based on the "show" and "patternMatching" properties.
   updateButton(window) {
     let tab = window.gBrowser.selectedTab;
-    let tabData = this.action.getContextData(tab);
+    let tabData = this.tabContext.get(tab);
     let last = this.lastValues.get(window);
 
     window.requestAnimationFrame(() => {
       // If we get called just before shutdown, we might have been destroyed by
       // this point.
       if (!this.browserPageAction) {
         return;
       }
 
       let title = tabData.title || this.extension.name;
       if (last.title !== title) {
         this.browserPageAction.setTitle(title, window);
         last.title = title;
       }
 
-      let enabled =
-        tabData.enabled != null ? tabData.enabled : tabData.patternMatching;
-      if (last.enabled !== enabled) {
-        this.browserPageAction.setDisabled(!enabled, window);
-        last.enabled = enabled;
+      let show = tabData.show != null ? tabData.show : tabData.patternMatching;
+      if (last.show !== show) {
+        this.browserPageAction.setDisabled(!show, window);
+        last.show = show;
       }
 
       let icon = tabData.icon;
       if (last.icon !== icon) {
         this.browserPageAction.setIconURL(icon, window);
         last.icon = icon;
       }
     });
   }
 
+  // Checks whether the tab action is shown when the specified tab becomes active.
+  // Does pattern matching if necessary, and caches the result as a tab-specific value.
+  // @param {XULElement} tab
+  //        The tab to be checked
+  // @return boolean
+  isShown(tab) {
+    let tabData = this.tabContext.get(tab);
+
+    // If there is a "show" value, return it. Can be due to show(), hide() or empty show_matches.
+    if (tabData.show !== undefined) {
+      return tabData.show;
+    }
+
+    // Otherwise pattern matching must have been configured. Do it, caching the result.
+    if (tabData.patternMatching === undefined) {
+      let uri = tab.linkedBrowser.currentURI;
+      tabData.patternMatching =
+        tabData.showMatches.matches(uri) && !tabData.hideMatches.matches(uri);
+    }
+    return tabData.patternMatching;
+  }
+
   /**
    * Triggers this page action for the given window, with the same effects as
    * if it were clicked by a user.
    *
    * This has no effect if the page action is hidden for the selected tab.
    *
    * @param {Window} window
    */
   triggerAction(window) {
-    if (this.action.isShownForTab(window.gBrowser.selectedTab)) {
+    if (this.isShown(window.gBrowser.selectedTab)) {
       this.lastClickInfo = { button: 0, modifiers: [] };
       this.handleClick(window);
     }
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "popupshowing":
@@ -235,17 +310,17 @@ this.pageAction = class extends Extensio
   // If the page action has a |popup| property, a panel is opened to
   // that URL. Otherwise, a "click" event is emitted, and dispatched to
   // the any click listeners in the add-on.
   async handleClick(window) {
     const { extension } = this;
 
     ExtensionTelemetry.pageActionPopupOpen.stopwatchStart(extension, this);
     let tab = window.gBrowser.selectedTab;
-    let popupURL = this.action.getProperty(tab, "popup");
+    let popupURL = this.tabContext.get(tab).popup;
 
     this.tabManager.addActiveTabPermission(tab);
 
     // If the widget has a popup URL defined, we open a popup, but do not
     // dispatch a click event to the extension.
     // If it has no popup URL defined, we dispatch a click event, but do not
     // open a popup.
     if (popupURL) {
@@ -280,45 +355,135 @@ this.pageAction = class extends Extensio
       );
       ExtensionTelemetry.pageActionPopupOpen.stopwatchFinish(extension, this);
     } else {
       ExtensionTelemetry.pageActionPopupOpen.stopwatchCancel(extension, this);
       this.emit("click", tab);
     }
   }
 
+  /**
+   * Updates the `tabData` for any location change, however it only updates the button
+   * when the selected tab has a location change, or the selected tab has changed.
+   *
+   * @param {string} eventType
+   *        The type of the event, should be "location-change".
+   * @param {XULElement} tab
+   *        The tab whose location changed, or which has become selected.
+   * @param {boolean} [fromBrowse]
+   *        - `true` if navigation occurred in `tab`.
+   *        - `false` if the location changed but no navigation occurred, e.g. due to
+               a hash change or `history.pushState`.
+   *        - Omitted if TabSelect has occurred, tabData does not need to be updated.
+   */
+  handleLocationChange(eventType, tab, fromBrowse) {
+    if (fromBrowse === true) {
+      // Clear tab data on navigation.
+      this.tabContext.clear(tab);
+    } else if (fromBrowse === false) {
+      // Clear pattern matching cache when URL changes.
+      let tabData = this.tabContext.get(tab);
+      if (tabData.patternMatching !== undefined) {
+        tabData.patternMatching = undefined;
+      }
+    }
+
+    if (tab.selected) {
+      // isShown will do pattern matching (if necessary) and store the result
+      // so that updateButton knows whether the page action should be shown.
+      this.isShown(tab);
+      this.updateButton(tab.ownerGlobal);
+    }
+  }
+
   getAPI(context) {
-    const { extension } = context;
+    let { extension } = context;
+
     const { tabManager } = extension;
-    const { action } = this;
+    const pageAction = this;
 
     return {
       pageAction: {
-        ...action.api(context),
-
         onClicked: new EventManager({
           context,
           name: "pageAction.onClicked",
           inputHandling: true,
           register: fire => {
             let listener = (evt, tab) => {
               context.withPendingBrowser(tab.linkedBrowser, () =>
                 fire.sync(tabManager.convert(tab), this.lastClickInfo)
               );
             };
 
-            this.on("click", listener);
+            pageAction.on("click", listener);
             return () => {
-              this.off("click", listener);
+              pageAction.off("click", listener);
             };
           },
         }).api(),
 
-        openPopup: () => {
+        show(tabId) {
+          let tab = tabTracker.getTab(tabId);
+          pageAction.setProperty(tab, "show", true);
+        },
+
+        hide(tabId) {
+          let tab = tabTracker.getTab(tabId);
+          pageAction.setProperty(tab, "show", false);
+        },
+
+        isShown(details) {
+          let tab = tabTracker.getTab(details.tabId);
+          return pageAction.isShown(tab);
+        },
+
+        setTitle(details) {
+          let tab = tabTracker.getTab(details.tabId);
+          pageAction.setProperty(tab, "title", details.title);
+        },
+
+        getTitle(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          let title = pageAction.getProperty(tab, "title");
+          return Promise.resolve(title);
+        },
+
+        setIcon(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          let icon = pageAction.normalize(details, context);
+
+          pageAction.setProperty(tab, "icon", icon);
+        },
+
+        setPopup(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          // Note: Chrome resolves arguments to setIcon relative to the calling
+          // context, but resolves arguments to setPopup relative to the extension
+          // root.
+          // For internal consistency, we currently resolve both relative to the
+          // calling context.
+          let url = details.popup && context.uri.resolve(details.popup);
+          if (url && !context.checkLoadURL(url)) {
+            return Promise.reject({ message: `Access denied for URL ${url}` });
+          }
+          pageAction.setProperty(tab, "popup", url);
+        },
+
+        getPopup(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          let popup = pageAction.getProperty(tab, "popup");
+          return Promise.resolve(popup);
+        },
+
+        openPopup: function() {
           let window = windowTracker.topWindow;
-          this.triggerAction(window);
+          pageAction.triggerAction(window);
         },
       },
     };
   }
 };
 
 global.pageActionFor = this.pageAction.for;
rename from toolkit/components/extensions/schemas/browser_action.json
rename to browser/components/extensions/schemas/browser_action.json
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -1,27 +1,29 @@
 # 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/.
 
 browser.jar:
     content/browser/schemas/bookmarks.json
+    content/browser/schemas/browser_action.json
     content/browser/schemas/browsing_data.json
     content/browser/schemas/chrome_settings_overrides.json
     content/browser/schemas/commands.json
     content/browser/schemas/devtools.json
     content/browser/schemas/devtools_inspected_window.json
     content/browser/schemas/devtools_network.json
     content/browser/schemas/devtools_panels.json
     content/browser/schemas/find.json
     content/browser/schemas/history.json
     content/browser/schemas/menus.json
     content/browser/schemas/menus_child.json
     content/browser/schemas/normandyAddonStudy.json
     content/browser/schemas/omnibox.json
+    content/browser/schemas/page_action.json
     content/browser/schemas/pkcs11.json
     content/browser/schemas/search.json
     content/browser/schemas/sessions.json
     content/browser/schemas/sidebar_action.json
     content/browser/schemas/tabs.json
     content/browser/schemas/top_sites.json
     content/browser/schemas/url_overrides.json
     content/browser/schemas/urlbar.json
rename from toolkit/components/extensions/schemas/page_action.json
rename to browser/components/extensions/schemas/page_action.json
--- a/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js
+++ b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js
@@ -17,21 +17,21 @@ this.pageActionExtras = class extends Ex
     const {
       Management: {
         global: { windowTracker },
       },
     } = ChromeUtils.import("resource://gre/modules/Extension.jsm", null);
     return {
       pageActionExtras: {
         async setDefaultTitle(title) {
-          pageActionAPI.action.getContextData(null).title = title;
+          pageActionAPI.defaults.title = title;
           // Make sure the new default title is considered right away
           for (const window of windowTracker.browserWindows()) {
             const tab = window.gBrowser.selectedTab;
-            if (pageActionAPI.action.isShownForTab(tab)) {
+            if (pageActionAPI.isShown(tab)) {
               pageActionAPI.updateButton(window);
             }
           }
         },
         async setLabelForHistogram(label) {
           pageActionAPI.browserPageAction._labelForHistogram = label;
         },
         async setTooltipText(text) {
--- a/image/imgITools.idl
+++ b/image/imgITools.idl
@@ -1,20 +1,18 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
 
 #include "nsISupports.idl"
 
-interface nsIChannel;
 interface nsIEventTarget;
 interface nsIInputStream;
-interface nsIURI;
 interface imgIContainer;
 interface imgILoader;
 interface imgICache;
 interface imgIScriptedNotificationObserver;
 interface imgINotificationObserver;
 interface imgIContainerCallback;
 
 webidl Document;
@@ -50,39 +48,16 @@ interface imgITools : nsISupports
      * @param aMimeType
      *        Type of image in the stream.
      */
     [implicit_jscontext]
     imgIContainer decodeImageFromArrayBuffer(in jsval aArrayBuffer,
                                              in ACString aMimeType);
 
     /**
-     * decodeImageFromChannelAsync
-     * See decodeImage. The main difference between this method and decodeImage
-     * is that here the operation is done async on a thread from the decode
-     * pool. When the operation is completed, the callback is executed with the
-     * result.
-     *
-     * @param aURI
-     *        The original URI of the image
-     * @param aChannel
-     *        Channel to the image to be decoded.
-     * @param aCallback
-     *        The callback is executed when the imgContainer is fully created.
-     * @param aObserver
-     *        Optional observer for the decoded image, the caller should make
-     *        sure the observer is kept alive as long as necessary, as ImageLib
-     *        does not keep a strong reference to the observer.
-     */
-    void decodeImageFromChannelAsync(in nsIURI aURI,
-                                     in nsIChannel aChannel,
-                                     in imgIContainerCallback aCallback,
-                                     in imgINotificationObserver aObserver);
-
-    /**
      * decodeImageAsync
      * See decodeImage. The main difference between this method and decodeImage
      * is that here the operation is done async on a thread from the decode
      * pool. When the operation is completed, the callback is executed with the
      * result.
      *
      * @param aStream
      *        An input stream for an encoded image file.
--- a/image/imgTools.cpp
+++ b/image/imgTools.cpp
@@ -18,173 +18,32 @@
 #include "imgICache.h"
 #include "imgIContainer.h"
 #include "imgIEncoder.h"
 #include "nsNetUtil.h"  // for NS_NewBufferedInputStream
 #include "nsStreamUtils.h"
 #include "nsStringStream.h"
 #include "nsContentUtils.h"
 #include "nsProxyRelease.h"
-#include "nsIStreamListener.h"
 #include "ImageFactory.h"
 #include "Image.h"
-#include "IProgressObserver.h"
 #include "ScriptedNotificationObserver.h"
 #include "imgIScriptedNotificationObserver.h"
 #include "gfxPlatform.h"
 #include "js/ArrayBuffer.h"
 #include "js/RootingAPI.h"  // JS::{Handle,Rooted}
 #include "js/Value.h"       // JS::Value
 
 using namespace mozilla::gfx;
 
 namespace mozilla {
 namespace image {
 
 namespace {
 
-static nsresult sniff_mimetype_callback(nsIInputStream* in, void* data,
-                                        const char* fromRawSegment,
-                                        uint32_t toOffset, uint32_t count,
-                                        uint32_t* writeCount) {
-  nsCString* mimeType = static_cast<nsCString*>(data);
-  MOZ_ASSERT(mimeType, "mimeType is null!");
-
-  if (count > 0) {
-    imgLoader::GetMimeTypeFromContent(fromRawSegment, count, *mimeType);
-  }
-
-  *writeCount = 0;
-  return NS_ERROR_FAILURE;
-}
-
-// Provides WeakPtr for imgINotificationObserver
-class NotificationObserverWrapper : public imgINotificationObserver,
-                                    public mozilla::SupportsWeakPtr<NotificationObserverWrapper> {
- public:
-  NS_DECL_ISUPPORTS
-  NS_FORWARD_IMGINOTIFICATIONOBSERVER(mObserver->)
-  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(nsGeolocationRequest)
-
-  explicit NotificationObserverWrapper(imgINotificationObserver* observer) : mObserver(observer) {}
-
- private:
-  virtual ~NotificationObserverWrapper() = default;
-  nsCOMPtr<imgINotificationObserver> mObserver;
-};
-
-NS_IMPL_ISUPPORTS(NotificationObserverWrapper, imgINotificationObserver)
-
-class ImageDecoderListener final : public nsIStreamListener,
-                                   public IProgressObserver,
-                                   public imgIContainer {
- public:
-  NS_DECL_ISUPPORTS
-
-  ImageDecoderListener(nsIURI* aURI, imgIContainerCallback* aCallback,
-                       imgINotificationObserver* aObserver)
-      : mURI(aURI),
-        mImage(nullptr),
-        mCallback(aCallback),
-        mObserver(new NotificationObserverWrapper(aObserver)) {
-    MOZ_ASSERT(NS_IsMainThread());
-  }
-
-  NS_IMETHOD
-  OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream,
-                  uint64_t aOffset, uint32_t aCount) override {
-    if (!mImage) {
-      nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
-
-      nsCString mimeType;
-      channel->GetContentType(mimeType);
-
-      if (aInputStream) {
-        // Look at the first few bytes and see if we can tell what the data is from
-        // that since servers tend to lie. :(
-        uint32_t unused;
-        aInputStream->ReadSegments(sniff_mimetype_callback, &mimeType, aCount, &unused);
-      }
-
-      RefPtr<ProgressTracker> tracker = new ProgressTracker();
-      if (mObserver) {
-        tracker->AddObserver(this);
-      }
-
-      mImage = ImageFactory::CreateImage(channel, tracker, mimeType, mURI,
-                                         /* aIsMultiPart */ false, 0);
-
-      if (mImage->HasError()) {
-        return NS_ERROR_FAILURE;
-      }
-    }
-
-    return mImage->OnImageDataAvailable(aRequest, nullptr, aInputStream,
-                                        aOffset, aCount);
-  }
-
-  NS_IMETHOD
-  OnStartRequest(nsIRequest* aRequest) override {
-    return NS_OK;
-  }
-
-  NS_IMETHOD
-  OnStopRequest(nsIRequest* aRequest, nsresult aStatus) override {
-    // Depending on the error, we might not have received any data yet, in which case we would not
-    // have an |mImage|
-    if (mImage) {
-      mImage->OnImageDataComplete(aRequest, nullptr, aStatus, true);
-    }
-
-    nsCOMPtr<imgIContainer> container;
-    if (NS_SUCCEEDED(aStatus)) {
-      container = this;
-    }
-
-    mCallback->OnImageReady(container, aStatus);
-    return NS_OK;
-  }
-
-  virtual void Notify(int32_t aType,
-                      const nsIntRect* aRect = nullptr) override {
-    if (mObserver) {
-      mObserver->Notify(nullptr, aType, aRect);
-    }
-  }
-
-  virtual void OnLoadComplete(bool aLastPart) override {}
-
-  // Other notifications are ignored.
-  virtual void SetHasImage() override {}
-  virtual bool NotificationsDeferred() const override { return false; }
-  virtual void MarkPendingNotify() override {}
-  virtual void ClearPendingNotify() override {}
-
-  // imgIContainer
-  NS_FORWARD_IMGICONTAINER(mImage->)
-
-  nsresult GetNativeSizes(nsTArray<nsIntSize>& aNativeSizes) const override {
-    return mImage->GetNativeSizes(aNativeSizes);
-  }
-
-  size_t GetNativeSizesLength() const override {
-    return mImage->GetNativeSizesLength();
-  }
-
- private:
-  virtual ~ImageDecoderListener() = default;
-
-  nsCOMPtr<nsIURI> mURI;
-  RefPtr<image::Image> mImage;
-  nsCOMPtr<imgIContainerCallback> mCallback;
-  WeakPtr<NotificationObserverWrapper> mObserver;
-};
-
-NS_IMPL_ISUPPORTS(ImageDecoderListener, nsIStreamListener, imgIContainer)
-
 class ImageDecoderHelper final : public Runnable,
                                  public nsIInputStreamCallback {
  public:
   NS_DECL_ISUPPORTS_INHERITED
 
   ImageDecoderHelper(already_AddRefed<image::Image> aImage,
                      already_AddRefed<nsIInputStream> aInputStream,
                      nsIEventTarget* aEventTarget,
@@ -371,32 +230,16 @@ imgTools::DecodeImageFromBuffer(const ch
   NS_ENSURE_SUCCESS(rv, rv);
 
   // All done.
   image.forget(aContainer);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-imgTools::DecodeImageFromChannelAsync(nsIURI* aURI, nsIChannel* aChannel,
-                                      imgIContainerCallback* aCallback,
-                                      imgINotificationObserver* aObserver) {
-  MOZ_ASSERT(NS_IsMainThread());
-
-  NS_ENSURE_ARG_POINTER(aURI);
-  NS_ENSURE_ARG_POINTER(aChannel);
-  NS_ENSURE_ARG_POINTER(aCallback);
-
-  RefPtr<ImageDecoderListener> listener =
-      new ImageDecoderListener(aURI, aCallback, aObserver);
-
-  return aChannel->AsyncOpen(listener);
-}
-
-NS_IMETHODIMP
 imgTools::DecodeImageAsync(nsIInputStream* aInStr, const nsACString& aMimeType,
                            imgIContainerCallback* aCallback,
                            nsIEventTarget* aEventTarget) {
   MOZ_ASSERT(NS_IsMainThread());
 
   NS_ENSURE_ARG_POINTER(aInStr);
   NS_ENSURE_ARG_POINTER(aCallback);
   NS_ENSURE_ARG_POINTER(aEventTarget);
--- a/mobile/android/chrome/geckoview/geckoview.js
+++ b/mobile/android/chrome/geckoview/geckoview.js
@@ -192,21 +192,16 @@ var ModuleManager = {
     this.browser.focus();
     return true;
   },
 
   _updateSettings(aSettings) {
     Object.assign(this._settings, aSettings);
     this._frozenSettings = Object.freeze(Object.assign({}, this._settings));
 
-    const windowType = aSettings.isPopup
-      ? "navigator:popup"
-      : "navigator:geckoview";
-    window.document.documentElement.setAttribute("windowType", windowType);
-
     this.forEach(module => {
       if (module.impl) {
         module.impl.onSettingsUpdate();
       }
     });
 
     this._browser.messageManager.sendAsyncMessage(
       "GeckoView:UpdateSettings",
--- a/mobile/android/components/extensions/ext-android.js
+++ b/mobile/android/components/extensions/ext-android.js
@@ -1,10 +1,16 @@
 "use strict";
 
+ChromeUtils.defineModuleGetter(
+  this,
+  "Services",
+  "resource://gre/modules/Services.jsm"
+);
+
 // This function is pretty tightly tied to Extension.jsm.
 // Its job is to fill in the |tab| property of the sender.
 const getSender = (extension, target, sender) => {
   let tabId = -1;
   if ("tabId" in sender) {
     // The message came from a privileged extension page running in a tab. In
     // that case, it should include a tabId property (which is filled in by the
     // page-open listener below).
@@ -67,37 +73,42 @@ global.openOptionsPage = extension => {
   }
 
   return Promise.resolve();
 };
 
 extensions.registerModules({
   browserAction: {
     url: "chrome://geckoview/content/ext-browserAction.js",
-    schema: "chrome://extensions/content/schemas/browser_action.json",
+    schema: "chrome://geckoview/content/schemas/browser_action.json",
     scopes: ["addon_parent"],
     manifest: ["browser_action"],
     paths: [["browserAction"]],
   },
   browsingData: {
     url: "chrome://geckoview/content/ext-browsingData.js",
     schema: "chrome://geckoview/content/schemas/browsing_data.json",
     scopes: ["addon_parent"],
     manifest: ["browsing_data"],
     paths: [["browsingData"]],
   },
   pageAction: {
     url: "chrome://geckoview/content/ext-pageAction.js",
-    schema: "chrome://extensions/content/schemas/page_action.json",
+    schema: "chrome://geckoview/content/schemas/page_action.json",
     scopes: ["addon_parent"],
     manifest: ["page_action"],
     paths: [["pageAction"]],
   },
   tabs: {
     url: "chrome://geckoview/content/ext-tabs.js",
     schema: "chrome://geckoview/content/schemas/tabs.json",
     scopes: ["addon_parent"],
     paths: [["tabs"]],
   },
-  geckoViewAddons: {
-    schema: "chrome://geckoview/content/schemas/gecko_view_addons.json",
-  },
 });
+
+if (!Services.androidBridge.isFennec) {
+  extensions.registerModules({
+    geckoViewAddons: {
+      schema: "chrome://geckoview/content/schemas/gecko_view_addons.json",
+    },
+  });
+}
--- a/mobile/android/components/extensions/ext-browserAction.js
+++ b/mobile/android/components/extensions/ext-browserAction.js
@@ -1,128 +1,203 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-utils.js */
 
-XPCOMUtils.defineLazyModuleGetters(this, {
-  GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm",
-  ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.jsm",
-});
-
-const { BrowserActionBase } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionActions.jsm"
+// Import the android BrowserActions module.
+ChromeUtils.defineModuleGetter(
+  this,
+  "BrowserActions",
+  "resource://gre/modules/BrowserActions.jsm"
 );
 
-const BROWSER_ACTION_PROPERTIES = [
-  "title",
-  "icon",
-  "popup",
-  "badgeText",
-  "badgeBackgroundColor",
-  "badgeTextColor",
-  "enabled",
-  "patternMatching",
-];
+// WeakMap[Extension -> BrowserAction]
+let browserActionMap = new WeakMap();
+
+class BrowserAction extends EventEmitter {
+  constructor(options, extension) {
+    super();
+
+    this.uuid = `{${extension.uuid}}`;
+
+    this.defaults = {
+      name: options.default_title || extension.name,
+      popup: options.default_popup,
+    };
+
+    this.tabContext = new TabContext(tabId => this.defaults);
+
+    this.tabManager = extension.tabManager;
+
+    // eslint-disable-next-line mozilla/balanced-listeners
+    this.tabContext.on("tab-selected", (evt, tabId) => {
+      this.onTabSelected(tabId);
+    });
+    // eslint-disable-next-line mozilla/balanced-listeners
+    this.tabContext.on("tab-closed", (evt, tabId) => {
+      this.onTabClosed(tabId);
+    });
 
-class BrowserAction extends BrowserActionBase {
-  constructor(extension, clickDelegate) {
-    const tabContext = new TabContext(tabId => this.getContextData(null));
-    super(tabContext, extension);
-    this.clickDelegate = clickDelegate;
-    this.helper = new ExtensionActionHelper({
-      extension,
-      tabTracker,
-      windowTracker,
-      tabContext,
-      properties: BROWSER_ACTION_PROPERTIES,
-    });
+    BrowserActions.register(this);
+  }
+
+  /**
+   * Required by the BrowserActions module. This event will get
+   * called whenever the browser action is clicked on.
+   */
+  onClicked() {
+    const tab = tabTracker.activeTab;
+
+    this.tabManager.addActiveTabPermission(tab);
+
+    let popup = this.tabContext.get(tab.id).popup || this.defaults.popup;
+    if (popup) {
+      tabTracker.openExtensionPopupTab(popup);
+    } else {
+      this.emit("click", tab);
+    }
+  }
+
+  /**
+   * Updates the browser action whenever a tab is selected.
+   * @param {string} tabId The tab id to update.
+   */
+  onTabSelected(tabId) {
+    let name = this.tabContext.get(tabId).name || this.defaults.name;
+    BrowserActions.update(this.uuid, { name });
   }
 
-  updateOnChange(tab) {
-    const tabId = tab ? tab.id : null;
-    const action = tab
-      ? this.getContextData(tab)
-      : this.helper.extractProperties(this.globals);
-    this.helper.sendRequestForResult(tabId, {
-      action,
-      type: "GeckoView:BrowserAction:Update",
-    });
+  /**
+   * Removes the tab from the property map now that it is closed.
+   * @param {string} tabId The tab id of the closed tab.
+   */
+  onTabClosed(tabId) {
+    this.tabContext.clear(tabId);
   }
 
-  openPopup() {
-    const tab = tabTracker.activeTab;
-    const action = this.getContextData(tab);
-    this.helper.sendRequest(tab.id, {
-      action,
-      type: "GeckoView:BrowserAction:OpenPopup",
-    });
+  /**
+   * Sets a property for the browser action for the specified tab. If the property is set
+   * for the active tab, the browser action is also updated.
+   *
+   * @param {Object} tab The tab to set. If null, the browser action's default value is set.
+   * @param {string} prop The property to update. Currently only "name" is supported.
+   * @param {string} value The value to set the property to.
+   */
+  setProperty(tab, prop, value) {
+    if (tab == null) {
+      if (value) {
+        this.defaults[prop] = value;
+      }
+    } else {
+      let properties = this.tabContext.get(tab.id);
+      if (value) {
+        properties[prop] = value;
+      } else {
+        delete properties[prop];
+      }
+    }
+
+    if (!tab || tab.getActive()) {
+      BrowserActions.update(this.uuid, { [prop]: value });
+    }
   }
 
-  getTab(tabId) {
-    return this.helper.getTab(tabId);
+  /**
+   * Retreives a property of the browser action for the specified tab.
+   *
+   * @param {Object} tab The tab to retrieve the property from. If null, the default value is returned.
+   * @param {string} prop The property to retreive. Currently only "name" is supported.
+   * @returns {string} the value stored for the specified property. If the value is undefined, then the
+   *    default value is returned.
+   */
+  getProperty(tab, prop) {
+    if (tab == null) {
+      return this.defaults[prop];
+    }
+
+    return this.tabContext.get(tab.id)[prop] || this.defaults[prop];
   }
 
-  getWindow(windowId) {
-    return this.helper.getWindow(windowId);
-  }
-
-  click() {
-    this.clickDelegate.onClick();
+  /**
+   * Unregister the browser action from the BrowserActions module.
+   */
+  shutdown() {
+    this.tabContext.shutdown();
+    BrowserActions.unregister(this.uuid);
   }
 }
 
 this.browserAction = class extends ExtensionAPI {
-  async onManifestEntry(entryName) {
-    const { extension } = this;
-    this.action = new BrowserAction(extension, this);
-    await this.action.loadIconData();
+  onManifestEntry(entryName) {
+    let { extension } = this;
+    let { manifest } = extension;
 
-    GeckoViewWebExtension.browserActions.set(extension, this.action);
-
-    // Notify the embedder of this action
-    this.action.updateOnChange(null);
+    let browserAction = new BrowserAction(manifest.browser_action, extension);
+    browserActionMap.set(extension, browserAction);
   }
 
   onShutdown() {
-    const { extension } = this;
-    this.action.onShutdown();
-    GeckoViewWebExtension.browserActions.delete(extension);
-  }
+    let { extension } = this;
 
-  onClick() {
-    this.emit("click", tabTracker.activeTab);
+    if (browserActionMap.has(extension)) {
+      browserActionMap.get(extension).shutdown();
+      browserActionMap.delete(extension);
+    }
   }
 
   getAPI(context) {
     const { extension } = context;
     const { tabManager } = extension;
-    const { action } = this;
+
+    function getTab(tabId) {
+      if (tabId !== null) {
+        return tabTracker.getTab(tabId);
+      }
+      return null;
+    }
 
     return {
       browserAction: {
-        ...action.api(context),
-
         onClicked: new EventManager({
           context,
           name: "browserAction.onClicked",
           register: fire => {
-            const listener = (event, tab) => {
+            let listener = (event, tab) => {
               fire.async(tabManager.convert(tab));
             };
-            this.on("click", listener);
+            browserActionMap.get(extension).on("click", listener);
             return () => {
-              this.off("click", listener);
+              browserActionMap.get(extension).off("click", listener);
             };
           },
         }).api(),
 
-        openPopup: function() {
-          action.openPopup();
+        setTitle: function(details) {
+          let { tabId, title } = details;
+          let tab = getTab(tabId);
+          browserActionMap.get(extension).setProperty(tab, "name", title);
+        },
+
+        getTitle: function(details) {
+          let { tabId } = details;
+          let tab = getTab(tabId);
+          let title = browserActionMap.get(extension).getProperty(tab, "name");
+          return Promise.resolve(title);
+        },
+
+        setPopup(details) {
+          let tab = getTab(details.tabId);
+          let url = details.popup && context.uri.resolve(details.popup);
+          browserActionMap.get(extension).setProperty(tab, "popup", url);
+        },
+
+        getPopup(details) {
+          let tab = getTab(details.tabId);
+          let popup = browserActionMap.get(extension).getProperty(tab, "popup");
+          return Promise.resolve(popup);
         },
       },
     };
   }
 };
-
-global.browserActionFor = this.browserAction.for;
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -1,122 +1,292 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 
 "use strict";
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-utils.js */
 
-XPCOMUtils.defineLazyModuleGetters(this, {
-  GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm",
-  ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.jsm",
-});
+ChromeUtils.defineModuleGetter(
+  this,
+  "Services",
+  "resource://gre/modules/Services.jsm"
+);
 
-const { PageActionBase } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionActions.jsm"
+// Import the android PageActions module.
+ChromeUtils.defineModuleGetter(
+  this,
+  "PageActions",
+  "resource://gre/modules/PageActions.jsm"
+);
+
+var { ExtensionParent } = ChromeUtils.import(
+  "resource://gre/modules/ExtensionParent.jsm"
 );
 
-const PAGE_ACTION_PROPERTIES = [
-  "title",
-  "icon",
-  "popup",
-  "badgeText",
-  "enabled",
-  "patternMatching",
-];
+var { IconDetails } = ExtensionParent;
+
+// WeakMap[Extension -> PageAction]
+let pageActionMap = new WeakMap();
+
+class PageAction extends EventEmitter {
+  constructor(manifest, extension) {
+    super();
+
+    this.id = null;
+
+    this.extension = extension;
+
+    this.defaults = {
+      icons: IconDetails.normalize({ path: manifest.default_icon }, extension),
+      popup: manifest.default_popup,
+    };
+
+    this.tabManager = extension.tabManager;
+    this.context = null;
+
+    this.tabContext = new TabContext(tabId => this.defaults);
 
-class PageAction extends PageActionBase {
-  constructor(extension, clickDelegate) {
-    const tabContext = new TabContext(tabId => this.getContextData(null));
-    super(tabContext, extension);
-    this.clickDelegate = clickDelegate;
-    this.helper = new ExtensionActionHelper({
-      extension,
-      tabTracker,
-      windowTracker,
-      tabContext,
-      properties: PAGE_ACTION_PROPERTIES,
+    this.options = {
+      title: manifest.default_title || extension.name,
+      id: `{${extension.uuid}}`,
+      clickCallback: () => {
+        let tab = tabTracker.activeTab;
+
+        this.tabManager.addActiveTabPermission(tab);
+
+        let popup = this.tabContext.get(tab.id).popup || this.defaults.popup;
+        if (popup) {
+          tabTracker.openExtensionPopupTab(popup);
+        } else {
+          this.emit("click", tab);
+        }
+      },
+    };
+
+    this.shouldShow = false;
+
+    // eslint-disable-next-line mozilla/balanced-listeners
+    this.tabContext.on("tab-selected", (evt, tabId) => {
+      this.onTabSelected(tabId);
+    });
+    // eslint-disable-next-line mozilla/balanced-listeners
+    this.tabContext.on("tab-closed", (evt, tabId) => {
+      this.onTabClosed(tabId);
     });
   }
 
-  updateOnChange(tab) {
-    const tabId = tab ? tab.id : null;
-    // The embedder only gets the override, not the full object
-    const action = tab
-      ? this.getContextData(tab)
-      : this.helper.extractProperties(this.globals);
-    this.helper.sendRequestForResult(tabId, {
-      action,
-      type: "GeckoView:PageAction:Update",
-    });
+  /**
+   * Updates the page action whenever a tab is selected.
+   * @param {Integer} tabId The ID of the selected tab.
+   */
+  onTabSelected(tabId) {
+    if (this.options.icon) {
+      this.hide();
+      let shouldShow = this.tabContext.get(tabId).show;
+      if (shouldShow) {
+        this.show();
+      }
+    }
+  }
+
+  /**
+   * Removes the tab from the property map now that it is closed.
+   * @param {Integer} tabId The ID of the closed tab.
+   */
+  onTabClosed(tabId) {
+    this.tabContext.clear(tabId);
+  }
+
+  /**
+   * Sets the context for the page action.
+   * @param {Object} context The extension context.
+   */
+  setContext(context) {
+    this.context = context;
+  }
+
+  /**
+   * Sets a property for the page action for the specified tab. If the property is set
+   * for the active tab, the page action is also updated.
+   *
+   * @param {Object} tab The tab to set.
+   * @param {string} prop The property to update - either "show" or "popup".
+   * @param {string} value The value to set the property to. If falsy, the property is deleted.
+   * @returns {Object} Promise which resolves when the property is set and the page action is
+   *    shown if necessary.
+   */
+  setProperty(tab, prop, value) {
+    if (tab == null) {
+      throw new Error("Tab must not be null");
+    }
+
+    let properties = this.tabContext.get(tab.id);
+    if (value) {
+      properties[prop] = value;
+    } else {
+      delete properties[prop];
+    }
+
+    if (prop === "show" && tab.id == tabTracker.activeTab.id) {
+      if (this.id && !value) {
+        return this.hide();
+      } else if (!this.id && value) {
+        return this.show();
+      }
+    }
   }
 
-  openPopup() {
-    const action = this.getContextData(tabTracker.activeTab);
-    this.helper.sendRequest(tabTracker.activeTab.id, {
-      action,
-      type: "GeckoView:PageAction:OpenPopup",
-    });
+  /**
+   * Retreives a property of the page action for the specified tab.
+   *
+   * @param {Object} tab The tab to retrieve the property from. If null, the default value is returned.
+   * @param {string} prop The property to retreive - currently only "popup" is supported.
+   * @returns {string} the value stored for the specified property. If the value for the tab is undefined, then the
+   *    default value is returned.
+   */
+  getProperty(tab, prop) {
+    if (tab == null) {
+      return this.defaults[prop];
+    }
+
+    return this.tabContext.get(tab.id)[prop] || this.defaults[prop];
   }
 
-  getTab(tabId) {
-    return this.helper.getTab(tabId);
+  /**
+   * Show the page action for the active tab.
+   * @returns {Promise} resolves when the page action is shown.
+   */
+  show() {
+    // The PageAction icon has been created or it is being converted.
+    if (this.id || this.shouldShow) {
+      return Promise.resolve();
+    }
+
+    if (this.options.icon) {
+      this.id = PageActions.add(this.options);
+      return Promise.resolve();
+    }
+
+    this.shouldShow = true;
+
+    // Bug 1372782: Remove dependency on contentWindow from this file. It should
+    // be put in a separate file called ext-c-pageAction.js.
+    // Note: Fennec is not going to be multi-process for the foreseaable future,
+    // so this layering violation has no immediate impact. However, it is should
+    // be done at some point.
+    let { contentWindow } = this.context.xulBrowser;
+
+    // Bug 1372783: Why is this contentWindow.devicePixelRatio, while
+    // convertImageURLToDataURL uses browserWindow.devicePixelRatio?
+    let { icon } = IconDetails.getPreferredIcon(
+      this.defaults.icons,
+      this.extension,
+      16 * contentWindow.devicePixelRatio
+    );
+
+    let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+    return IconDetails.convertImageURLToDataURL(
+      icon,
+      contentWindow,
+      browserWindow
+    )
+      .then(dataURI => {
+        if (this.shouldShow) {
+          this.options.icon = dataURI;
+          this.id = PageActions.add(this.options);
+        }
+      })
+      .catch(() => {
+        // The "icon conversion" promise has been rejected, set `this.shouldShow` to `false`
+        // so that we will try again on the next `pageAction.show` call.
+        this.shouldShow = false;
+
+        return Promise.reject({
+          message: "Failed to load PageAction icon",
+        });
+      });
   }
 
-  click() {
-    this.clickDelegate.onClick();
+  /**
+   * Hides the page action for the active tab.
+   */
+  hide() {
+    this.shouldShow = false;
+
+    if (this.id) {
+      PageActions.remove(this.id);
+      this.id = null;
+    }
+  }
+
+  shutdown() {
+    this.tabContext.shutdown();
+    this.hide();
   }
 }
 
 this.pageAction = class extends ExtensionAPI {
-  async onManifestEntry(entryName) {
-    const { extension } = this;
-    const action = new PageAction(extension, this);
-    await action.loadIconData();
-    this.action = action;
+  onManifestEntry(entryName) {
+    let { extension } = this;
+    let { manifest } = extension;
 
-    GeckoViewWebExtension.pageActions.set(extension, action);
-
-    // Notify the embedder of this action
-    action.updateOnChange(null);
-  }
-
-  onClick() {
-    this.emit("click", tabTracker.activeTab);
+    let pageAction = new PageAction(manifest.page_action, extension);
+    pageActionMap.set(extension, pageAction);
   }
 
   onShutdown() {
-    const { extension, action } = this;
-    action.onShutdown();
-    GeckoViewWebExtension.pageActions.delete(extension);
+    let { extension } = this;
+
+    if (pageActionMap.has(extension)) {
+      pageActionMap.get(extension).shutdown();
+      pageActionMap.delete(extension);
+    }
   }
 
   getAPI(context) {
     const { extension } = context;
     const { tabManager } = extension;
-    const { action } = this;
+
+    pageActionMap.get(extension).setContext(context);
 
     return {
       pageAction: {
-        ...action.api(context),
-
         onClicked: new EventManager({
           context,
           name: "pageAction.onClicked",
           register: fire => {
-            const listener = (event, tab) => {
+            let listener = (event, tab) => {
               fire.async(tabManager.convert(tab));
             };
-            this.on("click", listener);
+            pageActionMap.get(extension).on("click", listener);
             return () => {
-              this.off("click", listener);
+              pageActionMap.get(extension).off("click", listener);
             };
           },
         }).api(),
 
-        openPopup() {
-          action.openPopup();
+        show(tabId) {
+          let tab = tabTracker.getTab(tabId);
+          return pageActionMap.get(extension).setProperty(tab, "show", true);
+        },
+
+        hide(tabId) {
+          let tab = tabTracker.getTab(tabId);
+          pageActionMap.get(extension).setProperty(tab, "show", false);
+        },
+
+        setPopup(details) {
+          let tab = tabTracker.getTab(details.tabId);
+          let url = details.popup && context.uri.resolve(details.popup);
+          pageActionMap.get(extension).setProperty(tab, "popup", url);
+        },
+
+        getPopup(details) {
+          let tab = tabTracker.getTab(details.tabId);
+          let popup = pageActionMap.get(extension).getProperty(tab, "popup");
+          return Promise.resolve(popup);
         },
       },
     };
   }
 };
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -33,17 +33,19 @@ var { defineLazyGetter } = ExtensionComm
 global.GlobalEventDispatcher = EventDispatcher.instance;
 
 const BrowserStatusFilter = Components.Constructor(
   "@mozilla.org/appshell/component/browser-status-filter;1",
   "nsIWebProgress",
   "addProgressListener"
 );
 
-const WINDOW_TYPE = "navigator:geckoview";
+const WINDOW_TYPE = Services.androidBridge.isFennec
+  ? "navigator:browser"
+  : "navigator:geckoview";
 
 let tabTracker;
 let windowTracker;
 
 /**
  * A nsIWebProgressListener for a specific XUL browser, which delegates the
  * events that it receives to a tab progress listener, and prepends the browser
  * to their arguments list.
@@ -95,30 +97,116 @@ class BrowserProgressListener {
   onStateChange(webProgress, request, stateFlags, status) {
     this.delegate("onStateChange", webProgress, request, stateFlags, status);
   }
 }
 
 const PROGRESS_LISTENER_FLAGS =
   Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION;
 
-class ProgressListenerWrapper {
+class GeckoViewProgressListenerWrapper {
   constructor(window, listener) {
     this.listener = new BrowserProgressListener(
       window.BrowserApp.selectedBrowser,
       listener,
       PROGRESS_LISTENER_FLAGS
     );
   }
 
   destroy() {
     this.listener.destroy();
   }
 }
 
+/**
+ * Handles wrapping a tab progress listener in browser-specific
+ * BrowserProgressListener instances, an attaching them to each tab in a given
+ * browser window.
+ *
+ * @param {DOMWindow} window
+ *        The browser window to which to attach the listeners.
+ * @param {object} listener
+ *        The tab progress listener to wrap.
+ */
+class FennecProgressListenerWrapper {
+  constructor(window, listener) {
+    this.window = window;
+    this.listener = listener;
+    this.listeners = new WeakMap();
+
+    for (let nativeTab of this.window.BrowserApp.tabs) {
+      this.addBrowserProgressListener(nativeTab.browser);
+    }
+
+    this.window.BrowserApp.deck.addEventListener("TabOpen", this);
+  }
+
+  /**
+   * Destroy the wrapper, removing any remaining listeners it has added.
+   */
+  destroy() {
+    this.window.BrowserApp.deck.removeEventListener("TabOpen", this);
+
+    for (let nativeTab of this.window.BrowserApp.tabs) {
+      this.removeProgressListener(nativeTab.browser);
+    }
+  }
+
+  /**
+   * Adds a progress listener to the given XUL browser element.
+   *
+   * @param {XULElement} browser
+   *        The XUL browser to add the listener to.
+   * @private
+   */
+  addBrowserProgressListener(browser) {
+    this.removeProgressListener(browser);
+
+    let listener = new BrowserProgressListener(
+      browser,
+      this.listener,
+      this.flags
+    );
+    this.listeners.set(browser, listener);
+  }
+
+  /**
+   * Removes a progress listener from the given XUL browser element.
+   *
+   * @param {XULElement} browser
+   *        The XUL browser to remove the listener from.
+   * @private
+   */
+  removeProgressListener(browser) {
+    let listener = this.listeners.get(browser);
+    if (listener) {
+      listener.destroy();
+      this.listeners.delete(browser);
+    }
+  }
+
+  /**
+   * Handles tab open events, and adds the necessary progress listeners to the
+   * new tabs.
+   *
+   * @param {Event} event
+   *        The DOM event to handle.
+   * @private
+   */
+  handleEvent(event) {
+    if (event.type === "TabOpen") {
+      this.addBrowserProgressListener(event.originalTarget);
+    }
+  }
+}
+
+const ProgressListenerWrapper = Services.androidBridge.isFennec
+  ? FennecProgressListenerWrapper
+  : GeckoViewProgressListenerWrapper;
+
 class WindowTracker extends WindowTrackerBase {
   constructor(...args) {
     super(...args);
 
     this.progressListeners = new DefaultWeakMap(() => new WeakMap());
   }
 
   get topWindow() {
@@ -190,17 +278,17 @@ global.makeGlobalEvent = function makeGl
       GlobalEventDispatcher.registerListener(listener2, [event]);
       return () => {
         GlobalEventDispatcher.unregisterListener(listener2, [event]);
       };
     },
   }).api();
 };
 
-class TabTracker extends TabTrackerBase {
+class GeckoViewTabTracker extends TabTrackerBase {
   init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
 
     windowTracker.addOpenListener(window => {
       const nativeTab = window.BrowserApp.selectedTab;
@@ -263,18 +351,252 @@ class TabTracker extends TabTrackerBase 
     let win = windowTracker.topWindow;
     if (win && win.BrowserApp) {
       return win.BrowserApp.selectedTab;
     }
     return null;
   }
 }
 
+class FennecTabTracker extends TabTrackerBase {
+  constructor() {
+    super();
+
+    // Keep track of the extension popup tab.
+    this._extensionPopupTabWeak = null;
+    // Keep track of the selected tabId
+    this._selectedTabId = null;
+  }
+
+  init() {
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+
+    windowTracker.addListener("TabClose", this);
+    windowTracker.addListener("TabOpen", this);
+
+    // Register a listener for the Tab:Selected global event,
+    // so that we can close the popup when a popup tab has been
+    // unselected.
+    GlobalEventDispatcher.registerListener(this, ["Tab:Selected"]);
+  }
+
+  /**
+   * Returns the currently opened popup tab if any
+   */
+  get extensionPopupTab() {
+    if (this._extensionPopupTabWeak) {
+      const tab = this._extensionPopupTabWeak.get();
+
+      // Return the native tab only if the tab has not been removed in the meantime.
+      if (tab.browser) {
+        return tab;
+      }
+
+      // Clear the tracked popup tab if it has been closed in the meantime.
+      this._extensionPopupTabWeak = null;
+    }
+
+    return undefined;
+  }
+
+  /**
+   * Open a pageAction/browserAction popup url in a tab and keep track of
+   * its weak reference (to be able to customize the activedTab using the tab parentId,
+   * to skip it in the tabs.query and to set the parent tab as active when the popup
+   * tab is currently selected).
+   *
+   * @param {string} popup
+   *   The popup url to open in a tab.
+   */
+  openExtensionPopupTab(popup) {
+    let win = windowTracker.topWindow;
+    if (!win) {
+      throw new ExtensionError(
+        `Unable to open a popup without an active window`
+      );
+    }
+
+    if (this.extensionPopupTab) {
+      win.BrowserApp.closeTab(this.extensionPopupTab);
+    }
+
+    this.init();
+
+    let { browser, id } = win.BrowserApp.selectedTab;
+    let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
+    this._extensionPopupTabWeak = Cu.getWeakReference(
+      win.BrowserApp.addTab(popup, {
+        selected: true,
+        parentId: id,
+        isPrivate,
+      })
+    );
+  }
+
+  getId(nativeTab) {
+    return nativeTab.id;
+  }
+
+  getTab(id, default_ = undefined) {
+    let win = windowTracker.topWindow;
+    if (win) {
+      let nativeTab = win.BrowserApp.getTabForId(id);
+      if (nativeTab) {
+        return nativeTab;
+      }
+    }
+    if (default_ !== undefined) {
+      return default_;
+    }
+    throw new ExtensionError(`Invalid tab ID: ${id}`);
+  }
+
+  /**
+   * Handles tab open and close events, and emits the appropriate internal
+   * events for them.
+   *
+   * @param {Event} event
+   *        A DOM event to handle.
+   * @private
+   */
+  handleEvent(event) {
+    const { BrowserApp } = event.target.ownerGlobal;
+    const nativeTab = BrowserApp.getTabForBrowser(event.target);
+
+    switch (event.type) {
+      case "TabOpen":
+        this.emitCreated(nativeTab);
+        break;
+
+      case "TabClose":
+        this.emitRemoved(nativeTab, false);
+        break;
+    }
+  }
+
+  /**
+   * Required by the GlobalEventDispatcher module. This event will get
+   * called whenever one of the registered listeners fires.
+   * @param {string} event The event which fired.
+   * @param {object} data Information about the event which fired.
+   */
+  onEvent(event, data) {
+    const { BrowserApp } = windowTracker.topWindow;
+
+    switch (event) {
+      case "Tab:Selected": {
+        this._selectedTabId = data.id;
+
+        // If a new tab has been selected while an extension popup tab is still open,
+        // close it immediately.
+        const nativeTab = BrowserApp.getTabForId(data.id);
+
+        const popupTab = tabTracker.extensionPopupTab;
+        if (popupTab && popupTab !== nativeTab) {
+          BrowserApp.closeTab(popupTab);
+        }
+
+        break;
+      }
+    }
+  }
+
+  /**
+   * Emits a "tab-created" event for the given tab element.
+   *
+   * @param {NativeTab} nativeTab
+   *        The tab element which is being created.
+   * @private
+   */
+  emitCreated(nativeTab) {
+    this.emit("tab-created", { nativeTab });
+  }
+
+  /**
+   * Emits a "tab-removed" event for the given tab element.
+   *
+   * @param {NativeTab} nativeTab
+   *        The tab element which is being removed.
+   * @param {boolean} isWindowClosing
+   *        True if the tab is being removed because the browser window is
+   *        closing.
+   * @private
+   */
+  emitRemoved(nativeTab, isWindowClosing) {
+    let windowId = windowTracker.getId(nativeTab.browser.ownerGlobal);
+    let tabId = this.getId(nativeTab);
+
+    if (this.extensionPopupTab && this.extensionPopupTab === nativeTab) {
+      this._extensionPopupTabWeak = null;
+
+      // Do not switch to the parent tab of the extension popup tab
+      // if the popup tab is not the selected tab.
+      if (this._selectedTabId !== tabId) {
+        return;
+      }
+
+      // Select the parent tab when the closed tab was an extension popup tab.
+      const { BrowserApp } = windowTracker.topWindow;
+      const popupParentTab = BrowserApp.getTabForId(nativeTab.parentId);
+      if (popupParentTab) {
+        BrowserApp.selectTab(popupParentTab);
+      }
+    }
+
+    Services.tm.dispatchToMainThread(() => {
+      this.emit("tab-removed", { nativeTab, tabId, windowId, isWindowClosing });
+    });
+  }
+
+  getBrowserData(browser) {
+    let result = {
+      tabId: -1,
+      windowId: -1,
+    };
+
+    let { BrowserApp } = browser.ownerGlobal;
+    if (BrowserApp) {
+      result.windowId = windowTracker.getId(browser.ownerGlobal);
+
+      let nativeTab = BrowserApp.getTabForBrowser(browser);
+      if (nativeTab) {
+        result.tabId = this.getId(nativeTab);
+      }
+    }
+
+    return result;
+  }
+
+  get activeTab() {
+    let win = windowTracker.topWindow;
+    if (win && win.BrowserApp) {
+      const selectedTab = win.BrowserApp.selectedTab;
+
+      // If the current tab is an extension popup tab, we use the parentId to retrieve
+      // and return the tab that was selected when the popup tab has been opened.
+      if (selectedTab === this.extensionPopupTab) {
+        return win.BrowserApp.getTabForId(selectedTab.parentId);
+      }
+
+      return selectedTab;
+    }
+
+    return null;
+  }
+}
+
 windowTracker = new WindowTracker();
-tabTracker = new TabTracker();
+if (Services.androidBridge.isFennec) {
+  tabTracker = new FennecTabTracker();
+} else {
+  tabTracker = new GeckoViewTabTracker();
+}
 
 Object.assign(global, { tabTracker, windowTracker });
 
 class Tab extends TabBase {
   get _favIconUrl() {
     return undefined;
   }
 
@@ -399,62 +721,60 @@ class Tab extends TabBase {
   }
 }
 
 // Manages tab-specific context data and dispatches tab select and close events.
 class TabContext extends EventEmitter {
   constructor(getDefaultPrototype) {
     super();
 
-    windowTracker.addListener("progress", this);
-
     this.getDefaultPrototype = getDefaultPrototype;
     this.tabData = new Map();
-  }
 
-  onLocationChange(browser, webProgress, request, locationURI, flags) {
-    if (!webProgress.isTopLevel) {
-      // Only pageAction and browserAction are consuming the "location-change" event
-      // to update their per-tab status, and they should only do so in response of
-      // location changes related to the top level frame (See Bug 1493470 for a rationale).
-      return;
-    }
-    const gBrowser = browser.ownerGlobal.gBrowser;
-    const tab = gBrowser.getTabForBrowser(browser);
-    // fromBrowse will be false in case of e.g. a hash change or history.pushState
-    const fromBrowse = !(
-      flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
-    );
-    this.emit(
-      "location-change",
-      {
-        id: tab.id,
-        linkedBrowser: browser,
-        // TODO: we don't support selected so we just alway say we are
-        selected: true,
-      },
-      fromBrowse
-    );
+    GlobalEventDispatcher.registerListener(this, [
+      "Tab:Selected",
+      "Tab:Closed",
+    ]);
   }
 
   get(tabId) {
     if (!this.tabData.has(tabId)) {
       let data = Object.create(this.getDefaultPrototype(tabId));
       this.tabData.set(tabId, data);
     }
 
     return this.tabData.get(tabId);
   }
 
   clear(tabId) {
     this.tabData.delete(tabId);
   }
 
+  /**
+   * Required by the GlobalEventDispatcher module. This event will get
+   * called whenever one of the registered listeners fires.
+   * @param {string} event The event which fired.
+   * @param {object} data Information about the event which fired.
+   */
+  onEvent(event, data) {
+    switch (event) {
+      case "Tab:Selected":
+        this.emit("tab-selected", data.id);
+        break;
+      case "Tab:Closed":
+        this.emit("tab-closed", data.tabId);
+        break;
+    }
+  }
+
   shutdown() {
-    windowTracker.removeListener("progress", this);
+    GlobalEventDispatcher.unregisterListener(this, [
+      "Tab:Selected",
+      "Tab:Closed",
+    ]);
   }
 }
 
 class Window extends WindowBase {
   get focused() {
     return this.window.document.hasFocus();
   }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/browser_action.json
@@ -0,0 +1,448 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "browser_action": {
+            "type": "object",
+            "additionalProperties": { "$ref": "UnrecognizedProperty" },
+            "properties": {
+              "default_title": {
+                "type": "string",
+                "optional": true,
+                "preprocess": "localize"
+              },
+              "default_icon": {
+                "$ref": "IconPath",
+                "deprecated": "Unsupported on Android.",
+                "optional": true
+              },
+              "default_popup": {
+                "type": "string",
+                "format": "relativeUrl",
+                "optional": true,
+                "preprocess": "localize"
+              },
+              "browser_style": {
+                "type": "boolean",
+                "deprecated": "Unsupported on Android.",
+                "optional": true,
+                "default": false
+              },
+              "default_area": {
+                "description": "Defines the location the browserAction will appear by default.  The default location is navbar.",
+                "type": "string",
+                "enum": ["navbar", "menupanel", "tabstrip", "personaltoolbar"],
+                "deprecated": "Unsupported on Android.",
+                "optional": true
+              }
+            },
+            "optional": true
+          }
+        }
+      }
+    ]
+  },
+  {
+    "namespace": "browserAction",
+    "description": "Use browser actions to put icons in the main browser toolbar, to the right of the address bar. In addition to its icon, a browser action can also have a tooltip, a badge, and a popup.",
+    "permissions": ["manifest:browser_action"],
+    "types": [
+      {
+        "id": "ColorArray",
+        "type": "array",
+        "items": {
+          "type": "integer",
+          "minimum": 0,
+          "maximum": 255
+        },
+        "minItems": 4,
+        "maxItems": 4
+      },
+      {
+        "id": "ImageDataType",
+        "type": "object",
+        "isInstanceOf": "ImageData",
+        "additionalProperties": { "type": "any" },
+        "postprocess": "convertImageDataToURL",
+        "description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
+      }
+    ],
+    "functions": [
+      {
+        "name": "setTitle",
+        "type": "function",
+        "description": "Sets the title of the browser action. This shows up in the tooltip.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "title": {
+                "type": "string",
+                "description": "The string the browser action should display when moused over."
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "getTitle",
+        "type": "function",
+        "description": "Gets the title of the browser action.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Specify the tab to get the title from. If no tab is specified, the non-tab-specific title is returned."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "result",
+                "type": "string"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "setIcon",
+        "unsupported": true,
+        "type": "function",
+        "description": "Sets the icon for the browser action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "imageData": {
+                "choices": [
+                  { "$ref": "ImageDataType" },
+                  {
+                    "type": "object",
+                    "additionalProperties": {"$ref": "ImageDataType"}
+                  }
+                ],
+                "optional": true,
+                "description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
+              },
+              "path": {
+                "choices": [
+                  { "type": "string" },
+                  {
+                    "type": "object",
+                    "additionalProperties": {"type": "string"}
+                  }
+                ],
+                "optional": true,
+                "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "setPopup",
+        "type": "function",
+        "description": "Sets the html document to be opened as a popup when the user clicks on the browser action's icon.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": 0,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              },
+              "popup": {
+                "type": "string",
+                "description": "The html file to show in a popup.  If set to the empty string (''), no popup is shown."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "getPopup",
+        "type": "function",
+        "description": "Gets the html document set as the popup for this browser action.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": 0,
+                "description": "Specify the tab to get the popup from. If no tab is specified, the non-tab-specific popup is returned."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "result",
+                "type": "string"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "setBadgeText",
+        "unsupported": true,
+        "type": "function",
+        "description": "Sets the badge text for the browser action. The badge is displayed on top of the icon.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "text": {
+                "type": "string",
+                "description": "Any number of characters can be passed, but only about four can fit in the space."
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "getBadgeText",
+        "unsupported": true,
+        "type": "function",
+        "description": "Gets the badge text of the browser action. If no tab is specified, the non-tab-specific badge text is returned.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Specify the tab to get the badge text from. If no tab is specified, the non-tab-specific badge text is returned."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "result",
+                "type": "string"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "setBadgeBackgroundColor",
+        "unsupported": true,
+        "type": "function",
+        "description": "Sets the background color for the badge.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "color": {
+                "description": "An array of four integers in the range [0,255] that make up the RGBA color of the badge. For example, opaque red is <code>[255, 0, 0, 255]</code>. Can also be a string with a CSS value, with opaque red being <code>#FF0000</code> or <code>#F00</code>.",
+                "choices": [
+                  {"type": "string"},
+                  {"$ref": "ColorArray"}
+                ]
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "getBadgeBackgroundColor",
+        "unsupported": true,
+        "type": "function",
+        "description": "Gets the background color of the browser action.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Specify the tab to get the badge background color from. If no tab is specified, the non-tab-specific badge background color is returned."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "result",
+                "$ref": "ColorArray"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "enable",
+        "unsupported": true,
+        "type": "function",
+        "description": "Enables the browser action for a tab. By default, browser actions are enabled.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "integer",
+            "optional": true,
+            "name": "tabId",
+            "minimum": 0,
+            "description": "The id of the tab for which you want to modify the browser action."
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "disable",
+        "unsupported": true,
+        "type": "function",
+        "description": "Disables the browser action for a tab.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "integer",
+            "optional": true,
+            "name": "tabId",
+            "minimum": 0,
+            "description": "The id of the tab for which you want to modify the browser action."
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "openPopup",
+        "unsupported": true,
+        "type": "function",
+        "description": "Opens the extension popup window in the active window but does not grant tab permissions.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "popupView",
+                "type": "object",
+                "optional": true,
+                "description": "JavaScript 'window' object for the popup window if it was succesfully opened.",
+                "additionalProperties": { "type": "any" }
+              }
+            ]
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onClicked",
+        "type": "function",
+        "description": "Fired when a browser action icon is clicked.  This event will not fire if the browser action has a popup.",
+        "parameters": [
+          {
+            "name": "tab",
+            "$ref": "tabs.Tab"
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/mobile/android/components/extensions/schemas/jar.mn
+++ b/mobile/android/components/extensions/schemas/jar.mn
@@ -1,8 +1,10 @@
 # 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/.
 
 geckoview.jar:
+    content/schemas/browser_action.json
     content/schemas/browsing_data.json
     content/schemas/gecko_view_addons.json
+    content/schemas/page_action.json
     content/schemas/tabs.json
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -0,0 +1,240 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "page_action": {
+            "type": "object",
+            "additionalProperties": { "$ref": "UnrecognizedProperty" },
+            "properties": {
+              "default_title": {
+                "type": "string",
+                "optional": true,
+                "preprocess": "localize"
+              },
+              "default_icon": {
+                "$ref": "IconPath",
+                "optional": true
+              },
+              "default_popup": {
+                "type": "string",
+                "format": "relativeUrl",
+                "optional": true,
+                "preprocess": "localize"
+              },
+              "browser_style": {
+                "type": "boolean",
+                "optional": true,
+                "default": false
+              }
+            },
+            "optional": true
+          }
+        }
+      }
+    ]
+  },
+  {
+    "namespace": "pageAction",
+    "description": "Use the <code>browser.pageAction</code> API to put icons inside the address bar. Page actions represent actions that can be taken on the current page, but that aren't applicable to all pages.",
+    "permissions": ["manifest:page_action"],
+    "types": [
+      {
+        "id": "ImageDataType",
+        "type": "object",
+        "isInstanceOf": "ImageData",
+        "additionalProperties": { "type": "any" },
+        "description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
+      }
+    ],
+    "functions": [
+      {
+        "name": "show",
+        "type": "function",
+        "description": "Shows the page action. The page action is shown whenever the tab is selected.",
+        "async": "callback",
+        "parameters": [
+          {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "hide",
+        "type": "function",
+        "description": "Hides the page action.",
+        "async": "callback",
+        "parameters": [
+          {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "setTitle",
+        "unsupported": true,
+        "type": "function",
+        "description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+              "title": {"type": "string", "description": "The tooltip string."}
+            }
+          }
+        ]
+      },
+      {
+        "name": "getTitle",
+        "unsupported": true,
+        "type": "function",
+        "description": "Gets the title of the page action.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "description": "Specify the tab to get the title from."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "result",
+                "type": "string"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "setIcon",
+        "unsupported": true,
+        "type": "function",
+        "description": "Sets the icon for the page action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+              "imageData": {
+                "choices": [
+                  { "$ref": "ImageDataType" },
+                  {
+                    "type": "object",
+                    "additionalProperties": {"$ref": "ImageDataType"}
+                  }
+                ],
+                "optional": true,
+                "description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
+              },
+              "path": {
+                "choices": [
+                  { "type": "string" },
+                  {
+                    "type": "object",
+                    "additionalProperties": {"type": "string"}
+                  }
+                ],
+                "optional": true,
+                "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "setPopup",
+        "type": "function",
+        "async": "callback",
+        "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+              "popup": {
+                "type": "string",
+                "description": "The html file to show in a popup.  If set to the empty string (''), no popup is shown."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      },
+      {
+        "name": "getPopup",
+        "type": "function",
+        "description": "Gets the html document set as the popup for this page action.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "description": "Specify the tab to get the popup from."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onClicked",
+        "type": "function",
+        "description": "Fired when a page action icon is clicked.  This event will not fire if the page action has a popup.",
+        "parameters": [
+          {
+            "name": "tab",
+            "$ref": "tabs.Tab"
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/mobile/android/components/geckoview/GeckoViewStartup.js
+++ b/mobile/android/components/geckoview/GeckoViewStartup.js
@@ -64,18 +64,16 @@ GeckoViewStartup.prototype = {
 
         GeckoViewUtils.addLazyGetter(this, "GeckoViewConsole", {
           module: "resource://gre/modules/GeckoViewConsole.jsm",
         });
 
         GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", {
           module: "resource://gre/modules/GeckoViewWebExtension.jsm",
           ged: [
-            "GeckoView:BrowserAction:Click",
-            "GeckoView:PageAction:Click",
             "GeckoView:RegisterWebExtension",
             "GeckoView:UnregisterWebExtension",
             "GeckoView:WebExtension:PortDisconnect",
             "GeckoView:WebExtension:PortMessageFromApp",
           ],
         });
 
         GeckoViewUtils.addLazyGetter(this, "GeckoViewStorageController", {
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -21,16 +21,17 @@ import android.support.annotation.UiThre
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.view.ActionMode;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewStructure;
 import android.view.autofill.AutofillValue;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
@@ -596,17 +597,16 @@ package org.mozilla.geckoview {
     method @UiThread @Nullable public GeckoSession.ProgressDelegate getProgressDelegate();
     method @AnyThread @Nullable public GeckoSession.PromptDelegate getPromptDelegate();
     method @UiThread @Nullable public GeckoSession.ScrollDelegate getScrollDelegate();
     method @AnyThread @Nullable public GeckoSession.SelectionActionDelegate getSelectionActionDelegate();
     method @AnyThread @NonNull public GeckoSessionSettings getSettings();
     method @UiThread public void getSurfaceBounds(@NonNull Rect);
     method @AnyThread @NonNull public SessionTextInput getTextInput();
     method @AnyThread @NonNull public GeckoResult<String> getUserAgent();
-    method @AnyThread @Nullable public WebExtension.ActionDelegate getWebExtensionActionDelegate(@NonNull WebExtension);
     method @AnyThread public void goBack();
     method @AnyThread public void goForward();
     method @AnyThread public void gotoHistoryIndex(int);
     method @AnyThread public boolean isOpen();
     method @AnyThread public void loadData(@NonNull byte[], @Nullable String);
     method @AnyThread public void loadString(@NonNull String, @Nullable String);
     method @AnyThread public void loadUri(@NonNull String);
     method @AnyThread public void loadUri(@NonNull String, @Nullable Map<String,String>);
@@ -635,17 +635,16 @@ package org.mozilla.geckoview {
     method @AnyThread public void setMediaDelegate(@Nullable GeckoSession.MediaDelegate);
     method @AnyThread public void setMessageDelegate(@NonNull WebExtension, @Nullable WebExtension.MessageDelegate, @NonNull String);
     method @UiThread public void setNavigationDelegate(@Nullable GeckoSession.NavigationDelegate);
     method @UiThread public void setPermissionDelegate(@Nullable GeckoSession.PermissionDelegate);
     method @UiThread public void setProgressDelegate(@Nullable GeckoSession.ProgressDelegate);
     method @AnyThread public void setPromptDelegate(@Nullable GeckoSession.PromptDelegate);
     method @UiThread public void setScrollDelegate(@Nullable GeckoSession.ScrollDelegate);
     method @UiThread public void setSelectionActionDelegate(@Nullable GeckoSession.SelectionActionDelegate);
-    method @AnyThread public void setWebExtensionActionDelegate(@NonNull WebExtension, @Nullable WebExtension.ActionDelegate);
     method @AnyThread public void stop();
     method @UiThread protected void setShouldPinOnScreen(boolean);
     field public static final Parcelable.Creator<GeckoSession> CREATOR;
     field public static final int FINDER_DISPLAY_DIM_PAGE = 2;
     field public static final int FINDER_DISPLAY_DRAW_LINK_OUTLINE = 4;
     field public static final int FINDER_DISPLAY_HIGHLIGHT_ALL = 1;
     field public static final int FINDER_FIND_BACKWARDS = 1;
     field public static final int FINDER_FIND_LINKS_ONLY = 8;
@@ -1156,22 +1155,20 @@ package org.mozilla.geckoview {
     method @AnyThread @Nullable public GeckoSession getSession();
     method public int onGenericMotionEventForResult(@NonNull MotionEvent);
     method public int onTouchEventForResult(@NonNull MotionEvent);
     method @UiThread @Nullable public GeckoSession releaseSession();
     method public void setAutofillEnabled(boolean);
     method public void setDynamicToolbarMaxHeight(int);
     method @UiThread public void setSession(@NonNull GeckoSession);
     method public void setVerticalClipping(int);
-    method public void setViewBackend(int);
     method public boolean shouldPinOnScreen();
-    field public static final int BACKEND_SURFACE_VIEW = 1;
-    field public static final int BACKEND_TEXTURE_VIEW = 2;
     field @NonNull protected final GeckoView.Display mDisplay;
     field @Nullable protected GeckoSession mSession;
+    field @Nullable protected SurfaceView mSurfaceView;
   }
 
   @AnyThread public class GeckoWebExecutor {
     ctor public GeckoWebExecutor(@NonNull GeckoRuntime);
     method @NonNull public GeckoResult<WebResponse> fetch(@NonNull WebRequest);
     method @NonNull public GeckoResult<WebResponse> fetch(@NonNull WebRequest, int);
     method @NonNull public GeckoResult<InetAddress[]> resolve(@NonNull String);
     method public void speculativeConnect(@NonNull String);
@@ -1384,47 +1381,22 @@ package org.mozilla.geckoview {
     field public static final long PERMISSIONS = 64L;
     field public static final long SITE_DATA = 471L;
     field public static final long SITE_SETTINGS = 192L;
   }
 
   public class WebExtension {
     ctor public WebExtension(@NonNull String, @NonNull String, long);
     ctor public WebExtension(@NonNull String);
-    method @AnyThread public void setActionDelegate(@Nullable WebExtension.ActionDelegate);
     method @UiThread public void setMessageDelegate(@Nullable WebExtension.MessageDelegate, @NonNull String);
     field public final long flags;
     field @NonNull public final String id;
     field @NonNull public final String location;
   }
 
-  @AnyThread public static class WebExtension.Action {
-    ctor protected Action();
-    method @UiThread public void click();
-    method @NonNull public WebExtension.Action withDefault(@NonNull WebExtension.Action);
-    field @Nullable public final Integer badgeBackgroundColor;
-    field @Nullable public final String badgeText;
-    field @Nullable public final Integer badgeTextColor;
-    field @Nullable public final Boolean enabled;
-    field @Nullable public final WebExtension.ActionIcon icon;
-    field @Nullable public final String title;
-  }
-
-  public static interface WebExtension.ActionDelegate {
-    method @UiThread default public void onBrowserAction(@NonNull WebExtension, @Nullable GeckoSession, @NonNull WebExtension.Action);
-    method @UiThread @Nullable default public GeckoResult<GeckoSession> onOpenPopup(@NonNull WebExtension, @NonNull WebExtension.Action);
-    method @UiThread default public void onPageAction(@NonNull WebExtension, @Nullable GeckoSession, @NonNull WebExtension.Action);
-    method @UiThread @Nullable default public GeckoResult<GeckoSession> onTogglePopup(@NonNull WebExtension, @NonNull WebExtension.Action);
-  }
-
-  public static class WebExtension.ActionIcon {
-    ctor protected ActionIcon();
-    method @AnyThread @NonNull public GeckoResult<Bitmap> get(int);
-  }
-
   public static class WebExtension.Flags {
     ctor protected Flags();
     field public static final long ALLOW_CONTENT_MESSAGING = 1L;
     field public static final long NONE = 0L;
   }
 
   @UiThread public static interface WebExtension.MessageDelegate {
     method @Nullable default public void onConnect(@NonNull WebExtension.Port);
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/background.js
+++ /dev/null
@@ -1,140 +0,0 @@
-const port = browser.runtime.connectNative("browser");
-port.onMessage.addListener(message => {
-  handleMessage(message, null);
-});
-
-browser.runtime.onMessage.addListener((message, sender) => {
-  handleMessage(message, sender.tab.id);
-});
-
-browser.pageAction.onClicked.addListener(tab => {
-  port.postMessage({ method: "onClicked", tabId: tab.id, type: "pageAction" });
-});
-
-browser.browserAction.onClicked.addListener(tab => {
-  port.postMessage({
-    method: "onClicked",
-    tabId: tab.id,
-    type: "browserAction",
-  });
-});
-
-function handlePageActionMessage(message, tabId) {
-  switch (message.action) {
-    case "enable":
-      browser.pageAction.show(tabId);
-      break;
-
-    case "disable":
-      browser.pageAction.hide(tabId);
-      break;
-
-    case "setPopup":
-      browser.pageAction.setPopup({
-        tabId,
-        popup: message.popup,
-      });
-      break;
-
-    case "setTitle":
-      browser.pageAction.setTitle({
-        tabId,
-        title: message.title,
-      });
-      break;
-
-    case "setIcon":
-      browser.pageAction.setIcon({
-        tabId,
-        imageData: message.imageData,
-        path: message.path,
-      });
-      break;
-
-    default:
-      throw new Error(`Page Action does not support ${message.action}`);
-  }
-}
-
-function handleBrowserActionMessage(message, tabId) {
-  switch (message.action) {
-    case "enable":
-      browser.browserAction.enable(tabId);
-      break;
-
-    case "disable":
-      browser.browserAction.disable(tabId);
-      break;
-
-    case "setBadgeText":
-      browser.browserAction.setBadgeText({
-        tabId,
-        text: message.text,
-      });
-      break;
-
-    case "setBadgeTextColor":
-      browser.browserAction.setBadgeTextColor({
-        tabId,
-        color: message.color,
-      });
-      break;
-
-    case "setBadgeBackgroundColor":
-      browser.browserAction.setBadgeBackgroundColor({
-        tabId,
-        color: message.color,
-      });
-      break;
-
-    case "setPopup":
-      browser.browserAction.setPopup({
-        tabId,
-        popup: message.popup,
-      });
-      break;
-
-    case "setTitle":
-      browser.browserAction.setTitle({
-        tabId,
-        title: message.title,
-      });
-      break;
-
-    case "setIcon":
-      browser.browserAction.setIcon({
-        tabId,
-        imageData: message.imageData,
-        path: message.path,
-      });
-      break;
-
-    default:
-      throw new Error(`Browser Action does not support ${message.action}`);
-  }
-}
-
-function handleMessage(message, tabId) {
-  switch (message.type) {
-    case "ping":
-      port.postMessage({ method: "pong" });
-      return;
-
-    case "load":
-      browser.tabs.update(tabId, {
-        url: message.url,
-      });
-      return;
-
-    case "browserAction":
-      handleBrowserActionMessage(message, tabId);
-      return;
-
-    case "pageAction":
-      handlePageActionMessage(message, tabId);
-      return;
-
-    default:
-      throw new Error(`Unsupported message type ${message.type}`);
-  }
-}
deleted file mode 100644
index aea2c19784430b624d37821c2fc86386b40e52bd..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 90687de26d71e91b7c82565772a7df470ae277a6..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 90687de26d71e91b7c82565772a7df470ae277a6..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/icon.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<?xml version="1.0" ?><!DOCTYPE svg  PUBLIC '-//W3C//DTD SVG 1.1//EN'  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256  c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34  V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111  S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685  c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341  s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699  c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699  C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>
\ No newline at end of file
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/content.js
+++ /dev/null
@@ -1,4 +0,0 @@
-const port = browser.runtime.connectNative("browser");
-port.onMessage.addListener(message => {
-  browser.runtime.sendMessage(message);
-});
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/manifest.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
-  "manifest_version": 2,
-  "name": "actions",
-  "version": "1.0",
-  "description": "Defines Page and Browser actions",
-  "browser_action": {
-    "default_title": "Test action default"
-  },
-  "page_action": {
-    "default_title": "Test action default",
-    "default_icon": {
-      "19": "button/geo-19.png",
-      "38": "button/geo-38.png"
-    }
-  },
-  "background": {
-    "scripts": ["background.js"]
-  },
-  "content_scripts": [
-    {
-      "matches": ["<all_urls>"],
-      "js": ["content.js"]
-    }
-  ],
-  "permissions": [
-    "tabs",
-    "geckoViewAddons",
-    "nativeMessaging"
-  ]
-}
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-browser-action.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<html>
-<head>
-    <script type="text/javascript" src="test-open-popup-browser-action.js"></script>
-</head>
-<body>
-    <body style="height: 100%">
-        <p>Hello, world!</p>
-    </body>
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-browser-action.js
+++ /dev/null
@@ -1,7 +0,0 @@
-window.addEventListener("DOMContentLoaded", init);
-
-function init() {
-  document.body.addEventListener("click", event => {
-    browser.browserAction.openPopup();
-  });
-}
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-page-action.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<html>
-<head>
-    <script type="text/javascript" src="test-open-popup-page-action.js"></script>
-</head>
-<body>
-    <body style="height: 100%">
-        <p>Hello, world!</p>
-    </body>
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-page-action.js
+++ /dev/null
@@ -1,7 +0,0 @@
-window.addEventListener("DOMContentLoaded", init);
-
-function init() {
-  document.body.addEventListener("click", event => {
-    browser.pageAction.openPopup();
-  });
-}
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-popup.html
+++ /dev/null
@@ -1,1 +0,0 @@
-<h1> HELLO </h1>
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify/icons/icon.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<?xml version="1.0" ?><!DOCTYPE svg  PUBLIC '-//W3C//DTD SVG 1.1//EN'  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256  c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34  V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111  S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685  c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341  s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699  c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699  C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>
\ No newline at end of file
deleted file mode 100644
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ExtensionActionTest.kt
+++ /dev/null
@@ -1,540 +0,0 @@
-package org.mozilla.geckoview.test
-
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.support.test.InstrumentationRegistry
-import android.support.test.filters.MediumTest
-import org.hamcrest.Matchers.equalTo
-import org.json.JSONObject
-import org.junit.After
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Assume.assumeThat
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mozilla.geckoview.GeckoResult
-import org.mozilla.geckoview.GeckoSession
-import org.mozilla.geckoview.WebExtension
-import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
-
-@MediumTest
-@RunWith(Parameterized::class)
-class ExtensionActionTest : BaseSessionTest() {
-    var extension: WebExtension? = null
-    var default: WebExtension.Action? = null
-    var backgroundPort: WebExtension.Port? = null
-    var windowPort: WebExtension.Port? = null
-
-    companion object {
-        @get:Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        val parameters: List<Array<out Any>> = listOf(
-                arrayOf("#pageAction"),
-                arrayOf("#browserAction"))
-    }
-
-    @field:Parameterized.Parameter(0) @JvmField var id: String = ""
-
-    @Before
-    fun setup() {
-        // This method installs the extension, opens up ports with the background script and the
-        // content script and captures the default action definition from the manifest
-        val browserActionDefaultResult = GeckoResult<WebExtension.Action>()
-        val pageActionDefaultResult = GeckoResult<WebExtension.Action>()
-
-        val windowPortResult = GeckoResult<WebExtension.Port>()
-        val backgroundPortResult = GeckoResult<WebExtension.Port>()
-
-        extension = WebExtension("resource://android/assets/web_extensions/actions/",
-                "actions", WebExtension.Flags.ALLOW_CONTENT_MESSAGING)
-
-        sessionRule.session.setMessageDelegate(
-                extension!!,
-                object : WebExtension.MessageDelegate {
-                    override fun onConnect(port: WebExtension.Port) {
-                        windowPortResult.complete(port)
-                    }
-                }, "browser")
-        extension!!.setMessageDelegate(object : WebExtension.MessageDelegate {
-            override fun onConnect(port: WebExtension.Port) {
-                backgroundPortResult.complete(port)
-            }
-        }, "browser")
-
-        sessionRule.addExternalDelegateDuringNextWait(
-                WebExtension.ActionDelegate::class,
-                extension!!::setActionDelegate,
-                { extension!!.setActionDelegate(null) },
-        object : WebExtension.ActionDelegate {
-            override fun onBrowserAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
-                assertEquals(action.title, "Test action default")
-                browserActionDefaultResult.complete(action)
-            }
-            override fun onPageAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
-                assertEquals(action.title, "Test action default")
-                pageActionDefaultResult.complete(action)
-            }
-        })
-
-        sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(extension!!))
-
-        sessionRule.session.loadUri("http://example.com")
-        sessionRule.waitForPageStop()
-
-        default = when (id) {
-            "#pageAction" -> sessionRule.waitForResult(pageActionDefaultResult)
-            "#browserAction" -> sessionRule.waitForResult(browserActionDefaultResult)
-            else -> throw IllegalArgumentException()
-        }
-
-        windowPort = sessionRule.waitForResult(windowPortResult)
-        backgroundPort = sessionRule.waitForResult(backgroundPortResult)
-
-        if (id == "#pageAction") {
-            // Make sure that the pageAction starts enabled for this tab
-            testActionApi("""{"action": "enable"}""") { action ->
-                assertEquals(action.enabled, true)
-            }
-        }
-    }
-
-    private var type: String = ""
-        get() = when(id) {
-            "#pageAction" -> "pageAction"
-            "#browserAction" -> "browserAction"
-            else -> throw IllegalArgumentException()
-        }
-
-    @After
-    fun tearDown() {
-        sessionRule.waitForResult(sessionRule.runtime.unregisterWebExtension(extension!!))
-    }
-
-    private fun testBackgroundActionApi(message: String, tester: (WebExtension.Action) -> Unit) {
-        val result = GeckoResult<Void>()
-
-        val json = JSONObject(message)
-        json.put("type", type)
-
-        backgroundPort!!.postMessage(json)
-
-        sessionRule.addExternalDelegateDuringNextWait(
-                WebExtension.ActionDelegate::class,
-                extension!!::setActionDelegate,
-                { extension!!.setActionDelegate(null) },
-                object : WebExtension.ActionDelegate {
-            override fun onBrowserAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
-                assertEquals(id, "#browserAction")
-                default = action
-                tester(action)
-                result.complete(null)
-            }
-            override fun onPageAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
-                assertEquals(id, "#pageAction")
-                default = action
-                tester(action)
-                result.complete(null)
-            }
-        })
-
-        sessionRule.waitForResult(result)
-    }
-
-    private fun testActionApi(message: String, tester: (WebExtension.Action) -> Unit) {
-        val result = GeckoResult<Void>()
-
-        val json = JSONObject(message)
-        json.put("type", type)
-
-        windowPort!!.postMessage(json)
-
-        sessionRule.addExternalDelegateDuringNextWait(
-                WebExtension.ActionDelegate::class,
-                { delegate ->
-                    sessionRule.session.setWebExtensionActionDelegate(extension!!, delegate) },
-                { sessionRule.session.setWebExtensionActionDelegate(extension!!, null) },
-        object : WebExtension.ActionDelegate {
-            override fun onBrowserAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
-                assertEquals(id, "#browserAction")
-                val resolved = action.withDefault(default!!)
-                tester(resolved)
-                result.complete(null)
-            }
-            override fun onPageAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
-                assertEquals(id, "#pageAction")
-                val resolved = action.withDefault(default!!)
-                tester(resolved)
-                result.complete(null)
-            }
-        })
-
-        sessionRule.waitForResult(result)
-    }
-
-    @Test
-    fun disableTest() {
-        testActionApi("""{"action": "disable"}""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, false)
-        }
-    }
-
-    @Test
-    fun enableTest() {
-        // First, make sure the action is disabled
-        testActionApi("""{"action": "disable"}""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, false)
-        }
-
-        testActionApi("""{"action": "enable"}""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, true)
-        }
-    }
-
-    @Test
-    fun setOverridenTitle() {
-        testActionApi("""{
-               "action": "setTitle",
-               "title": "overridden title"
-            }""") { action ->
-            assertEquals(action.title, "overridden title")
-            assertEquals(action.enabled, true)
-        }
-    }
-
-    @Test
-    fun setBadgeText() {
-        assumeThat("Only browserAction supports this API.", id, equalTo("#browserAction"))
-
-        testActionApi("""{
-           "action": "setBadgeText",
-           "text": "12"
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.badgeText, "12")
-            assertEquals(action.enabled, true)
-        }
-    }
-
-    @Test
-    fun setBadgeBackgroundColor() {
-        assumeThat("Only browserAction supports this API.", id, equalTo("#browserAction"))
-
-        colorTest("setBadgeBackgroundColor", "#ABCDEF", "#FFABCDEF")
-        colorTest("setBadgeBackgroundColor", "#F0A", "#FFFF00AA")
-        colorTest("setBadgeBackgroundColor", "red", "#FFFF0000")
-        colorTest("setBadgeBackgroundColor", "rgb(0, 0, 255)", "#FF0000FF")
-        colorTest("setBadgeBackgroundColor", "rgba(0, 255, 0, 0.5)", "#8000FF00")
-        colorRawTest("setBadgeBackgroundColor", "[0, 0, 255, 128]", "#800000FF")
-    }
-
-    private fun colorTest(actionName: String, color: String, expectedHex: String) {
-        colorRawTest(actionName, "\"$color\"", expectedHex)
-    }
-
-    private fun colorRawTest(actionName: String, color: String, expectedHex: String) {
-        testActionApi("""{
-           "action": "$actionName",
-           "color": $color
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.badgeText, "")
-            assertEquals(action.enabled, true)
-
-            val result = when (actionName) {
-                "setBadgeTextColor" -> action.badgeTextColor!!
-                "setBadgeBackgroundColor" -> action.badgeBackgroundColor!!
-                else -> throw IllegalArgumentException()
-            }
-
-            val hexColor = String.format("#%08X", result)
-            assertEquals(hexColor, "$expectedHex")
-        }
-    }
-
-    @Test
-    fun setBadgeTextColor() {
-        assumeThat("Only browserAction supports this API.", id, equalTo("#browserAction"))
-
-        colorTest("setBadgeTextColor", "#ABCDEF", "#FFABCDEF")
-        colorTest("setBadgeTextColor", "#F0A", "#FFFF00AA")
-        colorTest("setBadgeTextColor", "red", "#FFFF0000")
-        colorTest("setBadgeTextColor", "rgb(0, 0, 255)", "#FF0000FF")
-        colorTest("setBadgeTextColor", "rgba(0, 255, 0, 0.5)", "#8000FF00")
-        colorRawTest("setBadgeTextColor", "[0, 0, 255, 128]", "#800000FF")
-    }
-
-    @Test
-    fun setDefaultTitle() {
-        assumeThat("Only browserAction supports default properties.", id, equalTo("#browserAction"))
-
-        // Setting a default value will trigger the default handler on the extension object
-        testBackgroundActionApi("""{
-            "action": "setTitle",
-            "title": "new default title"
-        }""") { action ->
-            assertEquals(action.title, "new default title")
-            assertEquals(action.badgeText, "")
-            assertEquals(action.enabled, true)
-        }
-
-        // When an overridden title is set, the default has no effect
-        testActionApi("""{
-           "action": "setTitle",
-           "title": "test override"
-        }""") { action ->
-            assertEquals(action.title, "test override")
-            assertEquals(action.badgeText, "")
-            assertEquals(action.enabled, true)
-        }
-
-        // When the override is null, the new default takes effect
-        testActionApi("""{
-           "action": "setTitle",
-           "title": null
-        }""") { action ->
-            assertEquals(action.title, "new default title")
-            assertEquals(action.badgeText, "")
-            assertEquals(action.enabled, true)
-        }
-
-        // When the default value is null, the manifest value is used
-        testBackgroundActionApi("""{
-           "action": "setTitle",
-           "title": null
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.badgeText, "")
-            assertEquals(action.enabled, true)
-        }
-    }
-
-    private fun compareBitmap(expectedLocation: String, actual: Bitmap) {
-        val stream = InstrumentationRegistry.getTargetContext().assets
-                .open(expectedLocation)
-
-        val expected = BitmapFactory.decodeStream(stream)
-        for (x in 0 until actual.height) {
-            for (y in 0 until actual.width) {
-                assertEquals(expected.getPixel(x, y), actual.getPixel(x, y))
-            }
-        }
-    }
-
-    @Test
-    fun setIconSvg() {
-        val svg = GeckoResult<Void>()
-
-        testActionApi("""{
-           "action": "setIcon",
-           "path": "button/icon.svg"
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, true)
-
-            action.icon!!.get(100).accept { actual ->
-                compareBitmap("web_extensions/actions/button/expected.png", actual!!)
-                svg.complete(null)
-            }
-        }
-
-        sessionRule.waitForResult(svg)
-    }
-
-    @Test
-    fun setIconPng() {
-        val png100 = GeckoResult<Void>()
-        val png38 = GeckoResult<Void>()
-        val png19 = GeckoResult<Void>()
-        val png10 = GeckoResult<Void>()
-
-        testActionApi("""{
-           "action": "setIcon",
-           "path": {
-             "19": "button/geo-19.png",
-             "38": "button/geo-38.png"
-           }
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, true)
-
-            action.icon!!.get(100).accept { actual ->
-                compareBitmap("web_extensions/actions/button/geo-38.png", actual!!)
-                png100.complete(null)
-            }
-
-            action.icon!!.get(38).accept { actual ->
-                compareBitmap("web_extensions/actions/button/geo-38.png", actual!!)
-                png38.complete(null)
-            }
-
-            action.icon!!.get(19).accept { actual ->
-                compareBitmap("web_extensions/actions/button/geo-19.png", actual!!)
-                png19.complete(null)
-            }
-
-            action.icon!!.get(10).accept { actual ->
-                compareBitmap("web_extensions/actions/button/geo-19.png", actual!!)
-                png10.complete(null)
-            }
-        }
-
-        sessionRule.waitForResult(png100)
-        sessionRule.waitForResult(png38)
-        sessionRule.waitForResult(png19)
-        sessionRule.waitForResult(png10)
-    }
-
-    @Test
-    fun setIconError() {
-        val error = GeckoResult<Void>()
-
-        testActionApi("""{
-            "action": "setIcon",
-            "path": "invalid/path/image.png"
-        }""") { action ->
-            action.icon!!.get(38).accept({
-                error.completeExceptionally(RuntimeException("Should not succeed."))
-            }, { exception ->
-                assertTrue(exception is IllegalArgumentException)
-                error.complete(null)
-            })
-        }
-
-        sessionRule.waitForResult(error)
-    }
-
-    @Test
-    @GeckoSessionTestRule.WithDisplay(width=100, height=100)
-    @Ignore // this test fails intermittently on try :(
-    fun testOpenPopup() {
-        // First, let's make sure we have a popup set
-        val actionResult = GeckoResult<Void>()
-        testActionApi("""{
-           "action": "setPopup",
-           "popup": "test-popup.html"
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, true)
-
-            actionResult.complete(null)
-        }
-
-        val url = when(id) {
-            "#browserAction" -> "/test-open-popup-browser-action.html"
-            "#pageAction" -> "/test-open-popup-page-action.html"
-            else -> throw IllegalArgumentException()
-        }
-
-        windowPort!!.postMessage(JSONObject("""{
-            "type": "load",
-            "url": "$url"
-        }"""))
-
-        val openPopup = GeckoResult<Void>()
-        sessionRule.session.setWebExtensionActionDelegate(extension!!,
-                object : WebExtension.ActionDelegate {
-            override fun onOpenPopup(extension: WebExtension,
-                                     popupAction: WebExtension.Action): GeckoResult<GeckoSession>? {
-                assertEquals(extension, this@ExtensionActionTest.extension)
-                // assertEquals(popupAction, this@ExtensionActionTest.default)
-                openPopup.complete(null)
-                return null
-            }
-        })
-
-        sessionRule.waitForPageStops(2)
-        // openPopup needs user activation
-        sessionRule.session.synthesizeTap(50, 50)
-
-        sessionRule.waitForResult(openPopup)
-    }
-
-    @Test
-    fun testClickWhenPopupIsNotDefined() {
-        val pong = GeckoResult<Void>()
-
-        backgroundPort!!.setDelegate(object : WebExtension.PortDelegate {
-            override fun onPortMessage(message: Any, port: WebExtension.Port) {
-                val json = message as JSONObject
-                if (json.getString("method") == "pong") {
-                    pong.complete(null)
-                } else {
-                    // We should NOT receive onClicked here
-                    pong.completeExceptionally(IllegalArgumentException(
-                            "Received unexpected: ${json.getString("method")}"))
-                }
-            }
-        })
-
-        val actionResult = GeckoResult<WebExtension.Action>()
-
-        testActionApi("""{
-           "action": "setPopup",
-           "popup": "test-popup.html"
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, true)
-
-            actionResult.complete(action)
-        }
-
-        val togglePopup = GeckoResult<Void>()
-        val action = sessionRule.waitForResult(actionResult)
-
-        extension!!.setActionDelegate(object : WebExtension.ActionDelegate {
-            override fun onTogglePopup(extension: WebExtension,
-                                     popupAction: WebExtension.Action): GeckoResult<GeckoSession>? {
-                assertEquals(extension, this@ExtensionActionTest.extension)
-                assertEquals(popupAction, action)
-                togglePopup.complete(null)
-                return null
-            }
-        })
-
-        // This click() will not cause an onClicked callback because popup is set
-        action.click()
-
-        // but it will cause togglePopup to be called
-        sessionRule.waitForResult(togglePopup)
-
-        // If the response to ping reaches us before the onClicked we know onClicked wasn't called
-        backgroundPort!!.postMessage(JSONObject("""{
-            "type": "ping"
-        }"""))
-
-        sessionRule.waitForResult(pong)
-    }
-
-    @Test
-    fun testClickWhenPopupIsDefined() {
-        val onClicked = GeckoResult<Void>()
-        backgroundPort!!.setDelegate(object : WebExtension.PortDelegate {
-            override fun onPortMessage(message: Any, port: WebExtension.Port) {
-                val json = message as JSONObject
-                assertEquals(json.getString("method"), "onClicked")
-                assertEquals(json.getString("type"), type)
-                onClicked.complete(null)
-            }
-        })
-
-        testActionApi("""{
-           "action": "setPopup",
-           "popup": null
-        }""") { action ->
-            assertEquals(action.title, "Test action default")
-            assertEquals(action.enabled, true)
-
-            // This click() WILL cause an onClicked callback
-            action.click()
-        }
-
-        sessionRule.waitForResult(onClicked)
-    }
-}
-
deleted file mode 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java
+++ /dev/null
@@ -1,174 +0,0 @@
-package org.mozilla.gecko;
-
-import android.content.Context;
-import android.graphics.SurfaceTexture;
-import android.util.Log;
-import android.view.Surface;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.TextureView;
-import android.view.View;
-
-/** Provides transparent access to either a SurfaceView or TextureView */
-public class SurfaceViewWrapper {
-    private static final String LOGTAG = "SurfaceViewWrapper";
-
-    private ListenerWrapper mListenerWrapper;
-    private View mView;
-
-    // Only one of these will be non-null at any point in time
-    SurfaceView mSurfaceView;
-    TextureView mTextureView;
-
-    public SurfaceViewWrapper(final Context context) {
-        // By default, use SurfaceView
-        mListenerWrapper = new ListenerWrapper();
-        mSurfaceView = new SurfaceView(context);
-        mView = mSurfaceView;
-    }
-
-    public void useSurfaceView(final Context context) {
-        if (mTextureView != null) {
-            mListenerWrapper.onSurfaceTextureDestroyed(
-                    mTextureView.getSurfaceTexture());
-            mTextureView = null;
-        }
-        mListenerWrapper.reset();
-        mSurfaceView = new SurfaceView(context);
-        mSurfaceView.getHolder().addCallback(mListenerWrapper);
-        mView = mSurfaceView;
-    }
-
-    public void useTextureView(final Context context) {
-        if (mSurfaceView != null) {
-            mListenerWrapper.surfaceDestroyed(mSurfaceView.getHolder());
-            mSurfaceView = null;
-        }
-        mListenerWrapper.reset();
-        mTextureView = new TextureView(context);
-        mTextureView.setSurfaceTextureListener(mListenerWrapper);
-        mView = mTextureView;
-    }
-
-    public void setBackgroundColor(final int color) {
-        if (mSurfaceView != null) {
-            mSurfaceView.setBackgroundColor(color);
-        } else {
-            Log.e(LOGTAG, "TextureView doesn't support background color.");
-        }
-    }
-
-    public void setListener(final Listener listener) {
-        mListenerWrapper.mListener = listener;
-        mSurfaceView.getHolder().addCallback(mListenerWrapper);
-    }
-
-    public int getWidth() {
-        if (mSurfaceView != null) {
-            return mSurfaceView.getHolder().getSurfaceFrame().right;
-        }
-        return mListenerWrapper.mWidth;
-    }
-
-    public int getHeight() {
-        if (mSurfaceView != null) {
-            return mSurfaceView.getHolder().getSurfaceFrame().bottom;
-        }
-        return mListenerWrapper.mHeight;
-    }
-
-    public Surface getSurface() {
-        if (mSurfaceView != null) {
-            return mSurfaceView.getHolder().getSurface();
-        }
-
-        return mListenerWrapper.mSurface;
-    }
-
-    public View getView() {
-        return mView;
-    }
-
-    /**
-     * Translates SurfaceTextureListener and SurfaceHolder.Callback into a common interface
-     * SurfaceViewWrapper.Listener
-     */
-    private static class ListenerWrapper implements TextureView.SurfaceTextureListener,
-            SurfaceHolder.Callback {
-        private Listener mListener;
-
-        // TextureView doesn't provide getters for these so we keep track of them here
-        private Surface mSurface;
-        private int mWidth;
-        private int mHeight;
-
-        public void reset() {
-            mWidth = 0;
-            mHeight = 0;
-            mSurface = null;
-        }
-
-        // TextureView
-        @Override
-        public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width,
-                                              final int height) {
-            mSurface = new Surface(surface);
-            mWidth = width;
-            mHeight = height;
-            if (mListener != null) {
-                mListener.onSurfaceChanged(mSurface, width, height);
-            }
-        }
-
-        @Override
-        public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width,
-                                                final int height) {
-            mWidth = width;
-            mHeight = height;
-            if (mListener != null) {
-                mListener.onSurfaceChanged(mSurface, mWidth, mHeight);
-            }
-        }
-
-        @Override
-        public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
-            if (mListener != null) {
-                mListener.onSurfaceDestroyed();
-            }
-            mSurface = null;
-            return false;
-        }
-
-        @Override
-        public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
-            mSurface = new Surface(surface);
-            if (mListener != null) {
-                mListener.onSurfaceChanged(mSurface, mWidth, mHeight);
-            }
-        }
-
-        // SurfaceView
-        @Override
-        public void surfaceCreated(final SurfaceHolder holder) {}
-
-        @Override
-        public void surfaceChanged(final SurfaceHolder holder, final int format, final int width,
-                                   final int height) {
-            if (mListener != null) {
-                mListener.onSurfaceChanged(holder.getSurface(), width, height);
-            }
-        }
-
-        @Override
-        public void surfaceDestroyed(final SurfaceHolder holder) {
-            if (mListener != null) {
-                mListener.onSurfaceDestroyed();
-            }
-        }
-    }
-
-    public interface Listener {
-        void onSurfaceChanged(Surface surface, int width, int height);
-        void onSurfaceDestroyed();
-    }
-}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -9,16 +9,17 @@ package org.mozilla.geckoview;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.nio.ByteBuffer;
 import java.util.AbstractSequentialList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.UUID;
@@ -326,32 +327,106 @@ public class GeckoSession implements Par
 
                     result.accept(
                         visited -> callback.sendSuccess(visited),
                         exception -> callback.sendError("Failed to fetch visited statuses for URIs"));
                 }
             }
         };
 
-    private final WebExtension.Listener mWebExtensionListener;
+    private static class WebExtensionSender {
+        public String webExtensionId;
+        public String nativeApp;
+
+        public WebExtensionSender(final String webExtensionId, final String nativeApp) {
+            this.webExtensionId = webExtensionId;
+            this.nativeApp = nativeApp;
+        }
+
+        @Override
+        public boolean equals(final Object other) {
+            if (!(other instanceof WebExtensionSender)) {
+                return false;
+            }
+
+            WebExtensionSender o = (WebExtensionSender) other;
+            return webExtensionId.equals(o.webExtensionId) &&
+                    nativeApp.equals(o.nativeApp);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = 17;
+            result = 31 * result + (webExtensionId != null ? webExtensionId.hashCode() : 0);
+            result = 31 * result + (nativeApp != null ? nativeApp.hashCode() : 0);
+            return result;
+        }
+    }
+
+    private final class WebExtensionListener implements BundleEventListener {
+        final private HashMap<WebExtensionSender, WebExtension.MessageDelegate> mMessageDelegates;
+
+        public WebExtensionListener() {
+            mMessageDelegates = new HashMap<>();
+        }
+
+        /* package */ void registerListeners() {
+            getEventDispatcher().registerUiThreadListener(this,
+                    "GeckoView:WebExtension:Message",
+                    "GeckoView:WebExtension:PortMessage",
+                    "GeckoView:WebExtension:Connect",
+                    "GeckoView:WebExtension:CloseTab",
+                    null);
+        }
+
+        public void setDelegate(final WebExtension webExtension,
+                                final WebExtension.MessageDelegate delegate,
+                                final String nativeApp) {
+            mMessageDelegates.put(new WebExtensionSender(webExtension.id, nativeApp), delegate);
+        }
+
+        public WebExtension.MessageDelegate getDelegate(final WebExtension webExtension,
+                                                        final String nativeApp) {
+            return mMessageDelegates.get(new WebExtensionSender(webExtension.id, nativeApp));
+        }
+
+        @Override
+        public void handleMessage(final String event, final GeckoBundle message,
+                                  final EventCallback callback) {
+            if (mWindow == null) {
+                return;
+            }
+
+            if ("GeckoView:WebExtension:Message".equals(event)
+                    || "GeckoView:WebExtension:PortMessage".equals(event)
+                    || "GeckoView:WebExtension:Connect".equals(event)) {
+                mWindow.runtime.getWebExtensionDispatcher()
+                        .handleMessage(event, message, callback, GeckoSession.this);
+            } else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
+                mWindow.runtime.getWebExtensionController().closeTab(message, callback, GeckoSession.this);
+            }
+        }
+    }
+
+    private final WebExtensionListener mWebExtensionListener;
 
     /**
      * Get the message delegate for <code>nativeApp</code>.
      *
      * @param webExtension {@link WebExtension} that this delegate receives messages from.
      * @param nativeApp identifier for the native app
      * @return The {@link WebExtension.MessageDelegate} attached to the
      *         <code>nativeApp</code>.  <code>null</code> if no delegate is
      *         present.
      */
     @AnyThread
     public @Nullable WebExtension.MessageDelegate getMessageDelegate(
             final @NonNull WebExtension webExtension,
             final @NonNull String nativeApp) {
-        return mWebExtensionListener.getMessageDelegate(webExtension, nativeApp);
+        return mWebExtensionListener.getDelegate(webExtension, nativeApp);
     }
 
     /**
      * Defines a message delegate for a Native App.
      *
      * If a delegate is already present, this delegate will replace the
      * existing one.
      *
@@ -370,51 +445,17 @@ public class GeckoSession implements Par
      * @param nativeApp which native app id this message delegate will handle
      *                  messaging for.
      * @see WebExtension#setMessageDelegate
      */
     @AnyThread
     public void setMessageDelegate(final @NonNull WebExtension webExtension,
                                    final @Nullable WebExtension.MessageDelegate delegate,
                                    final @NonNull String nativeApp) {
-        mWebExtensionListener.setMessageDelegate(webExtension, delegate, nativeApp);
-    }
-
-    /**
-     * Set the Action delegate for this session.
-     *
-     * This delegate will receive page and browser action overrides specific to
-     * this session.  The default Action will be received by the delegate set
-     * by {@link WebExtension#setActionDelegate}.
-     *
-     * @param webExtension the {@link WebExtension} object this delegate will
-     *                     receive updates for
-     * @param delegate the {@link WebExtension.ActionDelegate} that will
-     *                 receive updates.
-     * @see WebExtension.Action
-     */
-    @AnyThread
-    public void setWebExtensionActionDelegate(final @NonNull WebExtension webExtension,
-                                              final @Nullable WebExtension.ActionDelegate delegate) {
-        mWebExtensionListener.setActionDelegate(webExtension, delegate);
-    }
-
-    /**
-     * Get the Action delegate for this session.
-     *
-     * @param webExtension {@link WebExtension} that this delegates receive
-     *                     updates for.
-     * @return {@link WebExtension.ActionDelegate} for this
-     *         session
-     */
-    @AnyThread
-    @Nullable
-    public WebExtension.ActionDelegate getWebExtensionActionDelegate(
-            final @NonNull WebExtension webExtension) {
-        return mWebExtensionListener.getActionDelegate(webExtension);
+        mWebExtensionListener.setDelegate(webExtension, delegate, nativeApp);
     }
 
     private final GeckoSessionHandler<ContentDelegate> mContentHandler =
         new GeckoSessionHandler<ContentDelegate>(
             "GeckoViewContent", this,
             new String[]{
                 "GeckoView:ContentCrash",
                 "GeckoView:ContentKill",
@@ -1248,17 +1289,17 @@ public class GeckoSession implements Par
     public GeckoSession() {
         this(null);
     }
 
     public GeckoSession(final @Nullable GeckoSessionSettings settings) {
         mSettings = new GeckoSessionSettings(settings, this);
         mListener.registerListeners();
 
-        mWebExtensionListener = new WebExtension.Listener(this);
+        mWebExtensionListener = new WebExtensionListener();
         mWebExtensionListener.registerListeners();
 
         if (BuildConfig.DEBUG && handlersCount != mSessionHandlers.length) {
             throw new AssertionError("Add new handler to handlers list");
         }
     }
 
     /* package */ @Nullable GeckoRuntime getRuntime() {
@@ -1294,17 +1335,16 @@ public class GeckoSession implements Par
         mSettings = new GeckoSessionSettings(settings, this);
         mId = id;
 
         if (mWindow != null) {
             mWindow.transfer(this, mNativeQueue, mCompositor,
                     mEventDispatcher, mAccessibility != null ? mAccessibility.nativeProvider : null,
                     createInitData());
             onWindowChanged(WINDOW_TRANSFER_IN, /* inProgress */ false);
-            mWebExtensionListener.runtime = mWindow.runtime;
         }
     }
 
     /* package */ void transferFrom(final GeckoSession session) {
         transferFrom(session.mWindow, session.mSettings, session.mId);
         session.mWindow = null;
     }
 
@@ -1415,17 +1455,16 @@ public class GeckoSession implements Par
         }
 
         final String chromeUri = mSettings.getChromeUri();
         final int screenId = mSettings.getScreenId();
         final boolean isPrivate = mSettings.getUsePrivateMode();
         final boolean isRemote = mSettings.getUseMultiprocess();
 
         mWindow = new Window(runtime, this, mNativeQueue);
-        mWebExtensionListener.runtime = runtime;
 
         onWindowChanged(WINDOW_OPEN, /* inProgress */ true);
 
         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
             Window.open(mWindow, mNativeQueue, mCompositor, mEventDispatcher,
                         mAccessibility != null ? mAccessibility.nativeProvider : null,
                         createInitData(), mId, chromeUri, screenId, isPrivate, isRemote);
         } else {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionSettings.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionSettings.java
@@ -332,23 +332,16 @@ public final class GeckoSessionSettings 
             new Key<Boolean>("allowJavascript", /* initOnly */ false, /* values */ null);
     /**
      * Key to specify if entire accessible tree should be exposed with no caching.
      */
     private static final Key<Boolean> FULL_ACCESSIBILITY_TREE =
             new Key<Boolean>("fullAccessibilityTree", /* initOnly */ false, /* values */ null);
 
     /**
-     * Key to specify if this GeckoSession is a Popup or not. Popup sessions can paint over other
-     * sessions and are not exposed to the tabs WebExtension API.
-     */
-    private static final Key<Boolean> IS_POPUP =
-            new Key<Boolean>("isPopup", /* initOnly */ false, /* values */ null);
-
-    /**
      * Internal Gecko key to specify the session context ID.
      * Derived from `UNSAFE_CONTEXT_ID`.
      */
     private static final Key<String> CONTEXT_ID =
         new Key<String>("sessionContextId", /* initOnly */ true, /* values */ null);
 
     /**
      * User-provided key to specify the session context ID.
@@ -380,17 +373,16 @@ public final class GeckoSessionSettings 
         mBundle.putString(CHROME_URI.name, null);
         mBundle.putInt(SCREEN_ID.name, 0);
         mBundle.putBoolean(USE_TRACKING_PROTECTION.name, false);
         mBundle.putBoolean(USE_PRIVATE_MODE.name, false);
         mBundle.putBoolean(USE_MULTIPROCESS.name, true);
         mBundle.putBoolean(SUSPEND_MEDIA_WHEN_INACTIVE.name, false);
         mBundle.putBoolean(ALLOW_JAVASCRIPT.name, true);
         mBundle.putBoolean(FULL_ACCESSIBILITY_TREE.name, false);
-        mBundle.putBoolean(IS_POPUP.name, false);
         mBundle.putInt(USER_AGENT_MODE.name, USER_AGENT_MODE_MOBILE);
         mBundle.putString(USER_AGENT_OVERRIDE.name, null);
         mBundle.putInt(VIEWPORT_MODE.name, VIEWPORT_MODE_MOBILE);
         mBundle.putInt(DISPLAY_MODE.name, DISPLAY_MODE_BROWSER);
         mBundle.putString(CONTEXT_ID.name, null);
         mBundle.putString(UNSAFE_CONTEXT_ID.name, null);
     }
 
@@ -452,20 +444,16 @@ public final class GeckoSessionSettings 
      *
      * @param value A flag determining full accessibility tree should be exposed.
      *             Default is false.
      */
     public void setFullAccessibilityTree(final boolean value) {
         setBoolean(FULL_ACCESSIBILITY_TREE, value);
     }
 
-    /* package */ void setIsPopup(final boolean value) {
-        setBoolean(IS_POPUP, value);
-    }
-
     private void setBoolean(final Key<Boolean> key, final boolean value) {
         synchronized (mBundle) {
             if (valueChangedLocked(key, value)) {
                 mBundle.putBoolean(key.name, value);
                 dispatchUpdate();
             }
         }
     }
@@ -529,20 +517,16 @@ public final class GeckoSessionSettings 
      * Whether entire accessible tree is exposed with no caching.
      *
      * @return true if accessibility tree is exposed, false if not.
      */
     public boolean getFullAccessibilityTree() {
         return getBoolean(FULL_ACCESSIBILITY_TREE);
     }
 
-    /* package */ boolean getIsPopup() {
-        return getBoolean(IS_POPUP);
-    }
-
     private boolean getBoolean(final Key<Boolean> key) {
         synchronized (mBundle) {
             return mBundle.getBoolean(key.name);
         }
     }
 
     /**
      * Set the screen id.
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
@@ -4,17 +4,16 @@
  * 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/. */
 
 package org.mozilla.geckoview;
 
 import org.mozilla.gecko.AndroidGamepadManager;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.InputMethods;
-import org.mozilla.gecko.SurfaceViewWrapper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -25,53 +24,48 @@ import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.support.annotation.AnyThread;
-import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.v4.view.ViewCompat;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.Surface;
+import android.view.SurfaceHolder;
 import android.view.SurfaceView;
-import android.view.TextureView;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStructure;
 import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
 @UiThread
 public class GeckoView extends FrameLayout {
     private static final String LOGTAG = "GeckoView";
     private static final boolean DEBUG = false;
 
     protected final @NonNull Display mDisplay = new Display();
     protected @Nullable GeckoSession mSession;
     private boolean mStateSaved;
 
-    private @Nullable SurfaceViewWrapper mSurfaceWrapper;
+    protected @Nullable SurfaceView mSurfaceView;
 
     private boolean mIsResettingFocus;
 
     private boolean mAutofillEnabled = true;
 
     private GeckoSession.SelectionActionDelegate mSelectionActionDelegate;
     private Autofill.Delegate mAutofillDelegate;
 
@@ -102,17 +96,17 @@ public class GeckoView extends FrameLayo
 
             @Override
             public SavedState[] newArray(final int size) {
                 return new SavedState[size];
             }
         };
     }
 
-    private class Display implements SurfaceViewWrapper.Listener {
+    private class Display implements SurfaceHolder.Callback {
         private final int[] mOrigin = new int[2];
 
         private GeckoDisplay mDisplay;
         private boolean mValid;
 
         private int mClippingHeight;
         private int mDynamicToolbarMaxHeight;
 
@@ -122,20 +116,20 @@ public class GeckoView extends FrameLayo
             if (!mValid) {
                 return;
             }
 
             setVerticalClipping(mClippingHeight);
 
             // Tell display there is already a surface.
             onGlobalLayout();
-            if (GeckoView.this.mSurfaceWrapper != null) {
-                final SurfaceViewWrapper wrapper = GeckoView.this.mSurfaceWrapper;
-                mDisplay.surfaceChanged(wrapper.getSurface(),
-                        wrapper.getWidth(), wrapper.getHeight());
+            if (GeckoView.this.mSurfaceView != null) {
+                final SurfaceHolder holder = GeckoView.this.mSurfaceView.getHolder();
+                final Rect frame = holder.getSurfaceFrame();
+                mDisplay.surfaceChanged(holder.getSurface(), frame.right, frame.bottom);
                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
                 GeckoView.this.setActive(true);
             }
         }
 
         public GeckoDisplay release() {
             if (mValid) {
                 if (mDisplay != null) {
@@ -144,44 +138,48 @@ public class GeckoView extends FrameLayo
                 GeckoView.this.setActive(false);
             }
 
             final GeckoDisplay display = mDisplay;
             mDisplay = null;
             return display;
         }
 
-        @Override // SurfaceListener
-        public void onSurfaceChanged(final Surface surface,
+        @Override // SurfaceHolder.Callback
+        public void surfaceCreated(final SurfaceHolder holder) {
+        }
+
+        @Override // SurfaceHolder.Callback
+        public void surfaceChanged(final SurfaceHolder holder, final int format,
                                    final int width, final int height) {
             if (mDisplay != null) {
-                mDisplay.surfaceChanged(surface, width, height);
+                mDisplay.surfaceChanged(holder.getSurface(), width, height);
                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
                 if (!mValid) {
                     GeckoView.this.setActive(true);
                 }
             }
             mValid = true;
         }
 
-        @Override // SurfaceListener
-        public void onSurfaceDestroyed() {
+        @Override // SurfaceHolder.Callback
+        public void surfaceDestroyed(final SurfaceHolder holder) {
             if (mDisplay != null) {
                 mDisplay.surfaceDestroyed();
                 GeckoView.this.setActive(false);
             }
             mValid = false;
         }
 
         public void onGlobalLayout() {
             if (mDisplay == null) {
                 return;
             }
-            if (GeckoView.this.mSurfaceWrapper != null) {
-                GeckoView.this.mSurfaceWrapper.getView().getLocationOnScreen(mOrigin);
+            if (GeckoView.this.mSurfaceView != null) {
+                GeckoView.this.mSurfaceView.getLocationOnScreen(mOrigin);
                 mDisplay.screenOriginChanged(mOrigin[0], mOrigin[1]);
             }
         }
 
         public boolean shouldPinOnScreen() {
             return mDisplay != null ? mDisplay.shouldPinOnScreen() : false;
         }
 
@@ -236,23 +234,23 @@ public class GeckoView extends FrameLayo
         // descendants to affect the way LayerView retains its focus.
         setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
 
         // This will stop PropertyAnimator from creating a drawing cache (i.e. a
         // bitmap) from a SurfaceView, which is just not possible (the bitmap will be
         // transparent).
         setWillNotCacheDrawing(false);
 
-        mSurfaceWrapper = new SurfaceViewWrapper(getContext());
-        mSurfaceWrapper.setBackgroundColor(Color.WHITE);
-        addView(mSurfaceWrapper.getView(),
+        mSurfaceView = new SurfaceView(getContext());
+        mSurfaceView.setBackgroundColor(Color.WHITE);
+        addView(mSurfaceView,
                 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                            ViewGroup.LayoutParams.MATCH_PARENT));
 
-        mSurfaceWrapper.setListener(mDisplay);
+        mSurfaceView.getHolder().addCallback(mDisplay);
 
         final Activity activity = ActivityUtils.getActivityFromContext(getContext());
         if (activity != null) {
             mSelectionActionDelegate = new BasicSelectionActionDelegate(activity);
         }
 
         mAutofillDelegate = new AndroidAutofillDelegate();
     }
@@ -262,52 +260,18 @@ public class GeckoView extends FrameLayo
      * is automatically cleared once the new document starts painting. Set to
      * Color.TRANSPARENT to undo the cover.
      *
      * @param color Cover color.
      */
     public void coverUntilFirstPaint(final int color) {
         ThreadUtils.assertOnUiThread();
 
-        if (mSurfaceWrapper != null) {
-            mSurfaceWrapper.setBackgroundColor(color);
-        }
-    }
-
-    /**
-     * This GeckoView instance will be backed by a {@link SurfaceView}.
-     *
-     * This option offers the best performance at the price of not being
-     * able to animate GeckoView.
-     */
-    public static final int BACKEND_SURFACE_VIEW = 1;
-    /**
-     * This GeckoView instance will be backed by a {@link TextureView}.
-     *
-     * This option offers worse performance compared to {@link #BACKEND_SURFACE_VIEW}
-     * but allows you to animate GeckoView or to paint a GeckoView on top of another GeckoView.
-     */
-    public static final int BACKEND_TEXTURE_VIEW = 2;
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({BACKEND_SURFACE_VIEW, BACKEND_TEXTURE_VIEW})
-    /* protected */ @interface ViewBackend {}
-
-    /**
-     * Set which view should be used by this GeckoView instance to display content.
-     *
-     * By default, GeckoView will use a {@link SurfaceView}.
-     *
-     * @param backend Any of {@link #BACKEND_SURFACE_VIEW BACKEND_*}.
-     */
-    public void setViewBackend(final @ViewBackend int backend) {
-        if (backend == BACKEND_SURFACE_VIEW) {
-            mSurfaceWrapper.useSurfaceView(getContext());
-        } else if (backend == BACKEND_TEXTURE_VIEW) {
-            mSurfaceWrapper.useTextureView(getContext());
+        if (mSurfaceView != null) {
+            mSurfaceView.setBackgroundColor(color);
         }
     }
 
     /**
      * Return whether the view should be pinned on the screen. When pinned, the view
      * should not be moved on the screen due to animation, scrolling, etc. A common reason
      * for the view being pinned is when the user is dragging a selection caret inside
      * the view; normal user interaction would be disrupted in that case if the view
@@ -541,17 +505,17 @@ public class GeckoView extends FrameLayo
         }
     }
 
     @Override
     public boolean gatherTransparentRegion(final Region region) {
         // For detecting changes in SurfaceView layout, we take a shortcut here and
         // override gatherTransparentRegion, instead of registering a layout listener,
         // which is more expensive.
-        if (mSurfaceWrapper != null) {
+        if (mSurfaceView != null) {
             mDisplay.onGlobalLayout();
         }
         return super.gatherTransparentRegion(region);
     }
 
     @Override
     protected Parcelable onSaveInstanceState() {
         mStateSaved = true;
deleted file mode 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ImageDecoder.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package org.mozilla.geckoview;
-
-import android.graphics.Bitmap;
-import android.support.annotation.AnyThread;
-import android.support.annotation.NonNull;
-
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-/**
- * Provides access to Gecko's Image processing library.
- */
-@AnyThread
-/* protected */ class ImageDecoder {
-    private static ImageDecoder instance;
-
-    private ImageDecoder() {}
-
-    public static ImageDecoder instance() {
-        if (instance == null) {
-            instance = new ImageDecoder();
-        }
-
-        return instance;
-    }
-
-    @WrapForJNI(dispatchTo = "gecko", stubName = "Decode")
-    private static native void nativeDecode(final String uri, final int desiredLength,
-                                            GeckoResult<Bitmap> result);
-
-    /**
-     * Fetches and decodes an image at the specified location.
-     * This method supports SVG, PNG, Bitmap and other formats supported by Gecko.
-     *
-     * @param uri location of the image. Can be either a remote https:// location, file:/// if the
-     *            file is local or a resource://android/ if the file is located inside the APK.
-     *
-     *            e.g. if the image file is locate at /assets/test.png inside the apk, set the uri
-     *            to resource://android/assets/test.png.
-     * @return A {@link GeckoResult} to the decoded image.
-     */
-    @NonNull
-    public GeckoResult<Bitmap> decode(final @NonNull String uri) {
-        return decode(uri, 0);
-    }
-
-    /**
-     * Fetches and decodes an image at the specified location and resizes it to the desired length.
-     * This method supports SVG, PNG, Bitmap and other formats supported by Gecko.
-     *
-     * Note: The final size might differ slightly from the requested output.
-     *
-     * @param uri location of the image. Can be either a remote https:// location, file:/// if the
-     *            file is local or a resource://android/ if the file is located inside the APK.
-     *
-     *            e.g. if the image file is locate at /assets/test.png inside the apk, set the uri
-     *            to resource://android/assets/test.png.
-     * @param desiredLength Longest size for the image in device pixel units. The resulting image
-     *                      might be slightly different if the image cannot be resized efficiently.
-     *                      If desiredLength is 0 then the image will be decoded to its natural
-     *                      size.
-     * @return A {@link GeckoResult} to the decoded image.
-     */
-    @NonNull
-    public GeckoResult<Bitmap> decode(final @NonNull String uri, final int desiredLength) {
-        if (uri == null) {
-            throw new IllegalArgumentException("Uri cannot be null");
-        }
-
-        final GeckoResult<Bitmap> result = new GeckoResult<>();
-
-        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-            nativeDecode(uri, desiredLength, result);
-        } else {
-            GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, this,
-                    "nativeDecode", String.class, uri, int.class, desiredLength,
-                    GeckoResult.class, result);
-        }
-
-        return result;
-    }
-}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
@@ -1,34 +1,26 @@
 package org.mozilla.geckoview;
 
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.support.annotation.AnyThread;
 import android.support.annotation.IntDef;
 import android.support.annotation.LongDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.util.Log;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.util.BundleEventListener;
-import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
 /**
  * Represents a WebExtension that may be used by GeckoView.
  */
 public class WebExtension {
     /**
@@ -53,18 +45,16 @@ public class WebExtension {
      * {@link Flags} for this WebExtension.
      */
     public final @WebExtensionFlags long flags;
     /**
      * Delegates that handle messaging between this WebExtension and the app.
      */
     /* package */ final @NonNull Map<String, MessageDelegate> messageDelegates;
 
-    /* package */ @NonNull ActionDelegate actionDelegate;
-
     @Override
     public String toString() {
         return "WebExtension {" +
                 "location=" + location + ", " +
                 "id=" + id + ", " +
                 "flags=" + flags + "}";
     }
 
@@ -397,114 +387,16 @@ public class WebExtension {
 
         @NonNull
         @Override
         public void onDisconnect(final @NonNull Port port) {
             Log.d(LOGTAG, "Unhandled disconnect from " + port.sender.webExtension.id);
         }
     };
 
-    private static class Sender {
-        public String webExtensionId;
-        public String nativeApp;
-
-        public Sender(final String webExtensionId, final String nativeApp) {
-            this.webExtensionId = webExtensionId;
-            this.nativeApp = nativeApp;
-        }
-
-        @Override
-        public boolean equals(final Object other) {
-            if (!(other instanceof Sender)) {
-                return false;
-            }
-
-            Sender o = (Sender) other;
-            return webExtensionId.equals(o.webExtensionId) &&
-                    nativeApp.equals(o.nativeApp);
-        }
-
-        @Override
-        public int hashCode() {
-            int result = 17;
-            result = 31 * result + (webExtensionId != null ? webExtensionId.hashCode() : 0);
-            result = 31 * result + (nativeApp != null ? nativeApp.hashCode() : 0);
-            return result;
-        }
-    }
-
-    /* package */ final static class Listener implements BundleEventListener {
-        final private HashMap<Sender, WebExtension.MessageDelegate> mMessageDelegates;
-        final private HashMap<String, WebExtension.ActionDelegate> mActionDelegates;
-        final private GeckoSession mSession;
-        public GeckoRuntime runtime;
-
-        public Listener(final GeckoSession session) {
-            mMessageDelegates = new HashMap<>();
-            mActionDelegates = new HashMap<>();
-            mSession = session;
-        }
-
-        /* package */ void registerListeners() {
-            mSession.getEventDispatcher().registerUiThreadListener(this,
-                    "GeckoView:WebExtension:Message",
-                    "GeckoView:WebExtension:PortMessage",
-                    "GeckoView:WebExtension:Connect",
-                    "GeckoView:WebExtension:CloseTab",
-
-                    // Browser and Page Actions
-                    "GeckoView:BrowserAction:Update",
-                    "GeckoView:BrowserAction:OpenPopup",
-                    "GeckoView:PageAction:Update",
-                    "GeckoView:PageAction:OpenPopup");
-        }
-
-        public void setActionDelegate(final WebExtension webExtension,
-                                      final WebExtension.ActionDelegate delegate) {
-            mActionDelegates.put(webExtension.id, delegate);
-        }
-
-        public WebExtension.ActionDelegate getActionDelegate(final WebExtension webExtension) {
-            return mActionDelegates.get(webExtension.id);
-        }
-
-        public void setMessageDelegate(final WebExtension webExtension,
-                                       final WebExtension.MessageDelegate delegate,
-                                       final String nativeApp) {
-            mMessageDelegates.put(new Sender(webExtension.id, nativeApp), delegate);
-        }
-
-        public WebExtension.MessageDelegate getMessageDelegate(final WebExtension webExtension,
-                                                               final String nativeApp) {
-            return mMessageDelegates.get(new Sender(webExtension.id, nativeApp));
-        }
-
-        @Override
-        public void handleMessage(final String event, final GeckoBundle message,
-                                  final EventCallback callback) {
-            if (runtime == null) {
-                return;
-            }
-
-            if ("GeckoView:WebExtension:Message".equals(event)
-                    || "GeckoView:WebExtension:PortMessage".equals(event)
-                    || "GeckoView:WebExtension:Connect".equals(event)
-                    || "GeckoView:PageAction:Update".equals(event)
-                    || "GeckoView:PageAction:OpenPopup".equals(event)
-                    || "GeckoView:BrowserAction:Update".equals(event)
-                    || "GeckoView:BrowserAction:OpenPopup".equals(event)) {
-                runtime.getWebExtensionDispatcher()
-                        .handleMessage(event, message, callback, mSession);
-                return;
-            } else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
-                runtime.getWebExtensionController().closeTab(message, callback, mSession);
-                return;
-            }
-        }
-    }
 
     /**
      * Describes the sender of a message from a WebExtension.
      *
      * See also: <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender">
      *     WebExtensions/API/runtime/MessageSender</a>
      */
     @UiThread
@@ -577,459 +469,25 @@ public class WebExtension {
          * @return true if the MessageSender was sent from the top level frame,
          *         false otherwise.
          * */
         public boolean isTopLevel() {
             return this.isTopLevel;
         }
     }
 
-    /**
-     * Represents the Icon for a {@link Action}.
-     */
-    public static class ActionIcon {
-        private Map<Integer, String> mIconUris;
-
-        /**
-         * Get the best version of this icon for size <code>pixelSize</code>.
-         *
-         * Embedders are encouraged to cache the result of this method keyed with this instance.
-         *
-         * @param pixelSize pixel size at which this icon will be displayed at.
-         *
-         * @return A {@link GeckoResult} that resolves to the bitmap when ready.
-         */
-        @AnyThread
-        @NonNull
-        public GeckoResult<Bitmap> get(final int pixelSize) {
-            int size;
-
-            if (mIconUris.containsKey(pixelSize)) {
-                // If this size matches exactly, return it
-                size = pixelSize;
-            } else {
-                // Otherwise, find the smallest larger image (or the largest image if they are all
-                // smaller)
-                List<Integer> sizes = new ArrayList<>();
-                sizes.addAll(mIconUris.keySet());
-                Collections.sort(sizes, (a, b) -> Integer.compare(b - pixelSize, a - pixelSize));
-                size = sizes.get(0);
-            }
-
-            final String uri = mIconUris.get(size);
-            return ImageDecoder.instance().decode(uri, pixelSize);
-        }
-
-        /* package */ ActionIcon(final GeckoBundle bundle) {
-            mIconUris = new HashMap<>();
-
-            for (final String key: bundle.keys()) {
-                final Integer intKey = Integer.valueOf(key);
-                if (intKey == null) {
-                    Log.e(LOGTAG, "Non-integer icon key: " + intKey);
-                    if (BuildConfig.DEBUG) {
-                        throw new RuntimeException("Non-integer icon key: " + key);
-                    }
-                    continue;
-                }
-                mIconUris.put(intKey, bundle.getString(key));
-            }
-        }
-
-        /** Override for tests. */
-        protected ActionIcon() {
-            mIconUris = null;
-        }
-
+    private static final MessageDelegate NULL_MESSAGE_DELEGATE = new MessageDelegate() {
         @Override
-        public boolean equals(final Object o) {
-            if (o == this) {
-                return true;
-            }
-
-            if (!(o instanceof ActionIcon)) {
-                return false;
-            }
-
-            return mIconUris.equals(((ActionIcon) o).mIconUris);
+        public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
+                                             final @NonNull Object message,
+                                             final @NonNull MessageSender sender) {
+            Log.d(LOGTAG, "Unhandled message from " + nativeApp + " id=" +
+                    sender.webExtension.id + ": " + message.toString());
+            return null;
         }
 
         @Override
-        public int hashCode() {
-            return mIconUris.hashCode();
-        }
-    }
-
-    /**
-     * Represents either a Browser Action or a Page Action from the
-     * WebExtension API.
-     *
-     * Instances of this class may represent the default <code>Action</code>
-     * which applies to all WebExtension tabs or a tab-specific override. To
-     * reconstruct the full <code>Action</code> object, you can use
-     * {@link Action#withDefault}.
-     *
-     * Tab specific overrides can be obtained by registering a delegate using
-     * {@link GeckoSession#setWebExtensionActionDelegate}, while default values
-     * can be obtained by registering a delegate using
-     * {@link #setActionDelegate}.
-     *
-     * <br>
-     * See also
-     * <ul>
-     *     <li><a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction">
-     *         WebExtensions/API/browserAction
-     *     </a></li>
-     *     <li><a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction">
-     *         WebExtensions/API/pageAction
-     *     </a></li>
-     * </ul>
-     */
-    @AnyThread
-    public static class Action {
-        /**
-         * Title of this Action.
-         *
-         * See also:
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/getTitle">
-         *     pageAction/getTitle</a>,
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getTitle">
-         *     browserAction/getTitle</a>
-         */
-        final public @Nullable String title;
-        /**
-         * Icon for this Action.
-         *
-         * See also:
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/setIcon">
-         *     pageAction/setIcon</a>,
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setIcon">
-         *     browserAction/setIcon</a>
-         */
-        final public @Nullable ActionIcon icon;
-        /**
-         * URI of the Popup to display when the user taps on the icon for this
-         * Action.
-         *
-         * See also:
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/getPopup">
-         *     pageAction/getPopup</a>,
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getPopup">
-         *     browserAction/getPopup</a>
-         */
-        final private @Nullable String mPopupUri;
-        /**
-         * Whether this action is enabled and should be visible.
-         *
-         * Note: for page action, this is <code>true</code> when the extension calls
-         * <code>pageAction.show</code> and <code>false</code> when the extension
-         * calls <code>pageAction.hide</code>.
-         *
-         * See also:
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/show">
-         *     pageAction/show</a>,
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/enabled">
-         *     browserAction/enabled</a>
-         */
-        final public @Nullable Boolean enabled;
-        /**
-         * Badge text for this action.
-         *
-         * See also:
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeText">
-         *     browserAction/getBadgeText</a>
-         */
-        final public @Nullable String badgeText;
-        /**
-         * Background color for the badge for this Action.
-         *
-         * This method will return an Android color int that can be used in
-         * {@link android.widget.TextView#setBackgroundColor(int)} and similar
-         * methods.
-         *
-         * See also:
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeBackgroundColor">
-         *     browserAction/getBadgeBackgroundColor</a>
-         */
-        final public @Nullable Integer badgeBackgroundColor;
-        /**
-         * Text color for the badge for this Action.
-         *
-         * This method will return an Android color int that can be used in
-         * {@link android.widget.TextView#setTextColor(int)} and similar
-         * methods.
-         *
-         * See also:
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeTextColor">
-         *     browserAction/getBadgeTextColor</a>
-         */
-        final public @Nullable Integer badgeTextColor;
-
-        final private WebExtension mExtension;
-
-        /* package */ final static int TYPE_BROWSER_ACTION = 1;
-        /* package */ final static int TYPE_PAGE_ACTION = 2;
-        @Retention(RetentionPolicy.SOURCE)
-        @IntDef({TYPE_BROWSER_ACTION, TYPE_PAGE_ACTION})
-        /* package */ @interface ActionType {}
-
-        /* package */ final @ActionType int type;
-
-        /* package */ Action(final @ActionType int type,
-                             final GeckoBundle bundle, final WebExtension extension) {
-            mExtension = extension;
-            mPopupUri = bundle.getString("popup");
-
-            this.type = type;
-
-            title = bundle.getString("title");
-            badgeText = bundle.getString("badgeText");
-            badgeBackgroundColor = colorFromRgbaArray(
-                    bundle.getDoubleArray("badgeBackgroundColor"));
-            badgeTextColor = colorFromRgbaArray(
-                    bundle.getDoubleArray("badgeTextColor"));
-
-            if (bundle.containsKey("icon")) {
-                icon = new ActionIcon(bundle.getBundle("icon"));
-            } else {
-                icon = null;
-            }
-
-            if (bundle.getBoolean("patternMatching", false)) {
-                // This action was enabled by pattern matching
-                enabled = true;
-            } else if (bundle.containsKey("enabled")) {
-                enabled = bundle.getBoolean("enabled");
-            } else {
-                enabled = null;
-            }
-        }
-
-        private Integer colorFromRgbaArray(final double[] c) {
-            if (c == null) {
-                return null;
-            }
-
-            return Color.argb((int) c[3], (int) c[0], (int) c[1], (int) c[2]);
-        }
-
-        @Override
-        public String toString() {
-            return "Action {\n"
-                    + "\ttitle: " + this.title + ",\n"
-                    + "\ticon: " + this.icon + ",\n"
-                    + "\tpopupUri: " + this.mPopupUri + ",\n"
-                    + "\tenabled: " + this.enabled + ",\n"
-                    + "\tbadgeText: " + this.badgeText + ",\n"
-                    + "\tbadgeTextColor: " + this.badgeTextColor + ",\n"
-                    + "\tbadgeBackgroundColor: " + this.badgeBackgroundColor + ",\n"
-                    + "}";
-        }
-
-        // For testing
-        protected Action() {
-            type = TYPE_BROWSER_ACTION;
-            mExtension = null;
-            mPopupUri = null;
-            title = null;
-            icon = null;
-            enabled = null;
-            badgeText = null;
-            badgeTextColor = null;
-            badgeBackgroundColor = null;
+        public void onConnect(final @NonNull Port port) {
+            Log.d(LOGTAG, "Unhandled connect request from " +
+                    port.sender.webExtension.id);
         }
-
-        /**
-         * Merges values from this Action with the default Action.
-         *
-         * @param defaultValue the default Action as received from
-         *                     {@link ActionDelegate#onBrowserAction}
-         *                     or {@link ActionDelegate#onPageAction}.
-         *
-         * @return an {@link Action} where all <code>null</code> values from
-         *         this instance are replaced with values from
-         *         <code>defaultValue</code>.
-         * @throws IllegalArgumentException if defaultValue is not of the same
-         *         type, e.g. if this Action is a Page Action and default
-         *         value is a Browser Action.
-         */
-        @NonNull
-        public Action withDefault(final @NonNull Action defaultValue) {
-            return new Action(this, defaultValue);
-        }
-
-        /** @see Action#withDefault */
-        private Action(final Action source, final Action defaultValue) {
-            if (source.type != defaultValue.type) {
-                throw new IllegalArgumentException(
-                        "defaultValue must be of the same type.");
-            }
-
-            type = source.type;
-            mExtension = source.mExtension;
-
-            title = source.title != null ? source.title : defaultValue.title;
-            icon = source.icon != null ? source.icon : defaultValue.icon;
-            mPopupUri = source.mPopupUri != null ? source.mPopupUri : defaultValue.mPopupUri;
-            enabled = source.enabled != null  ? source.enabled : defaultValue.enabled;
-            badgeText = source.badgeText != null ? source.badgeText : defaultValue.badgeText;
-            badgeTextColor = source.badgeTextColor != null
-                    ? source.badgeTextColor : defaultValue.badgeTextColor;
-            badgeBackgroundColor = source.badgeBackgroundColor != null
-                    ? source.badgeBackgroundColor : defaultValue.badgeBackgroundColor;
-        }
-
-        @UiThread
-        public void click() {
-            if (mPopupUri != null && !mPopupUri.isEmpty()) {
-                if (mExtension.actionDelegate == null) {
-                    return;
-                }
-
-                GeckoResult<GeckoSession> popup =
-                        mExtension.actionDelegate.onTogglePopup(mExtension, this);
-                openPopup(popup);
-
-                // When popupUri is specified, the extension doesn't get a callback
-                return;
-            }
-
-            final GeckoBundle bundle = new GeckoBundle(1);
-            bundle.putString("extensionId", mExtension.id);
-
-            if (type == TYPE_BROWSER_ACTION) {
-                EventDispatcher.getInstance().dispatch(
-                        "GeckoView:BrowserAction:Click", bundle);
-            } else if (type == TYPE_PAGE_ACTION) {
-                EventDispatcher.getInstance().dispatch(
-                        "GeckoView:PageAction:Click", bundle);
-            } else {
-                throw new IllegalStateException("Unknown Action type");
-            }
-        }
-
-        /* package */ void openPopup(final GeckoResult<GeckoSession> popup) {
-            if (popup == null) {
-                return;
-            }
-
-            popup.accept(session -> {
-                if (session == null) {
-                    return;
-                }
-
-                session.getSettings().setIsPopup(true);
-                session.loadUri(mPopupUri);
-            });
-        }
-    }
-
-    /**
-     * Receives updates whenever a Browser action or a Page action has been
-     * defined by an extension.
-     *
-     * This delegate will receive the default action when registered with
-     * {@link WebExtension#setActionDelegate}. To receive
-     * {@link GeckoSession}-specific overrides you can use
-     * {@link GeckoSession#setWebExtensionActionDelegate}.
-     */
-    public interface ActionDelegate {
-        /**
-         * Called whenever a browser action is defined or updated.
-         *
-         * This method will be called whenever an extension that defines a
-         * browser action is registered or the properties of the Action are
-         * updated.
-         *
-         * See also <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction">
-         *  WebExtensions/API/browserAction
-         * </a>,
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action">
-         *    WebExtensions/manifest.json/browser_action
-         * </a>.
-         *
-         * @param extension The extension that defined this browser action.
-         * @param session Either the {@link GeckoSession} corresponding to the
-         *                tab to which this Action override applies.
-         *                <code>null</code> if <code>action</code> is the new
-         *                default value.
-         * @param action {@link Action} containing the override values for this
-         *               {@link GeckoSession} or the default value if
-         *               <code>session</code> is <code>null</code>.
-         */
-        @UiThread
-        default void onBrowserAction(final @NonNull WebExtension extension,
-                                     final @Nullable GeckoSession session,
-                                     final @NonNull Action action) {}
-        /**
-         * Called whenever a page action is defined or updated.
-         *
-         * This method will be called whenever an extension that defines a page
-         * action is registered or the properties of the Action are updated.
-         *
-         * See also <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction">
-         *  WebExtensions/API/pageAction
-         * </a>,
-         * <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action">
-         *    WebExtensions/manifest.json/page_action
-         * </a>.
-         *
-         * @param extension The extension that defined this page action.
-         * @param session Either the {@link GeckoSession} corresponding to the
-         *                tab to which this Action override applies.
-         *                <code>null</code> if <code>action</code> is the new
-         *                default value.
-         * @param action {@link Action} containing the override values for this
-         *               {@link GeckoSession} or the default value if
-         *               <code>session</code> is <code>null</code>.
-         */
-        @UiThread
-        default void onPageAction(final @NonNull WebExtension extension,
-                                  final @Nullable GeckoSession session,
-                                  final @NonNull Action action) {}
-
-        /**
-         * Called whenever the action wants to toggle a popup view.
-         *
-         * @param extension The extension that wants to display a popup
-         * @param action The action where the popup is defined
-         * @return A GeckoSession that will be used to display the pop-up,
-         *         null if no popup will be displayed.
-         */
-        @UiThread
-        @Nullable
-        default GeckoResult<GeckoSession> onTogglePopup(final @NonNull WebExtension extension,
-                                                        final @NonNull Action action) {
-            return null;
-        }
-
-        /**
-         * Called whenever the action wants to open a popup view.
-         *
-         * @param extension The extension that wants to display a popup
-         * @param action The action where the popup is defined
-         * @return A GeckoSession that will be used to display the pop-up,
-         *         null if no popup will be displayed.
-         */
-        @UiThread
-        @Nullable
-        default GeckoResult<GeckoSession> onOpenPopup(final @NonNull WebExtension extension,
-                                                      final @NonNull Action action) {
-            return null;
-        }
-    }
-
-    /**
-     * Set the Action delegate for this WebExtension.
-     *
-     * This delegate will receive updates every time the default Action value
-     * changes.
-     *
-     * To listen for {@link GeckoSession}-specific updates, use
-     * {@link GeckoSession#setWebExtensionActionDelegate}
-     *
-     * @param delegate {@link ActionDelegate} that will receive updates.
-     */
-    @AnyThread
-    public void setActionDelegate(final @Nullable ActionDelegate delegate) {
-        actionDelegate = delegate;
-    }
+    };
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionEventDispatcher.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionEventDispatcher.java
@@ -24,23 +24,17 @@ import java.util.Map;
 
     public void registerWebExtension(final WebExtension webExtension) {
         if (!mHandlerRegistered) {
             EventDispatcher.getInstance().registerUiThreadListener(
                     this,
                     "GeckoView:WebExtension:Message",
                     "GeckoView:WebExtension:PortMessage",
                     "GeckoView:WebExtension:Connect",
-                    "GeckoView:WebExtension:Disconnect",
-
-                    // {Browser,Page}Actions
-                    "GeckoView:BrowserAction:Update",
-                    "GeckoView:BrowserAction:OpenPopup",
-                    "GeckoView:PageAction:Update",
-                    "GeckoView:PageAction:OpenPopup"
+                    "GeckoView:WebExtension:Disconnect"
             );
             mHandlerRegistered = true;
         }
 
         mExtensions.put(webExtension.id, webExtension);
     }
 
     public void unregisterWebExtension(final WebExtension webExtension) {
@@ -232,99 +226,24 @@ import java.util.Map;
             return;
         }
 
         response.accept(
             value -> callback.sendSuccess(value),
             exception -> callback.sendError(exception));
     }
 
-    private WebExtension extensionFromBundle(final GeckoBundle message) {
-        final String extensionId = message.getString("extensionId");
-
-        final WebExtension extension = mExtensions.get(extensionId);
-        if (extension == null) {
-            if (BuildConfig.DEBUG) {
-                throw new RuntimeException("Could not find extension: " + extensionId);
-            }
-            Log.e(LOGTAG, "Could not find extension: " + extensionId);
-        }
-
-        return extension;
-    }
-
-    private void openPopup(final GeckoBundle message, final GeckoSession session,
-                           final @WebExtension.Action.ActionType int actionType) {
-        final WebExtension extension = extensionFromBundle(message);
-        if (extension == null) {
-            return;
-        }
-
-        final WebExtension.Action action = new WebExtension.Action(
-                actionType, message.getBundle("action"), extension);
-
-        final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, session);
-        if (delegate == null) {
-            return;
-        }
-
-        final GeckoResult<GeckoSession> popup = delegate.onOpenPopup(extension, action);
-        action.openPopup(popup);
-    }
-
-    private WebExtension.ActionDelegate actionDelegateFor(final WebExtension extension,
-                                                          final GeckoSession session) {
-        if (session == null) {
-            return extension.actionDelegate;
-        }
-
-        return session.getWebExtensionActionDelegate(extension);
-    }
-
-    private void actionUpdate(final GeckoBundle message, final GeckoSession session,
-                              final @WebExtension.Action.ActionType int actionType) {
-        final WebExtension extension = extensionFromBundle(message);
-        if (extension == null) {
-            return;
-        }
-
-        final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, session);
-        if (delegate == null) {
-            return;
-        }
-
-        final WebExtension.Action action = new WebExtension.Action(
-                actionType, message.getBundle("action"), extension);
-        if (actionType == WebExtension.Action.TYPE_BROWSER_ACTION) {
-            delegate.onBrowserAction(extension, session, action);
-        } else if (actionType == WebExtension.Action.TYPE_PAGE_ACTION) {
-            delegate.onPageAction(extension, session, action);
-        }
-    }
-
     public void handleMessage(final String event, final GeckoBundle message,
                               final EventCallback callback, final GeckoSession session) {
         if ("GeckoView:WebExtension:Disconnect".equals(event)) {
             disconnect(message.getLong("portId", -1), callback);
             return;
         } else if ("GeckoView:WebExtension:PortMessage".equals(event)) {
             portMessage(message, callback);
             return;
-        } else if ("GeckoView:BrowserAction:Update".equals(event)) {
-            actionUpdate(message, session, WebExtension.Action.TYPE_BROWSER_ACTION);
-            return;
-        } else if ("GeckoView:PageAction:Update".equals(event)) {
-            actionUpdate(message, session, WebExtension.Action.TYPE_PAGE_ACTION);
-            return;
-        } else if ("GeckoView:BrowserAction:OpenPopup".equals(event)) {
-            openPopup(message, session, WebExtension.Action.TYPE_BROWSER_ACTION);
-            return;
-        } else if ("GeckoView:PageAction:OpenPopup".equals(event)) {
-            openPopup(message, session, WebExtension.Action.TYPE_PAGE_ACTION);
-            return;
         }
 
         final String nativeApp = message.getString("nativeApp");
         if (nativeApp == null) {
             if (BuildConfig.DEBUG) {
                 throw new RuntimeException("Missing required nativeApp message parameter.");
             }
             callback.sendError("Missing nativeApp parameter.");
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -30,37 +30,27 @@ exclude: true
   and [`BasicSelectionActionDelegate.onShowActionRequest`][72.7]
   ([bug 1581161]({{bugzilla}}1581161))
 - Added text selection action methods to [`SelectionActionDelegate.Selection`][72.8]
   ([bug 1581161]({{bugzilla}}1581161))
 - Added [`BasicSelectionActionDelegate.getSelection`][72.9]
   ([bug 1581161]({{bugzilla}}1581161))
 - Changed [`BasicSelectionActionDelegate.clearSelection`][72.10] to public.
   ([bug 1581161]({{bugzilla}}1581161))
-- Added [`GeckoView.setViewBackend`][72.11] to set whether GeckoView should be
-  backed by a [`TextureView`][72.12] or a [`SurfaceView`][72.13].
-  ([bug 1530402]({{bugzilla}}1530402))
-- Added support for Browser and Page Action from the WebExtension API.
-  See [`WebExtension.Action`][72.14].
-  ([bug 1530402]({{bugzilla}}1530402))
 
 [72.1]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture-
 [72.2]: {{javadoc_uri}}/Autofill.html
 [72.3]: {{javadoc_uri}}/WebResponse.html#body
 [72.4]: {{javadoc_uri}}/WebResponse.html#setReadTimeoutMillis-long-
 [72.5]: {{javadoc_uri}}/WebResponse.html#DEFAULT_READ_TIMEOUT_MS
 [72.6]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.html#onShowActionRequest-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.Selection-
 [72.7]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#onShowActionRequest-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.Selection-
 [72.8]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.Selection.html
 [72.9]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#getSelection-
 [72.10]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#clearSelection-
-[72.11]: {{javadoc_uri}}/GeckoView.html#setViewBackend-int-
-[72.12]: https://developer.android.com/reference/android/view/TextureView
-[72.13]: https://developer.android.com/reference/android/view/SurfaceView
-[72.14]: {{javadoc_uri}}/WebExtension.Action.html
 
 ## v71
 - Added a content blocking flag for blocked social cookies to [`ContentBlocking`][70.17].
   ([bug 1584479]({{bugzilla}}1584479))
 - Added [`onBooleanScalar`][71.1], [`onLongScalar`][71.2],
   [`onStringScalar`][71.3] to [`RuntimeTelemetry.Delegate`][70.12] to support
   scalars in streaming telemetry. ⚠️  As part of this change,
   `onTelemetryReceived` has been renamed to [`onHistogram`][71.4], and
@@ -448,9 +438,9 @@ exclude: true
 [65.19]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest.html#isRedirect
 [65.20]: {{javadoc_uri}}/GeckoSession.html#LOAD_FLAGS_BYPASS_CLASSIFIER    
 [65.21]: {{javadoc_uri}}/GeckoSession.ContentDelegate.ContextElement.html
 [65.22]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onContextMenu-org.mozilla.geckoview.GeckoSession-int-int-org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement-
 [65.23]: {{javadoc_uri}}/GeckoSession.FinderResult.html
 [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: {{javadoc_uri}}/GeckoResult.html
 
-[api-version]: d4fbf3825322768a22d225f79c659bfd36eebbc6
+[api-version]: 8d6a09b6a33550dffb6303dc01c5e6ff2d3cc499
deleted file mode 100644
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.mozilla.geckoview_example;
-
-import android.graphics.Bitmap;
-
-public class ActionButton {
-    final Bitmap icon;
-    final String text;
-    final Integer textColor;
-    final Integer backgroundColor;
-
-    public ActionButton(final Bitmap icon, final String text, final Integer textColor,
-                        final Integer backgroundColor) {
-        this.icon = icon;
-        this.text = text;
-        this.textColor = textColor;
-        this.backgroundColor = backgroundColor;
-    }
-}
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -39,238 +39,71 @@ import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.Handler;
 import android.os.SystemClock;
+import android.security.keystore.KeyProperties;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationManagerCompat;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
 import android.util.Log;
-import android.util.LruCache;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.lang.ref.WeakReference;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Locale;
 
-interface BrowserActionDelegate {
-    default GeckoSession toggleBrowserActionPopup(boolean force) {
-        return null;
-    }
-    default void onActionButton(ActionButton button) {}
-    default TabSession getSession(GeckoSession session) {
-        return null;
-    }
-    default TabSession getCurrentSession() {
-        return null;
-    }
-}
-
-class WebExtensionManager implements WebExtension.ActionDelegate, TabSessionManager.TabObserver {
-    public WebExtension extension;
-
-    private LruCache<WebExtension.ActionIcon, Bitmap> mBitmapCache = new LruCache<>(5);
-    private GeckoRuntime mRuntime;
-    private WebExtension.Action mDefaultAction;
-
-    private WeakReference<BrowserActionDelegate> mActionDelegate;
-
-    // We only support either one browserAction or one pageAction
-    private void onAction(final WebExtension extension, final GeckoSession session,
-                          final WebExtension.Action action) {
-        BrowserActionDelegate delegate = mActionDelegate.get();
-        if (delegate == null) {
-            return;
-        }
-
-        WebExtension.Action resolved;
-
-        if (session == null) {
-            // This is the default action
-            mDefaultAction = action;
-            resolved = actionFor(delegate.getCurrentSession());
-        } else {
-            if (delegate.getSession(session) == null) {
-                return;
-            }
-            delegate.getSession(session).action = action;
-            if (delegate.getCurrentSession() != session) {
-                // This update is not for the session that we are currently displaying,
-                // no need to update the UI
-                return;
-            }
-            resolved = action.withDefault(mDefaultAction);
-        }
-
-        updateAction(resolved);
-    }
-
-    @Override
-    public void onPageAction(final WebExtension extension,
-                                final GeckoSession session,
-                                final WebExtension.Action action) {
-        onAction(extension, session, action);
-    }
-
-    @Override
-    public void onBrowserAction(final WebExtension extension,
-                                final GeckoSession session,
-                                final WebExtension.Action action) {
-        onAction(extension, session, action);
-    }
-
-    private GeckoResult<GeckoSession> togglePopup(boolean force) {
-        BrowserActionDelegate actionDelegate = mActionDelegate.get();
-        if (actionDelegate == null) {
-            return null;
-        }
-
-        GeckoSession session = actionDelegate.toggleBrowserActionPopup(false);
-        if (session == null) {
-            return null;
-        }
-
-        return GeckoResult.fromValue(session);
-    }
-
-    @Override
-    public GeckoResult<GeckoSession> onTogglePopup(final @NonNull WebExtension extension,
-                                                   final @NonNull WebExtension.Action action) {
-        return togglePopup(false);
-    }
-
-    @Override
-    public GeckoResult<GeckoSession> onOpenPopup(final @NonNull WebExtension extension,
-                                                 final @NonNull WebExtension.Action action) {
-        return togglePopup(true);
-    }
-
-    private WebExtension.Action actionFor(TabSession session) {
-        if (session.action == null) {
-            return mDefaultAction;
-        } else {
-            return session.action.withDefault(mDefaultAction);
-        }
-    }
-
-    private void updateAction(WebExtension.Action resolved) {
-        BrowserActionDelegate actionDelegate = mActionDelegate.get();
-        if (actionDelegate == null) {
-            return;
-        }
-
-        if (resolved.enabled == null || !resolved.enabled) {
-            actionDelegate.onActionButton(null);
-            return;
-        }
-
-        if (resolved.icon != null) {
-            if (mBitmapCache.get(resolved.icon) != null) {
-                actionDelegate.onActionButton(new ActionButton(
-                        mBitmapCache.get(resolved.icon), resolved.badgeText,
-                        resolved.badgeTextColor,
-                        resolved.badgeBackgroundColor
-                ));
-            } else {
-                resolved.icon.get(100).accept(bitmap -> {
-                    mBitmapCache.put(resolved.icon, bitmap);
-                    actionDelegate.onActionButton(new ActionButton(
-                        bitmap, resolved.badgeText,
-                        resolved.badgeTextColor,
-                        resolved.badgeBackgroundColor));
-                });
-            }
-        } else {
-            actionDelegate.onActionButton(null);
-        }
-    }
-
-    public void onClicked(TabSession session) {
-        actionFor(session).click();
-    }
-
-    public void setActionDelegate(BrowserActionDelegate delegate) {
-        mActionDelegate = new WeakReference<>(delegate);
-    }
-
-    @Override
-    public void onCurrentSession(TabSession session) {
-        if (mDefaultAction == null) {
-            // No action was ever defined, so nothing to do
-            return;
-        }
-
-        if (session.action != null) {
-            updateAction(session.action.withDefault(mDefaultAction));
-        } else {
-            updateAction(mDefaultAction);
-        }
-    }
-
-    public WebExtensionManager(GeckoRuntime runtime) {
-        mRuntime = runtime;
-        // TODO: allow users to install an extension from file
-        // extension = new WebExtension("resource://android/assets/chill-out/");
-        // extension.setActionDelegate(this);
-        // mRuntime.registerWebExtension(extension);
-    }
-}
-
-public class GeckoViewActivity
-        extends AppCompatActivity
-        implements ToolbarLayout.TabListener, BrowserActionDelegate {
+public class GeckoViewActivity extends AppCompatActivity {
     private static final String LOGTAG = "GeckoViewActivity";
     private static final String USE_MULTIPROCESS_EXTRA = "use_multiprocess";
     private static final String FULL_ACCESSIBILITY_TREE_EXTRA = "full_accessibility_tree";
     private static final String SEARCH_URI_BASE = "https://www.google.com/search?q=";
     private static final String ACTION_SHUTDOWN = "org.mozilla.geckoview_example.SHUTDOWN";
     private static final String CHANNEL_ID = "GeckoViewExample";
     private static final int REQUEST_FILE_PICKER = 1;
     private static final int REQUEST_PERMISSIONS = 2;
     private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 3;
 
     private static GeckoRuntime sGeckoRuntime;
-
-    private static WebExtensionManager sExtensionManager;
-
     private TabSessionManager mTabSessionManager;
     private GeckoView mGeckoView;
     private boolean mUseMultiprocess;
     private boolean mFullAccessibilityTree;
     private boolean mUseTrackingProtection;
     private boolean mUsePrivateBrowsing;
     private boolean mEnableRemoteDebugging;
     private boolean mKillProcessOnDestroy;
     private boolean mDesktopMode;
-    private TabSession mPopupSession;
-    private View mPopupView;
 
     private boolean mShowNotificationsRejected;
     private ArrayList<String> mAcceptedPersistentStorage = new ArrayList<String>();
 
     private ToolbarLayout mToolbarView;
     private String mCurrentUri;
     private boolean mCanGoBack;
     private boolean mCanGoForward;
@@ -306,17 +139,17 @@ public class GeckoViewActivity
         mGeckoView = findViewById(R.id.gecko_view);
 
         mTabSessionManager = new TabSessionManager();
 
         setSupportActionBar(findViewById(R.id.toolbar));
 
         mToolbarView = new ToolbarLayout(this, mTabSessionManager);
         mToolbarView.setId(R.id.toolbar_layout);
-        mToolbarView.setTabListener(this);
+        mToolbarView.setTabListener(this::switchToSessionAtIndex);
 
         getSupportActionBar().setCustomView(mToolbarView,
                 new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT,
                         ActionBar.LayoutParams.WRAP_CONTENT));
         getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
 
         mUseMultiprocess = getIntent().getBooleanExtra(USE_MULTIPROCESS_EXTRA, true);
         mEnableRemoteDebugging = true;
@@ -364,19 +197,16 @@ public class GeckoViewActivity
                 @Override
                 public GeckoResult<AllowOrDeny> onCloseTab(WebExtension source, GeckoSession session) {
                     TabSession tabSession = mTabSessionManager.getSession(session);
                     closeTab(tabSession);
                     return GeckoResult.fromValue(AllowOrDeny.ALLOW);
                 }
             });
 
-            sExtensionManager = new WebExtensionManager(sGeckoRuntime);
-            mTabSessionManager.setTabObserver(sExtensionManager);
-
             // `getSystemService` call requires API level 23
             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                 sGeckoRuntime.setWebNotificationDelegate(new WebNotificationDelegate() {
                     NotificationManager notificationManager = getSystemService(NotificationManager.class);
                     @Override
                     public void onShowNotification(@NonNull WebNotification notification) {
                         Intent clickIntent = new Intent(GeckoViewActivity.this, GeckoViewActivity.class);
                         clickIntent.putExtra("onClick",notification.tag);
@@ -425,18 +255,16 @@ public class GeckoViewActivity
             }
 
             sGeckoRuntime.setDelegate(() -> {
                 mKillProcessOnDestroy = true;
                 finish();
             });
         }
 
-        sExtensionManager.setActionDelegate(this);
-
         if(savedInstanceState == null) {
             TabSession session = getIntent().getParcelableExtra("session");
             if (session != null) {
                 connectSession(session);
 
                 if (!session.isOpen()) {
                     session.open(sGeckoRuntime);
                 }
@@ -454,79 +282,16 @@ public class GeckoViewActivity
             }
             loadFromIntent(getIntent());
         }
 
         mToolbarView.getLocationView().setCommitListener(mCommitListener);
         mToolbarView.updateTabCount();
     }
 
-    @Override
-    public TabSession getSession(GeckoSession session) {
-        return mTabSessionManager.getSession(session);
-    }
-
-    @Override
-    public TabSession getCurrentSession() {
-        return mTabSessionManager.getCurrentSession();
-    }
-
-    @Override
-    public void onActionButton(ActionButton button) {
-        mToolbarView.setBrowserActionButton(button);
-    }
-
-    @Override
-    public GeckoSession toggleBrowserActionPopup(boolean force) {
-        if (mPopupSession == null) {
-            openPopupSession();
-        }
-
-        ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
-        boolean shouldShow = force || params.width == 0;
-
-        if (shouldShow) {
-            params.height = 1100;
-            params.width = 1200;
-        } else {
-            params.height = 0;
-            params.width = 0;
-        }
-
-        mPopupView.setLayoutParams(params);
-        return shouldShow ? mPopupSession : null;
-    }
-
-    private void openPopupSession() {
-        LayoutInflater inflater = (LayoutInflater)
-                getSystemService(LAYOUT_INFLATER_SERVICE);
-        mPopupView = inflater.inflate(R.layout.browser_action_popup, null);
-        GeckoView geckoView = mPopupView.findViewById(R.id.gecko_view_popup);
-        geckoView.setViewBackend(GeckoView.BACKEND_TEXTURE_VIEW);
-        mPopupSession = new TabSession();
-        mPopupSession.open(sGeckoRuntime);
-        geckoView.setSession(mPopupSession);
-
-        mPopupView.setOnFocusChangeListener(this::hideBrowserAction);
-        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(0, 0);
-        params.addRule(RelativeLayout.ABOVE, R.id.toolbar);
-        mPopupView.setLayoutParams(params);
-        mPopupView.setFocusable(true);
-        ((ViewGroup) findViewById(R.id.main)).addView(mPopupView);
-    }
-
-    private void hideBrowserAction(View view, boolean hasFocus) {
-        if (!hasFocus) {
-            ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
-            params.height = 0;
-            params.width = 0;
-            mPopupView.setLayoutParams(params);
-        }
-    }
-
     private void createNotificationChannel() {
         // Create the NotificationChannel, but only on API 26+ because
         // the NotificationChannel class is new and not in the support library
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             CharSequence name = getString(R.string.app_name);
             String description = getString(R.string.activity_label);
             int importance = NotificationManager.IMPORTANCE_DEFAULT;
             NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
@@ -570,19 +335,16 @@ public class GeckoViewActivity
 
         final ExamplePermissionDelegate permission = new ExamplePermissionDelegate();
         permission.androidPermissionRequestCode = REQUEST_PERMISSIONS;
         session.setPermissionDelegate(permission);
 
         session.setMediaDelegate(new ExampleMediaDelegate(this));
 
         session.setSelectionActionDelegate(new BasicSelectionActionDelegate(this));
-        if (sExtensionManager.extension != null) {
-            session.setWebExtensionActionDelegate(sExtensionManager.extension, sExtensionManager);
-        }
 
         updateTrackingProtection(session);
         updateDesktopMode(session);
     }
 
     private void recreateSession() {
         recreateSession(mTabSessionManager.getCurrentSession());
     }
@@ -731,21 +493,17 @@ public class GeckoViewActivity
             setGeckoViewSession(tabSession);
             tabSession.reload();
             mToolbarView.updateTabCount();
         } else {
             recreateSession(session);
         }
     }
 
-    public void onBrowserActionClick() {
-        sExtensionManager.onClicked(mTabSessionManager.getCurrentSession());
-    }
-
-    public void switchToTab(int index) {
+    private void switchToSessionAtIndex(int index) {
         TabSession nextSession = mTabSessionManager.getSession(index);
         TabSession currentSession = mTabSessionManager.getCurrentSession();
         if(nextSession != currentSession) {
             setGeckoViewSession(nextSession);
             mCurrentUri = nextSession.getUri();
             mToolbarView.getLocationView().setText(mCurrentUri);
         }
     }
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
@@ -2,22 +2,20 @@ package org.mozilla.geckoview_example;
 
 import android.os.Parcel;
 import android.support.annotation.AnyThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.UiThread;
 
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
-import org.mozilla.geckoview.WebExtension;
 
 public class TabSession extends GeckoSession {
     private String mTitle;
     private String mUri;
-    public WebExtension.Action action;
 
     public TabSession() { super(); }
 
     public TabSession(GeckoSessionSettings settings) {
         super(settings);
     }
 
     public String getTitle() {
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
@@ -3,31 +3,22 @@ package org.mozilla.geckoview_example;
 import android.support.annotation.Nullable;
 
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 
 import java.util.ArrayList;
 
 public class TabSessionManager {
-    private static ArrayList<TabSession> mTabSessions = new ArrayList<>();
+    private static ArrayList<TabSession> mTabSessions = new ArrayList<TabSession>();
     private int mCurrentSessionIndex = 0;
-    private TabObserver mTabObserver;
-
-    public interface TabObserver {
-        void onCurrentSession(TabSession session);
-    }
 
     public TabSessionManager() {
     }
 
-    public void setTabObserver(TabObserver observer) {
-        mTabObserver = observer;
-    }
-
     public void addSession(TabSession session) {
         mTabSessions.add(session);
     }
 
     public TabSession getSession(int index) {
         return mTabSessions.get(index);
     }
 
@@ -45,20 +36,16 @@ public class TabSessionManager {
 
     public void setCurrentSession(TabSession session) {
         int index = mTabSessions.indexOf(session);
         if (index == -1) {
             mTabSessions.add(session);
             index = mTabSessions.size() - 1;
         }
         mCurrentSessionIndex = index;
-
-        if (mTabObserver != null) {
-            mTabObserver.onCurrentSession(session);
-        }
     }
 
     private boolean isCurrentSession(TabSession session) {
         return session == getCurrentSession();
     }
 
     public void closeSession(@Nullable TabSession session) {
         if (session == null) { return; }
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
@@ -1,35 +1,26 @@
 package org.mozilla.geckoview_example;
 
 import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.PorterDuff;
 import android.graphics.Typeface;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.ShapeDrawable;
 import android.support.v4.content.ContextCompat;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
-import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupMenu;
-import android.widget.TextView;
 
 public class ToolbarLayout extends LinearLayout {
+
     public interface TabListener {
         void switchToTab(int tabId);
-        void onBrowserActionClick();
     }
 
     private LocationView mLocationView;
     private Button mTabsCountButton;
-    private View mBrowserAction;
     private TabListener mTabListener;
     private TabSessionManager mSessionManager;
 
     public ToolbarLayout(Context context, TabSessionManager sessionManager) {
         super(context);
         mSessionManager = sessionManager;
         initView();
     }
@@ -39,72 +30,28 @@ public class ToolbarLayout extends Linea
         setOrientation(LinearLayout.HORIZONTAL);
         mLocationView = new LocationView(getContext());
         mLocationView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1.0f));
         mLocationView.setId(R.id.url_bar);
         addView(mLocationView);
 
         mTabsCountButton = getTabsCountButton();
         addView(mTabsCountButton);
-
-        mBrowserAction = getBrowserAction();
-        addView(mBrowserAction);
     }
 
     private Button getTabsCountButton() {
         Button button = new Button(getContext());
         button.setLayoutParams(new LayoutParams(150, LayoutParams.MATCH_PARENT));
         button.setId(R.id.tabs_button);
         button.setOnClickListener(this::onTabButtonClicked);
         button.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.tab_number_background));
         button.setTypeface(button.getTypeface(), Typeface.BOLD);
         return button;
     }
 
-    private View getBrowserAction() {
-        View browserAction = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE))
-            .inflate(R.layout.browser_action, this, false);
-        browserAction.setVisibility(GONE);
-        return browserAction;
-    }
-
-    public void setBrowserActionButton(ActionButton button) {
-        if (button == null) {
-            mBrowserAction.setVisibility(GONE);
-            return;
-        }
-
-        BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), button.icon);
-        ImageView view = mBrowserAction.findViewById(R.id.browser_action_icon);
-        view.setOnClickListener(this::onBrowserActionButtonClicked);
-        view.setBackground(drawable);
-
-        TextView badge = mBrowserAction.findViewById(R.id.browser_action_badge);
-        if (button.text != null && !button.text.equals("")) {
-            if (button.backgroundColor != null) {
-                GradientDrawable backgroundDrawable = ((GradientDrawable) badge.getBackground().mutate());
-                backgroundDrawable.setColor(button.backgroundColor);
-                backgroundDrawable.invalidateSelf();
-            }
-            if (button.textColor != null) {
-                badge.setTextColor(button.textColor);
-            }
-            badge.setText(button.text);
-            badge.setVisibility(VISIBLE);
-        } else {
-            badge.setVisibility(GONE);
-        }
-
-        mBrowserAction.setVisibility(VISIBLE);
-    }
-
-    public void onBrowserActionButtonClicked(View view) {
-        mTabListener.onBrowserActionClick();
-    }
-
     public LocationView getLocationView() {
         return mLocationView;
     }
 
     public void setTabListener(TabListener listener) {
         this.mTabListener = listener;
     }
 
deleted file mode 100644
--- a/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid
-        android:id="@+id/browser_action_badge_background"
-        android:color="#176d7a"
-        />
-    <corners android:radius="5dp" />
-</shape>
\ No newline at end of file
deleted file mode 100644
--- a/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="?android:actionBarSize"
-    android:layout_height="?android:actionBarSize"
-    android:gravity="center"
-    android:orientation="vertical">
-
-    <RelativeLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        >
-        <ImageView
-            android:id="@+id/browser_action_icon"
-            android:layout_width="36dp"
-            android:layout_height="36dp"
-            android:layout_centerInParent="true"
-            />
-    </RelativeLayout>
-
-    <TextView
-        android:id="@+id/browser_action_badge"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:background="@drawable/rounded_bg"
-        android:textColor="@color/colorPrimaryDark"
-        android:layout_alignParentRight="true"
-        android:paddingLeft="3dp"
-        android:paddingRight="3dp"
-        android:layout_marginTop="3dp"
-        android:layout_marginRight="3dp"
-        android:text="12"
-        />
-</RelativeLayout>
deleted file mode 100644
--- a/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <org.mozilla.geckoview.GeckoView
-        android:id="@+id/gecko_view_popup"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:scrollbars="none"
-        />
-</RelativeLayout>
--- a/mobile/android/geckoview_example/src/main/res/values/colors.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <color name="colorPrimary">#3F51B5</color>
-    <color name="colorBackgroundDark">#3F51B5</color>
-    <color name="colorPrimaryDark">#FFFFFF</color>
+    <color name="colorPrimaryDark">#303F9F</color>
     <color name="colorAccent">#FF4081</color>
 </resources>
--- a/mobile/android/geckoview_example/src/main/res/values/ids.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/ids.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <item name="toolbar_layout" type="id"/>
     <item name="url_bar" type="id"/>
-    <item name="browser_action" type="id"/>
     <item name="tabs_button" type="id"/>
 </resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/BrowserActions.jsm
@@ -0,0 +1,146 @@
+/* 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 { EventDispatcher } = ChromeUtils.import(
+  "resource://gre/modules/Messaging.jsm"
+);
+
+var EXPORTED_SYMBOLS = ["BrowserActions"];
+
+var BrowserActions = {
+  _browserActions: {},
+  _browserActionTitles: {},
+
+  _initialized: false,
+
+  /**
+   * Registers the listeners only if they have not been initialized
+   * already and there is at least one browser action.
+   */
+  _maybeRegisterListeners() {
+    if (!this._initialized && Object.keys(this._browserActions).length) {
+      this._initialized = true;
+      EventDispatcher.instance.registerListener(this, "Menu:Clicked");
+    }
+  },
+
+  /**
+   * Unregisters the listeners if they are already initizliaed and
+   * all of the browser actions have been removed.
+   */
+  _maybeUnregisterListeners() {
+    if (this._initialized && !Object.keys(this._browserActions).length) {
+      this._initialized = false;
+      EventDispatcher.instance.unregisterListener(this, "Menu:Clicked");
+    }
+  },
+
+  /**
+   * Called when a browser action is clicked on.
+   * @param {string} event The name of the event, which should always
+   *    be "Menu:Clicked".
+   * @param {Object} data An object containing information about the
+   *    browser action, which in this case should contain an `item`
+   *    property which is browser action's UUID.
+   */
+  onEvent(event, data) {
+    if (event !== "Menu:Clicked") {
+      throw new Error(
+        `Expected "Menu:Clicked" event - received "${event}" instead`
+      );
+    }
+
+    let browserAction = this._browserActions[data.item];
+    if (!browserAction) {
+      // This was probably meant for the NativeWindow menu handler.
+      return;
+    }
+    browserAction.onClicked();
+  },
+
+  /**
+   * Registers a new browser action.
+   * @param {Object} browserAction The browser action to add.
+   */
+  register(browserAction) {
+    EventDispatcher.instance.sendRequest({
+      type: "Menu:Add",
+      uuid: browserAction.uuid,
+      name: browserAction.defaults.name,
+    });
+
+    this._browserActions[browserAction.uuid] = browserAction;
+    this._browserActionTitles[browserAction.uuid] = browserAction.defaults.name;
+
+    this._maybeRegisterListeners();
+  },
+
+  /**
+   * Updates the browser action with the specified UUID.
+   * @param {string} uuid The UUID of the browser action.
+   * @param {Object} options The properties to update.
+   */
+  update(uuid, options) {
+    if (options.name) {
+      EventDispatcher.instance.sendRequest({
+        type: "Menu:Update",
+        uuid,
+        options,
+      });
+
+      this._browserActionTitles[uuid] = options.name;
+    }
+  },
+
+  /**
+   * Retrieves the name currently used for the browser action with the
+   * specified UUID. Used for testing only.
+   * @param {string} uuid The UUID of the browser action.
+   * @returns {string} the name currently used for the browser action.
+   */
+  getNameForActiveTab(uuid) {
+    return this._browserActionTitles[uuid];
+  },
+
+  /**
+   * Checks to see if the browser action is shown. Used for testing only.
+   * @param {string} uuid The UUID of the browser action.
+   * @returns {boolean} true if the browser action is shown; false otherwise.
+   */
+  isShown(uuid) {
+    return !!this._browserActions[uuid];
+  },
+
+  /**
+   * Synthesizes a click on the browser action. Used for testing only.
+   * @param {string} uuid The UUID of the browser action.
+   */
+  synthesizeClick(uuid) {
+    let browserAction = this._browserActions[uuid];
+    if (!browserAction) {
+      throw new Error(`No BrowserAction with UUID ${uuid} was found`);
+    }
+    browserAction.onClicked();
+  },
+
+  /**
+   * Unregisters the browser action with the specified UUID.
+   * @param {string} uuid The UUID of the browser action.
+   */
+  unregister(uuid) {
+    let browserAction = this._browserActions[uuid];
+    if (!browserAction) {
+      throw new Error(`No BrowserAction with UUID ${uuid} was found`);
+    }
+    EventDispatcher.instance.sendRequest({
+      type: "Menu:Remove",
+      uuid,
+    });
+    delete this._browserActions[uuid];
+    delete this._browserActionTitles[uuid];
+    this._maybeUnregisterListeners();
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/PageActions.jsm
@@ -0,0 +1,129 @@
+/* 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 { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { EventDispatcher } = ChromeUtils.import(
+  "resource://gre/modules/Messaging.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "uuidgen",
+  "@mozilla.org/uuid-generator;1",
+  "nsIUUIDGenerator"
+);
+
+var EXPORTED_SYMBOLS = ["PageActions"];
+
+// Copied from browser.js
+// TODO: We should move this method to a common importable location
+function resolveGeckoURI(aURI) {
+  if (!aURI) {
+    throw new Error("Can't resolve an empty uri");
+  }
+
+  if (aURI.startsWith("chrome://")) {
+    let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+      Ci.nsIChromeRegistry
+    );
+    return registry.convertChromeURL(Services.io.newURI(aURI)).spec;
+  } else if (aURI.startsWith("resource://")) {
+    let handler = Services.io
+      .getProtocolHandler("resource")
+      .QueryInterface(Ci.nsIResProtocolHandler);
+    return handler.resolveURI(Services.io.newURI(aURI));
+  }
+  return aURI;
+}
+
+var PageActions = {
+  _items: {},
+
+  _initialized: false,
+
+  _maybeInitialize: function() {
+    if (!this._initialized && Object.keys(this._items).length) {
+      this._initialized = true;
+      EventDispatcher.instance.registerListener(this, [
+        "PageActions:Clicked",
+        "PageActions:LongClicked",
+      ]);
+    }
+  },
+
+  _maybeUninitialize: function() {
+    if (this._initialized && !Object.keys(this._items).length) {
+      this._initialized = false;
+      EventDispatcher.instance.unregisterListener(this, [
+        "PageActions:Clicked",
+        "PageActions:LongClicked",
+      ]);
+    }
+  },
+
+  onEvent: function(event, data, callback) {
+    let item = this._items[data.id];
+    if (event == "PageActions:Clicked") {
+      if (item.clickCallback) {
+        item.clickCallback();
+      }
+    } else if (event == "PageActions:LongClicked") {
+      if (item.longClickCallback) {
+        item.longClickCallback();
+      }
+    }
+  },
+
+  isShown: function(id) {
+    return !!this._items[id];
+  },
+
+  synthesizeClick: function(id) {
+    let item = this._items[id];
+    if (item && item.clickCallback) {
+      item.clickCallback();
+    }
+  },
+
+  add: function(aOptions) {
+    let id = aOptions.id || uuidgen.generateUUID().toString();
+
+    EventDispatcher.instance.sendRequest({
+      type: "PageActions:Add",
+      id: id,
+      title: aOptions.title,
+      icon: resolveGeckoURI(aOptions.icon),
+      important: "important" in aOptions ? aOptions.important : false,
+      useTint: "useTint" in aOptions ? aOptions.useTint : false,
+    });
+
+    this._items[id] = {};
+
+    if (aOptions.clickCallback) {
+      this._items[id].clickCallback = aOptions.clickCallback;
+    }
+
+    if (aOptions.longClickCallback) {
+      this._items[id].longClickCallback = aOptions.longClickCallback;
+    }
+
+    this._maybeInitialize();
+    return id;
+  },
+
+  remove: function(id) {
+    EventDispatcher.instance.sendRequest({
+      type: "PageActions:Remove",
+      id: id,
+    });
+
+    delete this._items[id];
+    this._maybeUninitialize();
+  },
+};
--- a/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
@@ -1,102 +1,32 @@
 /* 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 = [
-  "ExtensionActionHelper",
-  "GeckoViewConnection",
-  "GeckoViewWebExtension",
-];
+var EXPORTED_SYMBOLS = ["GeckoViewConnection", "GeckoViewWebExtension"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 const { GeckoViewUtils } = ChromeUtils.import(
   "resource://gre/modules/GeckoViewUtils.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   EventDispatcher: "resource://gre/modules/Messaging.jsm",
   Extension: "resource://gre/modules/Extension.jsm",
   ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
-  GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.jsm",
 });
 
 const { debug, warn } = GeckoViewUtils.initLogging("Console"); // eslint-disable-line no-unused-vars
 
-/** Provides common logic between page and browser actions */
-class ExtensionActionHelper {
-  constructor({
-    tabTracker,
-    windowTracker,
-    tabContext,
-    properties,
-    extension,
-  }) {
-    this.tabTracker = tabTracker;
-    this.windowTracker = windowTracker;
-    this.tabContext = tabContext;
-    this.properties = properties;
-    this.extension = extension;
-  }
-
-  getTab(aTabId) {
-    if (aTabId !== null) {
-      return this.tabTracker.getTab(aTabId);
-    }
-    return null;
-  }
-
-  getWindow(aWindowId) {
-    if (aWindowId !== null) {
-      return this.windowTracker.getWindow(aWindowId);
-    }
-    return null;
-  }
-
-  extractProperties(aAction) {
-    const merged = {};
-    for (const p of this.properties) {
-      merged[p] = aAction[p];
-    }
-    return merged;
-  }
-
-  eventDispatcherFor(aTabId) {
-    if (!aTabId) {
-      return EventDispatcher.instance;
-    }
-
-    const windowId = GeckoViewTabBridge.tabIdToWindowId(aTabId);
-    const window = this.windowTracker.getWindow(windowId);
-    return window.WindowEventDispatcher;
-  }
-
-  sendRequestForResult(aTabId, aData) {
-    return this.eventDispatcherFor(aTabId).sendRequestForResult({
-      ...aData,
-      aTabId,
-      extensionId: this.extension.id,
-    });
-  }
-
-  sendRequest(aTabId, aData) {
-    return this.eventDispatcherFor(aTabId).sendRequest({
-      ...aData,
-      aTabId,
-      extensionId: this.extension.id,
-    });
-  }
-}
-
 class EmbedderPort extends ExtensionChild.Port {
   constructor(...args) {
     super(...args);
     EventDispatcher.instance.registerListener(this, [
       "GeckoView:WebExtension:PortMessageFromApp",
       "GeckoView:WebExtension:PortDisconnect",
     ]);
   }
@@ -262,57 +192,20 @@ var GeckoViewWebExtension = {
       await scope.shutdown();
       this.extensionScopes.delete(aId);
       aCallback.onSuccess();
     } catch (ex) {
       aCallback.onError(`Error unregistering WebExtension ${aId}. ${ex}`);
     }
   },
 
-  extensionById(aId) {
-    const scope = this.extensionScopes.get(aId);
-    if (!scope) {
-      return null;
-    }
-
-    return scope.extension;
-  },
-
   onEvent(aEvent, aData, aCallback) {
     debug`onEvent ${aEvent} ${aData}`;
 
     switch (aEvent) {
-      case "GeckoView:BrowserAction:Click": {
-        const extension = this.extensionById(aData.extensionId);
-        if (!extension) {
-          return;
-        }
-
-        const browserAction = this.browserActions.get(extension);
-        if (!browserAction) {
-          return;
-        }
-
-        browserAction.click();
-        break;
-      }
-      case "GeckoView:PageAction:Click": {
-        const extension = this.extensionById(aData.extensionId);
-        if (!extension) {
-          return;
-        }
-
-        const pageAction = this.pageActions.get(extension);
-        if (!pageAction) {
-          return;
-        }
-
-        pageAction.click();
-        break;
-      }
       case "GeckoView:RegisterWebExtension": {
         const uri = Services.io.newURI(aData.locationUri);
         if (
           uri == null ||
           (!(uri instanceof Ci.nsIFileURL) && !(uri instanceof Ci.nsIJARURI))
         ) {
           aCallback.onError(
             `Extension does not point to a resource URI or a file URL. extension=${
@@ -362,12 +255,8 @@ var GeckoViewWebExtension = {
 
         this.unregisterWebExtension(aData.id, aCallback);
         break;
     }
   },
 };
 
 GeckoViewWebExtension.extensionScopes = new Map();
-// WeakMap[Extension -> BrowserAction]
-GeckoViewWebExtension.browserActions = new WeakMap();
-// WeakMap[Extension -> PageAction]
-GeckoViewWebExtension.pageActions = new WeakMap();
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -17,28 +17,30 @@ with Files('HomeProvider.jsm'):
 with Files('geckoview/**'):
     BUG_COMPONENT = ('GeckoView', 'General')
 
 DIRS += ['geckoview']
 
 EXTRA_JS_MODULES += [
     'Accounts.jsm',
     'ActionBarHandler.jsm',
+    'BrowserActions.jsm',
     'dbg-browser-actors.js',
     'DownloadNotifications.jsm',
     'FormAssistant.jsm',
     'FxAccountsWebChannel.jsm',
     'HelperApps.jsm',
     'Home.jsm',
     'HomeProvider.jsm',
     'InputWidgetHelper.jsm',
     'LightweightThemeConsumer.jsm',
     'MediaPlayerApp.jsm',
     'NetErrorHelper.jsm',
     'Notifications.jsm',
+    'PageActions.jsm',
     'Prompt.jsm',
     'RuntimePermissions.jsm',
     'Sanitizer.jsm',
     'SelectHelper.jsm',
     'SharedPreferences.jsm',
     'Snackbars.jsm',
     'WebrtcUI.jsm',
     'WebsiteMetadata.jsm'
deleted file mode 100644
--- a/toolkit/components/extensions/ExtensionActions.jsm
+++ /dev/null
@@ -1,510 +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";
-
-var EXPORTED_SYMBOLS = ["BrowserActionBase", "PageActionBase"];
-
-const { ExtensionUtils } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionUtils.jsm"
-);
-const { ExtensionError } = ExtensionUtils;
-
-const { ExtensionParent } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionParent.jsm"
-);
-const { IconDetails, StartupCache } = ExtensionParent;
-
-function parseColor(color, kind) {
-  if (typeof color == "string") {
-    let rgba = InspectorUtils.colorToRGBA(color);
-    if (!rgba) {
-      throw new ExtensionError(`Invalid badge ${kind} color: "${color}"`);
-    }
-    color = [rgba.r, rgba.g, rgba.b, Math.round(rgba.a * 255)];
-  }
-  return color;
-}
-
-/** Common base class for Page and Browser actions. */
-class PanelActionBase {
-  constructor(options, tabContext, extension) {
-    this.tabContext = tabContext;
-    this.extension = extension;
-
-    // These are always defined on the action
-    this.defaults = {
-      enabled: true,
-      title: options.default_title || extension.name,
-      popup: options.default_popup || "",
-    };
-    this.globals = Object.create(this.defaults);
-
-    // eslint-disable-next-line mozilla/balanced-listeners
-    this.tabContext.on("location-change", this.handleLocationChange.bind(this));
-
-    // eslint-disable-next-line mozilla/balanced-listeners
-    this.tabContext.on("tab-select", (evt, tab) => {
-      this.updateOnChange(tab);
-    });
-  }
-
-  onShutdown() {
-    this.tabContext.shutdown();
-  }
-
-  setPropertyFromDetails(details, prop, value) {
-    return this.setProperty(this.getTargetFromDetails(details), prop, value);
-  }
-
-  /**
-   * Set a global, window specific or tab specific property.
-   *
-   * @param {XULElement|ChromeWindow|null} target
-   *        A XULElement tab, a ChromeWindow, or null for the global data.
-   * @param {string} prop
-   *        String property to set. Should should be one of "icon", "title", "badgeText",
-   *        "popup", "badgeBackgroundColor", "badgeTextColor" or "enabled".
-   * @param {string} value
-   *        Value for prop.
-   * @returns {Object}
-   *        The object to which the property has been set.
-   */
-  setProperty(target, prop, value) {
-    let values = this.getContextData(target);
-    if (value === null) {
-      delete values[prop];
-    } else {
-      values[prop] = value;
-    }
-
-    this.updateOnChange(target);
-    return values;
-  }
-
-  /**
-   * Gets the data associated with a tab, window, or the global one.
-   *
-   * @param {XULElement|ChromeWindow|null} target
-   *        A XULElement tab, a ChromeWindow, or null for the global data.
-   * @returns {Object}
-   *        The icon, title, badge, etc. associated with the target.
-   */
-  getContextData(target) {
-    if (target) {
-      return this.tabContext.get(target);
-    }
-    return this.globals;
-  }
-
-  /**
-   * Retrieve the value of a global, window specific or tab specific property.
-   *
-   * @param {XULElement|ChromeWindow|null} target
-   *        A XULElement tab, a ChromeWindow, or null for the global data.
-   * @param {string} prop
-   *        String property to retrieve. Should should be one of "icon", "title",
-   *        "badgeText", "popup", "badgeBackgroundColor" or "enabled".
-   * @returns {string} value
-   *          Value of prop.
-   */
-  getProperty(target, prop) {
-    return this.getContextData(target)[prop];
-  }
-
-  getPropertyFromDetails(details, prop) {
-    return this.getProperty(this.getTargetFromDetails(details), prop);
-  }
-
-  enable(tabId) {
-    this.setPropertyFromDetails({ tabId }, "enabled", true);
-  }
-
-  disable(tabId) {
-    this.setPropertyFromDetails({ tabId }, "enabled", false);
-  }
-
-  getIcon(details = {}) {
-    return this.getPropertyFromDetails(details, "icon");
-  }
-
-  normalizeIcon(details, extension, context) {
-    let icon = IconDetails.normalize(details, extension, context);
-    if (!Object.keys(icon).length) {
-      return null;
-    }
-    return icon;
-  }
-
-  /**
-   * Updates the `tabData` for any location change, however it only updates the button
-   * when the selected tab has a location change, or the selected tab has changed.
-   *
-   * @param {string} eventType
-   *        The type of the event, should be "location-change".
-   * @param {XULElement} tab
-   *        The tab whose location changed, or which has become selected.
-   * @param {boolean} [fromBrowse]
-   *        - `true` if navigation occurred in `tab`.
-   *        - `false` if the location changed but no navigation occurred, e.g. due to
-               a hash change or `history.pushState`.
-   *        - Omitted if TabSelect has occurred, tabData does not need to be updated.
-   */
-  handleLocationChange(eventType, tab, fromBrowse) {
-    if (fromBrowse) {
-      this.tabContext.clear(tab);
-    }
-  }
-
-  api(context) {
-    let { extension } = context;
-    return {
-      setTitle: details => {
-        this.setPropertyFromDetails(details, "title", details.title);
-      },
-      getTitle: details => {
-        return this.getPropertyFromDetails(details, "title");
-      },
-      setIcon: details => {
-        details.iconType = "browserAction";
-        this.setPropertyFromDetails(
-          details,
-          "icon",
-          this.normalizeIcon(details, extension, context)
-        );
-      },
-      setPopup: details => {
-        // Note: Chrome resolves arguments to setIcon relative to the calling
-        // context, but resolves arguments to setPopup relative to the extension
-        // root.
-        // For internal consistency, we currently resolve both relative to the
-        // calling context.
-        let url = details.popup && context.uri.resolve(details.popup);
-        if (url && !context.checkLoadURL(url)) {
-          return Promise.reject({ message: `Access denied for URL ${url}` });
-        }
-        this.setPropertyFromDetails(details, "popup", url);
-      },
-      getPopup: details => {
-        return this.getPropertyFromDetails(details, "popup");
-      },
-    };
-  }
-
-  // Override these
-
-  /**
-   * Update the toolbar button when the extension changes the icon, title, url, etc.
-   * If it only changes a parameter for a single tab, `target` will be that tab.
-   * If it only changes a parameter for a single window, `target` will be that window.
-   * Otherwise `target` will be null.
-   *
-   * @param {XULElement|ChromeWindow|null} target
-   *        Browser tab or browser chrome window, may be null.
-   */
-  updateOnChange(target) {}
-
-  /**
-   * Get tab object from tabId.
-   *
-   * @param {string} tabId
-   *        Internal id of the tab to get.
-   */
-  getTab(tabId) {}
-
-  /**
-   * Get window object from windowId
-   *
-   * @param {string} windowId
-   *        Internal id of the window to get.
-   */
-  getWindow(windowId) {}
-
-  /**
-   * Gets the target object corresponding to the `details` parameter of the various
-   * get* and set* API methods.
-   *
-   * @param {Object} details
-   *        An object with optional `tabId` or `windowId` properties.
-   * @throws if both `tabId` and `windowId` are specified, or if they are invalid.
-   * @returns {XULElement|ChromeWindow|null}
-   *        If a `tabId` was specified, the corresponding XULElement tab.
-   *        If a `windowId` was specified, the corresponding ChromeWindow.
-   *        Otherwise, `null`.
-   */
-  getTargetFromDetails({ tabId, windowId }) {
-    return null;
-  }
-}
-
-class PageActionBase extends PanelActionBase {
-  constructor(tabContext, extension) {
-    const options = extension.manifest.page_action;
-    super(options, tabContext, extension);
-
-    // `enabled` can have three different values:
-    // - `false`. This means the page action is not shown.
-    //   It's set as default if show_matches is empty. Can also be set in a tab via
-    //   `pageAction.hide(tabId)`, e.g. in order to override show_matches.
-    // - `true`. This means the page action is shown.
-    //   It's never set as default because <all_urls> doesn't really match all URLs
-    //   (e.g. "about:" URLs). But can be set in a tab via `pageAction.show(tabId)`.
-    // - `undefined`.
-    //   This is the default value when there are some patterns in show_matches.
-    //   Can't be set as a tab-specific value.
-    let enabled, showMatches, hideMatches;
-    let show_matches = options.show_matches || [];
-    let hide_matches = options.hide_matches || [];
-    if (!show_matches.length) {
-      // Always hide by default. No need to do any pattern matching.
-      enabled = false;
-    } else {
-      // Might show or hide depending on the URL. Enable pattern matching.
-      const { restrictSchemes } = extension;
-      showMatches = new MatchPatternSet(show_matches, { restrictSchemes });
-      hideMatches = new MatchPatternSet(hide_matches, { restrictSchemes });
-    }
-
-    this.defaults = {
-      ...this.defaults,
-      enabled,
-      showMatches,
-      hideMatches,
-      pinned: options.pinned,
-    };
-    this.globals = Object.create(this.defaults);
-  }
-
-  handleLocationChange(eventType, tab, fromBrowse) {
-    super.handleLocationChange(eventType, tab, fromBrowse);
-    if (fromBrowse === false) {
-      // Clear pattern matching cache when URL changes.
-      let tabData = this.tabContext.get(tab);
-      if (tabData.patternMatching !== undefined) {
-        tabData.patternMatching = undefined;
-      }
-    }
-
-    if (tab.selected) {
-      // isShownForTab will do pattern matching (if necessary) and store the result
-      // so that updateButton knows whether the page action should be shown.
-      this.isShownForTab(tab);
-      this.updateOnChange(tab);
-    }
-  }
-
-  // Checks whether the tab action is shown when the specified tab becomes active.
-  // Does pattern matching if necessary, and caches the result as a tab-specific value.
-  // @param {XULElement} tab
-  //        The tab to be checked
-  // @return boolean
-  isShownForTab(tab) {
-    let tabData = this.getContextData(tab);
-
-    // If there is a "show" value, return it. Can be due to show(), hide() or empty show_matches.
-    if (tabData.enabled !== undefined) {
-      return tabData.enabled;
-    }
-
-    // Otherwise pattern matching must have been configured. Do it, caching the result.
-    if (tabData.patternMatching === undefined) {
-      let uri = tab.linkedBrowser.currentURI;
-      tabData.patternMatching =
-        tabData.showMatches.matches(uri) && !tabData.hideMatches.matches(uri);
-    }
-    return tabData.patternMatching;
-  }
-
-  async loadIconData() {
-    const { extension } = this;
-    const options = extension.manifest.page_action;
-    this.defaults.icon = await StartupCache.get(
-      extension,
-      ["pageAction", "default_icon"],
-      () =>
-        this.normalizeIcon(
-          { path: options.default_icon || "" },
-          extension,
-          null
-        )
-    );
-  }
-
-  getPinned() {
-    return this.globals.pinned;
-  }
-
-  getTargetFromDetails({ tabId, windowId }) {
-    // PageActionBase doesn't support |windowId|
-    if (tabId != null) {
-      return this.getTab(tabId);
-    }
-    return null;
-  }
-
-  api(context) {
-    return {
-      ...super.api(context),
-      show: (...args) => this.enable(...args),
-      hide: (...args) => this.disable(...args),
-      isShown: ({ tabId }) => {
-        let tab = this.getTab(tabId);
-        return this.isShownForTab(tab);
-      },
-    };
-  }
-}
-
-class BrowserActionBase extends PanelActionBase {
-  constructor(tabContext, extension) {
-    const options = extension.manifest.browser_action;
-    super(options, tabContext, extension);
-
-    this.defaults = {
-      ...this.defaults,
-      badgeText: "",
-      badgeBackgroundColor: [0xd9, 0, 0, 255],
-      badgeDefaultColor: [255, 255, 255, 255],
-      badgeTextColor: null,
-      default_area: options.default_area || "navbar",
-    };
-    this.globals = Object.create(this.defaults);
-  }
-
-  async loadIconData() {
-    const { extension } = this;
-    const options = extension.manifest.browser_action;
-    this.defaults.icon = await StartupCache.get(
-      extension,
-      ["browserAction", "default_icon"],
-      () =>
-        IconDetails.normalize(
-          {
-            path: options.default_icon || extension.manifest.icons,
-            iconType: "browserAction",
-            themeIcons: options.theme_icons,
-          },
-          extension
-        )
-    );
-  }
-
-  handleLocationChange(eventType, tab, fromBrowse) {
-    super.handleLocationChange(eventType, tab, fromBrowse);
-    if (fromBrowse) {
-      this.updateOnChange(tab);
-    }
-  }
-
-  getTargetFromDetails({ tabId, windowId }) {
-    if (tabId != null && windowId != null) {
-      throw new ExtensionError(
-        "Only one of tabId and windowId can be specified."
-      );
-    }
-    if (tabId != null) {
-      return this.getTab(tabId);
-    } else if (windowId != null) {
-      return this.getWindow(windowId);
-    }
-    return null;
-  }
-
-  getDefaultArea() {
-    return this.globals.default_area;
-  }
-
-  /**
-   * Determines the text badge color to be used in a tab, window, or globally.
-   *
-   * @param {Object} values
-   *        The values associated with the tab or window, or global values.
-   * @returns {ColorArray}
-   */
-  getTextColor(values) {
-    // If a text color has been explicitly provided, use it.
-    let { badgeTextColor } = values;
-    if (badgeTextColor) {
-      return badgeTextColor;
-    }
-
-    // Otherwise, check if the default color to be used has been cached previously.
-    let { badgeDefaultColor } = values;
-    if (badgeDefaultColor) {
-      return badgeDefaultColor;
-    }
-
-    // Choose a color among white and black, maximizing contrast with background
-    // according to https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-procedure
-    let [r, g, b] = values.badgeBackgroundColor
-      .slice(0, 3)
-      .map(function(channel) {
-        channel /= 255;
-        if (channel <= 0.03928) {
-          return channel / 12.92;
-        }
-        return ((channel + 0.055) / 1.055) ** 2.4;
-      });
-    let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
-
-    // The luminance is 0 for black, 1 for white, and `lum` for the background color.
-    // Since `0 <= lum`, the contrast ratio for black is `c0 = (lum + 0.05) / 0.05`.
-    // Since `lum <= 1`, the contrast ratio for white is `c1 = 1.05 / (lum + 0.05)`.
-    // We want to maximize contrast, so black is chosen if `c1 < c0`, that is, if
-    // `1.05 * 0.05 < (L + 0.05) ** 2`. Otherwise white is chosen.
-    let channel = 1.05 * 0.05 < (lum + 0.05) ** 2 ? 0 : 255;
-    let result = [channel, channel, channel, 255];
-
-    // Cache the result as high as possible in the prototype chain
-    while (!Object.getOwnPropertyDescriptor(values, "badgeDefaultColor")) {
-      values = Object.getPrototypeOf(values);
-    }
-    values.badgeDefaultColor = result;
-    return result;
-  }
-
-  api(context) {
-    return {
-      ...super.api(context),
-      enable: (...args) => this.enable(...args),
-      disable: (...args) => this.disable(...args),
-      isEnabled: details => {
-        return this.getPropertyFromDetails(details, "enabled");
-      },
-      setBadgeText: details => {
-        this.setPropertyFromDetails(details, "badgeText", details.text);
-      },
-      getBadgeText: details => {
-        return this.getPropertyFromDetails(details, "badgeText");
-      },
-      setBadgeBackgroundColor: details => {
-        let color = parseColor(details.color, "background");
-        let values = this.setPropertyFromDetails(
-          details,
-          "badgeBackgroundColor",
-          color
-        );
-        if (color === null) {
-          // Let the default text color inherit after removing background color
-          delete values.badgeDefaultColor;
-        } else {
-          // Invalidate a cached default color calculated with the old background
-          values.badgeDefaultColor = null;
-        }
-      },
-      getBadgeBackgroundColor: details => {
-        return this.getPropertyFromDetails(details, "badgeBackgroundColor");
-      },
-      setBadgeTextColor: details => {
-        let color = parseColor(details.color, "text");
-        this.setPropertyFromDetails(details, "badgeTextColor", color);
-      },
-      getBadgeTextColor: details => {
-        let target = this.getTargetFromDetails(details);
-        let values = this.getContextData(target);
-        return this.getTextColor(values);
-      },
-    };
-  }
-}
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -5,17 +5,16 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 with Files('**'):
     BUG_COMPONENT = ('WebExtensions', 'General')
 
 EXTRA_JS_MODULES += [
     'Extension.jsm',
-    'ExtensionActions.jsm',
     'ExtensionActivityLog.jsm',
     'ExtensionChild.jsm',
     'ExtensionChildDevToolsUtils.jsm',
     'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
     'ExtensionPageChild.jsm',
     'ExtensionParent.jsm',
     'ExtensionPermissions.jsm',
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -1,17 +1,16 @@
 # 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 extensions %content/extensions/
     content/extensions/schemas/activity_log.json
     content/extensions/schemas/alarms.json
-    content/extensions/schemas/browser_action.json
     content/extensions/schemas/browser_settings.json
 #ifndef ANDROID
     content/extensions/schemas/captive_portal.json
 #endif
     content/extensions/schemas/clipboard.json
     content/extensions/schemas/content_scripts.json
     content/extensions/schemas/contextual_identities.json
     content/extensions/schemas/cookies.json
@@ -30,17 +29,16 @@ toolkit.jar:
     content/extensions/schemas/identity.json
 #endif
     content/extensions/schemas/idle.json
     content/extensions/schemas/management.json
     content/extensions/schemas/manifest.json
     content/extensions/schemas/native_manifest.json
     content/extensions/schemas/network_status.json
     content/extensions/schemas/notifications.json
-    content/extensions/schemas/page_action.json
     content/extensions/schemas/permissions.json
     content/extensions/schemas/proxy.json
     content/extensions/schemas/privacy.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
     content/extensions/schemas/telemetry.json
     content/extensions/schemas/test.json
     content/extensions/schemas/theme.json
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -51,16 +51,17 @@ DIRS += [
     'passwordmgr',
     'perfmonitoring',
     'pictureinpicture',
     'places',
     'processsingleton',
     'promiseworker',
     'prompts',
     'protobuf',
+    'reader',
     'remotebrowserutils',
     'remotepagemanager',
     'reflect',
     'reputationservice',
     'resistfingerprinting',
     'search',
     'securityreporter',
     'sessionstore',
@@ -79,20 +80,17 @@ DIRS += [
     'viewsource',
     'windowcreator',
     'windowwatcher',
     'workerloader',
     'xulstore',
 ]
 
 if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
-    DIRS += [
-      'narrate',
-      'reader',
-    ];
+    DIRS += ['narrate'];
 
     if CONFIG['NS_PRINTING']:
         DIRS += ['printing']
 
 if CONFIG['BUILD_CTYPES']:
     DIRS += ['ctypes']
 
 if CONFIG['MOZ_XUL']:
deleted file mode 100644
--- a/widget/android/ImageDecoderSupport.cpp
+++ /dev/null
@@ -1,172 +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/. */
-
-#include "ImageDecoderSupport.h"
-
-#include "imgITools.h"
-#include "gfxUtils.h"
-#include "AndroidGraphics.h"
-#include "JavaExceptions.h"
-#include "mozilla/gfx/Point.h"
-#include "mozilla/gfx/Swizzle.h"
-
-namespace mozilla {
-namespace widget {
-
-namespace {
-
-class ImageCallbackHelper;
-
-HashSet<RefPtr<ImageCallbackHelper>,
-      PointerHasher<ImageCallbackHelper*>>
-  gDecodeRequests;
-
-class ImageCallbackHelper : public imgIContainerCallback,
-                            public imgINotificationObserver {
- public:
-  NS_DECL_ISUPPORTS
-
-  void CompleteExceptionally(const char* aMessage) {
-    mResult->CompleteExceptionally(java::sdk::IllegalArgumentException::New(aMessage)
-        .Cast<jni::Throwable>());
-    gDecodeRequests.remove(this);
-  }
-
-  void Complete(DataSourceSurface::ScopedMap& aSourceSurface, int32_t width, int32_t height) {
-    auto pixels = mozilla::jni::ByteBuffer::New(
-        reinterpret_cast<int8_t*>(aSourceSurface.GetData()),
-        aSourceSurface.GetStride() * height);
-    auto bitmap = java::sdk::Bitmap::CreateBitmap(
-        width, height, java::sdk::Config::ARGB_8888());
-    bitmap->CopyPixelsFromBuffer(pixels);
-    mResult->Complete(bitmap);
-    gDecodeRequests.remove(this);
-  }
-
-  ImageCallbackHelper(java::GeckoResult::Param aResult, int32_t aDesiredLength)
-      : mResult(aResult), mDesiredLength(aDesiredLength), mImage(nullptr) {
-    MOZ_ASSERT(mResult);
-  }
-
-  NS_IMETHOD
-  OnImageReady(imgIContainer* aImage, nsresult aStatus) override {
-    // Let's make sure we are alive until the request completes
-    MOZ_ALWAYS_TRUE(gDecodeRequests.putNew(this));
-
-    if (NS_FAILED(aStatus)) {
-      CompleteExceptionally("Could not process image.");
-      return aStatus;
-    }
-
-    mImage = aImage;
-    return mImage->StartDecoding(
-        imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY,
-        imgIContainer::FRAME_FIRST);
-  }
-
-  // This method assumes that the image is ready to be processed
-  nsresult SendBitmap() {
-    RefPtr<gfx::SourceSurface> surface;
-
-    if (mDesiredLength > 0) {
-      surface = mImage->GetFrameAtSize(
-          gfx::IntSize(mDesiredLength, mDesiredLength),
-          imgIContainer::FRAME_FIRST,
-          imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
-    } else {
-      surface = mImage->GetFrame(
-          imgIContainer::FRAME_FIRST,
-          imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
-    }
-
-    RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
-
-    NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
-
-    int32_t width = dataSurface->GetSize().width;
-    int32_t height = dataSurface->GetSize().height;
-
-    DataSourceSurface::ScopedMap sourceMap(dataSurface,
-                                           DataSourceSurface::READ);
-
-    // Android's Bitmap only supports R8G8B8A8, so we need to convert the
-    // data to the right format
-    RefPtr<DataSourceSurface> destDataSurface =
-        Factory::CreateDataSourceSurfaceWithStride(dataSurface->GetSize(),
-                                                   SurfaceFormat::R8G8B8A8,
-                                                   sourceMap.GetStride());
-    NS_ENSURE_TRUE(destDataSurface, NS_ERROR_FAILURE);
-
-    DataSourceSurface::ScopedMap destMap(destDataSurface,
-                                         DataSourceSurface::READ_WRITE);
-
-    SwizzleData(sourceMap.GetData(), sourceMap.GetStride(),
-                surface->GetFormat(), destMap.GetData(), destMap.GetStride(),
-                SurfaceFormat::R8G8B8A8, destDataSurface->GetSize());
-
-    Complete(destMap, width, height);
-
-    return NS_OK;
-  }
-
-  NS_IMETHOD
-  Notify(imgIRequest* aRequest, int32_t aType,
-         const nsIntRect* aData) override {
-    if (aType == imgINotificationObserver::DECODE_COMPLETE) {
-      SendBitmap();
-    }
-
-    return NS_OK;
-  }
-
- private:
-  const java::GeckoResult::GlobalRef mResult;
-  int32_t mDesiredLength;
-  nsCOMPtr<imgIContainer> mImage;
-  virtual ~ImageCallbackHelper() {}
-};
-
-NS_IMPL_ISUPPORTS(ImageCallbackHelper, imgIContainerCallback,
-                  imgINotificationObserver)
-
-}  // namespace
-
-/* static */ void ImageDecoderSupport::Decode(jni::String::Param aUri,
-                                              int32_t aDesiredLength,
-                                              jni::Object::Param aResult) {
-  auto result = java::GeckoResult::LocalRef(aResult);
-  RefPtr<ImageCallbackHelper> helper =
-      new ImageCallbackHelper(result, aDesiredLength);
-
-  nsresult rv = DecodeInternal(aUri->ToString(), helper, helper);
-  if (NS_FAILED(rv)) {
-    helper->OnImageReady(nullptr, rv);
-  }
-}
-
-/* static */ nsresult ImageDecoderSupport::DecodeInternal(
-    const nsAString& aUri, imgIContainerCallback* aCallback,
-    imgINotificationObserver* aObserver) {
-  nsCOMPtr<imgITools> imgTools = do_GetService("@mozilla.org/image/tools;1");
-  if (NS_WARN_IF(!imgTools)) {
-    return NS_ERROR_FAILURE;
-  }
-
-  nsCOMPtr<nsIURI> uri;
-  nsresult rv = NS_NewURI(getter_AddRefs(uri), aUri);
-  NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
-
-  nsCOMPtr<nsIChannel> channel;
-  rv = NS_NewChannel(getter_AddRefs(channel), uri,
-                     nsContentUtils::GetSystemPrincipal(),
-                     nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                     nsIContentPolicy::TYPE_IMAGE);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  return imgTools->DecodeImageFromChannelAsync(uri, channel, aCallback,
-                                               aObserver);
-}
-
-}  // namespace widget
-}  // namespace mozilla
deleted file mode 100644
--- a/widget/android/ImageDecoderSupport.h
+++ /dev/null
@@ -1,30 +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/. */
-
-#ifndef ImageDecoderSupport_h__
-#define ImageDecoderSupport_h__
-
-#include "GeneratedJNINatives.h"
-
-class imgIContainerCallback;
-
-namespace mozilla {
-namespace widget {
-
-class ImageDecoderSupport final
-    : public java::ImageDecoder::Natives<ImageDecoderSupport> {
- public:
-  static void Decode(jni::String::Param aUri, int32_t aDesiredLength,
-                     jni::Object::Param aResult);
-
- private:
-  static nsresult DecodeInternal(const nsAString& aUri,
-                                 imgIContainerCallback* aCallback,
-                                 imgINotificationObserver* aObserver);
-};
-
-}  // namespace widget
-}  // namespace mozilla
-
-#endif  // ImageDecoderSupport_h__
--- a/widget/android/bindings/JavaExceptions-classes.txt
+++ b/widget/android/bindings/JavaExceptions-classes.txt
@@ -1,5 +1,2 @@
 [java.lang.IllegalStateException = skip:true]
-<init>(Ljava/lang/String;)V =
-
-[java.lang.IllegalArgumentException = skip:true]
-<init>(Ljava/lang/String;)V =
+<init>(Ljava/lang/String;)V =
\ No newline at end of file
--- a/widget/android/moz.build
+++ b/widget/android/moz.build
@@ -46,17 +46,16 @@ UNIFIED_SOURCES += [
     'AndroidAlerts.cpp',
     'AndroidBridge.cpp',
     'AndroidCompositorWidget.cpp',
     'AndroidContentController.cpp',
     'AndroidUiThread.cpp',
     'EventDispatcher.cpp',
     'GeckoEditableSupport.cpp',
     'GfxInfo.cpp',
-    'ImageDecoderSupport.cpp',
     'nsAndroidProtocolHandler.cpp',
     'nsAppShell.cpp',
     'nsClipboard.cpp',
     'nsDeviceContextAndroid.cpp',
     'nsIdleServiceAndroid.cpp',
     'nsLookAndFeel.cpp',
     'nsNativeThemeAndroid.cpp',
     'nsPrintSettingsServiceAndroid.cpp',
--- a/widget/android/nsAppShell.cpp
+++ b/widget/android/nsAppShell.cpp
@@ -61,17 +61,16 @@
 #include "AndroidUiThread.h"
 #include "GeckoBatteryManager.h"
 #include "GeckoNetworkManager.h"
 #include "GeckoProcessManager.h"
 #include "GeckoScreenOrientation.h"
 #include "GeckoSystemStateListener.h"
 #include "GeckoTelemetryDelegate.h"
 #include "GeckoVRManager.h"
-#include "ImageDecoderSupport.h"
 #include "PrefsHelper.h"
 #include "ScreenHelperAndroid.h"
 #include "Telemetry.h"
 #include "WebExecutorSupport.h"
 #include "Base64UtilsSupport.h"
 
 #ifdef DEBUG_ANDROID_EVENTS
 #  define EVLOG(args...) ALOG(args)
@@ -430,17 +429,16 @@ nsAppShell::nsAppShell()
     XPCOMEventTargetWrapper::Init();
     mozilla::GeckoBatteryManager::Init();
     mozilla::GeckoNetworkManager::Init();
     mozilla::GeckoProcessManager::Init();
     mozilla::GeckoScreenOrientation::Init();
     mozilla::GeckoSystemStateListener::Init();
     mozilla::PrefsHelper::Init();
     mozilla::widget::Telemetry::Init();
-    mozilla::widget::ImageDecoderSupport::Init();
     mozilla::widget::WebExecutorSupport::Init();
     mozilla::widget::Base64UtilsSupport::Init();
     nsWindow::InitNatives();
     mozilla::gl::AndroidSurfaceTexture::Init();
     mozilla::WebAuthnTokenManager::Init();
     mozilla::widget::GeckoTelemetryDelegate::Init();
 
     java::GeckoThread::SetState(java::GeckoThread::State::JNI_READY());