Merge autoland to mozilla-central. a=merge
authorCsoregi Natalia <ncsoregi@mozilla.com>
Mon, 18 Nov 2019 23:29:08 +0200
changeset 502502 f9829d8dd6387dfc7c575349f271fecb865acc61
parent 502501 edad970978193a3e5188e74dd09640039d6e2c76 (current diff)
parent 502442 5ebe15b529320ffc221ba354b30f526cb1a84a75 (diff)
child 502503 ee044234e17fe1b28a3aaaf1dd04ddbe0a975251
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)
reviewersmerge
milestone72.0a1
first release with
nightly linux32
f9829d8dd638 / 72.0a1 / 20191118213128 / files
nightly linux64
f9829d8dd638 / 72.0a1 / 20191118213128 / files
nightly mac
f9829d8dd638 / 72.0a1 / 20191118213128 / files
nightly win32
f9829d8dd638 / 72.0a1 / 20191118213128 / files
nightly win64
f9829d8dd638 / 72.0a1 / 20191118213128 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge autoland to mozilla-central. a=merge
browser/components/extensions/schemas/browser_action.json
browser/components/extensions/schemas/page_action.json
mobile/android/components/extensions/ext-browserAction.js
mobile/android/components/extensions/ext-pageAction.js
mobile/android/components/extensions/schemas/browser_action.json
mobile/android/components/extensions/schemas/page_action.json
mobile/android/components/extensions/test/mochitest/test_ext_activeTab_permission.html
mobile/android/components/extensions/test/mochitest/test_ext_browserAction_getPopup_setPopup.html
mobile/android/components/extensions/test/mochitest/test_ext_browserAction_getTitle_setTitle.html
mobile/android/components/extensions/test/mochitest/test_ext_browserAction_onClicked.html
mobile/android/components/extensions/test/mochitest/test_ext_pageAction_getPopup_setPopup.html
mobile/android/components/extensions/test/mochitest/test_ext_pageAction_show_hide.html
mobile/android/components/extensions/test/mochitest/test_ext_popup_behavior.html
mobile/android/modules/BrowserActions.jsm
mobile/android/modules/PageActions.jsm
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1573507879434">
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1573804224556">
   <emItems>
     <emItem blockID="i334" id="{0F827075-B026-42F3-885D-98981EE7B1AE}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="i1211" id="flvto@hotger.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
@@ -3544,16 +3544,24 @@
     <emItem blockID="11fd123a-e67d-44ab-909f-b776ea2e8d0a" id="/^((\{3c8970fa-1340-45ad-82fe-81f3beccfbdc\})|(\{4ab99b95-4d05-438c-8a3e-adb1b3fe8d81\})|(\{7f87a05d-dba7-448e-9af2-ee0f4a294c01\})|(\{59a219a8-45cd-458d-9b3e-8d86c19dfc31\})|(\{79f4bfc7-b1da-4dc4-85cc-ecbcc5dd152e\})|(\{484dc5ad-4d6a-4ee4-91b7-b5b8166e6b3d\})|(\{2643d75f-9d64-47ef-9c23-78f0f055c7b8\})|(\{76399bf2-8354-4b11-bf43-6c863b195b1d\})|(\{110791c0-2883-4301-8214-90be7549df43\})|(\{a33e004d-2ac0-4d77-8e14-50780bc231a3\})|(\{aaaa5840-6b3b-49d8-92c2-9696798c4e2a\})|(\{bfc55377-7210-4e7a-828f-6fdb9df02847\})|(\{c6c78b9a-370d-49c5-b9c6-96d7e38861c5\})|(\{c115eb3a-4746-472b-8f1f-d8596c49b3b6\})|(\{deaa22e5-33ed-440f-a734-c3175e6228a7\})|(\{e34d5840-6b3b-49d8-92c2-9696798c4e2a\})|(aapbdbdomjkkjkaonfhkkikfgjllcleb@[cC]hrome-?[sS]tore-?[fF]oxified--?\d+)|(babelfox_client@rami)|(blndkmebkmenignoajhoemebccmmfjib@chrome-store-foxified-\d+)|(bridge-translate-app@chrome-store-foxified-\d+)|(dephbpajmknbniclommefdlnflkfnpgh@chrome-store-foxified-\d+)|(extension@newtab\.biz)|(generated-74o6bact7xu7y32fvfju4s@chrome-store-foxified-\d+)|(generated-axbwzwbksnnig1ug9v5dly@chrome-store-foxified-\d+)|(googletranslateelement@developer\.org)|(icdahkkjdchifpnbebileaelbcgipepe@chrome-store-foxified-\d+)|(ifgljfjnflaadalpmkkgdailepedeehd@chrome-store-foxified-\d+)|(knpgbkpddpcepnloiijojmgbdhihkjkl@chrome-store-foxified-\d+)|(translate-4@chrome-store-foxified-\d+))$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="53d8f35e-99ec-44fd-8082-3b713a5afcb3" id="extension@safeguard.ws">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="19c04aaf-c02d-4583-9978-c519245cd4fb" id="/^((\{c8476e06-0a50-41ec-a840-a2db436cf38c\})|(youtube\.downloader@firefox\.dev)|(youtubedownloader@firefox\.com)|(youtubehddownloader@firefox\.com)|(youtube\.d@firefox\.dev)|(advblock@blocker)|(YouTube@HD\.Downloader)|(adt-3\.0\.7@blocker)|(\{7131880e-d327-4802-b5ed-fee33c281abd\})|(\{5cb84843-504e-406e-8fb7-051c7fc3c9d3\})|(\{d8686bde-e666-4084-ae01-c75aa7a30f93\})|(\{96d35545-d94a-4ee1-bc43-d3055650587c\})|(ali-image-search@4\.0)|(\{e7634c48-0d36-448e-891e-b2036beebcd0\})|(\{442de29c-b710-45d4-b121-7b4be387c327\})|(lite-vpn-4\.1\.14@gmail\.com))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="d39750c3-a305-4487-bc0f-21500c3597dd" id="/^((_1gMembers_@www\.inboxace\.com)|(_39Members_@www\.mapsgalaxy\.com)|(_5zMembers_@www\.couponxplorer\.com)|(_65Members_@download\.fromdoctopdf\.com)|(_flMembers_@free\.myformsfinder\.com))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
   </emItems>
   <pluginItems>
     <pluginItem blockID="p332">
       <match name="filename" exp="libflashplayer\.so"/>
       <match name="description" exp="^Shockwave Flash 11.(0|1) r[0-9]{1,3}$"/>
       <infoURL>https://get.adobe.com/flashplayer/</infoURL>
       <versionRange severity="0" vulnerabilitystatus="1">
         <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
--- a/browser/base/content/test/about/browser.ini
+++ b/browser/base/content/test/about/browser.ini
@@ -5,17 +5,16 @@ support-files =
   searchSuggestionEngine.sjs
   searchSuggestionEngine.xml
   POSTSearchEngine.xml
   dummy_page.html
 prefs =
   browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
 
 [browser_aboutCertError.js]
-skip-if = fission
 [browser_aboutCertError_clockSkew.js]
 [browser_aboutCertError_exception.js]
 [browser_aboutCertError_mitm.js]
 [browser_aboutCertError_noSubjectAltName.js]
 [browser_aboutHome_search_POST.js]
 [browser_aboutHome_search_composing.js]
 [browser_aboutHome_search_searchbar.js]
 [browser_aboutHome_search_suggestion.js]
--- a/browser/base/content/test/about/browser_aboutCertError.js
+++ b/browser/base/content/test/about/browser_aboutCertError.js
@@ -32,26 +32,24 @@ add_task(async function checkReturnToAbo
 
     // Populate the shistory entries manually, since it happens asynchronously
     // and the following tests will be too soon otherwise.
     await TabStateFlusher.flush(browser);
     let { entries } = JSON.parse(SessionStore.getTabState(tab));
     is(entries.length, 1, "there is one shistory entry");
 
     info("Clicking the go back button on about:certerror");
-    await ContentTask.spawn(browser, { frame: useFrame }, async function({
-      frame,
-    }) {
-      let doc = frame
-        ? content.document.querySelector("iframe").contentDocument
-        : content.document;
-
-      let returnButton = doc.getElementById("returnButton");
-      if (!frame) {
-        is(
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
+    await SpecialPowers.spawn(bc, [useFrame], async function(subFrame) {
+      let returnButton = content.document.getElementById("returnButton");
+      if (!subFrame) {
+        Assert.equal(
           returnButton.getAttribute("autofocus"),
           "true",
           "returnButton has autofocus"
         );
       }
       // Note that going back to about:newtab might cause a process flip, if
       // the browser is configured to run about:newtab in its own special
       // content process.
@@ -105,27 +103,31 @@ add_task(async function checkReturnToPre
 
     // Populate the shistory entries manually, since it happens asynchronously
     // and the following tests will be too soon otherwise.
     await TabStateFlusher.flush(browser);
     let { entries } = JSON.parse(SessionStore.getTabState(tab));
     is(entries.length, 2, "there are two shistory entries");
 
     info("Clicking the go back button on about:certerror");
-    await ContentTask.spawn(browser, { frame: useFrame }, async function({
-      frame,
-    }) {
-      let doc = frame
-        ? content.document.querySelector("iframe").contentDocument
-        : content.document;
-      let returnButton = doc.getElementById("returnButton");
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
+
+    let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+      browser,
+      "pageshow",
+      true
+    );
+    await SpecialPowers.spawn(bc, [useFrame], async function(subFrame) {
+      let returnButton = content.document.getElementById("returnButton");
       returnButton.click();
-
-      await ContentTaskUtils.waitForEvent(this, "pageshow", true);
     });
+    await pageShownPromise;
 
     is(browser.webNavigation.canGoBack, false, "!webNavigation.canGoBack");
     is(browser.webNavigation.canGoForward, true, "webNavigation.canGoForward");
     is(gBrowser.currentURI.spec, GOOD_PAGE, "Went back");
 
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
   }
 });
@@ -146,85 +148,74 @@ add_task(async function checkAppBuildIDI
 add_task(async function checkAdvancedDetails() {
   info(
     "Loading a bad cert page and verifying the main error and advanced details section"
   );
   for (let useFrame of [false, true]) {
     let tab = await openErrorPage(BAD_CERT, useFrame);
     let browser = tab.linkedBrowser;
 
-    let message = await ContentTask.spawn(
-      browser,
-      { frame: useFrame },
-      async function({ frame }) {
-        let doc = frame
-          ? content.document.querySelector("iframe").contentDocument
-          : content.document;
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
 
-        let shortDescText = doc.getElementById("errorShortDescText");
-        info("Main error text: " + shortDescText.textContent);
-        ok(
-          shortDescText.textContent.includes("expired.example.com"),
-          "Should list hostname in error message."
-        );
+    let message = await SpecialPowers.spawn(bc, [], async function() {
+      let doc = content.document;
+      let shortDescText = doc.getElementById("errorShortDescText");
+      Assert.ok(
+        shortDescText.textContent.includes("expired.example.com"),
+        "Should list hostname in error message."
+      );
 
-        let exceptionButton = doc.getElementById("exceptionDialogButton");
-        ok(
-          !exceptionButton.disabled,
-          "Exception button is not disabled by default."
-        );
+      let exceptionButton = doc.getElementById("exceptionDialogButton");
+      Assert.ok(
+        !exceptionButton.disabled,
+        "Exception button is not disabled by default."
+      );
 
-        let advancedButton = doc.getElementById("advancedButton");
-        advancedButton.click();
+      let advancedButton = doc.getElementById("advancedButton");
+      advancedButton.click();
 
-        // Wait until fluent sets the errorCode inner text.
-        let el;
-        await ContentTaskUtils.waitForCondition(() => {
-          el = doc.getElementById("errorCode");
-          return el.textContent != "";
-        }, "error code has been set inside the advanced button panel");
+      // Wait until fluent sets the errorCode inner text.
+      let el;
+      await ContentTaskUtils.waitForCondition(() => {
+        el = doc.getElementById("errorCode");
+        return el.textContent != "";
+      }, "error code has been set inside the advanced button panel");
 
-        return { textContent: el.textContent, tagName: el.tagName };
-      }
-    );
+      return { textContent: el.textContent, tagName: el.tagName };
+    });
     is(
       message.textContent,
       "SEC_ERROR_EXPIRED_CERTIFICATE",
       "Correct error message found"
     );
     is(message.tagName, "a", "Error message is a link");
 
-    message = await ContentTask.spawn(
-      browser,
-      { frame: useFrame },
-      async function({ frame }) {
-        let win = frame
-          ? content.document.querySelector("iframe").contentWindow
-          : content;
-        let doc = win.document;
-
-        let errorCode = doc.getElementById("errorCode");
-        errorCode.click();
-        let div = doc.getElementById("certificateErrorDebugInformation");
-        let text = doc.getElementById("certificateErrorText");
+    message = await SpecialPowers.spawn(bc, [], async function() {
+      let doc = content.document;
+      let errorCode = doc.getElementById("errorCode");
+      errorCode.click();
+      let div = doc.getElementById("certificateErrorDebugInformation");
+      let text = doc.getElementById("certificateErrorText");
 
-        let serhelper = Cc[
-          "@mozilla.org/network/serialization-helper;1"
-        ].getService(Ci.nsISerializationHelper);
-        let serializable = win.docShell.failedChannel.securityInfo
-          .QueryInterface(Ci.nsITransportSecurityInfo)
-          .QueryInterface(Ci.nsISerializable);
-        let serializedSecurityInfo = serhelper.serializeToString(serializable);
-        return {
-          divDisplay: content.getComputedStyle(div).display,
-          text: text.textContent,
-          securityInfoAsString: serializedSecurityInfo,
-        };
-      }
-    );
+      let serhelper = Cc[
+        "@mozilla.org/network/serialization-helper;1"
+      ].getService(Ci.nsISerializationHelper);
+      let serializable = content.docShell.failedChannel.securityInfo
+        .QueryInterface(Ci.nsITransportSecurityInfo)
+        .QueryInterface(Ci.nsISerializable);
+      let serializedSecurityInfo = serhelper.serializeToString(serializable);
+      return {
+        divDisplay: content.getComputedStyle(div).display,
+        text: text.textContent,
+        securityInfoAsString: serializedSecurityInfo,
+      };
+    });
     isnot(message.divDisplay, "none", "Debug information is visible");
     ok(message.text.includes(BAD_CERT), "Correct URL found");
     ok(
       message.text.includes("Certificate has expired"),
       "Correct error message found"
     );
     ok(
       message.text.includes("HTTP Strict Transport Security: false"),
@@ -244,83 +235,74 @@ add_task(async function checkAdvancedDet
 add_task(async function checkAdvancedDetailsForHSTS() {
   info(
     "Loading a bad STS cert page and verifying the advanced details section"
   );
   for (let useFrame of [false, true]) {
     let tab = await openErrorPage(BAD_STS_CERT, useFrame);
     let browser = tab.linkedBrowser;
 
-    let message = await ContentTask.spawn(
-      browser,
-      { frame: useFrame },
-      async function({ frame }) {
-        let doc = frame
-          ? content.document.querySelector("iframe").contentDocument
-          : content.document;
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
 
-        let advancedButton = doc.getElementById("advancedButton");
-        advancedButton.click();
+    let message = await SpecialPowers.spawn(bc, [], async function() {
+      let doc = content.document;
+      let advancedButton = doc.getElementById("advancedButton");
+      advancedButton.click();
 
-        // Wait until fluent sets the errorCode inner text.
-        let ec;
-        await ContentTaskUtils.waitForCondition(() => {
-          ec = doc.getElementById("errorCode");
-          return ec.textContent != "";
-        }, "error code has been set inside the advanced button panel");
+      // Wait until fluent sets the errorCode inner text.
+      let ec;
+      await ContentTaskUtils.waitForCondition(() => {
+        ec = doc.getElementById("errorCode");
+        return ec.textContent != "";
+      }, "error code has been set inside the advanced button panel");
 
-        let cdl = doc.getElementById("cert_domain_link");
-        return {
-          ecTextContent: ec.textContent,
-          ecTagName: ec.tagName,
-          cdlTextContent: cdl.textContent,
-          cdlTagName: cdl.tagName,
-        };
-      }
-    );
+      let cdl = doc.getElementById("cert_domain_link");
+      return {
+        ecTextContent: ec.textContent,
+        ecTagName: ec.tagName,
+        cdlTextContent: cdl.textContent,
+        cdlTagName: cdl.tagName,
+      };
+    });
 
     const badStsUri = Services.io.newURI(BAD_STS_CERT);
     is(
       message.ecTextContent,
       "SSL_ERROR_BAD_CERT_DOMAIN",
       "Correct error message found"
     );
     is(message.ecTagName, "a", "Error message is a link");
     const url = badStsUri.prePath.slice(badStsUri.prePath.indexOf(".") + 1);
     is(message.cdlTextContent, url, "Correct cert_domain_link contents found");
     is(message.cdlTagName, "a", "cert_domain_link is a link");
 
-    message = await ContentTask.spawn(
-      browser,
-      { frame: useFrame },
-      async function({ frame }) {
-        let win = frame
-          ? content.document.querySelector("iframe").contentWindow
-          : content;
-        let doc = win.document;
+    message = await SpecialPowers.spawn(bc, [], async function() {
+      let doc = content.document;
 
-        let errorCode = doc.getElementById("errorCode");
-        errorCode.click();
-        let div = doc.getElementById("certificateErrorDebugInformation");
-        let text = doc.getElementById("certificateErrorText");
+      let errorCode = doc.getElementById("errorCode");
+      errorCode.click();
+      let div = doc.getElementById("certificateErrorDebugInformation");
+      let text = doc.getElementById("certificateErrorText");
 
-        let serhelper = Cc[
-          "@mozilla.org/network/serialization-helper;1"
-        ].getService(Ci.nsISerializationHelper);
-        let serializable = win.docShell.failedChannel.securityInfo
-          .QueryInterface(Ci.nsITransportSecurityInfo)
-          .QueryInterface(Ci.nsISerializable);
-        let serializedSecurityInfo = serhelper.serializeToString(serializable);
-        return {
-          divDisplay: content.getComputedStyle(div).display,
-          text: text.textContent,
-          securityInfoAsString: serializedSecurityInfo,
-        };
-      }
-    );
+      let serhelper = Cc[
+        "@mozilla.org/network/serialization-helper;1"
+      ].getService(Ci.nsISerializationHelper);
+      let serializable = content.docShell.failedChannel.securityInfo
+        .QueryInterface(Ci.nsITransportSecurityInfo)
+        .QueryInterface(Ci.nsISerializable);
+      let serializedSecurityInfo = serhelper.serializeToString(serializable);
+      return {
+        divDisplay: content.getComputedStyle(div).display,
+        text: text.textContent,
+        securityInfoAsString: serializedSecurityInfo,
+      };
+    });
     isnot(message.divDisplay, "none", "Debug information is visible");
     ok(message.text.includes(badStsUri.spec), "Correct URL found");
     ok(
       message.text.includes(
         "requested domain name does not match the server\u2019s certificate"
       ),
       "Correct error message found"
     );
@@ -342,65 +324,57 @@ add_task(async function checkAdvancedDet
 add_task(async function checkUnknownIssuerLearnMoreLink() {
   info(
     "Loading a cert error for self-signed pages and checking the correct link is shown"
   );
   for (let useFrame of [false, true]) {
     let tab = await openErrorPage(UNKNOWN_ISSUER, useFrame);
     let browser = tab.linkedBrowser;
 
-    let href = await ContentTask.spawn(
-      browser,
-      { frame: useFrame },
-      async function({ frame }) {
-        let doc = frame
-          ? content.document.querySelector("iframe").contentDocument
-          : content.document;
-        let learnMoreLink = doc.getElementById("learnMoreLink");
-        return learnMoreLink.href;
-      }
-    );
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
+
+    let href = await SpecialPowers.spawn(bc, [], async function() {
+      let learnMoreLink = content.document.getElementById("learnMoreLink");
+      return learnMoreLink.href;
+    });
     ok(href.endsWith("security-error"), "security-error in the Learn More URL");
 
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
   }
 });
 
 add_task(async function checkCautionClass() {
   info("Checking that are potentially more dangerous get a 'caution' class");
   for (let useFrame of [false, true]) {
     let tab = await openErrorPage(UNKNOWN_ISSUER, useFrame);
     let browser = tab.linkedBrowser;
 
-    await ContentTask.spawn(browser, { frame: useFrame }, async function({
-      frame,
-    }) {
-      let doc = frame
-        ? content.document.querySelector("iframe").contentDocument
-        : content.document;
-      is(
-        doc.body.classList.contains("caution"),
-        !frame,
-        `Cert error body has ${frame ? "no" : ""} caution class`
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
+
+    await SpecialPowers.spawn(bc, [useFrame], async function(subFrame) {
+      Assert.equal(
+        content.document.body.classList.contains("caution"),
+        !subFrame,
+        `Cert error body has ${subFrame ? "no" : ""} caution class`
       );
     });
 
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
     tab = await openErrorPage(BAD_STS_CERT, useFrame);
-    browser = tab.linkedBrowser;
-
-    await ContentTask.spawn(browser, { frame: useFrame }, async function({
-      frame,
-    }) {
-      let doc = frame
-        ? content.document.querySelector("iframe").contentDocument
-        : content.document;
-      ok(
-        !doc.body.classList.contains("caution"),
+    bc = tab.linkedBrowser.browsingContext;
+    await SpecialPowers.spawn(bc, [], async function() {
+      Assert.ok(
+        !content.document.body.classList.contains("caution"),
         "Cert error body has no caution class"
       );
     });
 
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
   }
 });
 
@@ -412,63 +386,62 @@ add_task(async function checkViewCertifi
   for (let useFrame of [true, false]) {
     if (useFrame) {
       // Bug #1573502
       continue;
     }
     let tab = await openErrorPage(UNKNOWN_ISSUER, useFrame);
     let browser = tab.linkedBrowser;
 
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
+
     let loaded = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
-    await ContentTask.spawn(browser, { frame: useFrame }, async function({
-      frame,
-    }) {
-      let doc = frame
-        ? content.document.querySelector("iframe").contentDocument
-        : content.document;
-      let viewCertificate = doc.getElementById("viewCertificate");
+    await SpecialPowers.spawn(bc, [], async function() {
+      let viewCertificate = content.document.getElementById("viewCertificate");
       viewCertificate.click();
     });
     await loaded;
 
     let spec = gBrowser.selectedTab.linkedBrowser.documentURI.spec;
     Assert.ok(
       spec.startsWith("about:certificate"),
       "about:certificate is the new opened tab"
     );
 
-    await ContentTask.spawn(
+    await SpecialPowers.spawn(
       gBrowser.selectedTab.linkedBrowser,
-      null,
+      [],
       async function() {
         let doc = content.document;
-
         let certificateSection = await ContentTaskUtils.waitForCondition(() => {
           return doc.querySelector("certificate-section");
         }, "Certificate section found");
 
         let infoGroup = certificateSection.shadowRoot.querySelector(
           "info-group"
         );
         Assert.ok(infoGroup, "infoGroup found");
 
         let items = infoGroup.shadowRoot.querySelectorAll("info-item");
         let commonnameID = items[items.length - 1].shadowRoot
           .querySelector("label")
           .getAttribute("data-l10n-id");
-        is(
+        Assert.equal(
           commonnameID,
           "certificate-viewer-common-name",
           "The correct item was selected"
         );
 
         let commonnameValue = items[items.length - 1].shadowRoot.querySelector(
           ".info"
         ).textContent;
-        is(
+        Assert.equal(
           commonnameValue,
           "self-signed.example.com",
           "Shows the correct certificate in the page"
         );
       }
     );
     BrowserTestUtils.removeTab(gBrowser.selectedTab); // closes about:certificate
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
@@ -478,27 +451,25 @@ add_task(async function checkViewCertifi
 add_task(async function checkBadStsCertHeadline() {
   info(
     "Loading a bad sts cert error page and checking that the correct headline is shown"
   );
   for (let useFrame of [false, true]) {
     let tab = await openErrorPage(BAD_CERT, useFrame);
     let browser = tab.linkedBrowser;
 
-    let titleContent = await ContentTask.spawn(
-      browser,
-      { frame: useFrame },
-      async function({ frame }) {
-        let doc = frame
-          ? content.document.querySelector("iframe").contentDocument
-          : content.document;
-        let titleText = doc.querySelector(".title-text");
-        return titleText.textContent;
-      }
-    );
+    let bc = browser.browsingContext;
+    if (useFrame) {
+      bc = bc.getChildren()[0];
+    }
+
+    let titleContent = await SpecialPowers.spawn(bc, [], async function() {
+      let titleText = content.document.querySelector(".title-text");
+      return titleText.textContent;
+    });
     if (useFrame) {
       ok(
         titleContent.endsWith("Security Issue"),
         "Did Not Connect: Potential Security Issue"
       );
     } else {
       ok(
         titleContent.endsWith("Risk Ahead"),
@@ -513,33 +484,33 @@ add_task(async function checkSandboxedIf
   info(
     "Loading a bad sts cert error in a sandboxed iframe and check that the correct headline is shown"
   );
   let useFrame = true;
   let sandboxed = true;
   let tab = await openErrorPage(BAD_CERT, useFrame, sandboxed);
   let browser = tab.linkedBrowser;
 
-  await ContentTask.spawn(browser, {}, async function() {
-    let doc = content.document.querySelector("iframe").contentDocument;
-
+  let bc = browser.browsingContext.getChildren()[0];
+  await SpecialPowers.spawn(bc, [], async function() {
+    let doc = content.document;
     let titleText = doc.querySelector(".title-text");
-    ok(
+    Assert.ok(
       titleText.textContent.endsWith("Security Issue"),
       "Title shows Did Not Connect: Potential Security Issue"
     );
 
     // Wait until fluent sets the errorCode inner text.
     let el;
     await ContentTaskUtils.waitForCondition(() => {
       el = doc.getElementById("errorCode");
       return el.textContent != "";
     }, "error code has been set inside the advanced button panel");
 
-    is(
+    Assert.equal(
       el.textContent,
       "SEC_ERROR_EXPIRED_CERTIFICATE",
       "Correct error message found"
     );
-    is(el.tagName, "a", "Error message is a link");
+    Assert.equal(el.tagName, "a", "Error message is a link");
   });
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/base/content/test/about/head.js
+++ b/browser/base/content/test/about/head.js
@@ -29,40 +29,37 @@ function getPEMString(cert) {
   var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
   return (
     "-----BEGIN CERTIFICATE-----\r\n" +
     wrapped +
     "\r\n-----END CERTIFICATE-----\r\n"
   );
 }
 
-function injectErrorPageFrame(tab, src, sandboxed) {
-  return ContentTask.spawn(
+async function injectErrorPageFrame(tab, src, sandboxed) {
+  let loadedPromise = BrowserTestUtils.browserLoaded(
     tab.linkedBrowser,
-    { frameSrc: src, frameSandboxed: sandboxed },
-    async function({ frameSrc, frameSandboxed }) {
-      let loaded = ContentTaskUtils.waitForEvent(
-        content.wrappedJSObject,
-        "DOMFrameContentLoaded"
-      );
-      let iframe = content.document.createElement("iframe");
-      iframe.src = frameSrc;
-      if (frameSandboxed) {
-        iframe.setAttribute("sandbox", "allow-scripts");
-      }
-      content.document.body.appendChild(iframe);
-      await loaded;
-      // We will have race conditions when accessing the frame content after setting a src,
-      // so we can't wait for AboutNetErrorLoad. Let's wait for the certerror class to
-      // appear instead (which should happen at the same time as AboutNetErrorLoad).
-      await ContentTaskUtils.waitForCondition(() =>
-        iframe.contentDocument.body.classList.contains("certerror")
-      );
+    true,
+    null,
+    true
+  );
+
+  await SpecialPowers.spawn(tab.linkedBrowser, [src, sandboxed], async function(
+    frameSrc,
+    frameSandboxed
+  ) {
+    let iframe = content.document.createElement("iframe");
+    iframe.src = frameSrc;
+    if (frameSandboxed) {
+      iframe.setAttribute("sandbox", "allow-scripts");
     }
-  );
+    content.document.body.appendChild(iframe);
+  });
+
+  await loadedPromise;
 }
 
 async function openErrorPage(src, useFrame, sandboxed) {
   let dummyPage =
     getRootDirectory(gTestPath).replace(
       "chrome://mochitests/content",
       "https://example.com"
     ) + "dummy_page.html";
--- 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://browser/content/schemas/browser_action.json",
+    "schema": "chrome://extensions/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://browser/content/schemas/page_action.json",
+    "schema": "chrome://extensions/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,141 +27,140 @@ ChromeUtils.defineModuleGetter(
   "resource://gre/modules/Timer.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ViewPopup",
   "resource:///modules/ExtensionPopups.jsm"
 );
 
-var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
+var { DefaultWeakMap } = 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.defaults.title || this.extension.name,
-      tooltiptext: this.defaults.title || "",
-      defaultArea: this.defaults.area,
+      label: this.action.getProperty(null, "title"),
+      tooltiptext: this.action.getProperty(null, "title"),
+      defaultArea: browserAreas[this.action.getDefaultArea()],
       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");
@@ -197,17 +196,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.globals, true);
+        this.updateButton(node, this.action.getContextData(null), true);
       },
 
       onBeforeCommand: event => {
         this.lastClickInfo = {
           button: event.button || 0,
           modifiers: clickModifiersFromEvent(event),
         };
       },
@@ -218,17 +217,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.getProperty(tab, "popup");
+        let popupURL = this.action.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);
@@ -264,21 +263,16 @@ 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
@@ -291,24 +285,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.getProperty(tab, "enabled")) {
+    if (!widget.node || !this.action.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.getProperty(tab, "popup")) {
+    if (this.action.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,
       });
@@ -325,18 +319,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.getProperty(tab, "popup");
-          let enabled = this.getProperty(tab, "enabled");
+          let popupURL = this.action.getProperty(tab, "popup");
+          let enabled = this.action.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.
@@ -372,18 +366,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.getProperty(tab, "popup");
-        let enabled = this.getProperty(tab, "enabled");
+        let popupURL = this.action.getProperty(tab, "popup");
+        let enabled = this.action.getProperty(tab, "enabled");
 
         if (
           popupURL &&
           enabled &&
           (this.pendingPopup || !ViewPopup.for(this.extension, window))
         ) {
           this.eventQueue.push("Hover");
           this.pendingPopup = this.getPopup(window, popupURL, true);
@@ -423,17 +417,17 @@ this.browserAction = class extends Exten
         break;
 
       case "auxclick":
         if (event.button !== 1) {
           return;
         }
 
         let { gBrowser } = window;
-        if (this.getProperty(gBrowser.selectedTab, "enabled")) {
+        if (this.action.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);
@@ -542,17 +536,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.getTextColor(tabData))}`,
+          `color: ${serializeColor(this.action.getTextColor(tabData))}`,
         ].join("; ")
       );
 
       let style = this.iconData.get(tabData.icon);
       node.setAttribute("style", style);
     };
     if (sync) {
       callback();
@@ -592,318 +586,51 @@ 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.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);
+      this.updateButton(node, this.action.getContextData(tab));
     }
-    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 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;
-    }
+    let { action } = this;
 
     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),
-                  browserAction.lastClickInfo
+                  this.lastClickInfo
                 )
               );
             };
-            browserAction.on("click", listener);
+            this.on("click", listener);
             return () => {
-              browserAction.off("click", listener);
+              this.off("click", listener);
             };
           },
         }).api(),
 
-        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() {
+        openPopup: () => {
           let window = windowTracker.topWindow;
-          browserAction.triggerAction(window);
+          this.triggerAction(window);
         },
       },
     };
   }
 };
 
 global.browserActionFor = this.browserAction.for;
--- a/browser/components/extensions/parent/ext-pageAction.js
+++ b/browser/components/extensions/parent/ext-pageAction.js
@@ -17,87 +17,65 @@ ChromeUtils.defineModuleGetter(
   "resource:///modules/PageActions.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "PanelPopup",
   "resource:///modules/ExtensionPopups.jsm"
 );
 
-var { ExtensionParent } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionParent.jsm"
-);
+var { DefaultWeakMap } = ExtensionUtils;
 
-var { IconDetails, StartupCache } = ExtensionParent;
-
-var { DefaultWeakMap } = ExtensionUtils;
+var { PageActionBase } = ChromeUtils.import(
+  "resource://gre/modules/ExtensionActions.jsm"
+);
 
 // 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;
@@ -117,20 +95,20 @@ this.pageAction = class extends Extensio
           this.emit("click", tab);
         });
       };
 
       this.browserPageAction = PageActions.addAction(
         new PageActions.Action({
           id: widgetId,
           extensionID: extension.id,
-          title: this.defaults.title,
-          iconURL: this.defaults.icon,
-          pinnedToUrlbar: this.defaults.pinned,
-          disabled: !this.defaults.show,
+          title: this.action.getProperty(null, "title"),
+          iconURL: this.action.getProperty(null, "title"),
+          pinnedToUrlbar: this.action.getPinned(),
+          disabled: !this.action.getProperty(null, "enabled"),
           onCommand: (event, buttonNode) => {
             this.lastClickInfo = {
               button: event.button || 0,
               modifiers: clickModifiersFromEvent(event),
             };
             this.handleClick(event.target.ownerGlobal);
           },
           onBeforePlacedInWindow: browserWindow => {
@@ -146,143 +124,90 @@ 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 (show === undefined) {
+      if (this.action.getProperty(null, "enabled") === undefined) {
         for (let window of windowTracker.browserWindows()) {
           let tab = window.gBrowser.selectedTab;
-          if (this.isShown(tab)) {
+          if (this.action.isShownForTab(tab)) {
             this.updateButton(window);
           }
         }
       }
     }
   }
 
   onShutdown(isAppShutdown) {
     pageActionMap.delete(this.extension);
-
-    this.tabContext.shutdown();
+    this.action.onShutdown();
 
     // 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 "show" and "patternMatching" properties.
+  // Enables or disables the icon, based on the "enabled" and "patternMatching" properties.
   updateButton(window) {
     let tab = window.gBrowser.selectedTab;
-    let tabData = this.tabContext.get(tab);
+    let tabData = this.action.getContextData(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 show = tabData.show != null ? tabData.show : tabData.patternMatching;
-      if (last.show !== show) {
-        this.browserPageAction.setDisabled(!show, window);
-        last.show = show;
+      let enabled =
+        tabData.enabled != null ? tabData.enabled : tabData.patternMatching;
+      if (last.enabled !== enabled) {
+        this.browserPageAction.setDisabled(!enabled, window);
+        last.enabled = enabled;
       }
 
       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.isShown(window.gBrowser.selectedTab)) {
+    if (this.action.isShownForTab(window.gBrowser.selectedTab)) {
       this.lastClickInfo = { button: 0, modifiers: [] };
       this.handleClick(window);
     }
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "popupshowing":
@@ -310,17 +235,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.tabContext.get(tab).popup;
+    let popupURL = this.action.getProperty(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) {
@@ -355,135 +280,45 @@ 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) {
-    let { extension } = context;
-
+    const { extension } = context;
     const { tabManager } = extension;
-    const pageAction = this;
+    const { action } = 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)
               );
             };
 
-            pageAction.on("click", listener);
+            this.on("click", listener);
             return () => {
-              pageAction.off("click", listener);
+              this.off("click", listener);
             };
           },
         }).api(),
 
-        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() {
+        openPopup: () => {
           let window = windowTracker.topWindow;
-          pageAction.triggerAction(window);
+          this.triggerAction(window);
         },
       },
     };
   }
 };
 
 global.pageActionFor = this.pageAction.for;
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -1,29 +1,27 @@
 # 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
--- a/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
@@ -129,16 +129,24 @@ add_task(async function test_browserData
   await SiteDataTestUtils.addToIndexedDB("file:///fake/file", "foo", "bar", {});
 
   await extension.startup();
   await extension.awaitMessage("done");
   await extension.unload();
 });
 
 add_task(async function test_browserData_should_not_remove_extension_data() {
+  if (!Services.prefs.getBoolPref("dom.storage.next_gen")) {
+    // When LSNG isn't enabled, the browsingData API does still clear
+    // all the extensions localStorage if called without a list of specific
+    // origins to clear.
+    info("Test skipped because LSNG is currently disabled");
+    return;
+  }
+
   let extension = ExtensionTestUtils.loadExtension({
     async background() {
       window.localStorage.setItem("key", "value");
       await browser.browsingData.removeLocalStorage({}).catch(err => {
         browser.test.fail(`${err} :: ${err.stack}`);
       });
       browser.test.sendMessage("done", window.localStorage.getItem("key"));
     },
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js
@@ -33,16 +33,16 @@ add_task(async function() {
         "onItemChanged",
         (id, prop, isAnno, val) => prop == "title" && val == "folder"
       );
       fillBookmarkTextField("editBMPanel_namePicker", "folder", dialog);
       await promiseTitleChange;
     },
     dialog => {
       let savedItemId = dialog.gEditItemOverlay.itemId;
-      Assert.ok(savedItemId > 0, "Found the itemId");
+      Assert.greater(savedItemId, 0, "Found the itemId");
       return PlacesTestUtils.waitForNotification(
         "onItemRemoved",
         id => id === savedItemId
       );
     }
   );
 });
--- a/browser/components/places/tests/browser/browser_library_commands.js
+++ b/browser/components/places/tests/browser/browser_library_commands.js
@@ -140,17 +140,17 @@ add_task(async function test_query_on_to
     type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
     url: "place:sort=4",
     title: "special_query",
     parentGuid: PlacesUtils.bookmarks.toolbarGuid,
     index: 0,
   });
 
   // Get first child and check it is the just inserted query.
-  Assert.ok(toolbarNode.childCount > 0, "Toolbar node has children");
+  Assert.greater(toolbarNode.childCount, 0, "Toolbar node has children");
   let queryNode = toolbarNode.getChild(0);
   Assert.equal(
     queryNode.title,
     "special_query",
     "Query node is correctly selected"
   );
 
   // Select query node.
--- a/browser/components/places/tests/browser/browser_toolbar_overflow.js
+++ b/browser/components/places/tests/browser/browser_toolbar_overflow.js
@@ -127,17 +127,17 @@ add_task(async function test_separator_f
   promiseReady = BrowserTestUtils.waitForEvent(
     gToolbar,
     "BookmarksToolbarVisibilityUpdated"
   );
   await promiseSetToolbarVisibility(gToolbar, true);
   await promiseReady;
 
   let children = gToolbarContent.children;
-  Assert.ok(children.length > 2, "Multiple elements are visible");
+  Assert.greater(children.length, 2, "Multiple elements are visible");
   Assert.equal(
     children[1]._placesNode.uri,
     "http://test.places.0/",
     "Found the first bookmark"
   );
   Assert.equal(
     children[1].style.visibility,
     "visible",
--- a/browser/components/urlbar/docs/experiments.rst
+++ b/browser/components/urlbar/docs/experiments.rst
@@ -257,25 +257,29 @@ Unsigned
   builds include Nightly and Developer Edition but not Beta or Release
   [source__]. You can load extensions temporarily by visiting
   about:debugging#/runtime/this-firefox and clicking "Load Temporary Add-on."
   `web-ext <Workflow_>`__ also loads extensions temporarily.
 
   __ https://searchfox.org/mozilla-central/rev/3a61fb322f74a0396878468e50e4f4e97e369825/toolkit/components/extensions/Extension.jsm#1816
   __ https://searchfox.org/mozilla-central/search?q=MOZ_ALLOW_LEGACY_EXTENSIONS&redirect=false
 
-  They can be also be loaded normally (not temporarily) if you use a Firefox
-  build where the build-time setting ``AppConstants.MOZ_REQUIRE_SIGNING`` is
-  false [source__, source__] and you set the ``xpinstall.signatures.required``
-  pref to false. As in the previous paragraph, such builds include Nightly and
-  Developer Edition but not Beta or Release [source__].
+  They can be also be loaded normally (not temporarily) in a custom build where
+  ``AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS`` is true (as above), or where the
+  build-time setting ``AppConstants.MOZ_REQUIRE_SIGNING`` [source__, source__]
+  and ``xpinstall.signatures.required`` pref are both false. As in the previous
+  paragraph, such builds include Nightly and Developer Edition but not Beta or
+  Release [source__]. In addition, your custom build must modify the
+  ``Extension.isPrivileged`` getter__ to return true. This getter determines
+  whether an extension can access privileged APIs.
 
   __ https://searchfox.org/mozilla-central/rev/7088fc958db5935eba24b413b1f16d6ab7bd13ea/toolkit/mozapps/extensions/internal/XPIProvider.jsm#2378
   __ https://searchfox.org/mozilla-central/rev/7088fc958db5935eba24b413b1f16d6ab7bd13ea/toolkit/mozapps/extensions/internal/AddonSettings.jsm#36
   __ https://searchfox.org/mozilla-central/search?q=MOZ_REQUIRE_SIGNING&case=false&regexp=false&path=
+  __ https://searchfox.org/mozilla-central/rev/b58e44b74ef2b4a44bdfb4140c2565ac852504be/toolkit/components/extensions/Extension.jsm#1849
 
   Extensions remain unsigned as you develop them. See the Workflow_ section for
   more.
 
 Signed for testing (Signed for QA)
   Signed-for-testing extensions that use privileged APIs can be run using the
   same techniques for running unsigned extensions.
 
@@ -294,27 +298,20 @@ Signed for testing (Signed for QA)
 
 Signed for release
   Signed-for-release extensions that use privileged APIs can be run in any
   Firefox build with no special requirements.
 
   You encounter extensions that are signed for release when you are writing
   extensions for experiments. See the Experiments_ section for details.
 
-The ``Extension.isPrivileged`` getter__ determines whether an extension can
-access privileged APIs. If you have a custom Firefox build and you want to grant
-your extension access regardless of its signed state and how it's loaded, you
-can modify the getter to return true unconditionally. This can be useful in a
-pinch.
-
-To see console logs from extensions in the browser console, check the "Show
-Content Messages" checkbox in the console. This is necessary because extensions
-run outside the main process.
-
-__ https://searchfox.org/mozilla-central/rev/34cb8d0a2a324043bcfc2c56f37b31abe7fb23a8/toolkit/components/extensions/Extension.jsm#1812
+.. important::
+  To see console logs from extensions in the browser console, select the "Show
+  Content Messages" option in the console's settings. This is necessary because
+  extensions run outside the main process.
 
 Experiments
 -----------
 
 **Experiments** let us try out ideas in Firefox outside the usual six-week
 release cycle and on particular populations of users.
 
 For example, say we have a hunch that the top sites shown on the new-tab page
--- a/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js
+++ b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js
@@ -16,17 +16,17 @@ add_task(async function test() {
 
   Assert.equal(
     UrlbarTestUtils.getSelectedRowIndex(window),
     -1,
     "Nothing selected"
   );
 
   let resultCount = UrlbarTestUtils.getResultCount(window);
-  Assert.ok(resultCount > 0, "At least one result");
+  Assert.greater(resultCount, 0, "At least one result");
 
   for (let i = 0; i < resultCount; i++) {
     EventUtils.synthesizeKey("KEY_ArrowDown");
   }
   Assert.equal(
     UrlbarTestUtils.getSelectedRowIndex(window),
     resultCount - 1,
     "Last result selected"
--- 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.defaults.title = title;
+          pageActionAPI.action.getContextData(null).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.isShown(tab)) {
+            if (pageActionAPI.action.isShownForTab(tab)) {
               pageActionAPI.updateButton(window);
             }
           }
         },
         async setLabelForHistogram(label) {
           pageActionAPI.browserPageAction._labelForHistogram = label;
         },
         async setTooltipText(text) {
--- a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
+++ b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
@@ -570,17 +570,17 @@ add_task(async function test_shutdown_wh
  * browser.crashReports.unsubmittedCheck.shutdownWhileShowing is
  * set and the lastShownDate is today, then we don't decrement
  * browser.crashReports.unsubmittedCheck.chancesUntilSuppress.
  */
 add_task(async function test_dont_decrement_chances_on_same_day() {
   let initChances = UnsubmittedCrashHandler.prefs.getIntPref(
     "chancesUntilSuppress"
   );
-  Assert.ok(initChances > 1, "We should start with at least 1 chance.");
+  Assert.greater(initChances, 1, "We should start with at least 1 chance.");
 
   await createPendingCrashReports(1);
   let notification = await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.ok(notification, "There should be a notification");
 
   UnsubmittedCrashHandler.uninit();
 
   gNotificationBox.removeNotification(notification, true);
@@ -620,17 +620,17 @@ add_task(async function test_dont_decrem
  * browser.crashReports.unsubmittedCheck.shutdownWhileShowing is
  * set and the lastShownDate is before today, then we decrement
  * browser.crashReports.unsubmittedCheck.chancesUntilSuppress.
  */
 add_task(async function test_decrement_chances_on_other_day() {
   let initChances = UnsubmittedCrashHandler.prefs.getIntPref(
     "chancesUntilSuppress"
   );
-  Assert.ok(initChances > 1, "We should start with at least 1 chance.");
+  Assert.greater(initChances, 1, "We should start with at least 1 chance.");
 
   await createPendingCrashReports(1);
   let notification = await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.ok(notification, "There should be a notification");
 
   UnsubmittedCrashHandler.uninit();
 
   gNotificationBox.removeNotification(notification, true);
--- a/devtools/client/debugger/src/components/App.css
+++ b/devtools/client/debugger/src/components/App.css
@@ -83,21 +83,16 @@ button:focus {
 /*
   Prevents horizontal scrollbar from displaying when
   right pane collapsed (#7505)
 */
 .split-box > .splitter:last-child {
   display: none;
 }
 
-/* Enforce minimum sidebar widths for SplitBox */
-.split-box > .controlled {
-  min-width: 250px !important;
-}
-
 /* Launchpad Root */
 
 * {
   box-sizing: border-box;
 }
 
 html,
 body {
--- a/devtools/client/debugger/src/components/SecondaryPanes/index.js
+++ b/devtools/client/debugger/src/components/SecondaryPanes/index.js
@@ -485,16 +485,17 @@ class SecondaryPanes extends Component<P
       </div>
     );
   }
 
   renderVerticalLayout() {
     return (
       <SplitBox
         initialSize="300px"
+        minSize={10}
         maxSize="50%"
         splitterSize={1}
         startPanel={
           <div style={{ width: "inherit" }}>
             <WhyPaused delay={this.props.renderWhyPauseDelay} />
             <Accordion items={this.getStartItems()} />
           </div>
         }
--- a/devtools/client/performance-new/components/Description.js
+++ b/devtools/client/performance-new/components/Description.js
@@ -4,33 +4,33 @@
 "use strict";
 
 const { PureComponent } = require("devtools/client/shared/vendor/react");
 const {
   div,
   button,
   p,
 } = require("devtools/client/shared/vendor/react-dom-factories");
-const { openDocLink } = require("devtools/client/shared/link");
 
 /**
  * This component provides a helpful description for what is going on in the component
  * and provides some external links.
  */
 class Description extends PureComponent {
   static get propTypes() {
     return {};
   }
 
   constructor(props) {
     super(props);
     this.handleLinkClick = this.handleLinkClick.bind(this);
   }
 
   handleLinkClick(event) {
+    const { openDocLink } = require("devtools/client/shared/link");
     openDocLink(event.target.value);
   }
 
   /**
    * Implement links as buttons to avoid any risk of loading the link in the
    * the panel.
    */
   renderLink(href, text) {
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -169,19 +169,17 @@ StyleEditorUI.prototype = {
       );
     }
   },
 
   /**
    * Build the initial UI and wire buttons with event handlers.
    */
   createUI: function() {
-    const viewRoot = this._root.parentNode.querySelector(".splitview-root");
-
-    this._view = new SplitView(viewRoot);
+    this._view = new SplitView(this._root);
 
     wire(this._view.rootElement, ".style-editor-newButton", () => {
       this._debuggee.addStyleSheet(null);
     });
 
     wire(this._view.rootElement, ".style-editor-importButton", () => {
       this._importFromFile(this._mockImportFile || null, this._window);
     });
--- a/devtools/client/styleeditor/index.xhtml
+++ b/devtools/client/styleeditor/index.xhtml
@@ -81,83 +81,78 @@
   <commandset id="sourceEditorCommands">
     <command id="cmd_gotoLine" oncommand="goDoCommand('cmd_gotoLine')"/>
     <command id="cmd_find" oncommand="goDoCommand('cmd_find')"/>
     <command id="cmd_findAgain" oncommand="goDoCommand('cmd_findAgain')"/>
   </commandset>
 
   <keyset id="sourceEditorKeys"/>
 
-  <stack id="style-editor-chrome" class="loading theme-body">
-
-    <box class="splitview-root devtools-responsive-container" context="sidebar-context">
-      <box class="splitview-controller">
-        <box class="splitview-main">
-          <toolbar class="devtools-toolbar">
-             <hbox class="devtools-toolbarbutton-group">
-              <toolbarbutton class="style-editor-newButton devtools-toolbarbutton"
-                          accesskey="&newButton.accesskey;"
-                          tooltiptext="&newButton.tooltip;"/>
-              <toolbarbutton class="style-editor-importButton devtools-toolbarbutton"
-                          accesskey="&importButton.accesskey;"
-                          tooltiptext="&importButton.tooltip;"/>
-            </hbox>
-            <spacer/>
-            <toolbarbutton id="style-editor-options"
-                        class="devtools-toolbarbutton devtools-option-toolbarbutton"
-                        tooltiptext="&optionsButton.tooltip;"/>
-          </toolbar>
-        </box>
-        <box id="splitview-resizer-target" class="theme-sidebar splitview-nav-container"
-                persist="height">
-          <html:ol class="splitview-nav" tabindex="0"></html:ol>
-          <html:div class="splitview-nav placeholder empty">
-            <html:p><html:strong>&noStyleSheet.label;</html:strong></html:p>
-            <html:p>&noStyleSheet-tip-start.label;
-              <html:a href="#"
-                class="style-editor-newButton">&noStyleSheet-tip-action.label;</html:a>
-              &noStyleSheet-tip-end.label;</html:p>
-          </html:div>
-        </box> <!-- .splitview-nav-container -->
-      </box>   <!-- .splitview-controller -->
-      <splitter class="devtools-side-splitter devtools-invisible-splitter"/>
-      <box class="splitview-side-details devtools-main-content"/>
+  <box id="style-editor-chrome" class="devtools-responsive-container loading theme-body" context="sidebar-context">
+    <box class="splitview-controller">
+      <box class="splitview-main">
+        <toolbar class="devtools-toolbar">
+          <hbox class="devtools-toolbarbutton-group">
+            <toolbarbutton class="style-editor-newButton devtools-toolbarbutton"
+                        accesskey="&newButton.accesskey;"
+                        tooltiptext="&newButton.tooltip;"/>
+            <toolbarbutton class="style-editor-importButton devtools-toolbarbutton"
+                        accesskey="&importButton.accesskey;"
+                        tooltiptext="&importButton.tooltip;"/>
+          </hbox>
+          <spacer/>
+          <toolbarbutton id="style-editor-options"
+                      class="devtools-toolbarbutton devtools-option-toolbarbutton"
+                      tooltiptext="&optionsButton.tooltip;"/>
+        </toolbar>
+      </box>
+      <box id="splitview-resizer-target" class="theme-sidebar splitview-nav-container"
+              persist="height">
+        <html:ol class="splitview-nav" tabindex="0"></html:ol>
+        <html:div class="splitview-nav placeholder empty">
+          <html:p><html:strong>&noStyleSheet.label;</html:strong></html:p>
+          <html:p>&noStyleSheet-tip-start.label;
+            <html:a href="#"
+              class="style-editor-newButton">&noStyleSheet-tip-action.label;</html:a>
+            &noStyleSheet-tip-end.label;</html:p>
+        </html:div>
+      </box> <!-- .splitview-nav-container -->
+    </box>   <!-- .splitview-controller -->
+    <splitter class="devtools-side-splitter devtools-invisible-splitter"/>
+    <box class="splitview-side-details devtools-main-content"/>
 
-      <html:div id="splitview-templates" hidden="true">
-        <html:li id="splitview-tpl-summary-stylesheet" tabindex="0">
-          <label class="stylesheet-enabled" tabindex="0"
-            tooltiptext="&visibilityToggle.tooltip;"
-            accesskey="&saveButton.accesskey;"></label>
-          <html:hgroup class="stylesheet-info">
-            <html:h1><html:a class="stylesheet-name" tabindex="0"><label crop="center"/></html:a></html:h1>
-            <html:div class="stylesheet-more">
-              <html:h3 class="stylesheet-title"></html:h3>
-              <html:h3 class="stylesheet-linked-file"></html:h3>
-              <html:h3 class="stylesheet-rule-count"></html:h3>
-              <spacer/>
-              <html:h3><label class="stylesheet-saveButton"
-                    tooltiptext="&saveButton.tooltip;"
-                    accesskey="&saveButton.accesskey;">&saveButton.label;</label></html:h3>
-            </html:div>
-          </html:hgroup>
-        </html:li>
+    <html:div id="splitview-templates" hidden="true">
+      <html:li id="splitview-tpl-summary-stylesheet" tabindex="0">
+        <label class="stylesheet-enabled" tabindex="0"
+          tooltiptext="&visibilityToggle.tooltip;"
+          accesskey="&saveButton.accesskey;"></label>
+        <html:hgroup class="stylesheet-info">
+          <html:h1><html:a class="stylesheet-name" tabindex="0"><label crop="center"/></html:a></html:h1>
+          <html:div class="stylesheet-more">
+            <html:h3 class="stylesheet-title"></html:h3>
+            <html:h3 class="stylesheet-linked-file"></html:h3>
+            <html:h3 class="stylesheet-rule-count"></html:h3>
+            <spacer/>
+            <html:h3><label class="stylesheet-saveButton"
+                  tooltiptext="&saveButton.tooltip;"
+                  accesskey="&saveButton.accesskey;">&saveButton.label;</label></html:h3>
+          </html:div>
+        </html:hgroup>
+      </html:li>
 
-        <box id="splitview-tpl-details-stylesheet" class="splitview-details">
-          <hbox class="stylesheet-details-container">
-            <box class="stylesheet-editor-input textbox"
-                 data-placeholder="&editorTextbox.placeholder;"/>
-            <splitter class="devtools-side-splitter"/>
-            <vbox class="stylesheet-sidebar theme-sidebar" hidden="true">
-              <toolbar class="devtools-toolbar">
-                &mediaRules.label;
-              </toolbar>
-              <vbox class="stylesheet-media-container" flex="1">
-                <html:div class="stylesheet-media-list" />
-              </vbox>
+      <box id="splitview-tpl-details-stylesheet" class="splitview-details">
+        <hbox class="stylesheet-details-container">
+          <box class="stylesheet-editor-input textbox"
+               data-placeholder="&editorTextbox.placeholder;"/>
+          <splitter class="devtools-side-splitter"/>
+          <vbox class="stylesheet-sidebar theme-sidebar" hidden="true">
+            <toolbar class="devtools-toolbar">
+              &mediaRules.label;
+            </toolbar>
+            <vbox class="stylesheet-media-container" flex="1">
+              <html:div class="stylesheet-media-list" />
             </vbox>
-          </hbox>
-        </box>
-      </html:div> <!-- #splitview-templates -->
-    </box>   <!-- .splitview-root -->
-
-  </stack>
-
+          </vbox>
+        </hbox>
+      </box>
+    </html:div> <!-- #splitview-templates -->
+  </box>
 </window>
--- a/devtools/client/styleeditor/test/browser_styleeditor_loading.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_loading.js
@@ -14,21 +14,20 @@ add_task(async function() {
   const tabAdded = addTab(TESTCASE_URI);
   const target = await TargetFactory.forTab(gBrowser.selectedTab);
   const styleEditorLoaded = gDevTools.showToolbox(target, "styleeditor");
 
   await Promise.all([tabAdded, styleEditorLoaded]);
 
   const toolbox = gDevTools.getToolbox(target);
   const panel = toolbox.getPanel("styleeditor");
-  const { panelWindow } = panel;
+  const { panelWindow, UI: ui } = panel;
 
-  const root = panelWindow.document.querySelector(".splitview-root");
   ok(
-    !root.classList.contains("loading"),
+    !ui._root.classList.contains("loading"),
     "style editor root element does not have 'loading' class name anymore"
   );
 
   let button = panelWindow.document.querySelector(".style-editor-newButton");
   ok(!button.hasAttribute("disabled"), "new style sheet button is enabled");
 
   button = panelWindow.document.querySelector(".style-editor-importButton");
   ok(!button.hasAttribute("disabled"), "import button is enabled");
--- a/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js
@@ -3,25 +3,27 @@
 "use strict";
 
 // Test that 'no styles' indicator is shown if a page doesn't contain any style
 // sheets.
 
 const TESTCASE_URI = TEST_BASE_HTTP + "nostyle.html";
 
 add_task(async function() {
-  const { panel } = await openStyleEditorForURL(TESTCASE_URI);
+  const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
   const { panelWindow } = panel;
 
-  const root = panelWindow.document.querySelector(".splitview-root");
   ok(
-    !root.classList.contains("loading"),
+    !ui._root.classList.contains("loading"),
     "style editor root element does not have 'loading' class name anymore"
   );
 
-  ok(root.querySelector(".empty.placeholder"), "showing 'no style' indicator");
+  ok(
+    ui._root.querySelector(".empty.placeholder"),
+    "showing 'no style' indicator"
+  );
 
   let button = panelWindow.document.querySelector(".style-editor-newButton");
   ok(!button.hasAttribute("disabled"), "new style sheet button is enabled");
 
   button = panelWindow.document.querySelector(".style-editor-importButton");
   ok(!button.hasAttribute("disabled"), "import button is enabled");
 });
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -343,17 +343,16 @@ support-files = file_bug357450.js
 [test_bug366946.html]
 [test_bug367164.html]
 [test_bug368972.html]
 [test_bug371576-2.html]
 [test_bug371576-3.html]
 [test_bug371576-4.html]
 [test_bug371576-5.html]
 [test_bug372086.html]
-skip-if = !xbl
 [test_bug372964-2.html]
 [test_bug372964.html]
 [test_bug373181.xhtml]
 [test_bug375314.html]
 [test_bug378969.html]
 [test_bug380418.html]
 support-files = test_bug380418.html^headers^
 [test_bug382113.html]
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -3555,19 +3555,21 @@ nsresult ContentChild::AsyncOpenAnonymou
   // Remember the association with the callback.
   MOZ_ASSERT(!mPendingAnonymousTemporaryFiles.Get(newID));
   mPendingAnonymousTemporaryFiles.LookupOrAdd(newID, aCallback);
   return NS_OK;
 }
 
 mozilla::ipc::IPCResult ContentChild::RecvSetPermissionsWithKey(
     const nsCString& aPermissionKey, nsTArray<IPC::Permission>&& aPerms) {
-  nsCOMPtr<nsIPermissionManager> permissionManager =
-      services::GetPermissionManager();
-  permissionManager->SetPermissionsWithKey(aPermissionKey, aPerms);
+  RefPtr<nsPermissionManager> permManager = nsPermissionManager::GetInstance();
+  if (permManager) {
+    permManager->SetPermissionsWithKey(aPermissionKey, aPerms);
+  }
+
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult ContentChild::RecvRefreshScreens(
     nsTArray<ScreenDetails>&& aScreens) {
   ScreenManager& screenManager = ScreenManager::GetSingleton();
   screenManager.Refresh(std::move(aScreens));
   return IPC_OK();
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -5495,30 +5495,30 @@ nsresult ContentParent::TransmitPermissi
 }
 
 void ContentParent::EnsurePermissionsByKey(const nsCString& aKey) {
   // NOTE: Make sure to initialize the permission manager before updating the
   // mActivePermissionKeys list. If the permission manager is being initialized
   // by this call to GetPermissionManager, and we've added the key to
   // mActivePermissionKeys, then the permission manager will send down a
   // SendAddPermission before receiving the SendSetPermissionsWithKey message.
-  nsCOMPtr<nsIPermissionManager> permManager = services::GetPermissionManager();
+  RefPtr<nsPermissionManager> permManager = nsPermissionManager::GetInstance();
+  if (!permManager) {
+    return;
+  }
 
   if (mActivePermissionKeys.Contains(aKey)) {
     return;
   }
   mActivePermissionKeys.PutEntry(aKey);
 
   nsTArray<IPC::Permission> perms;
-  nsresult rv = permManager->GetPermissionsWithKey(aKey, perms);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return;
-  }
-
-  Unused << SendSetPermissionsWithKey(aKey, perms);
+  if (permManager->GetPermissionsWithKey(aKey, perms)) {
+    Unused << SendSetPermissionsWithKey(aKey, perms);
+  }
 }
 
 bool ContentParent::NeedsPermissionsUpdate(
     const nsACString& aPermissionKey) const {
   return mActivePermissionKeys.Contains(aPermissionKey);
 }
 
 mozilla::ipc::IPCResult ContentParent::RecvAccumulateChildHistograms(
--- a/dom/serviceworkers/ServiceWorkerManager.cpp
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -58,16 +58,17 @@
 #include "mozilla/ipc/PBackgroundChild.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/dom/ScriptLoader.h"
 #include "mozilla/Unused.h"
 #include "mozilla/EnumSet.h"
 
 #include "nsContentUtils.h"
 #include "nsNetUtil.h"
+#include "nsPermissionManager.h"
 #include "nsProxyRelease.h"
 #include "nsQueryObject.h"
 #include "nsTArray.h"
 
 #include "ServiceWorker.h"
 #include "ServiceWorkerContainer.h"
 #include "ServiceWorkerInfo.h"
 #include "ServiceWorkerJobQueue.h"
@@ -2126,30 +2127,34 @@ void ServiceWorkerManager::DispatchFetch
     // ClientSource to be updated immediately after the nsIChannel starts.
     // This is necessary to have the correct controller in place for immediate
     // follow-on requests.
     loadInfo->SetController(serviceWorker->Descriptor());
   }
 
   MOZ_DIAGNOSTIC_ASSERT(serviceWorker);
 
-  nsCOMPtr<nsIRunnable> continueRunnable =
+  RefPtr<ContinueDispatchFetchEventRunnable> continueRunnable =
       new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(),
                                              aChannel, loadGroup,
                                              loadInfo->GetIsDocshellReload());
 
   // When this service worker was registered, we also sent down the permissions
   // for the runnable. They should have arrived by now, but we still need to
   // wait for them if they have not.
   nsCOMPtr<nsIRunnable> permissionsRunnable = NS_NewRunnableFunction(
       "dom::ServiceWorkerManager::DispatchFetchEvent", [=]() {
-        nsCOMPtr<nsIPermissionManager> permMgr =
-            services::GetPermissionManager();
-        MOZ_ALWAYS_SUCCEEDS(permMgr->WhenPermissionsAvailable(
-            serviceWorker->Principal(), continueRunnable));
+        RefPtr<nsPermissionManager> permMgr =
+            nsPermissionManager::GetInstance();
+        if (permMgr) {
+          permMgr->WhenPermissionsAvailable(serviceWorker->Principal(),
+                                            continueRunnable);
+        } else {
+          continueRunnable->HandleError();
+        }
       });
 
   nsCOMPtr<nsIUploadChannel2> uploadChannel =
       do_QueryInterface(internalChannel);
 
   // If there is no upload stream, then continue immediately
   if (!uploadChannel) {
     MOZ_ALWAYS_SUCCEEDS(permissionsRunnable->Run());
--- a/dom/tests/browser/perfmetrics/browser_test_performance_metrics.js
+++ b/dom/tests/browser/perfmetrics/browser_test_performance_metrics.js
@@ -143,24 +143,24 @@ add_task(async function test() {
           }
         }
       }
 
       // get all metrics via the promise
       let results = await ChromeUtils.requestPerformanceMetrics();
       exploreResults(results);
 
-      Assert.ok(workerDuration > 0, "Worker duration should be positive");
-      Assert.ok(workerTotal > 0, "Worker count should be positive");
-      Assert.ok(duration > 0, "Duration should be positive");
-      Assert.ok(total > 0, "Should get a positive count");
+      Assert.greater(workerDuration, 0, "Worker duration should be positive");
+      Assert.greater(workerTotal, 0, "Worker count should be positive");
+      Assert.greater(duration, 0, "Duration should be positive");
+      Assert.greater(total, 0, "Should get a positive count");
       Assert.ok(parentProcessEvent, "parent process sent back some events");
       Assert.ok(isTopLevel, "example.com as a top level window");
       Assert.ok(aboutMemoryFound, "about:memory");
-      Assert.ok(heapUsage > 0, "got some memory value reported");
+      Assert.greater(heapUsage, 0, "got some memory value reported");
       Assert.ok(sharedWorker, "We got some info from a shared worker");
       let numCounters = counterIds.length;
       Assert.ok(
         numCounters > 5,
         "This test generated at least " + numCounters + " unique counters"
       );
 
       // checking that subframes are not orphans
@@ -180,51 +180,51 @@ add_task(async function test() {
       Assert.ok(
         workerDuration > previousWorkerDuration,
         "Worker duration should be positive"
       );
       Assert.ok(
         workerTotal > previousWorkerTotal,
         "Worker count should be positive"
       );
-      Assert.ok(duration > previousDuration, "Duration should be positive");
-      Assert.ok(total > previousTotal, "Should get a positive count");
+      Assert.greater(duration, previousDuration, "Duration should be positive");
+      Assert.greater(total, previousTotal, "Should get a positive count");
 
       // load a tab with a setInterval, we should get counters on TaskCategory::Timer
       await BrowserTestUtils.withNewTab(
         { gBrowser, url: INTERVAL_URL },
         async function(browser) {
           let tabId = gBrowser.selectedBrowser.outerWindowID;
           let previousTimerCalls = timerCalls;
           results = await ChromeUtils.requestPerformanceMetrics();
           exploreResults(results, tabId);
-          Assert.ok(timerCalls > previousTimerCalls, "Got timer calls");
+          Assert.greater(timerCalls, previousTimerCalls, "Got timer calls");
         }
       );
 
       // load a tab with a setTimeout, we should get counters on TaskCategory::Timer
       await BrowserTestUtils.withNewTab(
         { gBrowser, url: TIMEOUT_URL },
         async function(browser) {
           let tabId = gBrowser.selectedBrowser.outerWindowID;
           let previousTimerCalls = timerCalls;
           results = await ChromeUtils.requestPerformanceMetrics();
           exploreResults(results, tabId);
-          Assert.ok(timerCalls > previousTimerCalls, "Got timer calls");
+          Assert.greater(timerCalls, previousTimerCalls, "Got timer calls");
         }
       );
 
       // load a tab with a sound
       await BrowserTestUtils.withNewTab(
         { gBrowser, url: SOUND_URL },
         async function(browser) {
           let tabId = gBrowser.selectedBrowser.outerWindowID;
           results = await ChromeUtils.requestPerformanceMetrics();
           exploreResults(results, tabId);
-          Assert.ok(mediaMemory > 0, "Got some memory used for media");
+          Assert.greater(mediaMemory, 0, "Got some memory used for media");
         }
       );
     }
   );
 
   BrowserTestUtils.removeTab(page1);
   BrowserTestUtils.removeTab(page2);
   BrowserTestUtils.removeTab(page3);
--- a/dom/workers/remoteworkers/RemoteWorkerChild.cpp
+++ b/dom/workers/remoteworkers/RemoteWorkerChild.cpp
@@ -9,18 +9,18 @@
 #include <utility>
 
 #include "MainThreadUtils.h"
 #include "nsDebug.h"
 #include "nsError.h"
 #include "nsIConsoleReportCollector.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIPrincipal.h"
-#include "nsIPermissionManager.h"
 #include "nsNetUtil.h"
+#include "nsPermissionManager.h"
 #include "nsProxyRelease.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
 
 #include "RemoteWorkerService.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/BasePrincipal.h"
@@ -469,20 +469,22 @@ nsresult RemoteWorkerChild::ExecWorkerOn
         __func__, [initializeWorkerRunnable = std::move(runnable),
                    self = std::move(self)] {
           if (NS_WARN_IF(!initializeWorkerRunnable->Dispatch())) {
             self->TransitionStateToTerminated();
             self->CreationFailedOnAnyThread();
           }
         });
 
-    nsCOMPtr<nsIPermissionManager> permissionManager =
-        services::GetPermissionManager();
-    MOZ_ALWAYS_SUCCEEDS(
-        permissionManager->WhenPermissionsAvailable(principal, r));
+    RefPtr<nsPermissionManager> permissionManager =
+        nsPermissionManager::GetInstance();
+    if (!permissionManager) {
+      return NS_ERROR_FAILURE;
+    }
+    permissionManager->WhenPermissionsAvailable(principal, r);
   } else {
     if (NS_WARN_IF(!runnable->Dispatch())) {
       rv = NS_ERROR_FAILURE;
       return rv;
     }
   }
 
   scopeExit.release();
--- a/dom/workers/remoteworkers/moz.build
+++ b/dom/workers/remoteworkers/moz.build
@@ -24,16 +24,17 @@ UNIFIED_SOURCES += [
     'RemoteWorkerParent.cpp',
     'RemoteWorkerService.cpp',
     'RemoteWorkerServiceChild.cpp',
     'RemoteWorkerServiceParent.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/serviceworkers',
+    '/extensions/permissions',
     '/xpcom/build',
 ]
 
 IPDL_SOURCES += [
     'PRemoteWorker.ipdl',
     'PRemoteWorkerController.ipdl',
     'PRemoteWorkerService.ipdl',
     'RemoteWorkerTypes.ipdlh',
--- a/extensions/permissions/nsContentBlocker.cpp
+++ b/extensions/permissions/nsContentBlocker.cpp
@@ -288,17 +288,17 @@ nsresult nsContentBlocker::TestPermissio
   // check the permission list first; if we find an entry, it overrides
   // default prefs.
   // Don't forget the aContentType ranges from 1..8, while the
   // array is indexed 0..7
   // All permissions tested by this method are preload permissions, so don't
   // bother actually checking with the permission manager unless we have a
   // preload permission.
   uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
-  if (mPermissionManager->GetHasPreloadPermissions()) {
+  if (mPermissionManager->HasPreloadPermissions()) {
     rv = mPermissionManager->LegacyTestPermissionFromURI(
         aCurrentURI, nullptr, kTypeString[aContentType - 1], &permission);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // If there is nothing on the list, use the default.
   if (!permission) {
     permission = mBehaviorPref[aContentType - 1];
--- a/extensions/permissions/nsPermissionManager.cpp
+++ b/extensions/permissions/nsPermissionManager.cpp
@@ -2245,31 +2245,16 @@ nsresult nsPermissionManager::LegacyTest
     nsIURI* aURI, const mozilla::OriginAttributes* aOriginAttributes,
     const nsACString& aType, uint32_t* aPermission) {
   return CommonTestPermission(aURI, aOriginAttributes, -1, aType, aPermission,
                               nsIPermissionManager::UNKNOWN_ACTION, false,
                               false, true);
 }
 
 NS_IMETHODIMP
-nsPermissionManager::TestPermissionFromWindow(mozIDOMWindow* aWindow,
-                                              const nsACString& aType,
-                                              uint32_t* aPermission) {
-  NS_ENSURE_ARG(aWindow);
-  nsCOMPtr<nsPIDOMWindowInner> window = nsPIDOMWindowInner::From(aWindow);
-
-  // Get the document for security check
-  RefPtr<Document> document = window->GetExtantDoc();
-  NS_ENSURE_TRUE(document, NS_NOINTERFACE);
-
-  nsCOMPtr<nsIPrincipal> principal = document->NodePrincipal();
-  return TestPermissionFromPrincipal(principal, aType, aPermission);
-}
-
-NS_IMETHODIMP
 nsPermissionManager::TestPermissionFromPrincipal(nsIPrincipal* aPrincipal,
                                                  const nsACString& aType,
                                                  uint32_t* aPermission) {
   return CommonTestPermission(aPrincipal, -1, aType, aPermission,
                               nsIPermissionManager::UNKNOWN_ACTION, false,
                               false, true);
 }
 
@@ -2632,18 +2617,17 @@ nsresult nsPermissionManager::RemoveAllM
   ENSURE_NOT_CHILD_PROCESS;
 
   return RemovePermissionEntries(
       [aModificationTime](const PermissionEntry& aPermEntry) {
         return aModificationTime <= aPermEntry.mModificationTime;
       });
 }
 
-NS_IMETHODIMP
-nsPermissionManager::RemovePermissionsWithAttributes(
+nsresult nsPermissionManager::RemovePermissionsWithAttributes(
     const nsAString& aPattern) {
   ENSURE_NOT_CHILD_PROCESS;
   mozilla::OriginAttributesPattern pattern;
   if (!pattern.Init(aPattern)) {
     return NS_ERROR_INVALID_ARG;
   }
 
   return RemovePermissionsWithAttributes(pattern);
@@ -3008,71 +2992,21 @@ void nsPermissionManager::UpdateDB(
     return;
   }
 
   nsCOMPtr<mozIStoragePendingStatement> pending;
   rv = aStmt->ExecuteAsync(nullptr, getter_AddRefs(pending));
   MOZ_ASSERT(NS_SUCCEEDED(rv));
 }
 
-NS_IMETHODIMP
-nsPermissionManager::UpdateExpireTime(nsIPrincipal* aPrincipal,
-                                      const nsACString& aType,
-                                      bool aExactHostMatch,
-                                      uint64_t aSessionExpireTime,
-                                      uint64_t aPersistentExpireTime) {
-  NS_ENSURE_ARG_POINTER(aPrincipal);
-
-  uint64_t nowms = PR_Now() / 1000;
-  if (aSessionExpireTime < nowms || aPersistentExpireTime < nowms) {
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
-    return NS_OK;
-  }
-
-  // Setting the expire time of an nsEP is non-sensical.
-  if (IsExpandedPrincipal(aPrincipal)) {
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  MOZ_ASSERT(PermissionAvailable(aPrincipal, aType));
-
-  int32_t typeIndex = GetTypeIndex(aType, false);
-  // If type == -1, the type isn't known,
-  // so just return NS_OK
-  if (typeIndex == -1) return NS_OK;
-
-  PermissionHashKey* entry =
-      GetPermissionHashKey(aPrincipal, typeIndex, aExactHostMatch);
-  if (!entry) {
-    return NS_OK;
-  }
-
-  int32_t idx = entry->GetPermissionIndex(typeIndex);
-  if (-1 == idx) {
-    return NS_OK;
-  }
-
-  PermissionEntry& perm = entry->GetPermissions()[idx];
-  if (perm.mExpireType == EXPIRE_TIME) {
-    perm.mExpireTime = aPersistentExpireTime;
-  } else if (perm.mExpireType == EXPIRE_SESSION && perm.mExpireTime != 0) {
-    perm.mExpireTime = aSessionExpireTime;
-  }
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsPermissionManager::GetPermissionsWithKey(const nsACString& aPermissionKey,
-                                           nsTArray<IPC::Permission>& aPerms) {
+bool nsPermissionManager::GetPermissionsWithKey(
+    const nsACString& aPermissionKey, nsTArray<IPC::Permission>& aPerms) {
   aPerms.Clear();
   if (NS_WARN_IF(XRE_IsContentProcess())) {
-    return NS_ERROR_NOT_AVAILABLE;
+    return false;
   }
 
   for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
     PermissionHashKey* entry = iter.Get();
 
     nsAutoCString permissionKey;
     GetKeyForOrigin(entry->GetKey()->mOrigin, permissionKey);
 
@@ -3097,39 +3031,38 @@ nsPermissionManager::GetPermissionsWithK
         aPerms.AppendElement(
             IPC::Permission(entry->GetKey()->mOrigin,
                             mTypeArray[permEntry.mType], permEntry.mPermission,
                             permEntry.mExpireType, permEntry.mExpireTime));
       }
     }
   }
 
-  return NS_OK;
+  return true;
 }
 
-NS_IMETHODIMP
-nsPermissionManager::SetPermissionsWithKey(const nsACString& aPermissionKey,
-                                           nsTArray<IPC::Permission>& aPerms) {
+void nsPermissionManager::SetPermissionsWithKey(
+    const nsACString& aPermissionKey, nsTArray<IPC::Permission>& aPerms) {
   if (NS_WARN_IF(XRE_IsParentProcess())) {
-    return NS_ERROR_NOT_AVAILABLE;
+    return;
   }
 
   RefPtr<GenericNonExclusivePromise::Private> promise;
   bool foundKey =
       mPermissionKeyPromiseMap.Get(aPermissionKey, getter_AddRefs(promise));
   if (promise) {
     MOZ_ASSERT(foundKey);
     // NOTE: This will resolve asynchronously, so we can mark it as resolved
     // now, and be confident that we will have filled in the database before any
     // callbacks run.
     promise->Resolve(true, __func__);
   } else if (foundKey) {
     // NOTE: We shouldn't be sent two InitializePermissionsWithKey for the same
     // key, but it's possible.
-    return NS_OK;
+    return;
   }
   mPermissionKeyPromiseMap.Put(aPermissionKey, nullptr);
 
   // Add the permissions locally to our process
   for (IPC::Permission& perm : aPerms) {
     nsCOMPtr<nsIPrincipal> principal;
     nsresult rv =
         GetPrincipalFromOrigin(perm.origin, getter_AddRefs(principal));
@@ -3147,17 +3080,16 @@ nsPermissionManager::SetPermissionsWithK
     // The child process doesn't care about modification times - it neither
     // reads nor writes, nor removes them based on the date - so 0 (which
     // will end up as now()) is fine.
     uint64_t modificationTime = 0;
     AddInternal(principal, perm.type, perm.capability, 0, perm.expireType,
                 perm.expireTime, modificationTime, eNotify, eNoDBOperation,
                 true /* ignoreSessionPermissions */);
   }
-  return NS_OK;
 }
 
 /* static */
 void nsPermissionManager::GetKeyForOrigin(const nsACString& aOrigin,
                                           nsACString& aKey) {
   aKey.Truncate();
 
   // We only key origins for http, https, and ftp URIs. All origins begin with
@@ -3277,24 +3209,23 @@ bool nsPermissionManager::PermissionAvai
                                  permissionKey.get())
                      .get());
       return false;
     }
   }
   return true;
 }
 
-NS_IMETHODIMP
-nsPermissionManager::WhenPermissionsAvailable(nsIPrincipal* aPrincipal,
-                                              nsIRunnable* aRunnable) {
+void nsPermissionManager::WhenPermissionsAvailable(nsIPrincipal* aPrincipal,
+                                                   nsIRunnable* aRunnable) {
   MOZ_ASSERT(aRunnable);
 
   if (!XRE_IsContentProcess()) {
     aRunnable->Run();
-    return NS_OK;
+    return;
   }
 
   nsTArray<RefPtr<GenericNonExclusivePromise>> promises;
   for (auto& key : GetAllKeysForPrincipal(aPrincipal)) {
     RefPtr<GenericNonExclusivePromise::Private> promise;
     if (!mPermissionKeyPromiseMap.Get(key, getter_AddRefs(promise))) {
       // In this case we have found a permission which isn't available in the
       // content process and hasn't been requested yet. We need to create a new
@@ -3310,30 +3241,27 @@ nsPermissionManager::WhenPermissionsAvai
     }
   }
 
   // If all of our permissions are available, immediately run the runnable. This
   // avoids any extra overhead during fetch interception which is performance
   // sensitive.
   if (promises.IsEmpty()) {
     aRunnable->Run();
-    return NS_OK;
+    return;
   }
 
   auto* thread = SystemGroup::AbstractMainThreadFor(TaskCategory::Other);
 
   RefPtr<nsIRunnable> runnable = aRunnable;
   GenericNonExclusivePromise::All(thread, promises)
       ->Then(
           thread, __func__, [runnable]() { runnable->Run(); },
           []() {
             NS_WARNING(
                 "nsPermissionManager permission promise rejected. We're "
                 "probably shutting down.");
           });
-  return NS_OK;
 }
 
-NS_IMETHODIMP
-nsPermissionManager::GetHasPreloadPermissions(bool* aResult) {
-  *aResult = sPreloadPermissionCount > 0;
-  return NS_OK;
+bool nsPermissionManager::HasPreloadPermissions() {
+  return sPreloadPermissionCount > 0;
 }
--- a/extensions/permissions/nsPermissionManager.h
+++ b/extensions/permissions/nsPermissionManager.h
@@ -24,16 +24,20 @@
 #include "nsRefPtrHashtable.h"
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/ExpandedPrincipal.h"
 #include "mozilla/MozPromise.h"
 #include "mozilla/Unused.h"
 #include "mozilla/Variant.h"
 #include "mozilla/Vector.h"
 
+namespace IPC {
+struct Permission;
+}
+
 namespace mozilla {
 class OriginAttributesPattern;
 }
 
 class nsIPermission;
 class mozIStorageConnection;
 class mozIStorageAsyncStatement;
 
@@ -201,19 +205,16 @@ class nsPermissionManager final : public
    * Initialize the permission-manager service.
    * The permission manager is always initialized at startup because when it
    * was lazy-initialized on demand, it was possible for it to be created
    * once shutdown had begun, resulting in the manager failing to correctly
    * shutdown because it missed its shutdown observer notification.
    */
   static void Startup();
 
-  nsresult RemovePermissionsWithAttributes(
-      mozilla::OriginAttributesPattern& aAttrs);
-
   /**
    * See `nsIPermissionManager::GetPermissionsWithKey` for more info on
    * permission keys.
    *
    * Get the permission key corresponding to the given Principal. This method is
    * intentionally infallible, as we want to provide an permission key to every
    * principal. Principals which don't have meaningful URIs with http://,
    * https://, or ftp:// schemes are given the default "" Permission Key.
@@ -288,16 +289,78 @@ class nsPermissionManager final : public
    * Returns false if this permission manager wouldn't have the permission
    * requested available.
    *
    * If aType is empty, checks that the permission manager would have all
    * permissions available for the given principal.
    */
   bool PermissionAvailable(nsIPrincipal* aPrincipal, const nsACString& aType);
 
+  /**
+   * The content process doesn't have access to every permission. Instead, when
+   * LOAD_DOCUMENT_URI channels for http://, https://, and ftp:// URIs are
+   * opened, the permissions for those channels are sent down to the content
+   * process before the OnStartRequest message. Permissions for principals with
+   * other schemes are sent down at process startup.
+   *
+   * Permissions are keyed and grouped by "Permission Key"s.
+   * `nsPermissionManager::GetKeyForPrincipal` provides the mechanism for
+   * determining the permission key for a given principal.
+   *
+   * This method may only be called in the parent process. It fills the nsTArray
+   * argument with the IPC::Permission objects which have a matching permission
+   * key.
+   *
+   * @param permissionKey  The key to use to find the permissions of interest.
+   * @param perms  An array which will be filled with the permissions which
+   *               match the given permission key.
+   */
+  bool GetPermissionsWithKey(const nsACString& aPermissionKey,
+                             nsTArray<IPC::Permission>& aPerms);
+
+  /**
+   * See `nsPermissionManager::GetPermissionsWithKey` for more info on
+   * Permission keys.
+   *
+   * `SetPermissionsWithKey` may only be called in the Child process, and
+   * initializes the permission manager with the permissions for a given
+   * Permission key. marking permissions with that key as available.
+   *
+   * @param permissionKey  The key for the permissions which have been sent
+   * over.
+   * @param perms  An array with the permissions which match the given key.
+   */
+  void SetPermissionsWithKey(const nsACString& aPermissionKey,
+                             nsTArray<IPC::Permission>& aPerms);
+
+  /**
+   * Add a callback which should be run when all permissions are available for
+   * the given nsIPrincipal. This method invokes the callback runnable
+   * synchronously when the permissions are already available. Otherwise the
+   * callback will be run asynchronously in SystemGroup when all permissions
+   * are available in the future.
+   *
+   * NOTE: This method will not request the permissions be sent by the parent
+   * process. This should only be used to wait for permissions which may not
+   * have arrived yet in order to ensure they are present.
+   *
+   * @param aPrincipal The principal to wait for permissions to be available
+   * for.
+   * @param aRunnable  The runnable to run when permissions are available for
+   * the given principal.
+   */
+  void WhenPermissionsAvailable(nsIPrincipal* aPrincipal,
+                                nsIRunnable* aRunnable);
+
+  /**
+   * True if any "preload" permissions are present. This is used to avoid making
+   * potentially expensive permissions checks in nsContentBlocker.
+   */
+  bool HasPreloadPermissions();
+
  private:
   virtual ~nsPermissionManager();
 
   // NOTE: nullptr can be passed as aType - if it is this function will return
   // "false" unconditionally.
   static bool HasDefaultPref(const nsACString& aType) {
     // A list of permissions that can have a fallback default permission
     // set under the permissions.default.* pref.
@@ -522,16 +585,20 @@ class nsPermissionManager final : public
   /**
    * This method removes all permissions modified after the specified time.
    */
   nsresult RemoveAllModifiedSince(int64_t aModificationTime);
 
   template <class T>
   nsresult RemovePermissionEntries(T aCondition);
 
+  nsresult RemovePermissionsWithAttributes(const nsAString& aPattern);
+  nsresult RemovePermissionsWithAttributes(
+      mozilla::OriginAttributesPattern& aAttrs);
+
   nsRefPtrHashtable<nsCStringHashKey,
                     mozilla::GenericNonExclusivePromise::Private>
       mPermissionKeyPromiseMap;
 
   nsCOMPtr<mozIStorageConnection> mDBConn;
   nsCOMPtr<mozIStorageAsyncStatement> mStmtInsert;
   nsCOMPtr<mozIStorageAsyncStatement> mStmtDelete;
   nsCOMPtr<mozIStorageAsyncStatement> mStmtUpdate;
--- a/extensions/permissions/test/unit/test_permmanager_expiration.js
+++ b/extensions/permissions/test/unit/test_permmanager_expiration.js
@@ -79,72 +79,29 @@ function* do_run_test() {
   pm.addFromPrincipal(
     principal,
     "test/expiration-perm-nexp",
     1,
     pm.EXPIRE_NEVER,
     0
   );
 
-  // add a permission for renewal
-  pm.addFromPrincipal(
-    principal,
-    "test/expiration-perm-renewable",
-    1,
-    pm.EXPIRE_TIME,
-    now + 100
-  );
-  pm.addFromPrincipal(
-    principal,
-    "test/expiration-session-renewable",
-    1,
-    pm.EXPIRE_SESSION,
-    now + 100
-  );
-
-  // And immediately renew them with longer timeouts
-  pm.updateExpireTime(
-    principal,
-    "test/expiration-perm-renewable",
-    true,
-    now + 100,
-    now + 1e6
-  );
-  pm.updateExpireTime(
-    principal,
-    "test/expiration-session-renewable",
-    true,
-    now + 1e6,
-    now + 100
-  );
-
   // check that the second two haven't expired yet
   Assert.equal(
     1,
     pm.testPermissionFromPrincipal(principal, "test/expiration-perm-exp3")
   );
   Assert.equal(
     1,
     pm.testPermissionFromPrincipal(principal, "test/expiration-session-exp3")
   );
   Assert.equal(
     1,
     pm.testPermissionFromPrincipal(principal, "test/expiration-perm-nexp")
   );
-  Assert.equal(
-    1,
-    pm.testPermissionFromPrincipal(principal, "test/expiration-perm-renewable")
-  );
-  Assert.equal(
-    1,
-    pm.testPermissionFromPrincipal(
-      principal,
-      "test/expiration-session-renewable"
-    )
-  );
 
   // ... and the first one has
   do_timeout(10, continue_test);
   yield;
   Assert.equal(
     0,
     pm.testPermissionFromPrincipal(principal, "test/expiration-perm-exp")
   );
@@ -178,23 +135,10 @@ function* do_run_test() {
     null,
     pm.getPermissionObject(principal, "test/expiration-perm-exp2", false)
   );
   Assert.equal(
     null,
     pm.getPermissionObject(principal, "test/expiration-session-exp2", false)
   );
 
-  // Check that the renewable permissions actually got renewed
-  Assert.equal(
-    1,
-    pm.testPermissionFromPrincipal(principal, "test/expiration-perm-renewable")
-  );
-  Assert.equal(
-    1,
-    pm.testPermissionFromPrincipal(
-      principal,
-      "test/expiration-session-renewable"
-    )
-  );
-
   do_finish_generator_test(test_generator);
 }
--- a/extensions/permissions/test/unit/test_permmanager_load_invalid_entries.js
+++ b/extensions/permissions/test/unit/test_permmanager_load_invalid_entries.js
@@ -239,17 +239,21 @@ function run_test() {
   );
   let numMigrated = 0;
   while (select.executeStep()) {
     let thisModTime = select.getInt64(0);
     Assert.ok(thisModTime == 0, "new modifiedTime field is correct");
     numMigrated += 1;
   }
   // check we found at least 1 record that was migrated.
-  Assert.ok(numMigrated > 0, "we found at least 1 record that was migrated");
+  Assert.greater(
+    numMigrated,
+    0,
+    "we found at least 1 record that was migrated"
+  );
 
   // This permission should always be there.
   let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
     Ci.nsIScriptSecurityManager
   );
   let uri = NetUtil.newURI("http://example.org");
   let principal = ssm.createContentPrincipal(uri, {});
   Assert.equal(
--- a/gfx/layers/d3d11/TextureD3D11.cpp
+++ b/gfx/layers/d3d11/TextureD3D11.cpp
@@ -443,17 +443,19 @@ D3D11TextureData* D3D11TextureData::Crea
     newDesc.Format = DXGI_FORMAT_NV12;
   } else if (aFormat == SurfaceFormat::P010) {
     newDesc.Format = DXGI_FORMAT_P010;
   } else if (aFormat == SurfaceFormat::P016) {
     newDesc.Format = DXGI_FORMAT_P016;
   }
 
   newDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
-  if (!NS_IsMainThread() || !!(aFlags & ALLOC_FOR_OUT_OF_BAND_CONTENT)) {
+  // WebRender requests keyed mutex.
+  if (gfxVars::UseWebRender() || !NS_IsMainThread() ||
+      !!(aFlags & ALLOC_FOR_OUT_OF_BAND_CONTENT)) {
     // On the main thread we use the syncobject to handle synchronization.
     if (!(aFlags & ALLOC_MANUAL_SYNCHRONIZATION)) {
       newDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
     }
   }
 
   if (aSurface && newDesc.MiscFlags == D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX &&
       !DeviceManagerDx::Get()->CanInitializeKeyedMutexTextures()) {
--- a/gfx/layers/wr/WebRenderLayerManager.cpp
+++ b/gfx/layers/wr/WebRenderLayerManager.cpp
@@ -222,25 +222,16 @@ bool WebRenderLayerManager::EndEmptyTran
 
   // Get the time of when the refresh driver start its tick (if available),
   // otherwise use the time of when LayerManager::BeginTransaction was called.
   TimeStamp refreshStart = mTransactionIdAllocator->GetTransactionStart();
   if (!refreshStart) {
     refreshStart = mTransactionStart;
   }
 
-  // Skip the synchronization for buffer since we also skip the painting during
-  // device-reset status.
-  if (!gfxPlatform::GetPlatform()->DidRenderingDeviceReset()) {
-    if (WrBridge()->GetSyncObject() &&
-        WrBridge()->GetSyncObject()->IsSyncObjectValid()) {
-      WrBridge()->GetSyncObject()->Synchronize();
-    }
-  }
-
   GetCompositorBridgeChild()->EndCanvasTransaction();
 
   AutoTArray<RenderRootUpdates, wr::kRenderRootCount> renderRootUpdates;
   for (auto& stateManager : mStateManagers) {
     auto renderRoot = stateManager.GetRenderRoot();
     if (stateManager.mAsyncResourceUpdates ||
         !mPendingScrollUpdates[renderRoot].IsEmpty() ||
         WrBridge()->HasWebRenderParentCommands(renderRoot)) {
@@ -408,25 +399,16 @@ void WebRenderLayerManager::EndTransacti
   }
 
   for (auto renderRoot : wr::kRenderRoots) {
     if (resourceUpdates.HasSubQueue(renderRoot)) {
       WrBridge()->RemoveExpiredFontKeys(resourceUpdates.SubQueue(renderRoot));
     }
   }
 
-  // Skip the synchronization for buffer since we also skip the painting during
-  // device-reset status.
-  if (!gfxPlatform::GetPlatform()->DidRenderingDeviceReset()) {
-    if (WrBridge()->GetSyncObject() &&
-        WrBridge()->GetSyncObject()->IsSyncObjectValid()) {
-      WrBridge()->GetSyncObject()->Synchronize();
-    }
-  }
-
   GetCompositorBridgeChild()->EndCanvasTransaction();
 
   {
     AUTO_PROFILER_TRACING("Paint", "ForwardDPTransaction", GRAPHICS);
     nsTArray<RenderRootDisplayListData> renderRootDLs;
     for (auto renderRoot : wr::kRenderRoots) {
       if (builder.GetSendSubBuilderDisplayList(renderRoot)) {
         auto renderRootDL = renderRootDLs.AppendElement();
--- a/gfx/webrender_bindings/RenderCompositorANGLE.cpp
+++ b/gfx/webrender_bindings/RenderCompositorANGLE.cpp
@@ -241,16 +241,17 @@ bool RenderCompositorANGLE::Initialize()
     if (SUCCEEDED(hr)) {
       mSwapChain1 = swapChain1;
     }
   }
 
   // We need this because we don't want DXGI to respond to Alt+Enter.
   dxgiFactory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_WINDOW_CHANGES);
 
+  // SyncObject is used only by D3D11DXVA2Manager
   mSyncObject = layers::SyncObjectHost::CreateSyncObjectHost(mDevice);
   if (!mSyncObject->Init()) {
     // Some errors occur. Clear the mSyncObject here.
     // Then, there will be no texture synchronization.
     return false;
   }
 
   if (!UseCompositor()) {
@@ -397,23 +398,16 @@ bool RenderCompositorANGLE::BeginFrame()
     }
   }
 
   if (!MakeCurrent()) {
     gfxCriticalNote << "Failed to make render context current, can't draw.";
     return false;
   }
 
-  if (mSyncObject) {
-    if (!mSyncObject->Synchronize(/* aFallible */ true)) {
-      // It's timeout or other error. Handle the device-reset here.
-      RenderThread::Get()->HandleDeviceReset("SyncObject", /* aNotify */ true);
-      return false;
-    }
-  }
   return true;
 }
 
 RenderedFrameId RenderCompositorANGLE::EndFrame(
     const FfiVec<DeviceIntRect>& aDirtyRects) {
   RenderedFrameId frameId = GetNextRenderFrameId();
   InsertPresentWaitQuery(frameId);
 
--- a/image/imgITools.idl
+++ b/image/imgITools.idl
@@ -1,18 +1,20 @@
 /* -*- 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;
@@ -48,16 +50,39 @@ 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,32 +18,173 @@
 #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,
@@ -230,16 +371,32 @@ 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/intl/locale/tests/unit/test_osPreferences.js
+++ b/intl/locale/tests/unit/test_osPreferences.js
@@ -28,14 +28,14 @@ function run_test()
   ];
 
   for (let i = 0; i < getDateTimePatternTests.length; i++) {
     const test = getDateTimePatternTests[i];
 
     const pattern = osprefs.getDateTimePattern(...test);
     if (test[0] !== osprefs.dateTimeFormatStyleNone &&
         test[1] !== osprefs.dateTImeFormatStyleNone) {
-      Assert.ok(pattern.length > 0, "pattern is not empty.");
+      Assert.greater(pattern.length, 0, "pattern is not empty.");
     }
   }
 
   Assert.ok(1, "osprefs didn't crash");
 }
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -4476,26 +4476,26 @@ static MOZ_MUST_USE bool AsyncGeneratorR
 // 25.5.3.6 AsyncGeneratorEnqueue ( generator, completion )
 MOZ_MUST_USE bool js::AsyncGeneratorEnqueue(JSContext* cx,
                                             HandleValue asyncGenVal,
                                             CompletionKind completionKind,
                                             HandleValue completionValue,
                                             MutableHandleValue result) {
   // Step 1 (implicit).
 
-  // Step 2.
-  Rooted<PromiseObject*> resultPromise(
-      cx, CreatePromiseObjectForAsyncGenerator(cx));
-  if (!resultPromise) {
-    return false;
-  }
-
   // Step 3.
   if (!asyncGenVal.isObject() ||
-      !asyncGenVal.toObject().is<AsyncGeneratorObject>()) {
+      !asyncGenVal.toObject().canUnwrapAs<AsyncGeneratorObject>()) {
+    // Step 2.
+    Rooted<PromiseObject*> resultPromise(
+        cx, CreatePromiseObjectForAsyncGenerator(cx));
+    if (!resultPromise) {
+      return false;
+    }
+
     // Step 3.a.
     RootedValue badGeneratorError(cx);
     if (!GetTypeError(cx, JSMSG_NOT_AN_ASYNC_GENERATOR, &badGeneratorError)) {
       return false;
     }
 
     // Step 3.b.
     if (!RejectPromiseInternal(cx, resultPromise, badGeneratorError)) {
@@ -4503,42 +4503,71 @@ MOZ_MUST_USE bool js::AsyncGeneratorEnqu
     }
 
     // Step 3.c.
     result.setObject(*resultPromise);
     return true;
   }
 
   Rooted<AsyncGeneratorObject*> asyncGenObj(
-      cx, &asyncGenVal.toObject().as<AsyncGeneratorObject>());
-
-  // Step 5 (reordered).
-  Rooted<AsyncGeneratorRequest*> request(
-      cx, AsyncGeneratorObject::createRequest(cx, asyncGenObj, completionKind,
-                                              completionValue, resultPromise));
-  if (!request) {
-    return false;
-  }
-
-  // Steps 4, 6.
-  if (!AsyncGeneratorObject::enqueueRequest(cx, asyncGenObj, request)) {
-    return false;
-  }
-
-  // Step 7.
-  if (!asyncGenObj->isExecuting() && !asyncGenObj->isAwaitingYieldReturn()) {
-    // Step 8.
-    if (!AsyncGeneratorResumeNext(cx, asyncGenObj, ResumeNextKind::Enqueue)) {
+      cx, &asyncGenVal.toObject().unwrapAs<AsyncGeneratorObject>());
+
+  bool wrapResult = false;
+  {
+    // The |resultPromise| must be same-compartment with |asyncGenObj|, because
+    // it is stored in AsyncGeneratorRequest, which in turn is stored in a
+    // reserved slot of |asyncGenObj|.
+    // So we first enter the realm of |asyncGenObj|, then create the result
+    // promise and resume the generator, and finally wrap the result promise to
+    // match the original compartment.
+
+    mozilla::Maybe<AutoRealm> ar;
+    RootedValue completionVal(cx, completionValue);
+    if (asyncGenObj->compartment() != cx->compartment()) {
+      ar.emplace(cx, asyncGenObj);
+      wrapResult = true;
+
+      if (!cx->compartment()->wrap(cx, &completionVal)) {
+        return false;
+      }
+    }
+
+    // Step 2.
+    Rooted<PromiseObject*> resultPromise(
+        cx, CreatePromiseObjectForAsyncGenerator(cx));
+    if (!resultPromise) {
       return false;
     }
-  }
-
-  // Step 9.
-  result.setObject(*resultPromise);
-  return true;
+
+    // Step 5 (reordered).
+    Rooted<AsyncGeneratorRequest*> request(
+        cx, AsyncGeneratorObject::createRequest(cx, asyncGenObj, completionKind,
+                                                completionVal, resultPromise));
+    if (!request) {
+      return false;
+    }
+
+    // Steps 4, 6.
+    if (!AsyncGeneratorObject::enqueueRequest(cx, asyncGenObj, request)) {
+      return false;
+    }
+
+    // Step 7.
+    if (!asyncGenObj->isExecuting() && !asyncGenObj->isAwaitingYieldReturn()) {
+      // Step 8.
+      if (!AsyncGeneratorResumeNext(cx, asyncGenObj, ResumeNextKind::Enqueue)) {
+        return false;
+      }
+    }
+
+    // Step 9.
+    result.setObject(*resultPromise);
+  }
+
+  return !wrapResult || cx->compartment()->wrap(cx, result);
 }
 
 static bool Promise_catch_impl(JSContext* cx, unsigned argc, Value* vp,
                                bool rvalUsed) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   HandleValue thisVal = args.thisv();
   HandleValue onFulfilled = UndefinedHandleValue;
new file mode 100644
--- /dev/null
+++ b/js/src/tests/non262/AsyncGenerators/cross-compartment.js
@@ -0,0 +1,90 @@
+var g = newGlobal();
+g.mainGlobal = this;
+
+if (typeof isSameCompartment !== "function") {
+  var isSameCompartment = SpecialPowers.Cu.getJSTestingFunctions().isSameCompartment;
+}
+
+var next = async function*(){}.prototype.next;
+
+var f = g.eval(`(async function*() {
+  var x = yield {message: "yield"};
+
+  // Input completion values are correctly wrapped into |f|'s compartment.
+  assertEq(isSameCompartment(x, mainGlobal), true);
+  assertEq(x.message, "continue");
+
+  return {message: "return"};
+})`);
+
+var it = f();
+
+// The async iterator is same-compartment with |f|.
+assertEq(isSameCompartment(it, f), true);
+
+var p1 = next.call(it, {message: "initial yield"});
+
+// The promise object is same-compartment with |f|.
+assertEq(isSameCompartment(p1, f), true);
+
+// Note: This doesn't follow the spec, which requires that only |p1 instanceof Promise| is true.
+assertEq(p1 instanceof Promise || p1 instanceof g.Promise, true);
+
+p1.then(v => {
+  // The iterator result object is same-compartment with |f|.
+  assertEq(isSameCompartment(v, f), true);
+  assertEq(v.done, false);
+
+  assertEq(isSameCompartment(v.value, f), true);
+  assertEq(v.value.message, "yield");
+});
+
+var p2 = next.call(it, {message: "continue"});
+
+// The promise object is same-compartment with |f|.
+assertEq(isSameCompartment(p2, f), true);
+
+// Note: This doesn't follow the spec, which requires that only |p2 instanceof Promise| is true.
+assertEq(p2 instanceof Promise || p2 instanceof g.Promise, true);
+
+p2.then(v => {
+  // The iterator result object is same-compartment with |f|.
+  assertEq(isSameCompartment(v, f), true);
+  assertEq(v.done, true);
+
+  assertEq(isSameCompartment(v.value, f), true);
+  assertEq(v.value.message, "return");
+});
+
+var p3 = next.call(it, {message: "already finished"});
+
+// The promise object is same-compartment with |f|.
+assertEq(isSameCompartment(p3, f), true);
+
+// Note: This doesn't follow the spec, which requires that only |p3 instanceof Promise| is true.
+assertEq(p3 instanceof Promise || p3 instanceof g.Promise, true);
+
+p3.then(v => {
+  // The iterator result object is same-compartment with |f|.
+  assertEq(isSameCompartment(v, f), true);
+  assertEq(v.done, true);
+  assertEq(v.value, undefined);
+});
+
+var p4 = next.call({}, {message: "bad |this| argument"});
+
+// The promise object is same-compartment with |next|.
+assertEq(isSameCompartment(p4, next), true);
+
+// Only in this case we're following the spec and are creating the promise object
+// in the correct realm.
+assertEq(p4 instanceof Promise, true);
+
+p4.then(() => {
+  throw new Error("expected a TypeError");
+}, e => {
+  assertEq(e instanceof TypeError, true);
+});
+
+if (typeof reportCompare === "function")
+  reportCompare(0, 0);
--- a/js/xpconnect/tests/unit/test_watchdog_hibernate.js
+++ b/js/xpconnect/tests/unit/test_watchdog_hibernate.js
@@ -10,18 +10,18 @@ async function testBody() {
   // default to 0, so this should always be true.
   var now = Date.now() * 1000;
   var startHibernation = Cu.getWatchdogTimestamp("WatchdogHibernateStart");
   var stopHibernation = Cu.getWatchdogTimestamp("WatchdogHibernateStop");
   do_log_info("Pre-hibernation statistics:");
   do_log_info("now: " + now / 1000000);
   do_log_info("startHibernation: " + startHibernation / 1000000);
   do_log_info("stopHibernation: " + stopHibernation / 1000000);
-  Assert.ok(startHibernation < now, "startHibernation ok");
-  Assert.ok(stopHibernation < now, "stopHibernation ok");
+  Assert.less(startHibernation, now, "startHibernation ok");
+  Assert.less(stopHibernation, now, "stopHibernation ok");
 
   // When the watchdog runs, it hibernates if there's been no activity for the
   // last 2 seconds, otherwise it sleeps for 1 second. As such, given perfect
   // scheduling, we should never have more than 3 seconds of inactivity without
   // hibernating. To add some padding for automation, we mandate that hibernation
   // must begin between 2 and 5 seconds from now.
 
   // Sleep for 10 seconds. Note: we don't use nsITimer here because then we may run
@@ -37,13 +37,13 @@ async function testBody() {
   do_log_info("startHibernation: " + startHibernation / 1000000);
   do_log_info("stopHibernation: " + stopHibernation / 1000000);
   // XPCOM timers, JS times, and PR_Now() are apparently not directly
   // comparable, as evidenced by certain seemingly-impossible timing values
   // that occasionally get logged in windows automation. We're really just
   // making sure this behavior is roughly as expected on the macro scale,
   // so we add a 1 second fuzz factor here.
   const FUZZ_FACTOR = 1 * 1000 * 1000;
-  Assert.ok(stateChange > now + 10*1000*1000 - FUZZ_FACTOR, "stateChange ok");
-  Assert.ok(startHibernation > now + 2*1000*1000 - FUZZ_FACTOR, "startHibernation ok");
-  Assert.ok(startHibernation < now + 5*1000*1000 + FUZZ_FACTOR, "startHibernation ok");
-  Assert.ok(stopHibernation > now + 10*1000*1000 - FUZZ_FACTOR, "stopHibernation ok");
+  Assert.greater(stateChange, now + 10*1000*1000 - FUZZ_FACTOR, "stateChange ok");
+  Assert.greater(startHibernation, now + 2*1000*1000 - FUZZ_FACTOR, "startHibernation ok");
+  Assert.less(startHibernation, now + 5*1000*1000 + FUZZ_FACTOR, "startHibernation ok");
+  Assert.greater(stopHibernation, now + 10*1000*1000 - FUZZ_FACTOR, "stopHibernation ok");
 }
--- a/layout/tools/reftest/manifest.jsm
+++ b/layout/tools/reftest/manifest.jsm
@@ -512,18 +512,16 @@ function BuildConditionSandbox(aURL) {
 #endif
 
 #if MOZ_WEBRTC
     sandbox.webrtc = true;
 #else
     sandbox.webrtc = false;
 #endif
 
-    sandbox.xbl = false; // Keep this until all xbl reftests are removed in Bug 1587142.
-
 let retainedDisplayListsEnabled = prefs.getBoolPref("layout.display-list.retain", false);
 sandbox.retainedDisplayLists = retainedDisplayListsEnabled && !g.compareRetainedDisplayLists;
 sandbox.compareRetainedDisplayLists = g.compareRetainedDisplayLists;
 
     sandbox.skiaPdf = false;
 
 #ifdef RELEASE_OR_BETA
     sandbox.release_or_beta = true;
--- a/mobile/android/chrome/geckoview/geckoview.js
+++ b/mobile/android/chrome/geckoview/geckoview.js
@@ -192,16 +192,21 @@ 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,16 +1,10 @@
 "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).
@@ -73,42 +67,37 @@ global.openOptionsPage = extension => {
   }
 
   return Promise.resolve();
 };
 
 extensions.registerModules({
   browserAction: {
     url: "chrome://geckoview/content/ext-browserAction.js",
-    schema: "chrome://geckoview/content/schemas/browser_action.json",
+    schema: "chrome://extensions/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://geckoview/content/schemas/page_action.json",
+    schema: "chrome://extensions/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,203 +1,128 @@
 /* -*- 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 */
 
-// Import the android BrowserActions module.
-ChromeUtils.defineModuleGetter(
-  this,
-  "BrowserActions",
-  "resource://gre/modules/BrowserActions.jsm"
+XPCOMUtils.defineLazyModuleGetters(this, {
+  GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm",
+  ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.jsm",
+});
+
+const { BrowserActionBase } = ChromeUtils.import(
+  "resource://gre/modules/ExtensionActions.jsm"
 );
 
-// WeakMap[Extension -> BrowserAction]
-let browserActionMap = new WeakMap();
-
-class BrowserAction extends EventEmitter {
-  constructor(options, extension) {
-    super();
-
-    this.uuid = `{${extension.uuid}}`;
+const BROWSER_ACTION_PROPERTIES = [
+  "title",
+  "icon",
+  "popup",
+  "badgeText",
+  "badgeBackgroundColor",
+  "badgeTextColor",
+  "enabled",
+  "patternMatching",
+];
 
-    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);
+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,
     });
-    // eslint-disable-next-line mozilla/balanced-listeners
-    this.tabContext.on("tab-closed", (evt, tabId) => {
-      this.onTabClosed(tabId);
-    });
-
-    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 });
-  }
-
-  /**
-   * 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);
+  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",
+    });
   }
 
-  /**
-   * 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 });
-    }
+  openPopup() {
+    const tab = tabTracker.activeTab;
+    const action = this.getContextData(tab);
+    this.helper.sendRequest(tab.id, {
+      action,
+      type: "GeckoView:BrowserAction:OpenPopup",
+    });
   }
 
-  /**
-   * 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];
+  getTab(tabId) {
+    return this.helper.getTab(tabId);
   }
 
-  /**
-   * Unregister the browser action from the BrowserActions module.
-   */
-  shutdown() {
-    this.tabContext.shutdown();
-    BrowserActions.unregister(this.uuid);
+  getWindow(windowId) {
+    return this.helper.getWindow(windowId);
+  }
+
+  click() {
+    this.clickDelegate.onClick();
   }
 }
 
 this.browserAction = class extends ExtensionAPI {
-  onManifestEntry(entryName) {
-    let { extension } = this;
-    let { manifest } = extension;
+  async onManifestEntry(entryName) {
+    const { extension } = this;
+    this.action = new BrowserAction(extension, this);
+    await this.action.loadIconData();
 
-    let browserAction = new BrowserAction(manifest.browser_action, extension);
-    browserActionMap.set(extension, browserAction);
+    GeckoViewWebExtension.browserActions.set(extension, this.action);
+
+    // Notify the embedder of this action
+    this.action.updateOnChange(null);
   }
 
   onShutdown() {
-    let { extension } = this;
+    const { extension } = this;
+    this.action.onShutdown();
+    GeckoViewWebExtension.browserActions.delete(extension);
+  }
 
-    if (browserActionMap.has(extension)) {
-      browserActionMap.get(extension).shutdown();
-      browserActionMap.delete(extension);
-    }
+  onClick() {
+    this.emit("click", tabTracker.activeTab);
   }
 
   getAPI(context) {
     const { extension } = context;
     const { tabManager } = extension;
-
-    function getTab(tabId) {
-      if (tabId !== null) {
-        return tabTracker.getTab(tabId);
-      }
-      return null;
-    }
+    const { action } = this;
 
     return {
       browserAction: {
+        ...action.api(context),
+
         onClicked: new EventManager({
           context,
           name: "browserAction.onClicked",
           register: fire => {
-            let listener = (event, tab) => {
+            const listener = (event, tab) => {
               fire.async(tabManager.convert(tab));
             };
-            browserActionMap.get(extension).on("click", listener);
+            this.on("click", listener);
             return () => {
-              browserActionMap.get(extension).off("click", listener);
+              this.off("click", listener);
             };
           },
         }).api(),
 
-        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);
+        openPopup: function() {
+          action.openPopup();
         },
       },
     };
   }
 };
+
+global.browserActionFor = this.browserAction.for;
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -1,292 +1,122 @@
 /* -*- 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 */
 
-ChromeUtils.defineModuleGetter(
-  this,
-  "Services",
-  "resource://gre/modules/Services.jsm"
-);
+XPCOMUtils.defineLazyModuleGetters(this, {
+  GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm",
+  ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.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 { PageActionBase } = ChromeUtils.import(
+  "resource://gre/modules/ExtensionActions.jsm"
 );
 
-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);
+const PAGE_ACTION_PROPERTIES = [
+  "title",
+  "icon",
+  "popup",
+  "badgeText",
+  "enabled",
+  "patternMatching",
+];
 
-    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);
+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,
     });
   }
 
-  /**
-   * 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();
-      }
-    }
+  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",
+    });
   }
 
-  /**
-   * 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];
+  openPopup() {
+    const action = this.getContextData(tabTracker.activeTab);
+    this.helper.sendRequest(tabTracker.activeTab.id, {
+      action,
+      type: "GeckoView:PageAction:OpenPopup",
+    });
   }
 
-  /**
-   * 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",
-        });
-      });
+  getTab(tabId) {
+    return this.helper.getTab(tabId);
   }
 
-  /**
-   * 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();
+  click() {
+    this.clickDelegate.onClick();
   }
 }
 
 this.pageAction = class extends ExtensionAPI {
-  onManifestEntry(entryName) {
-    let { extension } = this;
-    let { manifest } = extension;
+  async onManifestEntry(entryName) {
+    const { extension } = this;
+    const action = new PageAction(extension, this);
+    await action.loadIconData();
+    this.action = action;
 
-    let pageAction = new PageAction(manifest.page_action, extension);
-    pageActionMap.set(extension, pageAction);
+    GeckoViewWebExtension.pageActions.set(extension, action);
+
+    // Notify the embedder of this action
+    action.updateOnChange(null);
+  }
+
+  onClick() {
+    this.emit("click", tabTracker.activeTab);
   }
 
   onShutdown() {
-    let { extension } = this;
-
-    if (pageActionMap.has(extension)) {
-      pageActionMap.get(extension).shutdown();
-      pageActionMap.delete(extension);
-    }
+    const { extension, action } = this;
+    action.onShutdown();
+    GeckoViewWebExtension.pageActions.delete(extension);
   }
 
   getAPI(context) {
     const { extension } = context;
     const { tabManager } = extension;
-
-    pageActionMap.get(extension).setContext(context);
+    const { action } = this;
 
     return {
       pageAction: {
+        ...action.api(context),
+
         onClicked: new EventManager({
           context,
           name: "pageAction.onClicked",
           register: fire => {
-            let listener = (event, tab) => {
+            const listener = (event, tab) => {
               fire.async(tabManager.convert(tab));
             };
-            pageActionMap.get(extension).on("click", listener);
+            this.on("click", listener);
             return () => {
-              pageActionMap.get(extension).off("click", listener);
+              this.off("click", listener);
             };
           },
         }).api(),
 
-        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);
+        openPopup() {
+          action.openPopup();
         },
       },
     };
   }
 };
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -33,19 +33,17 @@ var { defineLazyGetter } = ExtensionComm
 global.GlobalEventDispatcher = EventDispatcher.instance;
 
 const BrowserStatusFilter = Components.Constructor(
   "@mozilla.org/appshell/component/browser-status-filter;1",
   "nsIWebProgress",
   "addProgressListener"
 );
 
-const WINDOW_TYPE = Services.androidBridge.isFennec
-  ? "navigator:browser"
-  : "navigator:geckoview";
+const WINDOW_TYPE = "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.
@@ -97,116 +95,30 @@ 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 GeckoViewProgressListenerWrapper {
+class ProgressListenerWrapper {
   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() {
@@ -278,17 +190,17 @@ global.makeGlobalEvent = function makeGl
       GlobalEventDispatcher.registerListener(listener2, [event]);
       return () => {
         GlobalEventDispatcher.unregisterListener(listener2, [event]);
       };
     },
   }).api();
 };
 
-class GeckoViewTabTracker extends TabTrackerBase {
+class TabTracker extends TabTrackerBase {
   init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
 
     windowTracker.addOpenListener(window => {
       const nativeTab = window.BrowserApp.selectedTab;
@@ -351,252 +263,18 @@ class GeckoViewTabTracker extends TabTra
     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();
-if (Services.androidBridge.isFennec) {
-  tabTracker = new FennecTabTracker();
-} else {
-  tabTracker = new GeckoViewTabTracker();
-}
+tabTracker = new TabTracker();
 
 Object.assign(global, { tabTracker, windowTracker });
 
 class Tab extends TabBase {
   get _favIconUrl() {
     return undefined;
   }
 
@@ -721,60 +399,62 @@ 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();
+  }
 
-    GlobalEventDispatcher.registerListener(this, [
-      "Tab:Selected",
-      "Tab:Closed",
-    ]);
+  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
+    );
   }
 
   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() {
-    GlobalEventDispatcher.unregisterListener(this, [
-      "Tab:Selected",
-      "Tab:Closed",
-    ]);
+    windowTracker.removeListener("progress", this);
   }
 }
 
 class Window extends WindowBase {
   get focused() {
     return this.window.document.hasFocus();
   }
 
deleted file mode 100644
--- a/mobile/android/components/extensions/schemas/browser_action.json
+++ /dev/null
@@ -1,448 +0,0 @@
-// 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,10 +1,8 @@
 # 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
deleted file mode 100644
--- a/mobile/android/components/extensions/schemas/page_action.json
+++ /dev/null
@@ -1,240 +0,0 @@
-// 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/extensions/test/mochitest/chrome.ini
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -1,19 +1,11 @@
 [DEFAULT]
 support-files =
   head.js
   ../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js
 tags = webextensions
 
-[test_ext_activeTab_permission.html]
-[test_ext_browserAction_getPopup_setPopup.html]
-[test_ext_browserAction_getTitle_setTitle.html]
-[test_ext_browserAction_onClicked.html]
 [test_ext_browsingData_cookies_cache.html]
 [test_ext_browsingData_downloads.html]
 [test_ext_browsingData_formdata.html]
 [test_ext_browsingData_settings.html]
 [test_ext_options_ui.html]
-[test_ext_pageAction_show_hide.html]
-[test_ext_pageAction_getPopup_setPopup.html]
-[test_ext_popup_behavior.html]
-skip-if = (os == "android" && !debug) #Bug 1463383 - timeout
deleted file mode 100644
--- a/mobile/android/components/extensions/test/mochitest/test_ext_activeTab_permission.html
+++ /dev/null
@@ -1,469 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>PageAction Test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
-var {PageActions} = SpecialPowers.Cu.import("resource://gre/modules/PageActions.jsm", {});
-var {Services} = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
-
-function pageLoadedContentScript() {
-  browser.test.sendMessage("page-loaded", window.location.href);
-}
-
-add_task(async function test_activeTab_pageAction() {
-  async function background() {
-    function contentScriptCode() {
-      browser.test.log("content script executed");
-
-      return "tabs.executeScript result";
-    }
-
-    const createdTab = await browser.tabs.create({
-      url: "http://example.com/#test_activeTab_pageAction",
-    });
-
-    browser.test.log(`Created new tab with id: ${createdTab.id}`);
-
-    await browser.pageAction.show(createdTab.id);
-
-    browser.pageAction.onClicked.addListener(async (tab) => {
-      browser.test.assertEq(createdTab.id, tab.id,
-                            "pageAction clicked on the expected tab id");
-
-      const [result] = await browser.tabs.executeScript(tab.id, {
-        code: `(${contentScriptCode})()`,
-      }).catch(error => {
-        // Make the test to fail fast if something goes wrong.
-        browser.test.fail(`Unexpected exception on tabs.executeScript: ${error}`);
-        browser.tabs.remove(tab.id);
-        browser.test.notifyFail("page_action.activeTab.done");
-        throw error;
-      });
-
-      browser.test.assertEq("tabs.executeScript result", result,
-                            "Got the expected result from tabs.executeScript");
-
-      browser.tabs.onRemoved.addListener((tabId) => {
-        if (tabId !== tab.id) {
-          return;
-        }
-
-        browser.test.notifyPass("page_action.activeTab.done");
-      });
-      browser.tabs.remove(tab.id);
-    });
-
-    browser.test.sendMessage("background_page.ready");
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": "PageAction Extension",
-      "page_action": {
-        "default_title": "Page Action",
-        "default_icon": {
-          "18": "extension.png",
-        },
-      },
-      "content_scripts": [
-        {
-          "js": ["page_loaded.js"],
-          "matches": ["http://example.com/*"],
-          "run_at": "document_end",
-        },
-      ],
-      "permissions": ["activeTab"],
-    },
-    files: {
-      "extension.png": TEST_ICON_ARRAYBUFFER,
-      "page_loaded.js": pageLoadedContentScript,
-    },
-  });
-
-  await extension.startup();
-
-  await extension.awaitMessage("background_page.ready");
-
-  const uuid = `{${extension.uuid}}`;
-
-  ok(PageActions.isShown(uuid), "page action is shown");
-
-  info("Wait the new tab to be loaded");
-  const loadedURL = await extension.awaitMessage("page-loaded");
-
-  is(loadedURL, "http://example.com/#test_activeTab_pageAction",
-     "The expected URL has been loaded in a new tab");
-
-  info("Click the pageAction");
-  PageActions.synthesizeClick(uuid);
-
-  await extension.awaitFinish("page_action.activeTab.done");
-
-  await extension.unload();
-});
-
-add_task(async function test_activeTab_browserAction() {
-  async function background() {
-    let createdTab;
-
-    function contentScriptCode() {
-      browser.test.log("content script executed");
-
-      return "tabs.executeScript result";
-    }
-
-    browser.browserAction.onClicked.addListener(async (tab) => {
-      browser.test.assertEq(createdTab.id, tab.id,
-                            "browserAction clicked on the expected tab id");
-
-      const [result] = await browser.tabs.executeScript(tab.id, {
-        code: `(${contentScriptCode})()`,
-      }).catch(error => {
-        // Make the test to fail fast if something goes wrong.
-        browser.test.fail(`Unexpected exception on tabs.executeScript: ${error}`);
-        browser.tabs.remove(tab.id);
-        browser.test.notifyFail("browser_action.activeTab.done");
-        throw error;
-      });
-
-      browser.test.assertEq("tabs.executeScript result", result,
-                            "Got the expected result from tabs.executeScript");
-
-      browser.tabs.onRemoved.addListener((tabId) => {
-        if (tabId !== tab.id) {
-          return;
-        }
-
-        browser.test.notifyPass("browser_action.activeTab.done");
-      });
-      browser.tabs.remove(tab.id);
-    });
-
-    createdTab = await browser.tabs.create({
-      url: "http://example.com/#test_activeTab_browserAction",
-    });
-
-    browser.test.log(`Created a new tab with id: ${createdTab.id}`);
-
-    browser.test.sendMessage("background_page.ready");
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": "BrowserAction Extension",
-      "browser_action": {
-        "default_title": "Browser Action",
-      },
-      "content_scripts": [
-        {
-          "js": ["page_loaded.js"],
-          "matches": ["http://example.com/*"],
-          "run_at": "document_end",
-        },
-      ],
-      "permissions": ["activeTab"],
-    },
-    files: {
-      "page_loaded.js": pageLoadedContentScript,
-    },
-  });
-
-  await extension.startup();
-
-  await extension.awaitMessage("background_page.ready");
-
-  const uuid = `{${extension.uuid}}`;
-
-  ok(BrowserActions.isShown(uuid), "browser action is shown");
-
-  info("Wait the new tab to be loaded");
-  const loadedURL = await extension.awaitMessage("page-loaded");
-
-  is(loadedURL, "http://example.com/#test_activeTab_browserAction",
-     "The expected URL has been loaded in a new tab");
-
-  info("Click the browserAction");
-  BrowserActions.synthesizeClick(uuid);
-
-  await extension.awaitFinish("browser_action.activeTab.done");
-
-  await extension.unload();
-});
-
-add_task(async function test_activeTab_pageAction_popup() {
-  async function background() {
-    await browser.tabs.create({url: "http://example.com#test_activeTab_pageAction_popup"});
-    const tabs = await browser.tabs.query({active: true});
-    await browser.pageAction.show(tabs[0].id);
-
-    browser.test.log(`pageAction shown on tab ${tabs[0].id}`);
-
-    browser.test.sendMessage("background_page.ready", {activeTabId: tabs[0].id});
-  }
-
-  async function popupScript() {
-    function contentScriptCode() {
-      browser.test.log("content script executed");
-
-      return "tabs.executeScript result";
-    }
-
-    const tabs = await browser.tabs.query({active: true});
-    const tab = tabs[0];
-
-    browser.test.log(`extension popup tab opened loaded for activeTab ${tab.id}`);
-
-    browser.test.sendMessage("extension_popup.activeTab", tab.id);
-
-    const [result] = await browser.tabs.executeScript(tab.id, {
-      code: `(${contentScriptCode})()`,
-    }).catch(error => {
-      // Make the test to fail fast if something goes wrong.
-      browser.test.fail(`Unexpected exception on tabs.executeScript: ${error}`);
-      browser.test.notifyFail("page_action_popup.activeTab.done");
-      throw error;
-    });
-
-    browser.test.assertEq("tabs.executeScript result", result,
-                          "Got the expected result from tabs.executeScript");
-
-    browser.test.notifyPass("page_action_popup.activeTab.done");
-  }
-
-  let popupHtml = `<!DOCTYPE html>
-    <html>
-      <head>
-        <meta charset="utf-8">
-      </head>
-      <body>
-        <h1>Extension Popup</h1>
-        <script src="popup.js"><\/script>
-      </body>
-    </html>
-  `;
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": "PageAction Extension",
-      "page_action": {
-        "default_title": "Page Action",
-        "default_icon": {
-          "18": "extension.png",
-        },
-        "default_popup": "popup.html",
-      },
-      "content_scripts": [
-        {
-          "js": ["page_loaded.js"],
-          "matches": ["http://example.com/*"],
-          "run_at": "document_end",
-        },
-      ],
-      "permissions": ["activeTab"],
-    },
-    files: {
-      "extension.png": TEST_ICON_ARRAYBUFFER,
-      "page_loaded.js": pageLoadedContentScript,
-      "popup.html": popupHtml,
-      "popup.js": popupScript,
-    },
-  });
-
-  await extension.startup();
-
-  const {activeTabId} = await extension.awaitMessage("background_page.ready");
-
-  const uuid = `{${extension.uuid}}`;
-
-  ok(PageActions.isShown(uuid), "page action is shown");
-
-  info("Wait the new tab to be loaded");
-  const loadedURL = await extension.awaitMessage("page-loaded");
-
-  is(loadedURL, "http://example.com/#test_activeTab_pageAction_popup",
-     "The expected URL has been loaded in a new tab");
-
-  PageActions.synthesizeClick(uuid);
-
-  const popupActiveTabId = await extension.awaitMessage("extension_popup.activeTab");
-
-  // Check that while the extension popup tab is selected the active tab is still the tab
-  // from which the user has opened the extension popup.
-  is(popupActiveTabId, activeTabId,
-     "Got the expected tabId while the extension popup tab was selected");
-
-  await extension.awaitFinish("page_action_popup.activeTab.done");
-
-  const chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-  const BrowserApp = chromeWin.BrowserApp;
-
-  const popupTab = BrowserApp.selectedTab;
-  const popupTabId = popupTab.id;
-
-  let onceTabClosed = new Promise(resolve => {
-    BrowserApp.deck.addEventListener("TabClose", () => setTimeout(resolve, 0), {once: true});
-  });
-
-  // Switch to the parent tab of the popup tab.
-  // (which should make the extension popup tab to be closed automatically)
-  BrowserApp.selectTab(BrowserApp.getTabForId(popupTab.parentId));
-
-  info("Wait for the extension popup tab to be closed once the parent tab has been selected");
-
-  await onceTabClosed;
-
-  is(BrowserApp.getTabForId(popupTabId), null,
-     "The extension popup tab should have been closed");
-
-  // Close the tab that opened the extension popup before exiting the test.
-  BrowserApp.closeTab(BrowserApp.selectedTab);
-
-  await extension.unload();
-});
-
-add_task(async function test_activeTab_browserAction_popup() {
-  async function background() {
-    await browser.tabs.create({url: "http://example.com#test_activeTab_browserAction_popup"});
-    const tabs = await browser.tabs.query({active: true});
-
-    browser.test.sendMessage("background_page.ready", {activeTabId: tabs[0].id});
-  }
-
-  async function popupScript() {
-    function contentScriptCode() {
-      browser.test.log("content script executed");
-
-      return "tabs.executeScript result";
-    }
-
-    const tabs = await browser.tabs.query({active: true});
-    const tab = tabs[0];
-
-    browser.test.log(`extension popup tab opened loaded for activeTab ${tab.id}`);
-
-    browser.test.sendMessage("extension_popup.activeTab", tab.id);
-
-    const [result] = await browser.tabs.executeScript(tab.id, {
-      code: `(${contentScriptCode})()`,
-    }).catch(error => {
-      // Make the test to fail fast if something goes wrong.
-      browser.test.fail(`Unexpected exception on tabs.executeScript: ${error}`);
-      browser.test.notifyFail("browser_action_popup.activeTab.done");
-      throw error;
-    });
-
-    browser.test.assertEq("tabs.executeScript result", result,
-                          "Got the expected result from tabs.executeScript");
-
-    browser.test.notifyPass("browser_action_popup.activeTab.done");
-  }
-
-  let popupHtml = `<!DOCTYPE html>
-    <html>
-      <head>
-        <meta charset="utf-8">
-      </head>
-      <body>
-        <h1>Extension Popup</h1>
-        <script src="popup.js"><\/script>
-      </body>
-    </html>
-  `;
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": "BrowserAction Extension",
-      "browser_action": {
-        "default_title": "Browser Action",
-        "default_icon": {
-          "18": "extension.png",
-        },
-        "default_popup": "popup.html",
-      },
-      "content_scripts": [
-        {
-          "js": ["page_loaded.js"],
-          "matches": ["http://example.com/*"],
-          "run_at": "document_end",
-        },
-      ],
-      "permissions": ["activeTab"],
-    },
-    files: {
-      "extension.png": TEST_ICON_ARRAYBUFFER,
-      "page_loaded.js": pageLoadedContentScript,
-      "popup.html": popupHtml,
-      "popup.js": popupScript,
-    },
-  });
-
-  await extension.startup();
-
-  const {activeTabId} = await extension.awaitMessage("background_page.ready");
-
-  const uuid = `{${extension.uuid}}`;
-
-  ok(BrowserActions.isShown(uuid), "browser action is shown");
-
-  info("Wait the new tab to be loaded");
-  const loadedURL = await extension.awaitMessage("page-loaded");
-
-  is(loadedURL, "http://example.com/#test_activeTab_browserAction_popup",
-     "The expected URL has been loaded in a new tab");
-
-  BrowserActions.synthesizeClick(uuid);
-
-  const popupActiveTabId = await extension.awaitMessage("extension_popup.activeTab");
-
-  // Check that while the extension popup tab is selected the active tab is still the tab
-  // from which the user has opened the extension popup.
-  is(popupActiveTabId, activeTabId,
-     "Got the expected tabId while the extension popup tab was selected");
-
-  await extension.awaitFinish("browser_action_popup.activeTab.done");
-
-  const chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-  const BrowserApp = chromeWin.BrowserApp;
-
-  const popupTab = BrowserApp.selectedTab;
-  const popupTabId = popupTab.id;
-
-  let onceTabClosed = new Promise(resolve => {
-    BrowserApp.deck.addEventListener("TabClose", () => setTimeout(resolve, 0), {once: true});
-  });
-
-  // Switch to the parent tab of the popup tab.
-  // (which should make the extension popup tab to be closed automatically)
-  BrowserApp.selectTab(BrowserApp.getTabForId(popupTab.parentId));
-
-  info("Wait for the extension popup tab to be closed once the parent tab has been selected");
-
-  await onceTabClosed;
-
-  is(BrowserApp.getTabForId(popupTabId), null,
-     "The extension popup tab should have been closed");
-
-  // Close the tab that opened the extension popup before exiting the test.
-  BrowserApp.closeTab(BrowserApp.selectedTab);
-
-  await extension.unload();
-});
-
-</script>
-
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/components/extensions/test/mochitest/test_ext_browserAction_getPopup_setPopup.html
+++ /dev/null
@@ -1,191 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>BrowserAction Test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
-var {ContentTaskUtils} = SpecialPowers.Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
-var {Services} = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
-
-function createPopupHTML({text, js}) {
-  return `<!DOCTYPE html>
-    <html>
-      <head>
-        <meta charset="utf-8">
-        <title>${text}</title>
-      </head>
-      <body>
-        <h1>${text}</h1>
-        <script src="${js}"><\/script>
-      </body>
-    </html>
-  `;
-}
-
-async function ensureTabSelected(nativeTab) {
-  const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
-
-  BrowserApp.selectTab(nativeTab);
-
-  await ContentTaskUtils.waitForCondition(() => {
-    return nativeTab.getActive();
-  });
-}
-
-add_task(async function test_browserAction_setPopup_and_getPopup() {
-  const DEFAULT_POPUP = "/default_popup.html";
-  const CUSTOM_POPUP_1 = "/custom_popup_1.html";
-  const CUSTOM_POPUP_2 = "/custom_popup_2.html";
-
-  const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
-  const initialTab = BrowserApp.selectedTab;
-
-  const tab1 = BrowserApp.addTab("about:blank#tab1", {parentId: initialTab.id});
-  const tab2 = BrowserApp.addTab("about:blank#tab2", {parentId: initialTab.id});
-  const tab3 = BrowserApp.addTab("about:blank#tab3", {parentId: initialTab.id});
-
-  function background() {
-    async function handleBrowserActionSetPopup({tabId, popup}) {
-      await browser.browserAction.setPopup({tabId, popup});
-
-      browser.test.sendMessage("browserAction-setPopup:done", {tabId, popup});
-    }
-
-    async function handleBrowserActionGetPopup({tabId}) {
-      const popup = await browser.browserAction.getPopup({tabId});
-
-      const popupURL = new URL(popup);
-
-      browser.test.sendMessage("browserAction-getPopup:done", {
-        tabId,
-        popup: popupURL.pathname,
-      });
-    }
-
-    browser.test.onMessage.addListener((msg, args) => {
-      switch (msg) {
-        case "browserAction-setPopup":
-          handleBrowserActionSetPopup(args);
-          break;
-        case "browserAction-getPopup":
-          handleBrowserActionGetPopup(args);
-          break;
-        default:
-          browser.test.fail(`Unexpected test message received: ${msg}`);
-      }
-    });
-
-    browser.test.sendMessage("background-ready");
-  }
-
-  function popupScript() {
-    browser.test.sendMessage("popup-loaded", {popup: window.location.pathname});
-
-    window.close();
-  }
-
-  const extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "browser_action": {
-        "default_popup": "default_popup.html",
-        "default_title": "BrowserAction title",
-      },
-    },
-    files: {
-      "default_popup.html": createPopupHTML({text: "Default Popup", js: "popup.js"}),
-      "custom_popup_1.html": createPopupHTML({text: "Custom Popup 1", js: "popup.js"}),
-      "custom_popup_2.html": createPopupHTML({text: "Custom Popup 2", js: "popup.js"}),
-      "popup.js": popupScript,
-    },
-  });
-
-  await extension.startup();
-
-  const uuid = `{${extension.uuid}}`;
-
-  await extension.awaitMessage("background-ready");
-
-  // Test that the browserAction popup is currently the default popup for all the opened tabs.
-
-  for (const tab of [tab1, tab2, tab3]) {
-    extension.sendMessage("browserAction-getPopup", {tabId: tab.id});
-
-    const res = await extension.awaitMessage("browserAction-getPopup:done");
-
-    isDeeply(res, {tabId: tab.id, popup: DEFAULT_POPUP},
-             "All the tabs should have been associated the same default browserAction popup");
-  }
-
-  // Customize the popup for the first two tabs and checks that getPopup return the expected popup.
-
-  extension.sendMessage("browserAction-setPopup", {
-    tabId: tab1.id,
-    popup: CUSTOM_POPUP_1,
-  });
-
-  await extension.awaitMessage("browserAction-setPopup:done");
-
-  extension.sendMessage("browserAction-setPopup", {
-    tabId: tab2.id,
-    popup: CUSTOM_POPUP_2,
-  });
-
-  await extension.awaitMessage("browserAction-setPopup:done");
-
-  extension.sendMessage("browserAction-getPopup", {tabId: tab1.id});
-  const resTab1 = await extension.awaitMessage("browserAction-getPopup:done");
-  isDeeply(resTab1, {tabId: tab1.id, popup: CUSTOM_POPUP_1},
-           "The first tab should have been associated to the custom  popup 1");
-
-  extension.sendMessage("browserAction-getPopup", {tabId: tab2.id});
-  const resTab2 = await extension.awaitMessage("browserAction-getPopup:done");
-  isDeeply(resTab2, {tabId: tab2.id, popup: CUSTOM_POPUP_2},
-           "The second tab should have been associated to the custom popup 2");
-
-  extension.sendMessage("browserAction-getPopup", {tabId: tab3.id});
-  const resTab3 = await extension.awaitMessage("browserAction-getPopup:done");
-  isDeeply(resTab3, {tabId: tab3.id, popup: DEFAULT_POPUP},
-           "The third tab should still be associated to the default popup");
-
-  // Test browserAction popup opened by clicking on the browserAction.
-
-  await ensureTabSelected(tab1);
-  BrowserActions.synthesizeClick(uuid);
-  const popupLoadedTab1 = await extension.awaitMessage("popup-loaded");
-  isDeeply(popupLoadedTab1, {popup: CUSTOM_POPUP_1},
-           "The expected custom popup has been opened for the first tab");
-
-  await ensureTabSelected(tab2);
-  BrowserActions.synthesizeClick(uuid);
-  const popupLoadedTab2 = await extension.awaitMessage("popup-loaded");
-  isDeeply(popupLoadedTab2, {popup: CUSTOM_POPUP_2},
-           "The expected custom popup has been opened for the second tab");
-
-  await ensureTabSelected(tab3);
-  BrowserActions.synthesizeClick(uuid);
-  const popupLoadedTab3 = await extension.awaitMessage("popup-loaded");
-  isDeeply(popupLoadedTab3, {popup: DEFAULT_POPUP},
-           "The expected default popup has been opened for the third tab");
-
-  // Cleanup the browser before exiting.
-
-  BrowserApp.closeTab(tab1);
-  BrowserApp.closeTab(tab2);
-  BrowserApp.closeTab(tab3);
-
-  await extension.unload();
-});
-</script>
-
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/components/extensions/test/mochitest/test_ext_browserAction_getTitle_setTitle.html
+++ /dev/null
@@ -1,164 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>BrowserAction Test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
-var {ContentTaskUtils} = SpecialPowers.Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
-
-add_task(async function test_setTitle_and_getTitle() {
-  async function background() {
-    let tabCreatedPromise = new Promise(resolve => {
-      let onTabCreated = tab => {
-        browser.tabs.onCreated.removeListener(onTabCreated);
-        resolve();
-      };
-      browser.tabs.onCreated.addListener(onTabCreated);
-    });
-
-    async function createAndTestNewTab(title, url) {
-      // First make sure the default title is correct.
-      let defaultTitle = await browser.browserAction.getTitle({});
-      browser.test.assertEq("Browser Action", defaultTitle, "Expected the default title to be returned");
-
-      // Create a tab.
-      let [tab] = await Promise.all([
-        browser.tabs.create({url}),
-        tabCreatedPromise,
-      ]);
-
-      // Test that the default title is returned before the title is set for the tab.
-      let tabTitle = await browser.browserAction.getTitle({tabId: tab.id});
-      browser.test.assertEq("Browser Action", tabTitle, "Expected the default title to be returned");
-
-      // Set the title for the new tab and test that getTitle returns the correct title.
-      await browser.browserAction.setTitle({tabId: tab.id, title});
-      tabTitle = await browser.browserAction.getTitle({tabId: tab.id});
-      browser.test.assertEq(title, tabTitle, "Expected the new tab title to be returned");
-
-      return tab;
-    }
-
-    // Create and test 3 new tabs.
-    let tab1 = await createAndTestNewTab("tab 1", "about:blank");
-    let tab2 = await createAndTestNewTab("tab 2", "about:blank");
-    let tab3 = await createAndTestNewTab("tab 3", "about:blank");
-
-    // Test the default title again.
-    let title = await browser.browserAction.getTitle({});
-    browser.test.assertEq("Browser Action", title, "Expected the default title to be returned");
-
-    // Update the default title and confirm that the new title is returned.
-    await browser.browserAction.setTitle({title: "Updated Title"});
-    title = await browser.browserAction.getTitle({});
-    browser.test.assertEq("Updated Title", title, "Expected the default title to be updated");
-
-    // Try setting the default title to an empty string and confirm that the original title is still used.
-    browser.browserAction.setTitle({title: ""});
-    title = await browser.browserAction.getTitle({});
-    browser.test.assertEq("Updated Title", title, "Expected the default title to be returned");
-
-    // Check all of the created tabs now.
-    title = await browser.browserAction.getTitle({tabId: tab1.id});
-    browser.test.assertEq("tab 1", title, "Expected the first tab title");
-    title = await browser.browserAction.getTitle({tabId: tab2.id});
-    browser.test.assertEq("tab 2", title, "Expected the second tab title");
-    title = await browser.browserAction.getTitle({tabId: tab3.id});
-    browser.test.assertEq("tab 3", title, "Expected the third tab title");
-
-    // Unset the title for the first tab and confirm that it is unset.
-    browser.browserAction.setTitle({tabId: tab1.id, title: ""});
-    title = await browser.browserAction.getTitle({tabId: tab1.id});
-    browser.test.assertEq("Updated Title", title, `Expected the default title to be returned`);
-
-    browser.test.onMessage.addListener(async (msg, data) => {
-      if (msg === "select-tab") {
-        await browser.tabs.update(data, {active: true});
-        browser.test.sendMessage("tab-selected");
-      } else if (msg == "finish") {
-        // Close the tabs
-        await browser.tabs.remove([tab1.id, tab2.id, tab3.id]);
-        browser.test.notifyPass("browserAction.setTitleAndGetTitle");
-      }
-    });
-
-    browser.test.sendMessage("tabs", {tab1, tab2, tab3});
-  }
-
-  const extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": name,
-      "browser_action": {
-        "default_title": "Browser Action",
-      },
-    },
-  });
-
-  await extension.startup();
-
-  let tabs = await extension.awaitMessage("tabs");
-
-  async function checkTab(tab, name) {
-    extension.sendMessage("select-tab", tab.id);
-    await extension.awaitMessage("tab-selected");
-    // Wait until the browser action has updated to the correct title.
-    await ContentTaskUtils.waitForCondition(() => {
-      return BrowserActions.getNameForActiveTab(`{${extension.uuid}}`) === name;
-    });
-  }
-
-  await checkTab(tabs.tab1, "Updated Title");
-  await checkTab(tabs.tab2, "tab 2");
-  await checkTab(tabs.tab3, "tab 3");
-  await checkTab(tabs.tab1, "Updated Title");
-
-  extension.sendMessage("finish");
-  await extension.awaitFinish("browserAction.setTitleAndGetTitle");
-
-  await extension.unload();
-});
-
-add_task(async function test_setTitle_activeTab() {
-  async function background() {
-    const tabs = await browser.tabs.query({active: true});
-    const tabId = tabs[0].id;
-
-    const title = "Customized browserAction title";
-    await browser.browserAction.setTitle({tabId, title});
-
-    browser.test.notifyPass("browserAction_setTitle_activeTab.done");
-  }
-
-  const extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "browser_action": {
-        "default_title": "Browser Action default title",
-      },
-    },
-  });
-
-  await extension.startup();
-
-  await extension.awaitFinish("browserAction_setTitle_activeTab.done");
-
-  is(BrowserActions.getNameForActiveTab(`{${extension.uuid}}`),
-     "Customized browserAction title",
-     "The browserAction title has been updated on the currently activeTab");
-
-  await extension.unload();
-});
-</script>
-
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/components/extensions/test/mochitest/test_ext_browserAction_onClicked.html
+++ /dev/null
@@ -1,93 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>BrowserAction Test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
-
-async function background() {
-  browser.test.assertTrue("browserAction" in browser, "Namespace 'browserAction' exists in browser");
-  browser.test.assertTrue("onClicked" in browser.browserAction, "API method 'onClicked' exists in browser.browserAction");
-
-  const tabs = await browser.tabs.query({active: true, currentWindow: true});
-
-  browser.browserAction.onClicked.addListener(tab => {
-    browser.test.sendMessage("browser-action-clicked", tab);
-  });
-
-  browser.test.sendMessage("ready", tabs[0]);
-}
-
-function createExtension(name) {
-  return ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": name,
-      "browser_action": {
-        "default_title": "Browser Action",
-      },
-    },
-  });
-}
-
-function* checkBrowserAction(extension, id, tab) {
-  ok(BrowserActions.isShown(id), "The BrowerAction should be shown");
-  BrowserActions.synthesizeClick(id);
-  const clickedTab = yield extension.awaitMessage("browser-action-clicked");
-  is(clickedTab.id, tab.id, "Got the expected tab id in the browserAction.onClicked event");
-}
-
-add_task(async function test_browserAction() {
-  const extension = createExtension("BrowserAction Extension");
-  await extension.startup();
-  const tab = await extension.awaitMessage("ready");
-  let id = `{${extension.uuid}}`;
-  await checkBrowserAction(extension, id, tab);
-  await extension.unload();
-
-  ok(!BrowserActions.isShown(id), "The BrowserAction should be removed after the extension unloads");
-});
-
-add_task(async function test_multiple_browserActions() {
-  const ext1 = createExtension("BrowserAction Extension 1");
-  const ext2 = createExtension("BrowserAction Extension 2");
-
-  // Start the first extension and test its browser action.
-  await ext1.startup();
-  const tab1 = await ext1.awaitMessage("ready");
-  let id1 = `{${ext1.uuid}}`;
-  await checkBrowserAction(ext1, id1, tab1);
-
-  // Start the second extension and test its browser action.
-  await ext2.startup();
-  const tab2 = await ext2.awaitMessage("ready");
-  let id2 = `{${ext2.uuid}}`;
-  await checkBrowserAction(ext2, id2, tab2);
-
-  // Verify that the first browser action is still active.
-  await checkBrowserAction(ext1, id1, tab1);
-
-  // Unload the first extension and verify that the browser action is removed.
-  await ext1.unload();
-  ok(!BrowserActions.isShown(id1), "The first BrowserAction should be removed after ext1 unloads");
-
-  // Verify that the second browser action is still active.
-  await checkBrowserAction(ext2, id2, tab2);
-
-  // Unload the second extension and verify that the browser action is removed.
-  await ext2.unload();
-  ok(!BrowserActions.isShown(id2), "The second BrowserAction should be removed after ext2 unloads");
-});
-</script>
-
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_getPopup_setPopup.html
+++ /dev/null
@@ -1,218 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>PageAction Test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-var {PageActions} = ChromeUtils.import("resource://gre/modules/PageActions.jsm");
-
-add_task(async function test_setPopup_and_getPopup() {
-  async function background() {
-    let tabCreatedPromise = new Promise(resolve => {
-      let onTabCreated = tab => {
-        browser.tabs.onCreated.removeListener(onTabCreated);
-        resolve();
-      };
-      browser.tabs.onCreated.addListener(onTabCreated);
-    });
-
-    async function createAndTestNewTab(expectedPopup, url) {
-      // Create a tab.
-      let [, tab] = await Promise.all([
-        tabCreatedPromise,
-        browser.tabs.create({url}),
-      ]);
-
-      // Test that the default popup is returned before the popup is set for the tab.
-      let defaultPopup = await browser.pageAction.getPopup({tabId: tab.id});
-      browser.test.assertTrue(defaultPopup.includes("default.html"), "Expected the default popup to be returned");
-
-      // Set the title for the new tab and test that getTitle returns the correct title.
-      await browser.pageAction.setPopup({tabId: tab.id, popup: expectedPopup});
-      let actualPopup = await browser.pageAction.getPopup({tabId: tab.id});
-      browser.test.assertTrue(actualPopup.includes(expectedPopup), "Expected the new tab popup to be returned");
-
-      return tab;
-    }
-
-    // Create and test 2 new tabs.
-    let tab1 = await createAndTestNewTab("a.html", "about:blank");
-    let tab2 = await createAndTestNewTab("b.html", "about:blank");
-
-    // Check all of the created tabs now.
-    let popup = await browser.pageAction.getPopup({tabId: tab1.id});
-    browser.test.assertTrue(popup.includes("a.html"), "Expected the first tab popup");
-    popup = await browser.pageAction.getPopup({tabId: tab2.id});
-    browser.test.assertTrue(popup.includes("b.html"), "Expected the second tab popup");
-
-    // Unset the popup for the first tab and confirm that it is unset.
-    browser.pageAction.setPopup({tabId: tab1.id, popup: ""});
-    popup = await browser.pageAction.getPopup({tabId: tab1.id});
-    browser.test.assertTrue(popup.includes("default.html"), "Expected the default popup to be returned");
-
-    // Set the popup for the first tab.
-    browser.pageAction.setPopup({tabId: tab1.id, popup: "a.html"});
-    popup = await browser.pageAction.getPopup({tabId: tab1.id});
-    browser.test.assertTrue(popup.includes("a.html"), "Expected the first tab popup");
-
-    // Keeps track of the tabs for which onClicked should fire when the page action is clicked on.
-    let expectingOnClicked = {};
-
-    browser.pageAction.onClicked.addListener(tab => {
-      browser.test.assertTrue(expectingOnClicked[tab.id], "The onClicked listener should only fire when we expect it to.");
-      browser.test.sendMessage("page-action-onClicked-fired");
-    });
-
-    browser.test.onMessage.addListener(async (msg, data) => {
-      if (msg === "select-tab") {
-        // Check if the requested tabId is already selected.
-        const [activeTab] = await browser.tabs.query({active: true});
-        if (activeTab.id === data.tabId) {
-          browser.test.sendMessage("tab-selected");
-          return;
-        }
-
-        // Select the requested tabId and wait the tab to be activated.
-        const onActivatedListener = ({tabId}) => {
-          if (tabId === data.tabId) {
-            browser.tabs.onActivated.removeListener(onActivatedListener);
-            browser.test.sendMessage("tab-selected");
-          }
-        };
-        browser.tabs.onActivated.addListener(onActivatedListener);
-
-        await browser.tabs.update(data.tabId, {active: true});
-      } else if (msg === "page-action-show") {
-        await browser.pageAction.show(data.tabId);
-        browser.test.sendMessage("page-action-shown");
-      } else if (msg == "page-action-set-popup") {
-        if (data.popup == "") {
-          expectingOnClicked[data.tabId] = true;
-        } else {
-          delete expectingOnClicked[data.tabId];
-        }
-        await browser.pageAction.setPopup({tabId: data.tabId, popup: data.popup});
-        browser.test.sendMessage("page-action-popup-set");
-      } else if (msg == "page-action-get-popup") {
-        const url = await browser.pageAction.getPopup({tabId: data.tabId});
-        browser.test.sendMessage("page-action-got-popup", url);
-      } else if (msg === "finish") {
-        await browser.tabs.remove([tab1.id, tab2.id]);
-        browser.test.notifyPass("page-action-popup");
-      }
-    });
-
-    browser.test.sendMessage("tabs", {tab1, tab2});
-  }
-
-  function popupScript() {
-    window.onload = () => {
-      browser.test.sendMessage("page-action-from-popup", location.href);
-    };
-
-    browser.test.onMessage.addListener((msg, details) => {
-      if (msg == "page-action-close-popup") {
-        if (details.location == location.href) {
-          window.close();
-        }
-      }
-    });
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": "PageAction Extension",
-      "page_action": {
-        "default_title": "Page Action",
-        "default_popup": "default.html",
-        "default_icon": {
-          "18": "extension.png",
-        },
-      },
-    },
-    files: {
-      "default.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
-      "extension.png": TEST_ICON_ARRAYBUFFER,
-      "a.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
-      "b.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
-      "popup.js": popupScript,
-    },
-  });
-
-  let tabClosedPromise = () => {
-    return new Promise(resolve => {
-      let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-      let BrowserApp = chromeWin.BrowserApp;
-
-      let tabCloseListener = (event) => {
-        BrowserApp.deck.removeEventListener("TabClose", tabCloseListener);
-        let browser = event.target;
-        let url = browser.currentURI.spec;
-        resolve(url);
-      };
-
-      BrowserApp.deck.addEventListener("TabClose", tabCloseListener);
-    });
-  };
-
-  async function testPopup(tabId, expectedPopup, uuid) {
-    extension.sendMessage("page-action-get-popup", {tabId});
-    let actualPopup = await extension.awaitMessage("page-action-got-popup");
-    ok(actualPopup.includes(expectedPopup), `Expected the correct popup for tab ${tabId}`);
-
-    extension.sendMessage("select-tab", {tabId});
-    await extension.awaitMessage("tab-selected");
-
-    extension.sendMessage("page-action-show", {tabId});
-    await extension.awaitMessage("page-action-shown");
-
-    ok(PageActions.isShown(uuid), "page action is shown");
-
-    info(`Click on the pageAction on tab ${tabId} and wait the popup to be loaded`);
-    PageActions.synthesizeClick(uuid);
-    let location = await extension.awaitMessage("page-action-from-popup");
-
-    ok(location.includes(expectedPopup), "The popup with the correct URL should be shown.");
-
-    const onceTabClosed = tabClosedPromise();
-    extension.sendMessage("page-action-close-popup", {location});
-    location = await onceTabClosed;
-    ok(location.includes(expectedPopup), "The popup with the correct URL should be closed");
-  }
-
-  await extension.startup();
-
-  let {tab1, tab2} = await extension.awaitMessage("tabs");
-
-  const uuid = `{${extension.uuid}}`;
-  await testPopup(tab1.id, "a.html", uuid);
-  await testPopup(tab2.id, "b.html", uuid);
-
-  // Test that the default popup is used when the first tabs popup is unset.
-  extension.sendMessage("page-action-set-popup", {tabId: tab1.id, popup: ""});
-  await extension.awaitMessage("page-action-popup-set");
-
-  await testPopup(tab1.id, "default.html", uuid);
-
-  extension.sendMessage("finish");
-  await extension.awaitFinish("page-action-popup");
-
-  await extension.unload();
-});
-
-
-</script>
-
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_show_hide.html
+++ /dev/null
@@ -1,143 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>PageAction Test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-var {ContentTaskUtils} = SpecialPowers.Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
-var {PageActions} = SpecialPowers.Cu.import("resource://gre/modules/PageActions.jsm", {});
-
-add_task(async function test_pageAction() {
-  async function background() {
-    let tabCreatedPromise = new Promise(resolve => {
-      let onTabCreated = tab => {
-        browser.tabs.onCreated.removeListener(onTabCreated);
-        resolve();
-      };
-      browser.tabs.onCreated.addListener(onTabCreated);
-    });
-
-    async function createNewTab(url) {
-      let [tab] = await Promise.all([
-        browser.tabs.create({url}),
-        tabCreatedPromise,
-      ]);
-      return tab;
-    }
-
-    let tab1 = await createNewTab("about:blank");
-    let tab2 = await createNewTab("about:blank");
-
-    browser.test.onMessage.addListener(async (msg, data) => {
-      if (msg === "select-tab") {
-        const onActivatedListener = ({tabId}) => {
-          if (tabId === data) {
-            browser.tabs.onActivated.removeListener(onActivatedListener);
-            browser.test.sendMessage("tab-selected");
-          }
-        };
-        browser.tabs.onActivated.addListener(onActivatedListener);
-
-        await browser.tabs.update(data, {active: true});
-      } else if (msg === "pageAction-show") {
-        browser.pageAction.show(data).then(() => {
-          browser.test.sendMessage("page-action-shown");
-        });
-      } else if (msg === "pageAction-hide") {
-        browser.pageAction.hide(data).then(() => {
-          browser.test.sendMessage("page-action-hidden");
-        });
-      } else if (msg === "finish") {
-        await browser.tabs.remove([tab1.id, tab2.id]);
-        browser.test.notifyPass("pageAction");
-      }
-    });
-
-    browser.pageAction.onClicked.addListener(tab => {
-      browser.test.sendMessage("page-action-clicked", tab);
-    });
-
-    browser.test.sendMessage("tabs", {tab1, tab2});
-  }
-
-  const extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": "PageAction Extension",
-      "page_action": {
-        "default_title": "Page Action",
-        "default_icon": {
-          "18": "extension.png",
-        },
-      },
-      "applications": {
-        "gecko": {
-          "id": "foo@bar.com",
-        },
-      },
-    },
-    files: {
-      "extension.png": TEST_ICON_ARRAYBUFFER,
-    },
-  });
-
-  await extension.startup();
-
-  async function checkTab(tabId, uuid, showAfterSelecting) {
-    ok(!PageActions.isShown(uuid), "The PageAction should not be shown");
-
-    if (showAfterSelecting) {
-      extension.sendMessage("select-tab", tabId);
-      await extension.awaitMessage("tab-selected");
-      ok(!PageActions.isShown(uuid), "The PageAction should still not be shown");
-      extension.sendMessage("pageAction-show", tabId);
-      await extension.awaitMessage("page-action-shown");
-      ok(PageActions.isShown(uuid), "The PageAction should be shown");
-    } else {
-      extension.sendMessage("pageAction-show", tabId);
-      await extension.awaitMessage("page-action-shown");
-      ok(!PageActions.isShown(uuid), "The PageAction should still not be shown");
-      extension.sendMessage("select-tab", tabId);
-      await extension.awaitMessage("tab-selected");
-      // Wait until the page action is shown.
-      await ContentTaskUtils.waitForCondition(() => PageActions.isShown(uuid));
-    }
-
-    PageActions.synthesizeClick(uuid);
-    const clickedTab = await extension.awaitMessage("page-action-clicked");
-    is(clickedTab.id, tabId, "Got the expected tab id in the pageAction.onClicked event");
-
-    ok(PageActions.isShown(uuid), "The PageAction should be shown");
-    extension.sendMessage("pageAction-hide", tabId);
-    await extension.awaitMessage("page-action-hidden");
-  }
-
-  const tabs = await extension.awaitMessage("tabs");
-  const uuid = `{${extension.uuid}}`;
-
-  await checkTab(tabs.tab1.id, uuid, true /* showAfterSelecting */);
-  await checkTab(tabs.tab2.id, uuid, false /* showAfterSelecting */);
-
-  // Show the page action for the active tab.
-  extension.sendMessage("pageAction-show", tabs.tab2.id);
-  await extension.awaitMessage("page-action-shown");
-  ok(PageActions.isShown(uuid), "The PageAction should be shown");
-
-  extension.sendMessage("finish");
-  await extension.awaitFinish("pageAction");
-
-  await extension.unload();
-  ok(!PageActions.isShown(uuid), "The PageAction should be removed after unload");
-});
-</script>
-
-</body>
-</html>
deleted file mode 100644
--- a/mobile/android/components/extensions/test/mochitest/test_ext_popup_behavior.html
+++ /dev/null
@@ -1,224 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>PageAction Test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
-var {Services} = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
-
-function promiseDispatchedWindowEvent(eventName) {
-  return new Promise(resolve => {
-    let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-    let WindowEventDispatcher = chromeWin.WindowEventDispatcher;
-
-    let listener = (event) => {
-      WindowEventDispatcher.unregisterListener(listener, eventName);
-      resolve();
-    };
-
-    WindowEventDispatcher.registerListener(listener, eventName);
-  });
-}
-
-async function closeTabAndWaitTabClosed({BrowserApp, tab}) {
-  let onceTabClosed = promiseDispatchedWindowEvent("Tab:Closed");
-  BrowserApp.closeTab(tab);
-  await onceTabClosed;
-}
-
-async function selectTabAndWaitTabSelected({BrowserApp, tab}) {
-  let onceTabSelected = promiseDispatchedWindowEvent("Tab:Selected");
-  BrowserApp.selectTab(tab);
-  await onceTabSelected;
-}
-
-add_task(async function test_popup_behavior() {
-  const chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-  const BrowserApp = chromeWin.BrowserApp;
-
-  async function background() {
-    const tab1 = await browser.tabs.create({url: "http://example.com#test_popup_behavior_1"});
-    const tab2 = await browser.tabs.create({url: "http://example.com#test_popup_behavior_2"});
-    const tab3 = await browser.tabs.create({url: "http://example.com#test_popup_behavior_3"});
-
-    browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
-      if (![tab1.id, tab2.id, tab3.id].includes(tabId) ||
-          changeInfo.status !== "complete") {
-        return;
-      }
-
-      browser.test.sendMessage("page-loaded", tabId);
-    });
-
-    browser.test.sendMessage("background_page.ready", {
-      tabId1: tab1.id,
-      tabId2: tab2.id,
-      tabId3: tab3.id,
-    });
-  }
-
-  async function popupScript() {
-    browser.test.sendMessage("popup_script.loaded");
-  }
-
-  let popupHtml = `<!DOCTYPE html>
-    <html>
-      <head>
-        <meta charset="utf-8">
-      </head>
-      <body>
-        <h1>Extension Popup</h1>
-        <script src="popup.js"><\/script>
-      </body>
-    </html>
-  `;
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      "name": "BrowserAction Extension",
-      "browser_action": {
-        "default_title": "Browser Action",
-        "default_popup": "popup.html",
-        "default_icon": {
-          "18": "extension.png",
-        },
-      },
-      "permissions": ["activeTab"],
-    },
-    files: {
-      "popup.html": popupHtml,
-      "popup.js": popupScript,
-    },
-  });
-
-  await extension.startup();
-
-  const {
-    tabId1,
-    tabId2,
-    tabId3,
-  } = await extension.awaitMessage("background_page.ready");
-
-  const uuid = `{${extension.uuid}}`;
-
-  ok(BrowserActions.isShown(uuid), "browser action is shown");
-
-  info("Wait the new tabs to be loaded");
-
-  await extension.awaitMessage("page-loaded");
-  await extension.awaitMessage("page-loaded");
-  await extension.awaitMessage("page-loaded");
-
-  is(BrowserApp.selectedTab.id, tabId3, "The third of the new tabs has been selected");
-
-  BrowserActions.synthesizeClick(uuid);
-  await extension.awaitMessage("popup_script.loaded");
-
-  // Check that while the extension popup tab is selected the active tab is still the tab
-  // from which the user has opened the extension popup.
-  ok(BrowserApp.selectedBrowser.currentURI.spec.endsWith("popup.html"),
-     "The first popup tab has been selected");
-
-  let popupParentTabId = BrowserApp.selectedTab.parentId;
-  is(popupParentTabId, tabId3, "The parent of the first popup tab is the third tab");
-
-  // Close the popup and test that its parent tab is selected.
-  await closeTabAndWaitTabClosed({BrowserApp, tab: BrowserApp.selectedTab});
-
-  const tab1 = BrowserApp.getTabForId(tabId1);
-  const tab2 = BrowserApp.getTabForId(tabId2);
-  const tab3 = BrowserApp.getTabForId(tabId3);
-
-  BrowserActions.synthesizeClick(uuid);
-  await extension.awaitMessage("popup_script.loaded");
-
-  ok(BrowserApp.selectedBrowser.currentURI.spec.endsWith("popup.html"),
-     "The second popup tab has been selected");
-
-  popupParentTabId = BrowserApp.selectedTab.parentId;
-  is(popupParentTabId, tabId3, "The parent of the second popup tab is the third tab");
-
-  // Switch to the second tab and expect the popup tab to be closed.
-  let onceTabClosed = promiseDispatchedWindowEvent("Tab:Closed");
-  await Promise.all([
-    selectTabAndWaitTabSelected({BrowserApp, tab: tab2}),
-    onceTabClosed,
-  ]);
-
-  // Switch to the first tab and expect it to be the next selected tab.
-  // (which ensure that Bug 1373170 has been fixed and the closed popup tab
-  // has not selected its parent tab).
-  await selectTabAndWaitTabSelected({BrowserApp, tab: tab1});
-
-  is(BrowserApp.selectedTab.id, tabId1,
-     "The first tab is the currently selected tab");
-
-  // Close all the tabs before exiting the test.
-  await Promise.all([
-    closeTabAndWaitTabClosed({BrowserApp, tab: tab1}),
-    closeTabAndWaitTabClosed({BrowserApp, tab: tab2}),
-    closeTabAndWaitTabClosed({BrowserApp, tab: tab3}),
-  ]);
-
-  await extension.unload();
-});
-
-add_task(async function test_popup_incognito() {
-  const chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-  const BrowserApp = chromeWin.BrowserApp;
-  const extension = ExtensionTestUtils.loadExtension({
-    background() {
-      browser.tabs.onCreated.addListener(async (tab) => {
-        browser.test.sendMessage("page-loaded", tab);
-      });
-    },
-    manifest: {
-      "name": "BrowserAction Extension",
-      "browser_action": {
-        "default_popup": "popup.html",
-      },
-    },
-    files: {
-      "popup.html": "<h1>Extension Popup</h1>",
-    },
-  });
-
-  await extension.startup();
-  const uuid = `{${extension.uuid}}`;
-  ok(BrowserActions.isShown(uuid), "Browser action should be shown");
-
-  for (let isPrivate of [false, true]) {
-    // Open tab with provided privateness
-    let nativeTab = BrowserApp.addTab("http://example.com/#normal", {
-      isPrivate,
-      selected: true,
-    });
-    let tab = await extension.awaitMessage("page-loaded");
-    is(tab.incognito, isPrivate, "Tab should have expected privateness");
-
-    // Open browserAction popup in that tab
-    BrowserActions.synthesizeClick(uuid);
-    let popup = await extension.awaitMessage("page-loaded");
-    is(popup.incognito, isPrivate, "Popup should have same privateness as tab");
-
-    // Close popup and tab
-    await closeTabAndWaitTabClosed({BrowserApp, tab: BrowserApp.selectedTab});
-    await closeTabAndWaitTabClosed({BrowserApp, tab: nativeTab});
-  }
-
-  await extension.unload();
-});
-</script>
-
-</body>
-</html>
--- a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html
@@ -7,84 +7,16 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
-var {Services} = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
-
-add_task(async function test_query_highlighted() {
-  if (true) {
-    // GeckoView does not support extension popups
-    todo(false, 'skipping test_query_highlighted');
-    return;
-  }
-  let extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      "permissions": ["tabs"],
-      "browser_action": {
-        "default_popup": "popup.html",
-      },
-    },
-
-    background: async function() {
-      let tabs1 = await browser.tabs.query({highlighted: false});
-      browser.test.assertEq(3, tabs1.length, "should have three non-highlighted tabs");
-
-      let tabs2 = await browser.tabs.query({highlighted: true});
-      browser.test.assertEq(1, tabs2.length, "should have one highlighted tab");
-
-      for (let tab of [...tabs1, ...tabs2]) {
-        browser.test.assertEq(tab.active, tab.highlighted, "highlighted and active are equal in tab " + tab.index);
-      }
-
-      browser.test.notifyPass("tabs.query");
-    },
-
-    files: {
-      "popup.html": `<script src="popup.js"><\/script>`,
-      "popup.js": async function popupScript() {
-        let active = await browser.tabs.query({active: true});
-        let highlighted = await browser.tabs.query({highlighted: true});
-
-        browser.test.assertEq(1, active.length, "should have one active tab");
-        browser.test.assertEq(1, highlighted.length, "should have one highlighted tab");
-        browser.test.assertEq(active[0].id, highlighted[0].id, "the active and highlighted tabs are the same one");
-        browser.test.assertEq(true, active[0].active, "the tab should be considered to be active");
-        browser.test.assertEq(true, active[0].highlighted, "the tab should be considered to be highlighted");
-
-        browser.test.sendMessage("tabs.query.popup");
-      },
-    },
-  });
-
-  const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
-  let tabs = [];
-  for (let url of ["http://example.com/", "http://example.net/", "http://test1.example.org/MochiKit/"]) {
-    tabs.push(BrowserApp.addTab(url));
-  }
-
-  await extension.startup();
-  await extension.awaitFinish("tabs.query");
-
-  // Open popup
-  BrowserActions.synthesizeClick(`{${extension.uuid}}`);
-  await extension.awaitMessage("tabs.query.popup");
-
-  await extension.unload();
-
-  for (let tab of tabs) {
-    BrowserApp.closeTab(tab);
-  }
-});
-
 add_task(async function test_query_index() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["tabs"],
     },
 
     background: function() {
       browser.tabs.onCreated.addListener(async function({index, windowId, id}) {
--- a/mobile/android/components/geckoview/GeckoViewStartup.js
+++ b/mobile/android/components/geckoview/GeckoViewStartup.js
@@ -64,16 +64,18 @@ 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,17 +21,16 @@ 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;
@@ -597,16 +596,17 @@ 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,16 +635,17 @@ 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;
@@ -1155,20 +1156,22 @@ 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);
@@ -1381,22 +1384,47 @@ 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);
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/background.js
@@ -0,0 +1,140 @@
+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}`);
+  }
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..aea2c19784430b624d37821c2fc86386b40e52bd
GIT binary patch
literal 1074
zc%17D@N?(olHy`uVBq!ia0vp^DIm<j1|$m}O$`B3EX7WqAsj$Z!;#X#z`*>{)5S5Q
zV$R#S4+C!-h&0%5{TrVjP|lp+Fmpi((;d!J8aWe6m~JSPFkRxzQd5}g@Hf#>x=$do
z=*jm<-|s#xVxP41$LYzckwpg#cn$;63_0_1R-w!W|A&7~uN`o%Fz0h%jbSoBu(<f#
z@jVU4Cp<TjIecOXn^@0;4esZZWG+j1vg#D6?omu{boDWj6?Sn+Ia0E@F6g0*iIz^(
zu|US}OH==-Kb4#O>C76VC7v^@Wr7&BpWj*c(Is%t_v{BUH_n8%?`+kXQylB%_WrMY
zTVuR@oIy!u_JYE5wLUiQgnmq$6mA{sw0RTbx<<1{Kg1Q^sx6%JE@6I@hN$xHZ3iZ8
zag62Fai8|Jqv5ru=OTXr=a4PJQ*Ih5MM>;qP<OO_v}Daq+r}AA%xm1I#QR5Vi&(U%
zS<#5^;_s}E9uYqUuT_Hg&wXq7n(;QuNZ{(`MQ6;7&4P9_mCo$FePY=vy@2VnI+k(o
z&<&V=^TzF~sS?&x9$9U=sda33Q>y8?oio1ecw%0ZAGz?{(Pd>`Tr0n;a$Hs{XJ3EP
z{+-jz-z)fIC#rY3dAQ876jMDirE-d(kaN9K>boN?GXx&%`g9oJM|M6nduAoRzn}f)
z*YDFN>65Rj_1&qmHF(JKG$gxPzfoH~<zh+ZA*Hw358U5vENTro&|AFj?AjBC5BBb7
z-ZyDum0kFWQz<#ZOXe(pRzE+m?D?GCr%u#Qv3Q<k^zFoRmlN*;c6?-dU&~+PcXIZv
zx4AyY@(%6veDz=nm*uOk>x3tk^yMUI@CYlxQO`<s@oph8ult+!`u{zo`Yv#KgO{S?
z5mD>1sLJ=&ZF}Wk9a)jRY0vll5^S-j%_r$Lsx1j^)lyV6vU>E+{)>b7&i~Flt6B{1
z6iXOqelMBt#Cxknhx_SFUQ?$-Ul;hFxRt$Y+6qCZd&X<hs{dV$FyTD;^3bZ6|5gc3
z+T7rD`4P9N6I1L$o6QS-PkeW(Omm((;ldXkx0J|?taT1E1sKe2OpM)59a6PRDE7O;
z*_f_*GgD<zP{;Rw&qY8Cj*pYOUl}MKx}fzWZ;{EP3tCI&YxG(G!}toP<mKv1H!cUM
zw-g4y%vyGLK1h|ulb#(@pNc*;pOCiL&*Mh*My7oqcFoZGlfSf(zxz~cQ6k%SjrkU9
z^_==-kpc)5MswKNb4=dt*Kmv|jIW9Q64=MAru6g96BiHGFJh{<TVAa2oa27Fa{}*$
ztlI*`9*1^joSbpx%-$92C6}z&{ocS7m}OG_X`G$1Uo6)6kHUwQw(O~gzxgE`2Ios>
bnR>=-Ico;K<6C-wg#d%6tDnm{r-UW|4`}4>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..90687de26d71e91b7c82565772a7df470ae277a6
GIT binary patch
literal 225
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8wRq
zxP?KOkzv*x37}xJr;B4qM&sM7j(iOY0?rpNR{Ym~eNUieh4I>d+mEvHuIy!K@bZ41
z<G=gpjAyae-$dK=GF;u(nO|M2nf#xLWrf-0#+>J}N$e^&*#q7kxbW`Aeg?)>n&l0$
z8xrIlb~3+dVExT-N;ZLA=LS%o!8+lf-GRA$F@Klex9jiV-^0Mj@Zdh*s&<Z;Pny1y
QfX-p?boFyt=akR{0F1y+Z~y=R
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..90687de26d71e91b7c82565772a7df470ae277a6
GIT binary patch
literal 225
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8wRq
zxP?KOkzv*x37}xJr;B4qM&sM7j(iOY0?rpNR{Ym~eNUieh4I>d+mEvHuIy!K@bZ41
z<G=gpjAyae-$dK=GF;u(nO|M2nf#xLWrf-0#+>J}N$e^&*#q7kxbW`Aeg?)>n&l0$
z8xrIlb~3+dVExT-N;ZLA=LS%o!8+lf-GRA$F@Klex9jiV-^0Mj@Zdh*s&<Z;Pny1y
QfX-p?boFyt=akR{0F1y+Z~y=R
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/icon.svg
@@ -0,0 +1,1 @@
+<?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
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/content.js
@@ -0,0 +1,4 @@
+const port = browser.runtime.connectNative("browser");
+port.onMessage.addListener(message => {
+  browser.runtime.sendMessage(message);
+});
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/manifest.json
@@ -0,0 +1,30 @@
+{
+  "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"
+  ]
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-browser-action.html
@@ -0,0 +1,10 @@
+<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>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-browser-action.js
@@ -0,0 +1,7 @@
+window.addEventListener("DOMContentLoaded", init);
+
+function init() {
+  document.body.addEventListener("click", event => {
+    browser.browserAction.openPopup();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-page-action.html
@@ -0,0 +1,10 @@
+<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>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-open-popup-page-action.js
@@ -0,0 +1,7 @@
+window.addEventListener("DOMContentLoaded", init);
+
+function init() {
+  document.body.addEventListener("click", event => {
+    browser.pageAction.openPopup();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/test-popup.html
@@ -0,0 +1,1 @@
+<h1> HELLO </h1>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify/icons/icon.svg
@@ -0,0 +1,1 @@
+<?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
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ExtensionActionTest.kt
@@ -0,0 +1,540 @@
+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)
+    }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java
@@ -0,0 +1,174 @@
+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,17 +9,16 @@ 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;
@@ -327,106 +326,32 @@ public class GeckoSession implements Par
 
                     result.accept(
                         visited -> callback.sendSuccess(visited),
                         exception -> callback.sendError("Failed to fetch visited statuses for URIs"));
                 }
             }
         };
 
-    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;
+    private final WebExtension.Listener 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.getDelegate(webExtension, nativeApp);
+        return mWebExtensionListener.getMessageDelegate(webExtension, nativeApp);
     }
 
     /**
      * Defines a message delegate for a Native App.
      *
      * If a delegate is already present, this delegate will replace the
      * existing one.
      *
@@ -445,17 +370,51 @@ 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.setDelegate(webExtension, delegate, 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);
     }
 
     private final GeckoSessionHandler<ContentDelegate> mContentHandler =
         new GeckoSessionHandler<ContentDelegate>(
             "GeckoViewContent", this,
             new String[]{
                 "GeckoView:ContentCrash",
                 "GeckoView:ContentKill",
@@ -1289,17 +1248,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 WebExtensionListener();
+        mWebExtensionListener = new WebExtension.Listener(this);
         mWebExtensionListener.registerListeners();
 
         if (BuildConfig.DEBUG && handlersCount != mSessionHandlers.length) {
             throw new AssertionError("Add new handler to handlers list");
         }
     }
 
     /* package */ @Nullable GeckoRuntime getRuntime() {
@@ -1335,16 +1294,17 @@ 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;
     }
 
@@ -1455,16 +1415,17 @@ 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,16 +332,23 @@ 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.
@@ -373,16 +380,17 @@ 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);
     }
 
@@ -444,16 +452,20 @@ 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();
             }
         }
     }
@@ -517,16 +529,20 @@ 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,16 +4,17 @@
  * 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;
@@ -24,48 +25,53 @@ 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.SurfaceHolder;
+import android.view.Surface;
 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;
 
-    protected @Nullable SurfaceView mSurfaceView;
+    private @Nullable SurfaceViewWrapper mSurfaceWrapper;
 
     private boolean mIsResettingFocus;
 
     private boolean mAutofillEnabled = true;
 
     private GeckoSession.SelectionActionDelegate mSelectionActionDelegate;
     private Autofill.Delegate mAutofillDelegate;
 
@@ -96,17 +102,17 @@ public class GeckoView extends FrameLayo
 
             @Override
             public SavedState[] newArray(final int size) {
                 return new SavedState[size];
             }
         };
     }
 
-    private class Display implements SurfaceHolder.Callback {
+    private class Display implements SurfaceViewWrapper.Listener {
         private final int[] mOrigin = new int[2];
 
         private GeckoDisplay mDisplay;
         private boolean mValid;
 
         private int mClippingHeight;
         private int mDynamicToolbarMaxHeight;
 
@@ -116,20 +122,20 @@ public class GeckoView extends FrameLayo
             if (!mValid) {
                 return;
             }
 
             setVerticalClipping(mClippingHeight);
 
             // Tell display there is already a surface.
             onGlobalLayout();
-            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);
+            if (GeckoView.this.mSurfaceWrapper != null) {
+                final SurfaceViewWrapper wrapper = GeckoView.this.mSurfaceWrapper;
+                mDisplay.surfaceChanged(wrapper.getSurface(),
+                        wrapper.getWidth(), wrapper.getHeight());
                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
                 GeckoView.this.setActive(true);
             }
         }
 
         public GeckoDisplay release() {
             if (mValid) {
                 if (mDisplay != null) {
@@ -138,48 +144,44 @@ public class GeckoView extends FrameLayo
                 GeckoView.this.setActive(false);
             }
 
             final GeckoDisplay display = mDisplay;
             mDisplay = null;
             return display;
         }
 
-        @Override // SurfaceHolder.Callback
-        public void surfaceCreated(final SurfaceHolder holder) {
-        }
-
-        @Override // SurfaceHolder.Callback
-        public void surfaceChanged(final SurfaceHolder holder, final int format,
+        @Override // SurfaceListener
+        public void onSurfaceChanged(final Surface surface,
                                    final int width, final int height) {
             if (mDisplay != null) {
-                mDisplay.surfaceChanged(holder.getSurface(), width, height);
+                mDisplay.surfaceChanged(surface, width, height);
                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
                 if (!mValid) {
                     GeckoView.this.setActive(true);
                 }
             }
             mValid = true;
         }
 
-        @Override // SurfaceHolder.Callback
-        public void surfaceDestroyed(final SurfaceHolder holder) {
+        @Override // SurfaceListener
+        public void onSurfaceDestroyed() {
             if (mDisplay != null) {
                 mDisplay.surfaceDestroyed();
                 GeckoView.this.setActive(false);
             }
             mValid = false;
         }
 
         public void onGlobalLayout() {
             if (mDisplay == null) {
                 return;
             }
-            if (GeckoView.this.mSurfaceView != null) {
-                GeckoView.this.mSurfaceView.getLocationOnScreen(mOrigin);
+            if (GeckoView.this.mSurfaceWrapper != null) {
+                GeckoView.this.mSurfaceWrapper.getView().getLocationOnScreen(mOrigin);
                 mDisplay.screenOriginChanged(mOrigin[0], mOrigin[1]);
             }
         }
 
         public boolean shouldPinOnScreen() {
             return mDisplay != null ? mDisplay.shouldPinOnScreen() : false;
         }
 
@@ -234,23 +236,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);
 
-        mSurfaceView = new SurfaceView(getContext());
-        mSurfaceView.setBackgroundColor(Color.WHITE);
-        addView(mSurfaceView,
+        mSurfaceWrapper = new SurfaceViewWrapper(getContext());
+        mSurfaceWrapper.setBackgroundColor(Color.WHITE);
+        addView(mSurfaceWrapper.getView(),
                 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                            ViewGroup.LayoutParams.MATCH_PARENT));
 
-        mSurfaceView.getHolder().addCallback(mDisplay);
+        mSurfaceWrapper.setListener(mDisplay);
 
         final Activity activity = ActivityUtils.getActivityFromContext(getContext());
         if (activity != null) {
             mSelectionActionDelegate = new BasicSelectionActionDelegate(activity);
         }
 
         mAutofillDelegate = new AndroidAutofillDelegate();
     }
@@ -260,18 +262,52 @@ 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 (mSurfaceView != null) {
-            mSurfaceView.setBackgroundColor(color);
+        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());
         }
     }
 
     /**
      * 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
@@ -505,17 +541,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 (mSurfaceView != null) {
+        if (mSurfaceWrapper != null) {
             mDisplay.onGlobalLayout();
         }
         return super.gatherTransparentRegion(region);
     }
 
     @Override
     protected Parcelable onSaveInstanceState() {
         mStateSaved = true;
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ImageDecoder.java
@@ -0,0 +1,82 @@
+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,26 +1,34 @@
 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 {
     /**
@@ -45,16 +53,18 @@ 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 + "}";
     }
 
@@ -387,16 +397,114 @@ 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
@@ -469,25 +577,459 @@ public class WebExtension {
          * @return true if the MessageSender was sent from the top level frame,
          *         false otherwise.
          * */
         public boolean isTopLevel() {
             return this.isTopLevel;
         }
     }
 
-    private static final MessageDelegate NULL_MESSAGE_DELEGATE = new MessageDelegate() {
+    /**
+     * 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;
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (o == this) {
+                return true;
+            }
+
+            if (!(o instanceof ActionIcon)) {
+                return false;
+            }
+
+            return mIconUris.equals(((ActionIcon) o).mIconUris);
+        }
+
         @Override
-        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());
+        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;
+        }
+
+        /**
+         * 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.");
+            }
+