Bug 1260548: Part 6 - Add basic tabs API support for Android. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 29 Jan 2017 00:27:32 -0800
changeset 467790 6b324b135a70b8cc53bab9ccd74d4acafe4714d4
parent 467789 1ffa5569b4360e7c0a54050dc40f7a6a54c67375
child 467791 00652a6cf510512e9d64943916f11677ac1d3917
push id43274
push usermaglione.k@gmail.com
push dateSun, 29 Jan 2017 20:10:11 +0000
reviewersaswan
bugs1260548
milestone54.0a1
Bug 1260548: Part 6 - Add basic tabs API support for Android. r?aswan MozReview-Commit-ID: JJgcKp4AZ9S
mobile/android/components/extensions/.eslintrc.js
mobile/android/components/extensions/ext-c-tabs.js
mobile/android/components/extensions/ext-tabs.js
mobile/android/components/extensions/ext-utils.js
mobile/android/components/extensions/extensions-mobile.manifest
mobile/android/components/extensions/jar.mn
mobile/android/components/extensions/schemas/jar.mn
mobile/android/components/extensions/schemas/tabs.json
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/head_webrequest.js
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/test_ext_i18n.html
toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
--- a/mobile/android/components/extensions/.eslintrc.js
+++ b/mobile/android/components/extensions/.eslintrc.js
@@ -1,5 +1,14 @@
 "use strict";
 
 module.exports = {
   "extends": "../../../../toolkit/components/extensions/.eslintrc.js",
+
+  "globals": {
+    "getCookieStoreIdForTab": false,
+    "GlobalEventDispatcher": true,
+    "GlobalEventManager": true,
+    "tabTracker": true,
+    "WindowEventManager": true,
+    "windowTracker": true,
+  },
 };
copy from browser/components/extensions/ext-c-tabs.js
copy to mobile/android/components/extensions/ext-c-tabs.js
copy from browser/components/extensions/ext-tabs.js
copy to mobile/android/components/extensions/ext-tabs.js
--- a/browser/components/extensions/ext-tabs.js
+++ b/mobile/android/components/extensions/ext-tabs.js
@@ -51,44 +51,50 @@ global.tabGetSender = getSender;
 extensions.on("page-shutdown", (type, context) => {
   if (context.viewType == "tab") {
     if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) {
       // Only close extension tabs.
       // This check prevents about:addons from closing when it contains a
       // WebExtension as an embedded inline options page.
       return;
     }
-    let {gBrowser} = context.xulBrowser.ownerGlobal;
-    if (gBrowser) {
-      let tab = gBrowser.getTabForBrowser(context.xulBrowser);
+    let {BrowserApp} = context.xulBrowser.ownerGlobal;
+    if (BrowserApp) {
+      let tab = BrowserApp.getTabForBrowser(context.xulBrowser);
       if (tab) {
-        gBrowser.removeTab(tab);
+        BrowserApp.closeTab(tab);
       }
     }
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
+function getBrowserWindow(window) {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDocShell)
+               .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+               .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+}
+
 let tabListener = {
   tabReadyInitialized: false,
   tabReadyPromises: new WeakMap(),
   initializingTabs: new WeakSet(),
 
   initTabReady() {
     if (!this.tabReadyInitialized) {
       windowTracker.addListener("progress", this);
 
       this.tabReadyInitialized = true;
     }
   },
 
   onLocationChange(browser, webProgress, request, locationURI, flags) {
     if (webProgress.isTopLevel) {
-      let {gBrowser} = browser.ownerGlobal;
-      let tab = gBrowser.getTabForBrowser(browser);
+      let {BrowserApp} = browser.ownerGlobal;
+      let tab = BrowserApp.getTabForBrowser(browser);
 
       // Now we are certain that the first page in the tab was loaded.
       this.initializingTabs.delete(tab);
 
       // browser.innerWindowID is now set, resolve the promises if any.
       let deferred = this.tabReadyPromises.get(tab);
       if (deferred) {
         deferred.resolve(tab);
@@ -105,18 +111,18 @@ let tabListener = {
    *
    * @param {XULElement} tab The <tab> element.
    * @returns {Promise} Resolves with the given tab once ready.
    */
   awaitTabReady(tab) {
     let deferred = this.tabReadyPromises.get(tab);
     if (!deferred) {
       deferred = PromiseUtils.defer();
-      if (!this.initializingTabs.has(tab) && (tab.linkedBrowser.innerWindowID ||
-                                              tab.linkedBrowser.currentURI.spec === "about:blank")) {
+      if (!this.initializingTabs.has(tab) && (tab.browser.innerWindowID ||
+                                              tab.browser.currentURI.spec === "about:blank")) {
         deferred.resolve(tab);
       } else {
         this.initTabReady();
         this.tabReadyPromises.set(tab, deferred);
       }
     }
     return deferred.promise;
   },
@@ -144,21 +150,20 @@ extensions.registerSchemaAPI("tabs", "ad
 
     await tabListener.awaitTabReady(tab.tab);
 
     return tab;
   }
 
   let self = {
     tabs: {
-      onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
-        let tab = event.originalTarget;
-        let tabId = tabTracker.getId(tab);
-        let windowId = windowTracker.getId(tab.ownerGlobal);
-        fire({tabId, windowId});
+      onActivated: new GlobalEventManager(context, "tabs.onActivated", "Tab:Selected", (fire, data) => {
+        let tab = tabManager.get(data.id);
+
+        fire({tabId: tab.id, windowId: tab.windowId});
       }).api(),
 
       onCreated: new EventManager(context, "tabs.onCreated", fire => {
         let listener = (eventName, event) => {
           fire(tabManager.convert(event.tab));
         };
 
         tabTracker.on("tab-created", listener);
@@ -168,99 +173,45 @@ extensions.registerSchemaAPI("tabs", "ad
       }).api(),
 
       /**
        * Since multiple tabs currently can't be highlighted, onHighlighted
        * essentially acts an alias for self.tabs.onActivated but returns
        * the tabId in an array to match the API.
        * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
       */
-      onHighlighted: new WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => {
-        let tab = event.originalTarget;
-        let tabIds = [tabTracker.getId(tab)];
-        let windowId = windowTracker.getId(tab.ownerGlobal);
-        fire({tabIds, windowId});
+      onHighlighted: new GlobalEventManager(context, "tabs.onHighlighted", "Tab:Selected", (fire, data) => {
+        let tab = tabManager.get(data.id);
+
+        fire({tabIds: [tab.id], windowId: tab.windowId});
       }).api(),
 
       onAttached: new EventManager(context, "tabs.onAttached", fire => {
-        let listener = (eventName, event) => {
-          fire(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
-        };
-
-        tabTracker.on("tab-attached", listener);
-        return () => {
-          tabTracker.off("tab-attached", listener);
-        };
+        return () => {};
       }).api(),
 
       onDetached: new EventManager(context, "tabs.onDetached", fire => {
-        let listener = (eventName, event) => {
-          fire(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
-        };
-
-        tabTracker.on("tab-detached", listener);
-        return () => {
-          tabTracker.off("tab-detached", listener);
-        };
+        return () => {};
       }).api(),
 
       onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
         let listener = (eventName, event) => {
           fire(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
         };
 
         tabTracker.on("tab-removed", listener);
         return () => {
           tabTracker.off("tab-removed", listener);
         };
       }).api(),
 
       onReplaced: ignoreEvent(context, "tabs.onReplaced"),
 
       onMoved: new EventManager(context, "tabs.onMoved", fire => {
-        // There are certain circumstances where we need to ignore a move event.
-        //
-        // Namely, the first time the tab is moved after it's created, we need
-        // to report the final position as the initial position in the tab's
-        // onAttached or onCreated event. This is because most tabs are inserted
-        // in a temporary location and then moved after the TabOpen event fires,
-        // which generates a TabOpen event followed by a TabMove event, which
-        // does not match the contract of our API.
-        let ignoreNextMove = new WeakSet();
-
-        let openListener = event => {
-          ignoreNextMove.add(event.target);
-          // Remove the tab from the set on the next tick, since it will already
-          // have been moved by then.
-          Promise.resolve().then(() => {
-            ignoreNextMove.delete(event.target);
-          });
-        };
-
-        let moveListener = event => {
-          let tab = event.originalTarget;
-
-          if (ignoreNextMove.has(tab)) {
-            ignoreNextMove.delete(tab);
-            return;
-          }
-
-          fire(tabTracker.getId(tab), {
-            windowId: windowTracker.getId(tab.ownerGlobal),
-            fromIndex: event.detail,
-            toIndex: tab._tPos,
-          });
-        };
-
-        windowTracker.addListener("TabMove", moveListener);
-        windowTracker.addListener("TabOpen", openListener);
-        return () => {
-          windowTracker.removeListener("TabMove", moveListener);
-          windowTracker.removeListener("TabOpen", openListener);
-        };
+        return () => {};
       }).api(),
 
       onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
         const restricted = ["url", "favIconUrl", "title"];
 
         function sanitize(extension, changeInfo) {
           let result = {};
           let nonempty = false;
@@ -277,240 +228,169 @@ extensions.registerSchemaAPI("tabs", "ad
           let [needed, changeInfo] = sanitize(extension, changed);
           if (needed) {
             fire(tab.id, changeInfo, tab.convert());
           }
         };
 
         let listener = event => {
           let needed = [];
-          if (event.type == "TabAttrModified") {
-            let changed = event.detail.changed;
-            if (changed.includes("image")) {
-              needed.push("favIconUrl");
-            }
-            if (changed.includes("muted")) {
-              needed.push("mutedInfo");
+          let tab;
+          switch (event.type) {
+            case "DOMTitleChanged": {
+              let {BrowserApp} = getBrowserWindow(event.target.ownerGlobal);
+
+              tab = BrowserApp.getTabForWindow(event.target.ownerGlobal);
+              needed.push("title");
+              break;
             }
-            if (changed.includes("soundplaying")) {
+
+            case "DOMAudioPlaybackStarted":
+            case "DOMAudioPlaybackStopped": {
+              let {BrowserApp} = event.target.ownerGlobal;
+              tab = BrowserApp.getTabForBrowser(event.originalTarget);
               needed.push("audible");
+              break;
             }
-            if (changed.includes("label")) {
-              needed.push("title");
-            }
-          } else if (event.type == "TabPinned") {
-            needed.push("pinned");
-          } else if (event.type == "TabUnpinned") {
-            needed.push("pinned");
           }
 
-          let tab = tabManager.getWrapper(event.originalTarget);
+          if (!tab) {
+            return;
+          }
+
+          tab = tabManager.getWrapper(tab);
           let changeInfo = {};
           for (let prop of needed) {
             changeInfo[prop] = tab[prop];
           }
 
           fireForTab(tab, changeInfo);
         };
 
         let statusListener = ({browser, status, url}) => {
-          let {gBrowser} = browser.ownerGlobal;
-          let tabElem = gBrowser.getTabForBrowser(browser);
-          if (tabElem) {
+          let {BrowserApp} = browser.ownerGlobal;
+          let tab = BrowserApp.getTabForBrowser(browser);
+          if (tab) {
             let changed = {status};
             if (url) {
               changed.url = url;
             }
 
-            fireForTab(tabManager.wrapTab(tabElem), changed);
+            fireForTab(tabManager.wrapTab(tab), changed);
           }
         };
 
         windowTracker.addListener("status", statusListener);
-        windowTracker.addListener("TabAttrModified", listener);
-        windowTracker.addListener("TabPinned", listener);
-        windowTracker.addListener("TabUnpinned", listener);
-
+        windowTracker.addListener("DOMTitleChanged", listener);
         return () => {
           windowTracker.removeListener("status", statusListener);
-          windowTracker.removeListener("TabAttrModified", listener);
-          windowTracker.removeListener("TabPinned", listener);
-          windowTracker.removeListener("TabUnpinned", listener);
+          windowTracker.removeListener("DOMTitleChanged", listener);
         };
       }).api(),
 
-      create(createProperties) {
-        return new Promise((resolve, reject) => {
-          let window = createProperties.windowId !== null ?
-            windowTracker.getWindow(createProperties.windowId, context) :
-            windowTracker.topWindow;
+      async create(createProperties) {
+        let window = createProperties.windowId !== null ?
+          windowTracker.getWindow(createProperties.windowId, context) :
+          windowTracker.topWindow;
 
-          if (!window.gBrowser) {
-            let obs = (finishedWindow, topic, data) => {
-              if (finishedWindow != window) {
-                return;
-              }
-              Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
-              resolve(window);
-            };
-            Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
-          } else {
-            resolve(window);
-          }
-        }).then(window => {
-          let url;
-
-          if (createProperties.url !== null) {
-            url = context.uri.resolve(createProperties.url);
+        let {BrowserApp} = window;
+        let url;
 
-            if (!context.checkLoadURL(url, {dontReportErrors: true})) {
-              return Promise.reject({message: `Illegal URL: ${url}`});
-            }
-          }
-
-          if (createProperties.cookieStoreId && !extension.hasPermission("cookies")) {
-            return Promise.reject({message: `No permission for cookieStoreId: ${createProperties.cookieStoreId}`});
-          }
+        if (createProperties.url !== null) {
+          url = context.uri.resolve(createProperties.url);
 
-          let options = {};
-          if (createProperties.cookieStoreId) {
-            if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
-              return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
-            }
+          if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+            return Promise.reject({message: `Illegal URL: ${url}`});
+          }
+        }
 
-            let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
-            if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
-              return Promise.reject({message: `Illegal to set non-private cookieStoreId in a private window`});
-            }
-
-            if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
-              return Promise.reject({message: `Illegal to set private cookieStoreId in a non-private window`});
-            }
+        let options = {};
 
-            if (global.isContainerCookieStoreId(createProperties.cookieStoreId)) {
-              let containerId = global.getContainerForCookieStoreId(createProperties.cookieStoreId);
-              if (!containerId) {
-                return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
-              }
+        let active = true;
+        if (createProperties.active !== null) {
+          active = createProperties.active;
+        }
+        options.selected = active;
 
-              options.userContextId = containerId;
-            }
-          }
-
-          // Make sure things like about:blank and data: URIs never inherit,
-          // and instead always get a NullPrincipal.
-          options.disallowInheritPrincipal = true;
-
-          tabListener.initTabReady();
-          let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
+        if (createProperties.index !== null) {
+          options.tabIndex = createProperties.index;
+        }
 
-          let active = true;
-          if (createProperties.active !== null) {
-            active = createProperties.active;
-          }
-          if (active) {
-            window.gBrowser.selectedTab = tab;
-          }
-
-          if (createProperties.index !== null) {
-            window.gBrowser.moveTabTo(tab, createProperties.index);
-          }
+        // Make sure things like about:blank and data: URIs never inherit,
+        // and instead always get a NullPrincipal.
+        options.disallowInheritPrincipal = true;
 
-          if (createProperties.pinned) {
-            window.gBrowser.pinTab(tab);
-          }
-
-          if (createProperties.url && createProperties.url !== window.BROWSER_NEW_TAB_URL) {
-            // We can't wait for a location change event for about:newtab,
-            // since it may be pre-rendered, in which case its initial
-            // location change event has already fired.
+        tabListener.initTabReady();
+        let tab = BrowserApp.addTab(url, options);
 
-            // Mark the tab as initializing, so that operations like
-            // `executeScript` wait until the requested URL is loaded in
-            // the tab before dispatching messages to the inner window
-            // that contains the URL we're attempting to load.
-            tabListener.initializingTabs.add(tab);
-          }
+        if (createProperties.url) {
+          tabListener.initializingTabs.add(tab);
+        }
 
-          return tabManager.convert(tab);
-        });
+        return tabManager.convert(tab);
       },
 
-      async remove(tabs) {
+      remove(tabs) {
         if (!Array.isArray(tabs)) {
           tabs = [tabs];
         }
 
         for (let tabId of tabs) {
           let tab = tabTracker.getTab(tabId);
-          tab.ownerGlobal.gBrowser.removeTab(tab);
+          tab.browser.ownerGlobal.BrowserApp.closeTab(tab);
         }
+
+        return Promise.resolve();
       },
 
       async update(tabId, updateProperties) {
         let tab = getTabOrActive(tabId);
 
-        let tabbrowser = tab.ownerGlobal.gBrowser;
+        let {BrowserApp} = tab.browser.ownerGlobal;
 
         if (updateProperties.url !== null) {
           let url = context.uri.resolve(updateProperties.url);
 
           if (!context.checkLoadURL(url, {dontReportErrors: true})) {
             return Promise.reject({message: `Illegal URL: ${url}`});
           }
 
-          tab.linkedBrowser.loadURI(url);
+          tab.browser.loadURI(url);
         }
 
         if (updateProperties.active !== null) {
           if (updateProperties.active) {
-            tabbrowser.selectedTab = tab;
+            BrowserApp.selectTab(tab);
           } else {
             // Not sure what to do here? Which tab should we select?
           }
         }
-        if (updateProperties.muted !== null) {
-          if (tab.muted != updateProperties.muted) {
-            tab.toggleMuteAudio(extension.uuid);
-          }
-        }
-        if (updateProperties.pinned !== null) {
-          if (updateProperties.pinned) {
-            tabbrowser.pinTab(tab);
-          } else {
-            tabbrowser.unpinTab(tab);
-          }
-        }
-        // FIXME: highlighted/selected, openerTabId
+        // FIXME: highlighted/selected, muted, pinned, openerTabId
 
-        return tabManager.convert(tab);
+        return Promise.resolve(tabManager.convert(tab));
       },
 
       async reload(tabId, reloadProperties) {
         let tab = getTabOrActive(tabId);
 
         let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
         if (reloadProperties && reloadProperties.bypassCache) {
           flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
         }
-        tab.linkedBrowser.reloadWithFlags(flags);
+        tab.browser.reloadWithFlags(flags);
       },
 
       async get(tabId) {
-        let tab = tabTracker.getTab(tabId);
-
-        return tabManager.convert(tab);
+        return tabManager.get(tabId).convert();
       },
 
-      getCurrent() {
-        let tab;
+      async getCurrent() {
         if (context.tabId) {
-          tab = tabManager.get(context.tabId).convert();
+          return tabManager.get(context.tabId).convert();
         }
-        return Promise.resolve(tab);
       },
 
       async query(queryInfo) {
         if (queryInfo.url !== null) {
           if (!extension.hasPermission("tabs")) {
             return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
           }
 
@@ -526,19 +406,19 @@ extensions.registerSchemaAPI("tabs", "ad
         if (!extension.hasPermission("<all_urls>")) {
           return Promise.reject({message: "The <all_urls> permission is required to use the captureVisibleTab API"});
         }
 
         let window = windowId == null ?
           windowTracker.topWindow :
           windowTracker.getWindow(windowId, context);
 
-        let tab = window.gBrowser.selectedTab;
+        let tab = window.BrowserApp.selectedTab;
         return tabListener.awaitTabReady(tab).then(() => {
-          let browser = tab.linkedBrowser;
+          let {browser} = tab;
           let recipient = {
             innerWindowID: browser.innerWindowID,
           };
 
           if (!options) {
             options = {};
           }
           if (options.format == null) {
@@ -554,28 +434,16 @@ extensions.registerSchemaAPI("tabs", "ad
             height: browser.clientHeight,
           };
 
           return context.sendMessage(browser.messageManager, "Extension:Capture",
                                      message, {recipient});
         });
       },
 
-      async detectLanguage(tabId) {
-        let tab = getTabOrActive(tabId);
-
-        return tabListener.awaitTabReady(tab).then(() => {
-          let browser = tab.linkedBrowser;
-          let recipient = {innerWindowID: browser.innerWindowID};
-
-          return context.sendMessage(browser.messageManager, "Extension:DetectLanguage",
-                                     {}, {recipient});
-        });
-      },
-
       async executeScript(tabId, details) {
         let tab = await promiseTabOrActive(tabId);
 
         return tab.executeScript(context, details);
       },
 
       async insertCSS(tabId, details) {
         let tab = await promiseTabOrActive(tabId);
@@ -583,233 +451,12 @@ extensions.registerSchemaAPI("tabs", "ad
         return tab.insertCSS(context, details);
       },
 
       async removeCSS(tabId, details) {
         let tab = await promiseTabOrActive(tabId);
 
         return tab.removeCSS(context, details);
       },
-
-      async move(tabIds, moveProperties) {
-        let index = moveProperties.index;
-        let tabsMoved = [];
-        if (!Array.isArray(tabIds)) {
-          tabIds = [tabIds];
-        }
-
-        let destinationWindow = null;
-        if (moveProperties.windowId !== null) {
-          destinationWindow = windowTracker.getWindow(moveProperties.windowId);
-          // Fail on an invalid window.
-          if (!destinationWindow) {
-            return Promise.reject({message: `Invalid window ID: ${moveProperties.windowId}`});
-          }
-        }
-
-        /*
-          Indexes are maintained on a per window basis so that a call to
-            move([tabA, tabB], {index: 0})
-              -> tabA to 0, tabB to 1 if tabA and tabB are in the same window
-            move([tabA, tabB], {index: 0})
-              -> tabA to 0, tabB to 0 if tabA and tabB are in different windows
-        */
-        let indexMap = new Map();
-
-        let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
-        for (let tab of tabs) {
-          // If the window is not specified, use the window from the tab.
-          let window = destinationWindow || tab.ownerGlobal;
-          let gBrowser = window.gBrowser;
-
-          let insertionPoint = indexMap.get(window) || index;
-          // If the index is -1 it should go to the end of the tabs.
-          if (insertionPoint == -1) {
-            insertionPoint = gBrowser.tabs.length;
-          }
-
-          // We can only move pinned tabs to a point within, or just after,
-          // the current set of pinned tabs. Unpinned tabs, likewise, can only
-          // be moved to a position after the current set of pinned tabs.
-          // Attempts to move a tab to an illegal position are ignored.
-          let numPinned = gBrowser._numPinnedTabs;
-          let ok = tab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned;
-          if (!ok) {
-            continue;
-          }
-
-          indexMap.set(window, insertionPoint + 1);
-
-          if (tab.ownerGlobal != window) {
-            // If the window we are moving the tab in is different, then move the tab
-            // to the new window.
-            tab = gBrowser.adoptTab(tab, insertionPoint, false);
-          } else {
-            // If the window we are moving is the same, just move the tab.
-            gBrowser.moveTabTo(tab, insertionPoint);
-          }
-          tabsMoved.push(tab);
-        }
-
-        return tabsMoved.map(tab => tabManager.convert(tab));
-      },
-
-      duplicate(tabId) {
-        let tab = tabTracker.getTab(tabId);
-
-        let gBrowser = tab.ownerGlobal.gBrowser;
-        let newTab = gBrowser.duplicateTab(tab);
-
-        return new Promise(resolve => {
-          // We need to use SSTabRestoring because any attributes set before
-          // are ignored. SSTabRestored is too late and results in a jump in
-          // the UI. See http://bit.ly/session-store-api for more information.
-          newTab.addEventListener("SSTabRestoring", function listener() {
-            // As the tab is restoring, move it to the correct position.
-
-            // Pinned tabs that are duplicated are inserted
-            // after the existing pinned tab and pinned.
-            if (tab.pinned) {
-              gBrowser.pinTab(newTab);
-            }
-            gBrowser.moveTabTo(newTab, tab._tPos + 1);
-          }, {once: true});
-
-          newTab.addEventListener("SSTabRestored", function listener() {
-            // Once it has been restored, select it and return the promise.
-            gBrowser.selectedTab = newTab;
-
-            return resolve(tabManager.convert(newTab));
-          }, {once: true});
-        });
-      },
-
-      getZoom(tabId) {
-        let tab = getTabOrActive(tabId);
-
-        let {ZoomManager} = tab.ownerGlobal;
-        let zoom = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
-
-        return Promise.resolve(zoom);
-      },
-
-      setZoom(tabId, zoom) {
-        let tab = getTabOrActive(tabId);
-
-        let {FullZoom, ZoomManager} = tab.ownerGlobal;
-
-        if (zoom === 0) {
-          // A value of zero means use the default zoom factor.
-          return FullZoom.reset(tab.linkedBrowser);
-        } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
-          FullZoom.setZoom(zoom, tab.linkedBrowser);
-        } else {
-          return Promise.reject({
-            message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
-          });
-        }
-
-        return Promise.resolve();
-      },
-
-      _getZoomSettings(tabId) {
-        let tab = getTabOrActive(tabId);
-
-        let {FullZoom} = tab.ownerGlobal;
-
-        return {
-          mode: "automatic",
-          scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
-          defaultZoomFactor: 1,
-        };
-      },
-
-      getZoomSettings(tabId) {
-        return Promise.resolve(this._getZoomSettings(tabId));
-      },
-
-      setZoomSettings(tabId, settings) {
-        let tab = getTabOrActive(tabId);
-
-        let currentSettings = this._getZoomSettings(tab.id);
-
-        if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
-          return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
-        }
-        return Promise.resolve();
-      },
-
-      onZoomChange: new EventManager(context, "tabs.onZoomChange", fire => {
-        let getZoomLevel = browser => {
-          let {ZoomManager} = browser.ownerGlobal;
-
-          return ZoomManager.getZoomForBrowser(browser);
-        };
-
-        // Stores the last known zoom level for each tab's browser.
-        // WeakMap[<browser> -> number]
-        let zoomLevels = new WeakMap();
-
-        // Store the zoom level for all existing tabs.
-        for (let window of windowTracker.browserWindows()) {
-          for (let tab of window.gBrowser.tabs) {
-            let browser = tab.linkedBrowser;
-            zoomLevels.set(browser, getZoomLevel(browser));
-          }
-        }
-
-        let tabCreated = (eventName, event) => {
-          let browser = event.tab.linkedBrowser;
-          zoomLevels.set(browser, getZoomLevel(browser));
-        };
-
-
-        let zoomListener = event => {
-          let browser = event.originalTarget;
-
-          // For non-remote browsers, this event is dispatched on the document
-          // rather than on the <browser>.
-          if (browser instanceof Ci.nsIDOMDocument) {
-            browser = browser.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDocShell)
-                             .chromeEventHandler;
-          }
-
-          let {gBrowser} = browser.ownerGlobal;
-          let tab = gBrowser.getTabForBrowser(browser);
-          if (!tab) {
-            // We only care about zoom events in the top-level browser of a tab.
-            return;
-          }
-
-          let oldZoomFactor = zoomLevels.get(browser);
-          let newZoomFactor = getZoomLevel(browser);
-
-          if (oldZoomFactor != newZoomFactor) {
-            zoomLevels.set(browser, newZoomFactor);
-
-            let tabId = tabTracker.getId(tab);
-            fire({
-              tabId,
-              oldZoomFactor,
-              newZoomFactor,
-              zoomSettings: self.tabs._getZoomSettings(tabId),
-            });
-          }
-        };
-
-        tabTracker.on("tab-attached", tabCreated);
-        tabTracker.on("tab-created", tabCreated);
-
-        windowTracker.addListener("FullZoomChange", zoomListener);
-        windowTracker.addListener("TextZoomChange", zoomListener);
-        return () => {
-          tabTracker.off("tab-attached", tabCreated);
-          tabTracker.off("tab-created", tabCreated);
-
-          windowTracker.removeListener("FullZoomChange", zoomListener);
-          windowTracker.removeListener("TextZoomChange", zoomListener);
-        };
-      }).api(),
     },
   };
   return self;
 });
copy from browser/components/extensions/ext-utils.js
copy to mobile/android/components/extensions/ext-utils.js
--- a/browser/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -2,499 +2,326 @@
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 /* globals TabBase, WindowBase, TabTrackerBase, WindowTrackerBase, TabManagerBase, WindowManagerBase */
 Cu.import("resource://gre/modules/ExtensionTabs.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
-                                   "@mozilla.org/content/style-sheet-service;1",
-                                   "nsIStyleSheetService");
+/* globals EventDispatcher */
+Cu.import("resource://gre/modules/Messaging.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   EventManager,
   ExtensionError,
+  DefaultWeakMap,
   defineLazyGetter,
 } = ExtensionUtils;
 
+global.GlobalEventDispatcher = EventDispatcher.instance;
+
+const BrowserStatusFilter = Components.Constructor(
+  "@mozilla.org/appshell/component/browser-status-filter;1", "nsIWebProgress",
+  "addProgressListener");
+
 let tabTracker;
 let windowTracker;
 
-// This file provides some useful code for the |tabs| and |windows|
-// modules. All of the code is installed on |global|, which is a scope
-// shared among the different ext-*.js scripts.
+class BrowserProgressListener {
+  constructor(browser, listener, flags) {
+    this.listener = listener;
+    this.browser = browser;
+    this.filter = new BrowserStatusFilter(this, flags);
+  }
 
-global.makeWidgetId = id => {
-  id = id.toLowerCase();
-  // FIXME: This allows for collisions.
-  return id.replace(/[^a-z0-9_-]/g, "_");
-};
+  destroy() {
+    this.filter.removeProgressListener(this);
+  }
+
+  delegate(method, ...args) {
+    if (this.listener[method]) {
+      this.listener[method](this.browser, ...args);
+    }
+  }
 
-// Manages tab-specific context data, and dispatching tab select events
-// across all windows.
-global.TabContext = function TabContext(getDefaults, extension) {
-  this.extension = extension;
-  this.getDefaults = getDefaults;
+  onLocationChange(webProgress, request, locationURI, flags) {
+    this.delegate("onLocationChange", webProgress, request, locationURI, flags);
+  }
+  onStateChange(webProgress, request, stateFlags, status) {
+    this.delegate("onStateChange", webProgress, request, stateFlags, status);
+  }
 
-  this.tabData = new WeakMap();
-  this.lastLocation = new WeakMap();
+  onProgressChange(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {}
+  onStatusChange(webProgress, request, status, message) {}
+  onSecurityChange(webProgress, request, state) {}
+}
 
-  windowTracker.addListener("progress", this);
-  windowTracker.addListener("TabSelect", this);
+class ProgressListenerWrapper {
+  constructor(window, listener) {
+    this.window = window;
+    this.listener = listener;
+    this.listeners = new WeakMap();
 
-  EventEmitter.decorate(this);
-};
+    this.flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
+                 Ci.nsIWebProgress.NOTIFY_LOCATION;
 
-TabContext.prototype = {
-  get(tab) {
-    if (!this.tabData.has(tab)) {
-      this.tabData.set(tab, this.getDefaults(tab));
+    for (let tab of this.window.BrowserApp.tabs) {
+      this.addBrowserProgressListener(tab.browser);
     }
 
-    return this.tabData.get(tab);
-  },
+    this.window.BrowserApp.deck.addEventListener("TabOpen", this);
+  }
+
+  destroy() {
+    this.window.BrowserApp.deck.removeEventListener("TabOpen", this);
+
+    for (let tab of this.window.BrowserApp.tabs) {
+      this.removeProgressListener(tab.browser);
+    }
+  }
+
+  addBrowserProgressListener(browser) {
+    this.removeProgressListener(browser);
 
-  clear(tab) {
-    this.tabData.delete(tab);
-  },
+    let listener = new BrowserProgressListener(browser, this.listener, this.flags);
+    this.listeners.set(browser, listener);
+
+    browser.addProgressListener(listener.filter, this.flags);
+  }
+
+  removeProgressListener(browser) {
+    let listener = this.listeners.get(browser);
+    if (listener) {
+      browser.removeProgressListener(listener.filter);
+      listener.destroy();
+      this.listeners.delete(browser);
+    }
+  }
 
   handleEvent(event) {
-    if (event.type == "TabSelect") {
-      let tab = event.target;
-      this.emit("tab-select", tab);
-      this.emit("location-change", tab);
+    if (event.type === "TabOpen") {
+      this.addBrowserProgressListener(event.originalTarget);
     }
-  },
-
-  onStateChange(browser, webProgress, request, stateFlags, statusCode) {
-    let flags = Ci.nsIWebProgressListener;
-
-    if (!(~stateFlags & (flags.STATE_IS_WINDOW | flags.STATE_START) ||
-          this.lastLocation.has(browser))) {
-      this.lastLocation.set(browser, request.URI);
-    }
-  },
+  }
 
-  onLocationChange(browser, webProgress, request, locationURI, flags) {
-    let gBrowser = browser.ownerGlobal.gBrowser;
-    let lastLocation = this.lastLocation.get(browser);
-    if (browser === gBrowser.selectedBrowser &&
-        !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
-      let tab = gBrowser.getTabForBrowser(browser);
-      this.emit("location-change", tab, true);
-    }
-    this.lastLocation.set(browser, browser.currentURI);
-  },
-
-  shutdown() {
-    windowTracker.removeListener("progress", this);
-    windowTracker.removeListener("TabSelect", this);
-  },
-};
+}
 
 
 class WindowTracker extends WindowTrackerBase {
+  constructor(...args) {
+    super(...args);
+
+    this.progressListeners = new DefaultWeakMap(() => new WeakMap());
+  }
+
   addProgressListener(window, listener) {
-    window.gBrowser.addTabsProgressListener(listener);
+    let listeners = this.progressListeners.get(window);
+    if (!listeners.has(listener)) {
+      let wrapper = new ProgressListenerWrapper(window, listener);
+      listeners.set(listener, wrapper);
+    }
   }
 
   removeProgressListener(window, listener) {
-    window.gBrowser.removeTabsProgressListener(listener);
+    let listeners = this.progressListeners.get(window);
+    let wrapper = listeners.get(listener);
+    if (wrapper) {
+      wrapper.destroy();
+      listeners.delete(listener);
+    }
   }
 }
+global.GlobalEventManager = class extends EventManager {
+  constructor(context, name, event, listener) {
+    super(context, name, fire => {
+      let listener2 = {
+        onEvent(event, data, callback) {
+          listener(fire, data);
+        },
+      };
+
+      GlobalEventDispatcher.registerListener(listener2, [event]);
+      return () => {
+        GlobalEventDispatcher.unregisterListener(listener2, [event]);
+      };
+    });
+  }
+};
 
 global.WindowEventManager = class extends EventManager {
   constructor(context, name, event, listener) {
     super(context, name, fire => {
       let listener2 = listener.bind(null, fire);
 
       windowTracker.addListener(event, listener2);
       return () => {
         windowTracker.removeListener(event, listener2);
       };
     });
   }
 };
 
 class TabTracker extends TabTrackerBase {
-  constructor() {
-    super();
-
-    this._tabs = new WeakMap();
-    this._tabIds = new Map();
-    this._nextId = 1;
-
-    this.handleTabDestroyed = this.handleTabDestroyed.bind(this);
-  }
-
   init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
 
-    this.adoptedTabs = new WeakMap();
-
-    this.handleWindowOpen = this.handleWindowOpen.bind(this);
-    this.handleWindowClose = this.handleWindowClose.bind(this);
-
     windowTracker.addListener("TabClose", this);
     windowTracker.addListener("TabOpen", this);
-    windowTracker.addOpenListener(this.handleWindowOpen);
-    windowTracker.addCloseListener(this.handleWindowClose);
 
-    /* eslint-disable mozilla/balanced-listeners */
-    this.on("tab-detached", this.handleTabDestroyed);
-    this.on("tab-removed", this.handleTabDestroyed);
-    /* eslint-enable mozilla/balanced-listeners */
+    this.initialized = true;
   }
 
   getId(tab) {
-    if (this._tabs.has(tab)) {
-      return this._tabs.get(tab);
-    }
-
-    this.init();
-
-    let id = this._nextId++;
-    this.setId(tab, id);
-    return id;
-  }
-
-  setId(tab, id) {
-    this._tabs.set(tab, id);
-    this._tabIds.set(id, tab);
+    return tab.id;
   }
 
-  handleTabDestroyed(event, {tab}) {
-    let id = this._tabs.get(tab);
-    if (id) {
-      this._tabs.delete(tab);
-      if (this._tabIds.get(id) === tab) {
-        this._tabIds.delete(tab);
-      }
-    }
-  }
-
-  /**
-   * Returns the XUL <tab> element associated with the given tab ID. If no tab
-   * with the given ID exists, and no default value is provided, an error is
-   * raised, belonging to the scope of the given context.
-   *
-   * @param {integer} tabId
-   *        The ID of the tab to retrieve.
-   * @param {*} default_
-   *        The value to return if no tab exists with the given ID.
-   * @returns {Element<tab>}
-   *        A XUL <tab> element.
-   */
-  getTab(tabId, default_ = undefined) {
-    let tab = this._tabIds.get(tabId);
+  getTab(id, default_ = undefined) {
+    let tab = windowTracker.topWindow.BrowserApp.getTabForId(id);
     if (tab) {
       return tab;
     }
     if (default_ !== undefined) {
       return default_;
     }
-    throw new ExtensionError(`Invalid tab ID: ${tabId}`);
+    throw new ExtensionError(`Invalid tab ID: ${id}`);
   }
 
   handleEvent(event) {
-    let tab = event.target;
+    const {BrowserApp} = event.target.ownerGlobal;
+    let tab = BrowserApp.getTabForBrowser(event.target);
 
     switch (event.type) {
       case "TabOpen":
-        let {adoptedTab} = event.detail;
-        if (adoptedTab) {
-          this.adoptedTabs.set(adoptedTab, event.target);
-
-          // This tab is being created to adopt a tab from a different window.
-          // Copy the ID from the old tab to the new.
-          this.setId(tab, this.getId(adoptedTab));
-
-          adoptedTab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
-            windowId: windowTracker.getId(tab.ownerGlobal),
-          });
-        }
-
-        // We need to delay sending this event until the next tick, since the
-        // tab does not have its final index when the TabOpen event is dispatched.
-        Promise.resolve().then(() => {
-          if (event.detail.adoptedTab) {
-            this.emitAttached(event.originalTarget);
-          } else {
-            this.emitCreated(event.originalTarget);
-          }
-        });
+        this.emitCreated(tab);
         break;
 
       case "TabClose":
-        let {adoptedBy} = event.detail;
-        if (adoptedBy) {
-          // This tab is being closed because it was adopted by a new window.
-          // Copy its ID to the new tab, in case it was created as the first tab
-          // of a new window, and did not have an `adoptedTab` detail when it was
-          // opened.
-          this.setId(adoptedBy, this.getId(tab));
-
-          this.emitDetached(tab, adoptedBy);
-        } else {
-          this.emitRemoved(tab, false);
-        }
+        this.emitRemoved(tab, false);
         break;
     }
   }
 
-  handleWindowOpen(window) {
-    if (window.arguments && window.arguments[0] instanceof window.XULElement) {
-      // If the first window argument is a XUL element, it means the
-      // window is about to adopt a tab from another window to replace its
-      // initial tab.
-      //
-      // Note that this event handler depends on running before the
-      // delayed startup code in browser.js, which is currently triggered
-      // by the first MozAfterPaint event. That code handles finally
-      // adopting the tab, and clears it from the arguments list in the
-      // process, so if we run later than it, we're too late.
-      let tab = window.arguments[0];
-      let adoptedBy = window.gBrowser.tabs[0];
-
-      this.adoptedTabs.set(tab, adoptedBy);
-      this.setId(adoptedBy, this.getId(tab));
-
-      // We need to be sure to fire this event after the onDetached event
-      // for the original tab.
-      let listener = (event, details) => {
-        if (details.tab === tab) {
-          this.off("tab-detached", listener);
-
-          Promise.resolve().then(() => {
-            this.emitAttached(details.adoptedBy);
-          });
-        }
-      };
-
-      this.on("tab-detached", listener);
-    } else {
-      for (let tab of window.gBrowser.tabs) {
-        this.emitCreated(tab);
-      }
-    }
-  }
-
-  handleWindowClose(window) {
-    for (let tab of window.gBrowser.tabs) {
-      if (this.adoptedTabs.has(tab)) {
-        this.emitDetached(tab, this.adoptedTabs.get(tab));
-      } else {
-        this.emitRemoved(tab, true);
-      }
-    }
-  }
-
-  emitAttached(tab) {
-    let newWindowId = windowTracker.getId(tab.ownerGlobal);
-    let tabId = this.getId(tab);
-
-    this.emit("tab-attached", {tab, tabId, newWindowId, newPosition: tab._tPos});
-  }
-
-  emitDetached(tab, adoptedBy) {
-    let oldWindowId = windowTracker.getId(tab.ownerGlobal);
-    let tabId = this.getId(tab);
-
-    this.emit("tab-detached", {tab, adoptedBy, tabId, oldWindowId, oldPosition: tab._tPos});
-  }
-
   emitCreated(tab) {
     this.emit("tab-created", {tab});
   }
 
   emitRemoved(tab, isWindowClosing) {
-    let windowId = windowTracker.getId(tab.ownerGlobal);
+    let windowId = windowTracker.getId(tab.browser.ownerGlobal);
     let tabId = this.getId(tab);
 
-    // When addons run in-process, `window.close()` is synchronous. Most other
-    // addon-invoked calls are asynchronous since they go through a proxy
-    // context via the message manager. This includes event registrations such
-    // as `tabs.onRemoved.addListener`.
-    //
-    // So, even if `window.close()` were to be called (in-process) after calling
-    // `tabs.onRemoved.addListener`, then the tab would be closed before the
-    // event listener is registered. To make sure that the event listener is
-    // notified, we dispatch `tabs.onRemoved` asynchronously.
     Services.tm.mainThread.dispatch(() => {
       this.emit("tab-removed", {tab, tabId, windowId, isWindowClosing});
     }, Ci.nsIThread.DISPATCH_NORMAL);
   }
 
   getBrowserData(browser) {
-    if (!browser.ownerGlobal.location.href === "about:addons") {
-      // When we're loaded into a <browser> inside about:addons, we need to go up
-      // one more level.
-      browser = browser.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell)
-                       .chromeEventHandler;
-    }
-
     let result = {
       tabId: -1,
       windowId: -1,
     };
 
-    let {gBrowser} = browser.ownerGlobal;
-    // Some non-browser windows have gBrowser but not
-    // getTabForBrowser!
-    if (gBrowser && gBrowser.getTabForBrowser) {
+    let {BrowserApp} = browser.ownerGlobal;
+    if (BrowserApp) {
       result.windowId = windowTracker.getId(browser.ownerGlobal);
 
-      let tab = gBrowser.getTabForBrowser(browser);
+      let tab = BrowserApp.getTabForBrowser(browser);
       if (tab) {
         result.tabId = this.getId(tab);
       }
     }
 
     return result;
   }
 
   get activeTab() {
     let window = windowTracker.topWindow;
-    if (window && window.gBrowser) {
-      return window.gBrowser.selectedTab;
+    if (window && window.BrowserApp) {
+      return window.BrowserApp.selectedTab;
     }
     return null;
   }
 }
 
 windowTracker = new WindowTracker();
 tabTracker = new TabTracker();
 
 Object.assign(global, {tabTracker, windowTracker});
 
 class Tab extends TabBase {
   get _favIconUrl() {
-    return this.window.gBrowser.getIcon(this.tab);
+    return undefined;
   }
 
   get audible() {
-    return this.tab.soundPlaying;
+    return this.tab.playingAudio;
   }
 
   get browser() {
-    return this.tab.linkedBrowser;
+    return this.tab.browser;
   }
 
   get cookieStoreId() {
     return getCookieStoreIdForTab(this, this.tab);
   }
 
   get height() {
     return this.browser.clientHeight;
   }
 
-  get index() {
-    return this.tab._tPos;
+  get incognito() {
+    return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
   }
 
-  get innerWindowID() {
-    return this.browser.innerWindowID;
+  get index() {
+    return this.window.BrowserApp.tabs.indexOf(this.tab);
   }
 
   get mutedInfo() {
-    let tab = this.tab;
-
-    let mutedInfo = {muted: tab.muted};
-    if (tab.muteReason === null) {
-      mutedInfo.reason = "user";
-    } else if (tab.muteReason) {
-      mutedInfo.reason = "extension";
-      mutedInfo.extensionId = tab.muteReason;
-    }
-
-    return mutedInfo;
+    return {muted: false};
   }
 
   get pinned() {
-    return this.tab.pinned;
+    return false;
   }
 
   get active() {
-    return this.tab.selected;
+    return this.tab.getActive();
   }
 
   get selected() {
-    return this.tab.selected;
+    return this.tab.getActive();
   }
 
   get status() {
-    if (this.tab.getAttribute("busy") === "true") {
+    if (this.browser.webProgress.isLoadingDocument) {
       return "loading";
     }
     return "complete";
   }
 
   get width() {
     return this.browser.clientWidth;
   }
 
   get window() {
-    return this.tab.ownerGlobal;
+    return this.browser.ownerGlobal;
   }
 
   get windowId() {
     return windowTracker.getId(this.window);
   }
-
-  static convertFromSessionStoreClosedData(extension, tab, window = null) {
-    let result = {
-      sessionId: String(tab.closedId),
-      index: tab.pos ? tab.pos : 0,
-      windowId: window && windowTracker.getId(window),
-      selected: false,
-      highlighted: false,
-      active: false,
-      pinned: false,
-      incognito: Boolean(tab.state && tab.state.isPrivate),
-    };
-
-    if (extension.tabManager.hasTabPermission(tab)) {
-      let entries = tab.state ? tab.state.entries : tab.entries;
-      result.url = entries[0].url;
-      result.title = entries[0].title;
-      if (tab.image) {
-        result.favIconUrl = tab.image;
-      }
-    }
-
-    return result;
-  }
 }
 
 class Window extends WindowBase {
-  updateGeometry(options) {
-    let {window} = this;
-
-    if (options.left !== null || options.top !== null) {
-      let left = options.left !== null ? options.left : window.screenX;
-      let top = options.top !== null ? options.top : window.screenY;
-      window.moveTo(left, top);
-    }
-
-    if (options.width !== null || options.height !== null) {
-      let width = options.width !== null ? options.width : window.outerWidth;
-      let height = options.height !== null ? options.height : window.outerHeight;
-      window.resizeTo(width, height);
-    }
-  }
-
   get focused() {
     return this.window.document.hasFocus();
   }
 
   get top() {
     return this.window.screenY;
   }
 
@@ -510,107 +337,34 @@ class Window extends WindowBase {
     return this.window.outerHeight;
   }
 
   get incognito() {
     return PrivateBrowsingUtils.isWindowPrivate(this.window);
   }
 
   get alwaysOnTop() {
-    return this.xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ;
+    return false;
   }
 
   get isLastFocused() {
     return this.window === windowTracker.topWindow;
   }
 
-  static getState(window) {
-    const STATES = {
-      [window.STATE_MAXIMIZED]: "maximized",
-      [window.STATE_MINIMIZED]: "minimized",
-      [window.STATE_NORMAL]: "normal",
-    };
-    let state = STATES[window.windowState];
-    if (window.fullScreen) {
-      state = "fullscreen";
-    }
-    return state;
-  }
-
   get state() {
-    return Window.getState(this.window);
-  }
-
-  set state(state) {
-    let {window} = this;
-    if (state !== "fullscreen" && window.fullScreen) {
-      window.fullScreen = false;
-    }
-
-    switch (state) {
-      case "maximized":
-        window.maximize();
-        break;
-
-      case "minimized":
-      case "docked":
-        window.minimize();
-        break;
-
-      case "normal":
-        // Restore sometimes returns the window to its previous state, rather
-        // than to the "normal" state, so it may need to be called anywhere from
-        // zero to two times.
-        window.restore();
-        if (window.windowState !== window.STATE_NORMAL) {
-          window.restore();
-        }
-        if (window.windowState !== window.STATE_NORMAL) {
-          // And on OS-X, where normal vs. maximized is basically a heuristic,
-          // we need to cheat.
-          window.sizeToContent();
-        }
-        break;
-
-      case "fullscreen":
-        window.fullScreen = true;
-        break;
-
-      default:
-        throw new Error(`Unexpected window state: ${state}`);
-    }
+    return "fullscreen";
   }
 
   * getTabs() {
     let {tabManager} = this.extension;
 
-    for (let tab of this.window.gBrowser.tabs) {
+    for (let tab of this.window.BrowserApp.tabs) {
       yield tabManager.getWrapper(tab);
     }
   }
-
-  static convertFromSessionStoreClosedData(extension, window) {
-    let result = {
-      sessionId: String(window.closedId),
-      focused: false,
-      incognito: false,
-      type: "normal", // this is always "normal" for a closed window
-      // Surely this does not actually work?
-      state: this.getState(window),
-      alwaysOnTop: false,
-    };
-
-    if (window.tabs.length) {
-      result.tabs = window.tabs.map(tab => {
-        return Tab.convertFromSessionStoreClosedData(extension, tab);
-      });
-    }
-
-    return result;
-  }
 }
 
 Object.assign(global, {Tab, Window});
 
 class TabManager extends TabManagerBase {
   get(tabId, default_ = undefined) {
     let tab = tabTracker.getTab(tabId, default_);
 
@@ -624,17 +378,17 @@ class TabManager extends TabManagerBase 
     return super.addActiveTabPermission(tab);
   }
 
   revokeActiveTabPermission(tab = tabTracker.activeTab) {
     return super.revokeActiveTabPermission(tab);
   }
 
   wrapTab(tab) {
-    return new Tab(this.extension, tab, tabTracker.getId(tab));
+    return new Tab(this.extension, tab, tab.id);
   }
 }
 
 class WindowManager extends WindowManagerBase {
   get(windowId, context) {
     let window = windowTracker.getWindow(windowId, context);
 
     return this.getWrapper(window);
--- a/mobile/android/components/extensions/extensions-mobile.manifest
+++ b/mobile/android/components/extensions/extensions-mobile.manifest
@@ -1,5 +1,9 @@
 # scripts
 category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
+category webextension-scripts tabs chrome://browser/content/ext-tabs.js
+category webextension-scripts utils chrome://browser/content/ext-utils.js
+category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
 
 # schemas
-category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
\ No newline at end of file
+category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
+category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
--- a/mobile/android/components/extensions/jar.mn
+++ b/mobile/android/components/extensions/jar.mn
@@ -1,6 +1,9 @@
 # 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/.
 
 chrome.jar:
-    content/ext-pageAction.js
\ No newline at end of file
+    content/ext-c-tabs.js
+    content/ext-pageAction.js
+    content/ext-tabs.js
+    content/ext-utils.js
--- a/mobile/android/components/extensions/schemas/jar.mn
+++ b/mobile/android/components/extensions/schemas/jar.mn
@@ -1,6 +1,7 @@
 # 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/.
 
 chrome.jar:
-    content/schemas/page_action.json
\ No newline at end of file
+    content/schemas/page_action.json
+    content/schemas/tabs.json
copy from browser/components/extensions/schemas/tabs.json
copy to mobile/android/components/extensions/schemas/tabs.json
--- a/browser/components/extensions/schemas/tabs.json
+++ b/mobile/android/components/extensions/schemas/tabs.json
@@ -411,16 +411,17 @@
                 "description": "Details about the created tab. Will contain the ID of the new tab."
               }
             ]
           }
         ]
       },
       {
         "name": "duplicate",
+        "unsupported": true,
         "type": "function",
         "description": "Duplicates a tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
             "minimum": 0,
@@ -619,21 +620,23 @@
                 "unsupported": true,
                 "deprecated": "Please use <em>highlighted</em>.",
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tab should be selected."
               },
               "pinned": {
                 "type": "boolean",
+                "unsupported": true,
                 "optional": true,
                 "description": "Whether the tab should be pinned."
               },
               "muted": {
                 "type": "boolean",
+                "unsupported": true,
                 "optional": true,
                 "description": "Whether the tab should be muted."
               },
               "openerTabId": {
                 "unsupported": true,
                 "type": "integer",
                 "minimum": 0,
                 "optional": true,
@@ -653,16 +656,17 @@
                 "description": "Details about the updated tab. The $(ref:tabs.Tab) object doesn't contain <code>url</code>, <code>title</code> and <code>favIconUrl</code> if the <code>\"tabs\"</code> permission has not been requested."
               }
             ]
           }
         ]
       },
       {
         "name": "move",
+        "unsupported": true,
         "type": "function",
         "description": "Moves one or more tabs to a new position within its window, or to a new window. Note that tabs can only be moved to and from normal (window.type === \"normal\") windows.",
         "async": "callback",
         "parameters": [
           {
             "name": "tabIds",
             "description": "The tab or list of tabs to move.",
             "choices": [
@@ -756,16 +760,17 @@
             "name": "callback",
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
         "name": "detectLanguage",
+        "unsupported": true,
         "type": "function",
         "description": "Detects the primary language of the content in a tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
             "minimum": 0,
@@ -903,16 +908,17 @@
             "optional": true,
             "description": "Called when all the CSS has been removed.",
             "parameters": []
           }
         ]
       },
       {
         "name": "setZoom",
+        "unsupported": true,
         "type": "function",
         "description": "Zooms a specified tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
             "minimum": 0,
@@ -930,16 +936,17 @@
             "optional": true,
             "description": "Called after the zoom factor has been changed.",
             "parameters": []
           }
         ]
       },
       {
         "name": "getZoom",
+        "unsupported": true,
         "type": "function",
         "description": "Gets the current zoom factor of a specified tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
             "minimum": 0,
@@ -957,16 +964,17 @@
                 "description": "The tab's current zoom factor."
               }
             ]
           }
         ]
       },
       {
         "name": "setZoomSettings",
+        "unsupported": true,
         "type": "function",
         "description": "Sets the zoom settings for a specified tab, which define how zoom changes are handled. These settings are reset to defaults upon navigating the tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
             "optional": true,
@@ -984,16 +992,17 @@
             "optional": true,
             "description": "Called after the zoom settings have been changed.",
             "parameters": []
           }
         ]
       },
       {
         "name": "getZoomSettings",
+        "unsupported": true,
         "type": "function",
         "description": "Gets the current zoom settings of a specified tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
             "optional": true,
@@ -1272,16 +1281,17 @@
         "description": "Fired when a tab is replaced with another tab due to prerendering or instant.",
         "parameters": [
           {"type": "integer", "name": "addedTabId", "minimum": 0},
           {"type": "integer", "name": "removedTabId", "minimum": 0}
         ]
       },
       {
         "name": "onZoomChange",
+        "unsupported": true,
         "type": "function",
         "description": "Fired when a tab is zoomed.",
         "parameters": [{
           "type": "object",
           "name": "ZoomChangeInfo",
           "properties": {
             "tabId": {"type": "integer", "minimum": 0},
             "oldZoomFactor": {"type": "number"},
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -205,17 +205,17 @@ ProxyMessenger = {
    */
   getMessageManagerForRecipient(recipient) {
     let {tabId} = recipient;
     // tabs.sendMessage / tabs.connect
     if (tabId) {
       // `tabId` being set implies that the tabs API is supported, so we don't
       // need to check whether `tabTracker` exists.
       let tab = apiManager.global.tabTracker.getTab(tabId, null);
-      return tab && tab.linkedBrowser.messageManager;
+      return tab && (tab.linkedBrowser || tab.browser).messageManager;
     }
 
     // runtime.sendMessage / runtime.connect
     let extension = GlobalManager.extensionMap.get(recipient.extensionId);
     if (extension) {
       return extension.parentMessageManager;
     }
 
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -12,28 +12,25 @@ support-files =
 tags = webextensions
 
 [test_chrome_ext_background_debug_global.html]
 skip-if = (os == 'android') # android doesn't have devtools
 [test_chrome_ext_background_page.html]
 skip-if = (toolkit == 'android') # android doesn't have devtools
 [test_chrome_ext_eventpage_warning.html]
 [test_chrome_ext_contentscript_unrecognizedprop_warning.html]
-skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
 [test_chrome_ext_hybrid_addons.html]
 [test_chrome_ext_trustworthy_origin.html]
 [test_chrome_ext_webnavigation_resolved_urls.html]
-skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
 [test_chrome_native_messaging_paths.html]
 skip-if = os != "mac" && os != "linux"
 [test_ext_cookies_expiry.html]
 [test_ext_cookies_permissions_bad.html]
 [test_ext_cookies_permissions_good.html]
 [test_ext_cookies_containers.html]
 [test_ext_jsversion.html]
 [test_ext_schema.html]
 [test_chrome_ext_storage_cleanup.html]
 [test_chrome_ext_idle.html]
 [test_chrome_ext_identity.html]
 skip-if = os == 'android' # unsupported.
 [test_chrome_ext_downloads_saveAs.html]
 [test_chrome_ext_webrequest_background_events.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
--- a/toolkit/components/extensions/test/mochitest/head_webrequest.js
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -7,19 +7,22 @@ let commonEvents = {
   "onBeforeRedirect":    [{urls: ["<all_urls>"]}],
   "onHeadersReceived":   [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]],
   "onResponseStarted":   [{urls: ["<all_urls>"]}],
   "onCompleted":         [{urls: ["<all_urls>"]}, ["responseHeaders"]],
   "onErrorOccurred":     [{urls: ["<all_urls>"]}],
 };
 
 function background(events) {
+  const IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
+
   let expect;
   let ignore;
   let defaultOrigin;
+  let expectedIp = null;
 
   browser.test.onMessage.addListener((msg, expected) => {
     if (msg !== "set-expected") {
       return;
     }
     expect = expected.expect;
     defaultOrigin = expected.origin;
     ignore = expected.ignore;
@@ -220,17 +223,22 @@ function background(events) {
       if (name == "onCompleted") {
         // If we have already completed a GET request for this url,
         // and it was found, we expect for the response to come fromCache.
         // expected.cached may be undefined, force boolean.
         let expectCached = !!expected.cached && details.method === "GET" && details.statusCode != 404;
         browser.test.assertEq(expectCached, details.fromCache, "fromCache is correct");
         // We can only tell IPs for non-cached HTTP requests.
         if (!details.fromCache && /^https?:/.test(details.url)) {
-          browser.test.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`);
+          browser.test.assertTrue(IP_PATTERN.test(details.ip), `IP for ${details.url} looks IP-ish: ${details.ip}`);
+
+          // We can't easily predict the IP ahead of time, so just make
+          // sure they're all consistent.
+          expectedIp = expectedIp || details.ip;
+          browser.test.assertEq(expectedIp, details.ip, `correct ip for ${details.url}`);
         }
         if (expected.headers && expected.headers.response) {
           checkHeaders("response", expected, details);
         }
       }
 
       if (expected.cancel && expected.cancel == name) {
         browser.test.log(`${name} cancel request`);
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -37,82 +37,62 @@ support-files =
   file_permission_xhr.html
   file_teardown_test.js
   return_headers.sjs
   webrequest_worker.js
 
 [test_clipboard.html]
 # skip-if = # disabled test case with_permission_allow_copy, see inline comment.
 [test_ext_inIncognitoContext_window.html]
-skip-if = os == 'android' # Android does not currently support windows.
+skip-if = os == 'android' # Android does not support multiple windows.
 [test_ext_geturl.html]
 [test_ext_background_canvas.html]
 [test_ext_content_security_policy.html]
 [test_ext_contentscript.html]
 [test_ext_contentscript_api_injection.html]
 [test_ext_contentscript_context.html]
 [test_ext_contentscript_create_iframe.html]
 [test_ext_contentscript_devtools_metadata.html]
 [test_ext_contentscript_exporthelpers.html]
 [test_ext_contentscript_incognito.html]
-skip-if = os == 'android' # Android does not multiple windows.
+skip-if = os == 'android' # Android does not support multiple windows.
 [test_ext_contentscript_css.html]
 [test_ext_contentscript_about_blank.html]
 [test_ext_contentscript_permission.html]
-skip-if = os == 'android' # Android does not support tabs API. Bug 1260250
 [test_ext_contentscript_teardown.html]
-skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
 [test_ext_exclude_include_globs.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
 [test_ext_notifications.html]
 [test_ext_permission_xhr.html]
 [test_ext_runtime_connect.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_runtime_connect_twoway.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_runtime_connect2.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_runtime_disconnect.html]
 [test_ext_runtime_id.html]
 [test_ext_sandbox_var.html]
 [test_ext_sendmessage_reply.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_sendmessage_reply2.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_sendmessage_doublereply.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_sendmessage_no_receiver.html]
 [test_ext_storage_content.html]
 [test_ext_storage_tab.html]
-skip-if = os == 'android' # Android does not currently support tabs.
 [test_ext_test.html]
 [test_ext_cookies.html]
-skip-if = os == 'android' # Bug 1258975 on android.
 [test_ext_background_api_injection.html]
 [test_ext_background_generated_url.html]
 [test_ext_background_teardown.html]
 [test_ext_tab_teardown.html]
-skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
+skip-if = os == 'android' # Bug 1258975 on android.
 [test_ext_unload_frame.html]
 [test_ext_i18n.html]
-skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_listener_proxies.html]
 [test_ext_web_accessible_resources.html]
-skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_webrequest_background_events.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_basic.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_filter.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_suspend.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_upload.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webnavigation.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_webnavigation_filters.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_window_postMessage.html]
 [test_ext_subframes_privileges.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_xhr_capabilities.html]
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -173,17 +173,17 @@ add_task(function* test_get_accept_langu
           expected[index],
           lang,
           `got expected language in ${source}`);
       });
     }
 
     let tabId;
 
-    browser.tabs.query({currentWindow: true, active: true}, tabs => {
+    browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
       tabId = tabs[0].id;
       browser.test.sendMessage("ready");
     });
 
     browser.test.onMessage.addListener(async ([msg, expected]) => {
       let contentResults = await browser.tabs.sendMessage(tabId, "get-results");
       let backgroundResults = await browser.i18n.getAcceptLanguages();
 
@@ -313,16 +313,21 @@ add_task(function* test_get_ui_language(
 
   win.close();
 
   yield extension.unload();
 });
 
 
 add_task(function* test_detect_language() {
+  if (AppConstants.MOZ_BUILD_APP !== "browser") {
+    // This is not supported on Android.
+    return;
+  }
+
   const af_string = " aam skukuza die naam beteken hy wat skoonvee of hy wat alles onderstebo keer wysig " +
     "bosveldkampe boskampe is kleiner afgeleë ruskampe wat oor min fasiliteite beskik daar is geen restaurante " +
     "of winkels nie en slegs oornagbesoekers word toegelaat bateleur";
   // String with intermixed French/English text
   const fr_en_string = "France is the largest country in Western Europe and the third-largest in Europe as a whole. " +
     "A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " +
     "Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " +
     "Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." +
--- a/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
@@ -140,16 +140,21 @@ add_task(function* testSendMessage_and_r
 
   window.open("file_sample.html");
 
   yield extension.awaitFinish("Received sendMessage from closing frame");
   yield extension.unload();
 });
 
 add_task(function* testConnect_and_remove_window() {
+  if (AppConstants.MOZ_BUILD_APP !== "browser") {
+    // We can't rely on this timing on Android.
+    return;
+  }
+
   let extension = createTestExtension("window", connect_background, connect_contentScript);
   yield extension.startup();
 
   window.open("file_sample.html");
 
   yield extension.awaitFinish("Received onDisconnect from closing frame");
   yield extension.unload();
 });
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -327,18 +327,23 @@ add_task(function* webnav_transitions_pr
   });
 
   found = received.find((data) => (data.event == "onCommitted" &&
                                    data.url == FRAME_MANUAL_PAGE2));
 
   ok(found, "Got the onCommitted event");
 
   if (found) {
-    is(found.details.transitionType, "manual_subframe",
-       "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+    if (AppConstants.MOZ_BUILD_APP === "browser") {
+      is(found.details.transitionType, "manual_subframe",
+         "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+    } else {
+      is(found.details.transitionType, "auto_subframe",
+         "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+    }
   }
 
   // cleanup phase
   win.close();
 
   yield extension.unload();
   info("webnavigation extension unloaded");
 });
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
@@ -21,29 +21,37 @@ add_task(function* setup() {
   // Fetch the windowId and tabId we need to filter with WebRequest.
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: [
         "tabs",
       ],
     },
     background() {
-      browser.windows.getCurrent({populate: true}).then(window => {
-        browser.test.log(`current window ${window.id} tabs: ${JSON.stringify(window.tabs.map(tab => [tab.id, tab.url]))}`);
-        browser.test.sendMessage("windowData", {windowId: window.id, tabId: window.tabs[0].id});
+      browser.tabs.query({currentWindow: true}).then(tabs => {
+        let tab = tabs.find(tab => tab.active);
+        let {windowId} = tab;
+
+        browser.test.log(`current window ${windowId} tabs: ${JSON.stringify(tabs.map(tab => [tab.id, tab.url]))}`);
+        browser.test.sendMessage("windowData", {windowId, tabId: tab.id});
       });
     },
   });
   yield extension.startup();
   windowData = yield extension.awaitMessage("windowData");
   info(`window is ${JSON.stringify(windowData)}`);
   yield extension.unload();
 });
 
 add_task(function* test_webRequest_filter_window() {
+  if (AppConstants.MOZ_BUILD_APP !== "browser") {
+    // Android does not support multiple windows.
+    return;
+  }
+
   yield SpecialPowers.pushPrefEnv({
     set: [["dom.serviceWorkers.testing.enabled", true]],
   });
 
   let events = {
     "onBeforeRequest":     [{urls: ["<all_urls>"], windowId: windowData.windowId}],
     "onBeforeSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]],
     "onSendHeaders":       [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]],
@@ -95,29 +103,31 @@ add_task(function* test_webRequest_filte
     "onResponseStarted":   [{urls: ["<all_urls>"], tabId: windowData.tabId}],
     "onCompleted":         [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]],
     "onErrorOccurred":     [{urls: ["<all_urls>"], tabId: windowData.tabId}],
   };
   let expect = {
     "file_image_good.png": {
       optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"],
       type: "main_frame",
-      cached: true,
+      cached: AppConstants.MOZ_BUILD_APP === "browser",
     },
   };
 
   let extension = makeExtension(events);
   yield extension.startup();
   extension.sendMessage("set-expected", {expect, origin: location.href});
   yield extension.awaitMessage("continue");
 
-  // We should not get events for a new window load.
-  let newWindow = window.open("file_image_good.png", "_blank", "width=100,height=100");
-  yield waitForLoad(newWindow);
-  newWindow.close();
+  if (AppConstants.MOZ_BUILD_APP === "browser") {
+    // We should not get events for a new window load.
+    let newWindow = window.open("file_image_good.png", "_blank", "width=100,height=100");
+    yield waitForLoad(newWindow);
+    newWindow.close();
+  }
 
   // We should not get background events.
   let registration = yield navigator.serviceWorker.register("webrequest_worker.js?test1", {scope: "."});
 
   // We should get events for the reload.
   testWindow.location = "file_image_good.png";
   yield extension.awaitMessage("done");