Bug 1260548: Part 1 - Factor out the common functionality of the tabs API. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 29 Jan 2017 00:45:27 -0800
changeset 467785 deca6e126f817ab863c470b51a52835342345074
parent 465972 d7ad91f903facea2a0ff088c14d6ca4a501fb509
child 467786 676df4b7af61e1495d31b09f0c85776412d55f04
push id43274
push usermaglione.k@gmail.com
push dateSun, 29 Jan 2017 20:10:11 +0000
reviewersaswan
bugs1260548
milestone54.0a1
Bug 1260548: Part 1 - Factor out the common functionality of the tabs API. r?aswan MozReview-Commit-ID: AS7asn6nXzr
browser/components/extensions/.eslintrc.js
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-commands.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-desktop-runtime.js
browser/components/extensions/ext-devtools.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-sessions.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/ext-windows.js
browser/components/extensions/test/browser/browser_ext_currentWindow.js
browser/components/extensions/test/browser/browser_ext_getViews.js
browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
browser/components/extensions/test/browser/browser_ext_sessions_restore.js
browser/components/extensions/test/browser/browser_ext_tabs_audio.js
browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
browser/components/extensions/test/browser/browser_ext_windows_events.js
toolkit/components/extensions/.eslintrc.js
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ExtensionTabs.jsm
toolkit/components/extensions/ext-cookies.js
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/moz.build
--- a/browser/components/extensions/.eslintrc.js
+++ b/browser/components/extensions/.eslintrc.js
@@ -1,23 +1,23 @@
 "use strict";
 
 module.exports = {  // eslint-disable-line no-undef
   "extends": "../../../toolkit/components/extensions/.eslintrc.js",
 
   "globals": {
-    "AllWindowEvents": true,
+    "EventEmitter": true,
+    "IconDetails": true,
+    "PanelPopup": true,
+    "Tab": true,
+    "TabContext": true,
+    "ViewPopup": true,
+    "Window": true,
+    "WindowEventManager": true,
     "browserActionFor": true,
-    "currentWindow": true,
-    "EventEmitter": true,
     "getBrowserInfo": true,
     "getCookieStoreIdForTab": true,
-    "IconDetails": true,
     "makeWidgetId": true,
     "pageActionFor": true,
-    "PanelPopup": true,
-    "TabContext": true,
-    "ViewPopup": true,
-    "WindowEventManager": true,
-    "WindowListManager": true,
-    "WindowManager": true,
+    "tabTracker": true,
+    "windowTracker": true,
   },
 };
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -46,17 +46,17 @@ function BrowserAction(options, extensio
   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.tabManager = TabManager.for(extension);
+  this.tabManager = extension.tabManager;
 
   this.defaults = {
     enabled: true,
     title: options.default_title || extension.name,
     badgeText: "",
     badgeBackgroundColor: null,
     icon: IconDetails.normalize({path: options.default_icon}, extension),
     popup: options.default_popup || "",
@@ -381,17 +381,17 @@ BrowserAction.prototype = {
   // title, badge, etc. If it only changes a parameter for a single
   // tab, |tab| will be that tab. Otherwise it will be null.
   updateOnChange(tab) {
     if (tab) {
       if (tab.selected) {
         this.updateWindow(tab.ownerGlobal);
       }
     } else {
-      for (let window of WindowListManager.browserWindows()) {
+      for (let window of windowTracker.browserWindows()) {
         this.updateWindow(window);
       }
     }
   },
 
   // tab is allowed to be null.
   // prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
   setProperty(tab, prop, value) {
@@ -439,107 +439,116 @@ extensions.on("shutdown", (type, extensi
     browserActionMap.get(extension).shutdown();
     browserActionMap.delete(extension);
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 extensions.registerSchemaAPI("browserAction", "addon_parent", context => {
   let {extension} = context;
+
+  let {tabManager} = extension;
+
+  function getTab(tabId) {
+    if (tabId !== null) {
+      return tabTracker.getTab(tabId);
+    }
+    return null;
+  }
+
   return {
     browserAction: {
       onClicked: new EventManager(context, "browserAction.onClicked", fire => {
         let listener = () => {
-          let tab = TabManager.activeTab;
-          fire(TabManager.convert(extension, tab));
+          fire(tabManager.convert(tabTracker.activeTab));
         };
         BrowserAction.for(extension).on("click", listener);
         return () => {
           BrowserAction.for(extension).off("click", listener);
         };
       }).api(),
 
       enable: function(tabId) {
-        let tab = tabId !== null ? TabManager.getTab(tabId, context) : null;
+        let tab = getTab(tabId);
         BrowserAction.for(extension).setProperty(tab, "enabled", true);
       },
 
       disable: function(tabId) {
-        let tab = tabId !== null ? TabManager.getTab(tabId, context) : null;
+        let tab = getTab(tabId);
         BrowserAction.for(extension).setProperty(tab, "enabled", false);
       },
 
       setTitle: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
 
         let title = details.title;
         // Clear the tab-specific title when given a null string.
         if (tab && title == "") {
           title = null;
         }
         BrowserAction.for(extension).setProperty(tab, "title", title);
       },
 
       getTitle: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
 
         let title = BrowserAction.for(extension).getProperty(tab, "title");
         return Promise.resolve(title);
       },
 
       setIcon: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
 
         let icon = IconDetails.normalize(details, extension, context);
         BrowserAction.for(extension).setProperty(tab, "icon", icon);
       },
 
       setBadgeText: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
 
         BrowserAction.for(extension).setProperty(tab, "badgeText", details.text);
       },
 
       getBadgeText: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
 
         let text = BrowserAction.for(extension).getProperty(tab, "badgeText");
         return Promise.resolve(text);
       },
 
       setPopup: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = 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);
         BrowserAction.for(extension).setProperty(tab, "popup", url);
       },
 
       getPopup: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
 
         let popup = BrowserAction.for(extension).getProperty(tab, "popup");
         return Promise.resolve(popup);
       },
 
       setBadgeBackgroundColor: function(details) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
         let color = details.color;
         if (!Array.isArray(color)) {
           let col = DOMUtils.colorToRGBA(color);
           color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
         }
         BrowserAction.for(extension).setProperty(tab, "badgeBackgroundColor", color);
       },
 
       getBadgeBackgroundColor: function(details, callback) {
-        let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+        let tab = getTab(details.tabId);
 
         let color = BrowserAction.for(extension).getProperty(tab, "badgeBackgroundColor");
         return Promise.resolve(color || [0xd9, 0, 0, 255]);
       },
     },
   };
 });
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -31,41 +31,41 @@ function CommandList(manifest, extension
 }
 
 CommandList.prototype = {
   /**
    * Registers the commands to all open windows and to any which
    * are later created.
    */
   register() {
-    for (let window of WindowListManager.browserWindows()) {
+    for (let window of windowTracker.browserWindows()) {
       this.registerKeysToDocument(window);
     }
 
     this.windowOpenListener = (window) => {
       if (!this.keysetsMap.has(window)) {
         this.registerKeysToDocument(window);
       }
     };
 
-    WindowListManager.addOpenListener(this.windowOpenListener);
+    windowTracker.addOpenListener(this.windowOpenListener);
   },
 
   /**
    * Unregisters the commands from all open windows and stops commands
    * from being registered to windows which are later created.
    */
   unregister() {
-    for (let window of WindowListManager.browserWindows()) {
+    for (let window of windowTracker.browserWindows()) {
       if (this.keysetsMap.has(window)) {
         this.keysetsMap.get(window).remove();
       }
     }
 
-    WindowListManager.removeOpenListener(this.windowOpenListener);
+    windowTracker.removeOpenListener(this.windowOpenListener);
   },
 
   /**
    * Creates a Map from commands for each command in the manifest.commands object.
    *
    * @param {Object} manifest The manifest JSON object.
    * @returns {Map<string, object>}
    */
@@ -127,18 +127,18 @@ CommandList.prototype = {
     keyElement.addEventListener("command", (event) => {
       if (name == "_execute_page_action") {
         let win = event.target.ownerDocument.defaultView;
         pageActionFor(this.extension).triggerAction(win);
       } else if (name == "_execute_browser_action") {
         let win = event.target.ownerDocument.defaultView;
         browserActionFor(this.extension).triggerAction(win);
       } else {
-        TabManager.for(this.extension)
-                  .addActiveTabPermission(TabManager.activeTab);
+        this.extension.tabManager
+            .addActiveTabPermission();
         this.emit("command", name);
       }
     });
     /* eslint-enable mozilla/balanced-listeners */
 
     return keyElement;
   },
 
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -84,17 +84,17 @@ var gMenuBuilder = {
       this.itemsToCleanUp.add(rootElement);
     }
   },
 
   // Builds a context menu for browserAction and pageAction buttons.
   buildActionContextMenu(contextData) {
     const {menu} = contextData;
 
-    contextData.tab = TabManager.activeTab;
+    contextData.tab = tabTracker.activeTab;
     contextData.pageUrl = contextData.tab.linkedBrowser.currentURI.spec;
 
     const root = gRootItems.get(contextData.extension);
     const children = this.buildChildren(root, contextData);
     const visible = children.slice(0, ACTION_MENU_TOP_LEVEL_LIMIT);
 
     if (visible.length) {
       this.xulMenu = menu;
@@ -315,17 +315,17 @@ function getContexts(contextData) {
 
   return contexts;
 }
 
 function MenuItem(extension, createProperties, isRoot = false) {
   this.extension = extension;
   this.children = [];
   this.parent = null;
-  this.tabManager = TabManager.for(extension);
+  this.tabManager = extension.tabManager;
 
   this.setDefaults();
   this.setProps(createProperties);
 
   if (!this.hasOwnProperty("_id")) {
     this.id = gNextMenuItemID++;
   }
   // If the item is not the root and has no parent
@@ -533,46 +533,46 @@ MenuItem.prototype = {
   },
 };
 
 // While any extensions are active, this Tracker registers to observe/listen
 // for contex-menu events from both content and chrome.
 const contextMenuTracker = {
   register() {
     Services.obs.addObserver(this, "on-build-contextmenu", false);
-    for (const window of WindowListManager.browserWindows()) {
+    for (const window of windowTracker.browserWindows()) {
       this.onWindowOpen(window);
     }
-    WindowListManager.addOpenListener(this.onWindowOpen);
+    windowTracker.addOpenListener(this.onWindowOpen);
   },
 
   unregister() {
     Services.obs.removeObserver(this, "on-build-contextmenu");
-    for (const window of WindowListManager.browserWindows()) {
+    for (const window of windowTracker.browserWindows()) {
       const menu = window.document.getElementById("tabContextMenu");
       menu.removeEventListener("popupshowing", this);
     }
-    WindowListManager.removeOpenListener(this.onWindowOpen);
+    windowTracker.removeOpenListener(this.onWindowOpen);
   },
 
   observe(subject, topic, data) {
     subject = subject.wrappedJSObject;
     gMenuBuilder.build(subject);
   },
 
   onWindowOpen(window) {
     const menu = window.document.getElementById("tabContextMenu");
     menu.addEventListener("popupshowing", contextMenuTracker);
   },
 
   handleEvent(event) {
     const menu = event.target;
     if (menu.id === "tabContextMenu") {
       const trigger = menu.triggerNode;
-      const tab = trigger.localName === "tab" ? trigger : TabManager.activeTab;
+      const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
       const pageUrl = tab.linkedBrowser.currentURI.spec;
       gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
     }
   },
 };
 
 var gExtensionCount = 0;
 /* eslint-disable mozilla/balanced-listeners */
--- a/browser/components/extensions/ext-desktop-runtime.js
+++ b/browser/components/extensions/ext-desktop-runtime.js
@@ -1,20 +1,20 @@
 "use strict";
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("uninstall", (msg, extension) => {
   if (extension.uninstallURL) {
-    let browser = WindowManager.topWindow.gBrowser;
+    let browser = windowTracker.topWindow.gBrowser;
     browser.addTab(extension.uninstallURL, {relatedToCurrent: true});
   }
 });
 
 global.openOptionsPage = (extension) => {
-  let window = WindowManager.topWindow;
+  let window = windowTracker.topWindow;
   if (!window) {
     return Promise.reject({message: "No browser window available"});
   }
 
   if (extension.manifest.options_ui.open_in_tab) {
     window.switchToTabHavingURI(extension.manifest.options_ui.page, true);
     return Promise.resolve();
   }
--- a/browser/components/extensions/ext-devtools.js
+++ b/browser/components/extensions/ext-devtools.js
@@ -76,17 +76,17 @@ global.getTargetTabIdForToolbox = (toolb
 
   if (!target.isLocalTab) {
     throw new Error("Unexpected target type: only local tabs are currently supported.");
   }
 
   let parentWindow = target.tab.linkedBrowser.ownerDocument.defaultView;
   let tab = parentWindow.gBrowser.getTabForBrowser(target.tab.linkedBrowser);
 
-  return TabManager.getId(tab);
+  return tabTracker.getId(tab);
 };
 
 /**
  * The DevToolsPage represents the "devtools_page" related to a particular
  * Toolbox and WebExtension.
  *
  * The devtools_page contexts are invisible WebExtensions contexts, similar to the
  * background page, associated to a single developer toolbox (e.g. If an add-on
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -13,17 +13,17 @@ var {
 var pageActionMap = new WeakMap();
 
 // Handles URL bar icons, including the |page_action| manifest entry
 // and associated API.
 function PageAction(options, extension) {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-page-action";
 
-  this.tabManager = TabManager.for(extension);
+  this.tabManager = extension.tabManager;
 
   this.defaults = {
     show: false,
     title: options.default_title || extension.name,
     icon: IconDetails.normalize({path: options.default_icon}, extension),
     popup: options.default_popup || "",
   };
 
@@ -206,17 +206,17 @@ PageAction.prototype = {
       this.tabContext.clear(tab);
     }
     this.updateButton(tab.ownerGlobal);
   },
 
   shutdown() {
     this.tabContext.shutdown();
 
-    for (let window of WindowListManager.browserWindows()) {
+    for (let window of windowTracker.browserWindows()) {
       if (this.buttons.has(window)) {
         this.buttons.get(window).remove();
         window.removeEventListener("popupshowing", this);
       }
     }
   },
 };
 
@@ -237,74 +237,77 @@ extensions.on("shutdown", (type, extensi
 PageAction.for = extension => {
   return pageActionMap.get(extension);
 };
 
 global.pageActionFor = PageAction.for;
 
 extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
   let {extension} = context;
+
+  const {tabManager} = extension;
+
   return {
     pageAction: {
       onClicked: new EventManager(context, "pageAction.onClicked", fire => {
         let listener = (evt, tab) => {
-          fire(TabManager.convert(extension, tab));
+          fire(tabManager.convert(tab));
         };
         let pageAction = PageAction.for(extension);
 
         pageAction.on("click", listener);
         return () => {
           pageAction.off("click", listener);
         };
       }).api(),
 
       show(tabId) {
-        let tab = TabManager.getTab(tabId, context);
+        let tab = tabTracker.getTab(tabId);
         PageAction.for(extension).setProperty(tab, "show", true);
       },
 
       hide(tabId) {
-        let tab = TabManager.getTab(tabId, context);
+        let tab = tabTracker.getTab(tabId);
         PageAction.for(extension).setProperty(tab, "show", false);
       },
 
       setTitle(details) {
-        let tab = TabManager.getTab(details.tabId, context);
+        let tab = tabTracker.getTab(details.tabId);
 
         // Clear the tab-specific title when given a null string.
         PageAction.for(extension).setProperty(tab, "title", details.title || null);
       },
 
       getTitle(details) {
-        let tab = TabManager.getTab(details.tabId, context);
+        let tab = tabTracker.getTab(details.tabId);
 
         let title = PageAction.for(extension).getProperty(tab, "title");
         return Promise.resolve(title);
       },
 
       setIcon(details) {
-        let tab = TabManager.getTab(details.tabId, context);
+        let tab = tabTracker.getTab(details.tabId);
 
         let icon = IconDetails.normalize(details, extension, context);
         PageAction.for(extension).setProperty(tab, "icon", icon);
       },
 
       setPopup(details) {
-        let tab = TabManager.getTab(details.tabId, context);
+        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);
         PageAction.for(extension).setProperty(tab, "popup", url);
       },
 
       getPopup(details) {
-        let tab = TabManager.getTab(details.tabId, context);
+        let tab = tabTracker.getTab(details.tabId);
 
         let popup = PageAction.for(extension).getProperty(tab, "popup");
         return Promise.resolve(popup);
       },
     },
   };
 });
--- a/browser/components/extensions/ext-sessions.js
+++ b/browser/components/extensions/ext-sessions.js
@@ -16,46 +16,46 @@ const SS_ON_CLOSED_OBJECTS_CHANGED = "se
 function getRecentlyClosed(maxResults, extension) {
   let recentlyClosed = [];
 
   // Get closed windows
   let closedWindowData = SessionStore.getClosedWindowData(false);
   for (let window of closedWindowData) {
     recentlyClosed.push({
       lastModified: window.closedAt,
-      window: WindowManager.convertFromSessionStoreClosedData(window, extension)});
+      window: Window.convertFromSessionStoreClosedData(extension, window)});
   }
 
   // Get closed tabs
-  for (let window of WindowListManager.browserWindows()) {
+  for (let window of windowTracker.browserWindows()) {
     let closedTabData = SessionStore.getClosedTabData(window, false);
     for (let tab of closedTabData) {
       recentlyClosed.push({
         lastModified: tab.closedAt,
-        tab: TabManager.for(extension).convertFromSessionStoreClosedData(tab, window)});
+        tab: Tab.convertFromSessionStoreClosedData(extension, tab, window)});
     }
   }
 
   // Sort windows and tabs
   recentlyClosed.sort((a, b) => b.lastModified - a.lastModified);
   return recentlyClosed.slice(0, maxResults);
 }
 
 function createSession(restored, extension, sessionId) {
   if (!restored) {
     return Promise.reject({message: `Could not restore object using sessionId ${sessionId}.`});
   }
   let sessionObj = {lastModified: Date.now()};
   if (restored instanceof Ci.nsIDOMChromeWindow) {
     return promiseObserved("sessionstore-single-window-restored", subject => subject == restored).then(() => {
-      sessionObj.window = WindowManager.convert(extension, restored, {populate: true});
+      sessionObj.window = extension.windowManager.convert(restored, {populate: true});
       return Promise.resolve([sessionObj]);
     });
   }
-  sessionObj.tab = TabManager.for(extension).convert(restored);
+  sessionObj.tab = extension.tabManager.convert(restored);
   return Promise.resolve([sessionObj]);
 }
 
 extensions.registerSchemaAPI("sessions", "addon_parent", context => {
   let {extension} = context;
   return {
     sessions: {
       getRecentlyClosed: function(filter) {
@@ -70,17 +70,17 @@ extensions.registerSchemaAPI("sessions",
           session = SessionStore.undoCloseById(closedId);
         } else if (SessionStore.lastClosedObjectType == "window") {
           // If the most recently closed object is a window, just undo closing the most recent window.
           session = SessionStore.undoCloseWindow(0);
         } else {
           // It is a tab, and we cannot call SessionStore.undoCloseTab without a window,
           // so we must find the tab in which case we can just use its closedId.
           let recentlyClosedTabs = [];
-          for (let window of WindowListManager.browserWindows()) {
+          for (let window of windowTracker.browserWindows()) {
             let closedTabData = SessionStore.getClosedTabData(window, false);
             for (let tab of closedTabData) {
               recentlyClosedTabs.push(tab);
             }
           }
 
           // Sort the tabs.
           recentlyClosedTabs.sort((a, b) => b.closedAt - a.closedAt);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -32,19 +32,19 @@ function getSender(extension, target, se
     // page-open listener below).
     tabId = sender.tabId;
     delete sender.tabId;
   } else if (target instanceof Ci.nsIDOMXULElement) {
     tabId = getBrowserInfo(target).tabId;
   }
 
   if (tabId) {
-    let tab = TabManager.getTab(tabId, null, null);
+    let tab = extension.tabManager.get(tabId, null);
     if (tab) {
-      sender.tab = TabManager.convert(extension, tab);
+      sender.tab = tab.convert();
     }
   }
 }
 
 // Used by Extension.jsm
 global.tabGetSender = getSender;
 
 /* eslint-disable mozilla/balanced-listeners */
@@ -73,161 +73,24 @@ extensions.on("fill-browser-data", (type
     ({tabId, windowId} = getBrowserInfo(browser));
   }
 
   data.tabId = tabId || -1;
   data.windowId = windowId || -1;
 });
 /* eslint-enable mozilla/balanced-listeners */
 
-global.currentWindow = function(context) {
-  let {xulWindow} = context;
-  if (xulWindow && context.viewType != "background") {
-    return xulWindow;
-  }
-  return WindowManager.topWindow;
-};
-
 let tabListener = {
-  init() {
-    if (this.initialized) {
-      return;
-    }
-
-    this.adoptedTabs = new WeakMap();
-
-    this.handleWindowOpen = this.handleWindowOpen.bind(this);
-    this.handleWindowClose = this.handleWindowClose.bind(this);
-
-    AllWindowEvents.addListener("TabClose", this);
-    AllWindowEvents.addListener("TabOpen", this);
-    WindowListManager.addOpenListener(this.handleWindowOpen);
-    WindowListManager.addCloseListener(this.handleWindowClose);
-
-    EventEmitter.decorate(this);
-
-    this.initialized = true;
-  },
-
-  handleEvent(event) {
-    switch (event.type) {
-      case "TabOpen":
-        if (event.detail.adoptedTab) {
-          this.adoptedTabs.set(event.detail.adoptedTab, event.target);
-        }
-
-        // 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);
-          }
-        });
-        break;
-
-      case "TabClose":
-        let tab = event.originalTarget;
-
-        if (event.detail.adoptedBy) {
-          this.emitDetached(tab, event.detail.adoptedBy);
-        } else {
-          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];
-      this.adoptedTabs.set(tab, window.gBrowser.tabs[0]);
-
-      // 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 = WindowManager.getId(tab.ownerGlobal);
-    let tabId = TabManager.getId(tab);
-
-    this.emit("tab-attached", {tab, tabId, newWindowId, newPosition: tab._tPos});
-  },
-
-  emitDetached(tab, adoptedBy) {
-    let oldWindowId = WindowManager.getId(tab.ownerGlobal);
-    let tabId = TabManager.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 = WindowManager.getId(tab.ownerGlobal);
-    let tabId = TabManager.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);
-  },
-
   tabReadyInitialized: false,
   tabReadyPromises: new WeakMap(),
   initializingTabs: new WeakSet(),
 
   initTabReady() {
     if (!this.tabReadyInitialized) {
-      AllWindowEvents.addListener("progress", this);
+      windowTracker.addListener("progress", this);
 
       this.tabReadyInitialized = true;
     }
   },
 
   onLocationChange(browser, webProgress, request, locationURI, flags) {
     if (webProgress.isTopLevel) {
       let gBrowser = browser.ownerGlobal.gBrowser;
@@ -264,87 +127,91 @@ let tabListener = {
         this.initTabReady();
         this.tabReadyPromises.set(tab, deferred);
       }
     }
     return deferred.promise;
   },
 };
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("startup", () => {
-  tabListener.init();
-});
-/* eslint-enable mozilla/balanced-listeners */
-
 extensions.registerSchemaAPI("tabs", "addon_parent", context => {
   let {extension} = context;
+
+  let {tabManager} = extension;
+
+  function getTabOrActive(tabId) {
+    if (tabId !== null) {
+      return tabTracker.getTab(tabId);
+    }
+    return tabTracker.activeTab;
+  }
+
   let self = {
     tabs: {
       onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
         let tab = event.originalTarget;
-        let tabId = TabManager.getId(tab);
-        let windowId = WindowManager.getId(tab.ownerGlobal);
+        let tabId = tabTracker.getId(tab);
+        let windowId = windowTracker.getId(tab.ownerGlobal);
         fire({tabId, windowId});
       }).api(),
 
       onCreated: new EventManager(context, "tabs.onCreated", fire => {
         let listener = (eventName, event) => {
-          fire(TabManager.convert(extension, event.tab));
+          fire(tabManager.convert(event.tab));
         };
 
-        tabListener.on("tab-created", listener);
+        tabTracker.on("tab-created", listener);
         return () => {
-          tabListener.off("tab-created", listener);
+          tabTracker.off("tab-created", listener);
         };
       }).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 = [TabManager.getId(tab)];
-        let windowId = WindowManager.getId(tab.ownerGlobal);
+        let tabIds = [tabTracker.getId(tab)];
+        let windowId = windowTracker.getId(tab.ownerGlobal);
         fire({tabIds, windowId});
       }).api(),
 
       onAttached: new EventManager(context, "tabs.onAttached", fire => {
         let listener = (eventName, event) => {
           fire(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
         };
 
-        tabListener.on("tab-attached", listener);
+        tabTracker.on("tab-attached", listener);
         return () => {
-          tabListener.off("tab-attached", listener);
+          tabTracker.off("tab-attached", listener);
         };
       }).api(),
 
       onDetached: new EventManager(context, "tabs.onDetached", fire => {
         let listener = (eventName, event) => {
           fire(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
         };
 
-        tabListener.on("tab-detached", listener);
+        tabTracker.on("tab-detached", listener);
         return () => {
-          tabListener.off("tab-detached", listener);
+          tabTracker.off("tab-detached", listener);
         };
       }).api(),
 
       onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
         let listener = (eventName, event) => {
           fire(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
         };
 
-        tabListener.on("tab-removed", listener);
+        tabTracker.on("tab-removed", listener);
         return () => {
-          tabListener.off("tab-removed", listener);
+          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.
         //
@@ -368,28 +235,28 @@ extensions.registerSchemaAPI("tabs", "ad
         let moveListener = event => {
           let tab = event.originalTarget;
 
           if (ignoreNextMove.has(tab)) {
             ignoreNextMove.delete(tab);
             return;
           }
 
-          fire(TabManager.getId(tab), {
-            windowId: WindowManager.getId(tab.ownerGlobal),
+          fire(tabTracker.getId(tab), {
+            windowId: windowTracker.getId(tab.ownerGlobal),
             fromIndex: event.detail,
             toIndex: tab._tPos,
           });
         };
 
-        AllWindowEvents.addListener("TabMove", moveListener);
-        AllWindowEvents.addListener("TabOpen", openListener);
+        windowTracker.addListener("TabMove", moveListener);
+        windowTracker.addListener("TabOpen", openListener);
         return () => {
-          AllWindowEvents.removeListener("TabMove", moveListener);
-          AllWindowEvents.removeListener("TabOpen", openListener);
+          windowTracker.removeListener("TabMove", moveListener);
+          windowTracker.removeListener("TabOpen", openListener);
         };
       }).api(),
 
       onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
         const restricted = ["url", "favIconUrl", "title"];
 
         function sanitize(extension, changeInfo) {
           let result = {};
@@ -404,17 +271,17 @@ extensions.registerSchemaAPI("tabs", "ad
         }
 
         let fireForBrowser = (browser, changed) => {
           let [needed, changeInfo] = sanitize(extension, changed);
           if (needed) {
             let gBrowser = browser.ownerGlobal.gBrowser;
             let tabElem = gBrowser.getTabForBrowser(browser);
 
-            let tab = TabManager.convert(extension, tabElem);
+            let tab = tabManager.convert(tabElem);
             fire(tab.id, changeInfo, tab);
           }
         };
 
         let listener = event => {
           let needed = [];
           if (event.type == "TabAttrModified") {
             let changed = event.detail.changed;
@@ -436,17 +303,17 @@ extensions.registerSchemaAPI("tabs", "ad
             needed.push("pinned");
           }
 
           if (needed.length && !extension.hasPermission("tabs")) {
             needed = needed.filter(attr => !restricted.includes(attr));
           }
 
           if (needed.length) {
-            let tab = TabManager.convert(extension, event.originalTarget);
+            let tab = tabManager.convert(event.originalTarget);
 
             let changeInfo = {};
             for (let prop of needed) {
               changeInfo[prop] = tab[prop];
             }
             fire(tab.id, changeInfo, tab);
           }
         };
@@ -478,34 +345,35 @@ extensions.registerSchemaAPI("tabs", "ad
 
             fireForBrowser(browser, {
               status: webProgress.isLoadingDocument ? "loading" : "complete",
               url: locationURI.spec,
             });
           },
         };
 
-        AllWindowEvents.addListener("progress", progressListener);
-        AllWindowEvents.addListener("TabAttrModified", listener);
-        AllWindowEvents.addListener("TabPinned", listener);
-        AllWindowEvents.addListener("TabUnpinned", listener);
+        windowTracker.addListener("progress", progressListener);
+        windowTracker.addListener("TabAttrModified", listener);
+        windowTracker.addListener("TabPinned", listener);
+        windowTracker.addListener("TabUnpinned", listener);
 
         return () => {
-          AllWindowEvents.removeListener("progress", progressListener);
-          AllWindowEvents.removeListener("TabAttrModified", listener);
-          AllWindowEvents.removeListener("TabPinned", listener);
-          AllWindowEvents.removeListener("TabUnpinned", listener);
+          windowTracker.removeListener("progress", progressListener);
+          windowTracker.removeListener("TabAttrModified", listener);
+          windowTracker.removeListener("TabPinned", listener);
+          windowTracker.removeListener("TabUnpinned", listener);
         };
       }).api(),
 
-      create: function(createProperties) {
+      create(createProperties) {
         return new Promise((resolve, reject) => {
           let window = createProperties.windowId !== null ?
-            WindowManager.getWindow(createProperties.windowId, context) :
-            WindowManager.topWindow;
+            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);
             };
@@ -531,21 +399,21 @@ extensions.registerSchemaAPI("tabs", "ad
           let options = {};
           if (createProperties.cookieStoreId) {
             if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
               return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
             }
 
             let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
             if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
-              return Promise.reject({message: `Illegal to set non-private cookieStorageId in a private window`});
+              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 cookieStorageId in a non-private window`});
+              return Promise.reject({message: `Illegal to set private cookieStoreId in a non-private window`});
             }
 
             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}`});
               }
 
@@ -583,35 +451,33 @@ extensions.registerSchemaAPI("tabs", "ad
 
             // 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);
           }
 
-          return TabManager.convert(extension, tab);
+          return tabManager.convert(tab);
         });
       },
 
-      remove: function(tabs) {
+      async remove(tabs) {
         if (!Array.isArray(tabs)) {
           tabs = [tabs];
         }
 
         for (let tabId of tabs) {
-          let tab = TabManager.getTab(tabId, context);
+          let tab = tabTracker.getTab(tabId);
           tab.ownerGlobal.gBrowser.removeTab(tab);
         }
-
-        return Promise.resolve();
       },
 
-      update: function(tabId, updateProperties) {
-        let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+      async update(tabId, updateProperties) {
+        let tab = getTabOrActive(tabId);
 
         let tabbrowser = tab.ownerGlobal.gBrowser;
 
         if (updateProperties.url !== null) {
           let url = context.uri.resolve(updateProperties.url);
 
           if (!context.checkLoadURL(url, {dontReportErrors: true})) {
             return Promise.reject({message: `Illegal URL: ${url}`});
@@ -636,134 +502,65 @@ extensions.registerSchemaAPI("tabs", "ad
           if (updateProperties.pinned) {
             tabbrowser.pinTab(tab);
           } else {
             tabbrowser.unpinTab(tab);
           }
         }
         // FIXME: highlighted/selected, openerTabId
 
-        return Promise.resolve(TabManager.convert(extension, tab));
+        return tabManager.convert(tab);
       },
 
-      reload: function(tabId, reloadProperties) {
-        let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+      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);
-
-        return Promise.resolve();
       },
 
-      get: function(tabId) {
-        let tab = TabManager.getTab(tabId, context);
+      async get(tabId) {
+        let tab = tabTracker.getTab(tabId);
 
-        return Promise.resolve(TabManager.convert(extension, tab));
+        return tabManager.convert(tab);
       },
 
       getCurrent() {
         let tab;
         if (context.tabId) {
-          tab = TabManager.convert(extension, TabManager.getTab(context.tabId, context));
+          tab = tabManager.get(context.tabId).convert();
         }
         return Promise.resolve(tab);
       },
 
-      query: function(queryInfo) {
-        let pattern = null;
+      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'});
           }
 
-          pattern = new MatchPattern(queryInfo.url);
-        }
-
-        function matches(tab) {
-          let props = ["active", "pinned", "highlighted", "status", "title", "index"];
-          for (let prop of props) {
-            if (queryInfo[prop] !== null && queryInfo[prop] != tab[prop]) {
-              return false;
-            }
-          }
-
-          if (queryInfo.audible !== null) {
-            if (queryInfo.audible != tab.audible) {
-              return false;
-            }
-          }
-
-          if (queryInfo.muted !== null) {
-            if (queryInfo.muted != tab.mutedInfo.muted) {
-              return false;
-            }
-          }
-
-          if (queryInfo.cookieStoreId !== null &&
-              tab.cookieStoreId != queryInfo.cookieStoreId) {
-            return false;
-          }
-
-          if (pattern && !pattern.matches(Services.io.newURI(tab.url))) {
-            return false;
-          }
-
-          return true;
+          queryInfo = Object.assign({}, queryInfo);
+          queryInfo.url = new MatchPattern(queryInfo.url);
         }
 
-        let result = [];
-        for (let window of WindowListManager.browserWindows()) {
-          let lastFocused = window === WindowManager.topWindow;
-          if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow !== lastFocused) {
-            continue;
-          }
-
-          let windowType = WindowManager.windowType(window);
-          if (queryInfo.windowType !== null && queryInfo.windowType !== windowType) {
-            continue;
-          }
-
-          if (queryInfo.windowId !== null) {
-            if (queryInfo.windowId === WindowManager.WINDOW_ID_CURRENT) {
-              if (currentWindow(context) !== window) {
-                continue;
-              }
-            } else if (queryInfo.windowId !== WindowManager.getId(window)) {
-              continue;
-            }
-          }
-
-          if (queryInfo.currentWindow !== null) {
-            let eq = window === currentWindow(context);
-            if (queryInfo.currentWindow != eq) {
-              continue;
-            }
-          }
-
-          let tabs = TabManager.for(extension).getTabs(window);
-          for (let tab of tabs) {
-            if (matches(tab)) {
-              result.push(tab);
-            }
-          }
-        }
-        return Promise.resolve(result);
+        return Array.from(tabManager.query(queryInfo, context),
+                          tab => tab.convert());
       },
 
-      captureVisibleTab: function(windowId, options) {
+      captureVisibleTab(windowId, options) {
         if (!extension.hasPermission("<all_urls>")) {
           return Promise.reject({message: "The <all_urls> permission is required to use the captureVisibleTab API"});
         }
 
         let window = windowId == null ?
-          WindowManager.topWindow :
-          WindowManager.getWindow(windowId, context);
+          windowTracker.topWindow :
+          windowTracker.getWindow(windowId, context);
 
         let tab = window.gBrowser.selectedTab;
         return tabListener.awaitTabReady(tab).then(() => {
           let browser = tab.linkedBrowser;
           let recipient = {
             innerWindowID: browser.innerWindowID,
           };
 
@@ -783,48 +580,48 @@ extensions.registerSchemaAPI("tabs", "ad
             height: browser.clientHeight,
           };
 
           return context.sendMessage(browser.messageManager, "Extension:Capture",
                                      message, {recipient});
         });
       },
 
-      detectLanguage: function(tabId) {
-        let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+      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});
         });
       },
 
       // Used to executeScript, insertCSS and removeCSS.
       _execute: function(tabId, details, kind, method) {
-        let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+        let tab = getTabOrActive(tabId);
 
         let options = {
           js: [],
           css: [],
           remove_css: method == "removeCSS",
         };
 
         // We require a `code` or a `file` property, but we can't accept both.
         if ((details.code === null) == (details.file === null)) {
           return Promise.reject({message: `${method} requires either a 'code' or a 'file' property, but not both`});
         }
 
         if (details.frameId !== null && details.allFrames) {
           return Promise.reject({message: `'frameId' and 'allFrames' are mutually exclusive`});
         }
 
-        if (TabManager.for(extension).hasActiveTabPermission(tab)) {
+        if (tabManager.hasActiveTabPermission(tab)) {
           // If we have the "activeTab" permission for this tab, ignore
           // the host whitelist.
           options.matchesHost = ["<all_urls>"];
         } else {
           options.matchesHost = extension.whiteListedHosts.serialize();
         }
 
         if (details.code !== null) {
@@ -874,42 +671,42 @@ extensions.registerSchemaAPI("tabs", "ad
       insertCSS: function(tabId, details) {
         return self.tabs._execute(tabId, details, "css", "insertCSS").then(() => {});
       },
 
       removeCSS: function(tabId, details) {
         return self.tabs._execute(tabId, details, "css", "removeCSS").then(() => {});
       },
 
-      move: function(tabIds, moveProperties) {
+      async move(tabIds, moveProperties) {
         let index = moveProperties.index;
         let tabsMoved = [];
         if (!Array.isArray(tabIds)) {
           tabIds = [tabIds];
         }
 
         let destinationWindow = null;
         if (moveProperties.windowId !== null) {
-          destinationWindow = WindowManager.getWindow(moveProperties.windowId, context);
+          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 => TabManager.getTab(tabId, context));
+        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) {
@@ -934,60 +731,60 @@ extensions.registerSchemaAPI("tabs", "ad
             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 Promise.resolve(tabsMoved.map(tab => TabManager.convert(extension, tab)));
+        return tabsMoved.map(tab => tabManager.convert(tab));
       },
 
-      duplicate: function(tabId) {
-        let tab = TabManager.getTab(tabId, context);
+      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.
-            newTab.removeEventListener("SSTabRestoring", listener);
+
             // 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.
-            newTab.removeEventListener("SSTabRestored", listener);
             gBrowser.selectedTab = newTab;
-            return resolve(TabManager.convert(extension, newTab));
-          });
+
+            return resolve(tabManager.convert(newTab));
+          }, {once: true});
         });
       },
 
       getZoom(tabId) {
-        let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+        let tab = getTabOrActive(tabId);
 
         let {ZoomManager} = tab.ownerGlobal;
         let zoom = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
 
         return Promise.resolve(zoom);
       },
 
       setZoom(tabId, zoom) {
-        let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+        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);
@@ -996,33 +793,33 @@ extensions.registerSchemaAPI("tabs", "ad
             message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
           });
         }
 
         return Promise.resolve();
       },
 
       _getZoomSettings(tabId) {
-        let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+        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 = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+        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();
       },
@@ -1034,17 +831,17 @@ extensions.registerSchemaAPI("tabs", "ad
           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 WindowListManager.browserWindows()) {
+        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;
@@ -1071,35 +868,35 @@ extensions.registerSchemaAPI("tabs", "ad
           }
 
           let oldZoomFactor = zoomLevels.get(browser);
           let newZoomFactor = getZoomLevel(browser);
 
           if (oldZoomFactor != newZoomFactor) {
             zoomLevels.set(browser, newZoomFactor);
 
-            let tabId = TabManager.getId(tab);
+            let tabId = tabTracker.getId(tab);
             fire({
               tabId,
               oldZoomFactor,
               newZoomFactor,
               zoomSettings: self.tabs._getZoomSettings(tabId),
             });
           }
         };
 
-        tabListener.on("tab-attached", tabCreated);
-        tabListener.on("tab-created", tabCreated);
+        tabTracker.on("tab-attached", tabCreated);
+        tabTracker.on("tab-created", tabCreated);
 
-        AllWindowEvents.addListener("FullZoomChange", zoomListener);
-        AllWindowEvents.addListener("TextZoomChange", zoomListener);
+        windowTracker.addListener("FullZoomChange", zoomListener);
+        windowTracker.addListener("TextZoomChange", zoomListener);
         return () => {
-          tabListener.off("tab-attached", tabCreated);
-          tabListener.off("tab-created", tabCreated);
+          tabTracker.off("tab-attached", tabCreated);
+          tabTracker.off("tab-created", tabCreated);
 
-          AllWindowEvents.removeListener("FullZoomChange", zoomListener);
-          AllWindowEvents.removeListener("TextZoomChange", zoomListener);
+          windowTracker.removeListener("FullZoomChange", zoomListener);
+          windowTracker.removeListener("TextZoomChange", zoomListener);
         };
       }).api(),
     },
   };
   return self;
 });
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -1,42 +1,48 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
                                   "resource://gre/modules/Timer.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");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const POPUP_LOAD_TIMEOUT_MS = 200;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 var {
   DefaultWeakMap,
   EventManager,
+  ExtensionError,
+  defineLazyGetter,
   promiseEvent,
 } = ExtensionUtils;
 
+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.
 
 global.makeWidgetId = id => {
   id = id.toLowerCase();
   // FIXME: This allows for collisions.
   return id.replace(/[^a-z0-9_-]/g, "_");
@@ -552,18 +558,18 @@ Object.assign(global, {PanelPopup, ViewP
 // across all windows.
 global.TabContext = function TabContext(getDefaults, extension) {
   this.extension = extension;
   this.getDefaults = getDefaults;
 
   this.tabData = new WeakMap();
   this.lastLocation = new WeakMap();
 
-  AllWindowEvents.addListener("progress", this);
-  AllWindowEvents.addListener("TabSelect", this);
+  windowTracker.addListener("progress", this);
+  windowTracker.addListener("TabSelect", this);
 
   EventEmitter.decorate(this);
 };
 
 TabContext.prototype = {
   get(tab) {
     if (!this.tabData.has(tab)) {
       this.tabData.set(tab, this.getDefaults(tab));
@@ -600,143 +606,18 @@ TabContext.prototype = {
         !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
       let tab = gBrowser.getTabForBrowser(browser);
       this.emit("location-change", tab, true);
     }
     this.lastLocation.set(browser, browser.currentURI);
   },
 
   shutdown() {
-    AllWindowEvents.removeListener("progress", this);
-    AllWindowEvents.removeListener("TabSelect", this);
-  },
-};
-
-// Manages tab mappings and permissions for a specific extension.
-function ExtensionTabManager(extension) {
-  this.extension = extension;
-
-  // A mapping of tab objects to the inner window ID the extension currently has
-  // the active tab permission for. The active permission for a given tab is
-  // valid only for the inner window that was active when the permission was
-  // granted. If the tab navigates, the inner window ID changes, and the
-  // permission automatically becomes stale.
-  //
-  // WeakMap[tab => inner-window-id<int>]
-  this.hasTabPermissionFor = new WeakMap();
-}
-
-ExtensionTabManager.prototype = {
-  addActiveTabPermission(tab = TabManager.activeTab) {
-    if (this.extension.hasPermission("activeTab")) {
-      // Note that, unlike Chrome, we don't currently clear this permission with
-      // the tab navigates. If the inner window is revived from BFCache before
-      // we've granted this permission to a new inner window, the extension
-      // maintains its permissions for it.
-      this.hasTabPermissionFor.set(tab, tab.linkedBrowser.innerWindowID);
-    }
-  },
-
-  revokeActiveTabPermission(tab = TabManager.activeTab) {
-    this.hasTabPermissionFor.delete(tab);
-  },
-
-  // Returns true if the extension has the "activeTab" permission for this tab.
-  // This is somewhat more permissive than the generic "tabs" permission, as
-  // checked by |hasTabPermission|, in that it also allows programmatic script
-  // injection without an explicit host permission.
-  hasActiveTabPermission(tab) {
-    // This check is redundant with addTabPermission, but cheap.
-    if (this.extension.hasPermission("activeTab")) {
-      return (this.hasTabPermissionFor.has(tab) &&
-              this.hasTabPermissionFor.get(tab) === tab.linkedBrowser.innerWindowID);
-    }
-    return false;
-  },
-
-  hasTabPermission(tab) {
-    return this.extension.hasPermission("tabs") || this.hasActiveTabPermission(tab);
-  },
-
-  convert(tab) {
-    let window = tab.ownerGlobal;
-    let browser = tab.linkedBrowser;
-
-    let mutedInfo = {muted: tab.muted};
-    if (tab.muteReason === null) {
-      mutedInfo.reason = "user";
-    } else if (tab.muteReason) {
-      mutedInfo.reason = "extension";
-      mutedInfo.extensionId = tab.muteReason;
-    }
-
-    let result = {
-      id: TabManager.getId(tab),
-      index: tab._tPos,
-      windowId: WindowManager.getId(window),
-      selected: tab.selected,
-      highlighted: tab.selected,
-      active: tab.selected,
-      pinned: tab.pinned,
-      status: TabManager.getStatus(tab),
-      incognito: WindowManager.isBrowserPrivate(browser),
-      width: browser.frameLoader.lazyWidth || browser.clientWidth,
-      height: browser.frameLoader.lazyHeight || browser.clientHeight,
-      audible: tab.soundPlaying,
-      mutedInfo,
-    };
-    if (this.extension.hasPermission("cookies")) {
-      result.cookieStoreId = getCookieStoreIdForTab(result, tab);
-    }
-
-    if (this.hasTabPermission(tab)) {
-      result.url = browser.currentURI.spec;
-      let title = browser.contentTitle || tab.label;
-      if (title) {
-        result.title = title;
-      }
-      let icon = window.gBrowser.getIcon(tab);
-      if (icon) {
-        result.favIconUrl = icon;
-      }
-    }
-
-    return result;
-  },
-
-  // Converts tabs returned from SessionStore.getClosedTabData and
-  // SessionStore.getClosedWindowData into API tab objects
-  convertFromSessionStoreClosedData(tab, window) {
-    let result = {
-      sessionId: String(tab.closedId),
-      index: tab.pos ? tab.pos : 0,
-      windowId: WindowManager.getId(window),
-      selected: false,
-      highlighted: false,
-      active: false,
-      pinned: false,
-      incognito: Boolean(tab.state && tab.state.isPrivate),
-    };
-
-    if (this.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;
-  },
-
-  getTabs(window) {
-    return Array.from(window.gBrowser.tabs)
-                .filter(tab => !tab.closing)
-                .map(tab => this.convert(tab));
+    windowTracker.removeListener("progress", this);
+    windowTracker.removeListener("TabSelect", this);
   },
 };
 
 function getBrowserInfo(browser) {
   if (!browser.ownerGlobal.gBrowser) {
     // When we're loaded into a <browser> inside about:addons, we need to go up
     // one more level.
     browser = browser.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -749,20 +630,20 @@ function getBrowserInfo(browser) {
   }
 
   let result = {};
 
   let window = browser.ownerGlobal;
   if (window.gBrowser) {
     let tab = window.gBrowser.getTabForBrowser(browser);
     if (tab) {
-      result.tabId = TabManager.getId(tab);
+      result.tabId = tabTracker.getId(tab);
     }
 
-    result.windowId = WindowManager.getId(window);
+    result.windowId = windowTracker.getId(window);
   }
 
   return result;
 }
 global.getBrowserInfo = getBrowserInfo;
 
 // Sends the tab and windowId upon request. This is primarily used to support
 // the synchronous `browser.extension.getViews` API.
@@ -778,252 +659,446 @@ let onGetTabAndWindowId = {
     }
   },
 };
 /* eslint-disable mozilla/balanced-listeners */
 Services.mm.addMessageListener("Extension:GetTabAndWindowId", onGetTabAndWindowId);
 /* eslint-enable mozilla/balanced-listeners */
 
 
-// Manages global mappings between XUL tabs and extension tab IDs.
-global.TabManager = {
-  _tabs: new WeakMap(),
-  _nextId: 1,
-  _initialized: false,
+class WindowTracker extends WindowTrackerBase {
+  addProgressListener(window, listener) {
+    window.gBrowser.addTabsProgressListener(listener);
+  }
+
+  removeProgressListener(window, listener) {
+    window.gBrowser.removeTabsProgressListener(listener);
+  }
+}
+
+global.WindowEventManager = class extends EventManager {
+  constructor(context, name, event, listener) {
+    super(context, name, fire => {
+      let listener2 = listener.bind(null, fire);
 
-  // We begin listening for TabOpen and TabClose events once we've started
-  // assigning IDs to tabs, so that we can remap the IDs of tabs which are moved
-  // between windows.
-  initListener() {
-    if (this._initialized) {
+      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;
 
-    AllWindowEvents.addListener("TabOpen", this);
-    AllWindowEvents.addListener("TabClose", this);
-    WindowListManager.addOpenListener(this.handleWindowOpen.bind(this));
+    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 */
+  }
+
+  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);
+  }
 
-    this._initialized = true;
-  },
+  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);
+    if (tab) {
+      return tab;
+    }
+    if (default_ !== undefined) {
+      return default_;
+    }
+    throw new ExtensionError(`Invalid tab ID: ${tabId}`);
+  }
 
   handleEvent(event) {
-    if (event.type == "TabOpen") {
-      let {adoptedTab} = event.detail;
-      if (adoptedTab) {
-        // This tab is being created to adopt a tab from a different window.
-        // Copy the ID from the old tab to the new.
-        let tab = event.target;
-        this._tabs.set(tab, this.getId(adoptedTab));
+    let tab = 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));
 
-        tab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
-          windowId: WindowManager.getId(tab.ownerGlobal),
+          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);
+          }
         });
-      }
-    } else if (event.type == "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._tabs.set(adoptedBy, this.getId(event.target));
+        break;
 
-        adoptedBy.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
-          windowId: WindowManager.getId(adoptedBy),
-        });
-      }
+      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);
+        }
+        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.
-      let adoptedTab = window.arguments[0];
+      //
+      // 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));
 
-      this._tabs.set(window.gBrowser.tabs[0], this.getId(adoptedTab));
+      // 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);
+      }
     }
-  },
+  }
 
-  getId(tab) {
-    if (this._tabs.has(tab)) {
-      return this._tabs.get(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);
+      }
     }
-    this.initListener();
+  }
+
+  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});
+  }
 
-    let id = this._nextId++;
-    this._tabs.set(tab, id);
-    return id;
-  },
+  emitCreated(tab) {
+    this.emit("tab-created", {tab});
+  }
+
+  emitRemoved(tab, isWindowClosing) {
+    let windowId = windowTracker.getId(tab.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);
+  }
 
   getBrowserId(browser) {
-    let gBrowser = browser.ownerGlobal.gBrowser;
+    let {gBrowser} = browser.ownerGlobal;
     // Some non-browser windows have gBrowser but not
     // getTabForBrowser!
     if (gBrowser && gBrowser.getTabForBrowser) {
       let tab = gBrowser.getTabForBrowser(browser);
       if (tab) {
         return this.getId(tab);
       }
     }
     return -1;
-  },
-
-  /**
-   * 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 {ExtensionContext} context
-   *        The context of the caller.
-   *        This value may be omitted if `default_` is not `undefined`.
-   * @param {*} default_
-   *        The value to return if no tab exists with the given ID.
-   * @returns {Element<tab>}
-   *        A XUL <tab> element.
-   */
-  getTab(tabId, context, default_ = undefined) {
-    // FIXME: Speed this up without leaking memory somehow.
-    for (let window of WindowListManager.browserWindows()) {
-      if (!window.gBrowser) {
-        continue;
-      }
-      for (let tab of window.gBrowser.tabs) {
-        if (this.getId(tab) == tabId) {
-          return tab;
-        }
-      }
-    }
-    if (default_ !== undefined) {
-      return default_;
-    }
-    throw new context.cloneScope.Error(`Invalid tab ID: ${tabId}`);
-  },
+  }
 
   get activeTab() {
-    let window = WindowManager.topWindow;
+    let window = windowTracker.topWindow;
     if (window && window.gBrowser) {
       return window.gBrowser.selectedTab;
     }
     return null;
-  },
-
-  getStatus(tab) {
-    return tab.getAttribute("busy") == "true" ? "loading" : "complete";
-  },
-
-  convert(extension, tab) {
-    return TabManager.for(extension).convert(tab);
-  },
-};
-
-// WeakMap[Extension -> ExtensionTabManager]
-let tabManagers = new WeakMap();
-
-// Returns the extension-specific tab manager for the given extension, or
-// creates one if it doesn't already exist.
-TabManager.for = function(extension) {
-  if (!tabManagers.has(extension)) {
-    tabManagers.set(extension, new ExtensionTabManager(extension));
   }
-  return tabManagers.get(extension);
-};
-
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("shutdown", (type, extension) => {
-  tabManagers.delete(extension);
-});
-/* eslint-enable mozilla/balanced-listeners */
-
-function memoize(fn) {
-  let weakMap = new DefaultWeakMap(fn);
-  return weakMap.get.bind(weakMap);
 }
 
-// Manages mapping between XUL windows and extension window IDs.
-global.WindowManager = {
-  // Note: These must match the values in windows.json.
-  WINDOW_ID_NONE: -1,
-  WINDOW_ID_CURRENT: -2,
+windowTracker = new WindowTracker();
+tabTracker = new TabTracker();
+
+Object.assign(global, {tabTracker, windowTracker});
+
+class Tab extends TabBase {
+  get _favIconUrl() {
+    return this.window.gBrowser.getIcon(this.tab);
+  }
 
-  get topWindow() {
-    return Services.wm.getMostRecentWindow("navigator:browser");
-  },
+  get audible() {
+    return this.tab.soundPlaying;
+  }
+
+  get browser() {
+    return this.tab.linkedBrowser;
+  }
+
+  get cookieStoreId() {
+    return getCookieStoreIdForTab(this, this.tab);
+  }
 
-  windowType(window) {
-    // TODO: Make this work.
+  get height() {
+    return this.browser.clientHeight;
+  }
+
+  get index() {
+    return this.tab._tPos;
+  }
 
-    let {chromeFlags} = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                              .getInterface(Ci.nsIDocShell)
-                              .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
-                              .getInterface(Ci.nsIXULWindow);
+  get innerWindowID() {
+    return this.browser.innerWindowID;
+  }
+
+  get mutedInfo() {
+    let tab = this.tab;
 
-    if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) {
-      return "popup";
+    let mutedInfo = {muted: tab.muted};
+    if (tab.muteReason === null) {
+      mutedInfo.reason = "user";
+    } else if (tab.muteReason) {
+      mutedInfo.reason = "extension";
+      mutedInfo.extensionId = tab.muteReason;
     }
 
-    return "normal";
-  },
+    return mutedInfo;
+  }
+
+  get pinned() {
+    return this.tab.pinned;
+  }
+
+  get active() {
+    return this.tab.selected;
+  }
+
+  get selected() {
+    return this.tab.selected;
+  }
+
+  get status() {
+    if (this.tab.getAttribute("busy") === "true") {
+      return "loading";
+    }
+    return "complete";
+  }
+
+  get width() {
+    return this.browser.clientWidth;
+  }
+
+  get window() {
+    return this.tab.ownerGlobal;
+  }
 
-  updateGeometry(window, options) {
+  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);
     }
-  },
+  }
 
-  isBrowserPrivate: memoize(browser => {
-    return PrivateBrowsingUtils.isBrowserPrivate(browser);
-  }),
+  get focused() {
+    return this.window.document.hasFocus();
+  }
 
-  getId: memoize(window => {
-    if (window instanceof Ci.nsIInterfaceRequestor) {
-      return window.getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
-    }
-    return null;
-  }),
+  get top() {
+    return this.window.screenY;
+  }
+
+  get left() {
+    return this.window.screenX;
+  }
+
+  get width() {
+    return this.window.outerWidth;
+  }
 
-  getWindow(id, context) {
-    if (id == this.WINDOW_ID_CURRENT) {
-      return currentWindow(context);
-    }
+  get height() {
+    return this.window.outerHeight;
+  }
+
+  get incognito() {
+    return PrivateBrowsingUtils.isWindowPrivate(this.window);
+  }
 
-    for (let window of WindowListManager.browserWindows(true)) {
-      if (this.getId(window) == id) {
-        return window;
-      }
-    }
-    return null;
-  },
+  get alwaysOnTop() {
+    return this.xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ;
+  }
 
-  getState(window) {
+  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;
-  },
+  }
 
-  setState(window, state) {
-    if (state != "fullscreen" && window.fullScreen) {
+  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;
 
@@ -1032,266 +1107,106 @@ global.WindowManager = {
         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) {
+        if (window.windowState !== window.STATE_NORMAL) {
           window.restore();
         }
-        if (window.windowState != window.STATE_NORMAL) {
+        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}`);
     }
-  },
+  }
 
-  convert(extension, window, getInfo) {
-    let xulWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIDocShell)
-                          .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIXULWindow);
+  * getTabs() {
+    let {tabManager} = this.extension;
 
-    let result = {
-      id: this.getId(window),
-      focused: window.document.hasFocus(),
-      top: window.screenY,
-      left: window.screenX,
-      width: window.outerWidth,
-      height: window.outerHeight,
-      incognito: PrivateBrowsingUtils.isWindowPrivate(window),
-      type: this.windowType(window),
-      state: this.getState(window),
-      alwaysOnTop: xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ,
-    };
+    for (let tab of this.window.gBrowser.tabs) {
+      yield tabManager.getWrapper(tab);
+    }
+  }
 
-    if (getInfo && getInfo.populate) {
-      result.tabs = TabManager.for(extension).getTabs(window);
-    }
-
-    return result;
-  },
-
-  // Converts windows returned from SessionStore.getClosedWindowData
-  // into API window objects
-  convertFromSessionStoreClosedData(window, extension) {
+  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.forEach((tab, index) => {
-        result.tabs.push(TabManager.for(extension).convertFromSessionStoreClosedData(tab, window, index));
+      result.tabs = window.tabs.map(tab => {
+        return Tab.convertFromSessionStoreClosedData(extension, tab);
       });
     }
 
     return result;
-  },
-};
-
-// Manages listeners for window opening and closing. A window is
-// considered open when the "load" event fires on it. A window is
-// closed when a "domwindowclosed" notification fires for it.
-global.WindowListManager = {
-  _openListeners: new Set(),
-  _closeListeners: new Set(),
+  }
+}
 
-  // Returns an iterator for all browser windows. Unless |includeIncomplete| is
-  // true, only fully-loaded windows are returned.
-  * browserWindows(includeIncomplete = false) {
-    // The window type parameter is only available once the window's document
-    // element has been created. This means that, when looking for incomplete
-    // browser windows, we need to ignore the type entirely for windows which
-    // haven't finished loading, since we would otherwise skip browser windows
-    // in their early loading stages.
-    // This is particularly important given that the "domwindowcreated" event
-    // fires for browser windows when they're in that in-between state, and just
-    // before we register our own "domwindowcreated" listener.
-
-    let e = Services.wm.getEnumerator("");
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
-
-      let ok = includeIncomplete;
-      if (window.document.readyState == "complete") {
-        ok = window.document.documentElement.getAttribute("windowtype") == "navigator:browser";
-      }
-
-      if (ok) {
-        yield window;
-      }
-    }
-  },
-
-  addOpenListener(listener) {
-    if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
-      Services.ww.registerNotification(this);
-    }
-    this._openListeners.add(listener);
+Object.assign(global, {Tab, Window});
 
-    for (let window of this.browserWindows(true)) {
-      if (window.document.readyState != "complete") {
-        window.addEventListener("load", this);
-      }
-    }
-  },
-
-  removeOpenListener(listener) {
-    this._openListeners.delete(listener);
-    if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
-      Services.ww.unregisterNotification(this);
-    }
-  },
-
-  addCloseListener(listener) {
-    if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
-      Services.ww.registerNotification(this);
-    }
-    this._closeListeners.add(listener);
-  },
+class TabManager extends TabManagerBase {
+  get(tabId, default_ = undefined) {
+    let tab = tabTracker.getTab(tabId, default_);
 
-  removeCloseListener(listener) {
-    this._closeListeners.delete(listener);
-    if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
-      Services.ww.unregisterNotification(this);
-    }
-  },
-
-  handleEvent(event) {
-    event.currentTarget.removeEventListener(event.type, this);
-    let window = event.target.defaultView;
-    if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
-      return;
-    }
-
-    for (let listener of this._openListeners) {
-      listener(window);
-    }
-  },
-
-  observe(window, topic, data) {
-    if (topic == "domwindowclosed") {
-      if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
-        return;
-      }
-
-      window.removeEventListener("load", this);
-      for (let listener of this._closeListeners) {
-        listener(window);
-      }
-    } else {
-      window.addEventListener("load", this);
+    if (tab) {
+      return this.getWrapper(tab);
     }
-  },
-};
-
-// Provides a facility to listen for DOM events across all XUL windows.
-global.AllWindowEvents = {
-  _listeners: new Map(),
+    return default_;
+  }
 
-  // If |type| is a normal event type, invoke |listener| each time
-  // that event fires in any open window. If |type| is "progress", add
-  // a web progress listener that covers all open windows.
-  addListener(type, listener) {
-    if (type == "domwindowopened") {
-      return WindowListManager.addOpenListener(listener);
-    } else if (type == "domwindowclosed") {
-      return WindowListManager.addCloseListener(listener);
-    }
-
-    if (this._listeners.size == 0) {
-      WindowListManager.addOpenListener(this.openListener);
-    }
+  addActiveTabPermission(tab = tabTracker.activeTab) {
+    return super.addActiveTabPermission(tab);
+  }
 
-    if (!this._listeners.has(type)) {
-      this._listeners.set(type, new Set());
-    }
-    let list = this._listeners.get(type);
-    list.add(listener);
-
-    // Register listener on all existing windows.
-    for (let window of WindowListManager.browserWindows()) {
-      this.addWindowListener(window, type, listener);
-    }
-  },
+  revokeActiveTabPermission(tab = tabTracker.activeTab) {
+    return super.revokeActiveTabPermission(tab);
+  }
 
-  removeListener(eventType, listener) {
-    if (eventType == "domwindowopened") {
-      return WindowListManager.removeOpenListener(listener);
-    } else if (eventType == "domwindowclosed") {
-      return WindowListManager.removeCloseListener(listener);
-    }
+  wrapTab(tab) {
+    return new Tab(this.extension, tab, tabTracker.getId(tab));
+  }
+}
 
-    let listeners = this._listeners.get(eventType);
-    listeners.delete(listener);
-    if (listeners.size == 0) {
-      this._listeners.delete(eventType);
-      if (this._listeners.size == 0) {
-        WindowListManager.removeOpenListener(this.openListener);
-      }
-    }
+class WindowManager extends WindowManagerBase {
+  get(windowId, context) {
+    let window = windowTracker.getWindow(windowId, context);
 
-    // Unregister listener from all existing windows.
-    let useCapture = eventType === "focus" || eventType === "blur";
-    for (let window of WindowListManager.browserWindows()) {
-      if (eventType == "progress") {
-        window.gBrowser.removeTabsProgressListener(listener);
-      } else {
-        window.removeEventListener(eventType, listener, useCapture);
-      }
-    }
-  },
+    return this.getWrapper(window);
+  }
 
-  /* eslint-disable mozilla/balanced-listeners */
-  addWindowListener(window, eventType, listener) {
-    let useCapture = eventType === "focus" || eventType === "blur";
-
-    if (eventType == "progress") {
-      window.gBrowser.addTabsProgressListener(listener);
-    } else {
-      window.addEventListener(eventType, listener, useCapture);
+  * getAll() {
+    for (let window of windowTracker.browserWindows()) {
+      yield this.getWrapper(window);
     }
-  },
-  /* eslint-enable mozilla/balanced-listeners */
+  }
 
-  // Runs whenever the "load" event fires for a new window.
-  openListener(window) {
-    for (let [eventType, listeners] of AllWindowEvents._listeners) {
-      for (let listener of listeners) {
-        this.addWindowListener(window, eventType, listener);
-      }
-    }
-  },
-};
+  wrapWindow(window) {
+    return new Window(this.extension, window, windowTracker.getId(window));
+  }
+}
 
-AllWindowEvents.openListener = AllWindowEvents.openListener.bind(AllWindowEvents);
 
-// Subclass of EventManager where we just need to call
-// add/removeEventListener on each XUL window.
-global.WindowEventManager = function(context, name, event, listener) {
-  EventManager.call(this, context, name, fire => {
-    let listener2 = (...args) => listener(fire, ...args);
-    AllWindowEvents.addListener(event, listener2);
-    return () => {
-      AllWindowEvents.removeListener(event, listener2);
-    };
-  });
-};
-
-WindowEventManager.prototype = Object.create(EventManager.prototype);
+extensions.on("startup", (type, extension) => { // eslint-disable-line mozilla/balanced-listeners
+  defineLazyGetter(extension, "tabManager",
+                   () => new TabManager(extension));
+  defineLazyGetter(extension, "windowManager",
+                   () => new WindowManager(extension));
+});
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -17,73 +17,76 @@ var {
 } = ExtensionUtils;
 
 function onXULFrameLoaderCreated({target}) {
   target.messageManager.sendAsyncMessage("AllowScriptsToClose", {});
 }
 
 extensions.registerSchemaAPI("windows", "addon_parent", context => {
   let {extension} = context;
+
+  const {windowManager} = extension;
+
   return {
     windows: {
       onCreated:
       new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
-        fire(WindowManager.convert(extension, window));
+        fire(windowManager.convert(window));
       }).api(),
 
       onRemoved:
       new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
-        fire(WindowManager.getId(window));
+        fire(windowTracker.getId(window));
       }).api(),
 
       onFocusChanged: new EventManager(context, "windows.onFocusChanged", fire => {
         // Keep track of the last windowId used to fire an onFocusChanged event
         let lastOnFocusChangedWindowId;
 
         let listener = event => {
           // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
           // event when switching focus between two Firefox windows.
           Promise.resolve().then(() => {
             let window = Services.focus.activeWindow;
-            let windowId = window ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE;
+            let windowId = window ? windowTracker.getId(window) : Window.WINDOW_ID_NONE;
             if (windowId !== lastOnFocusChangedWindowId) {
               fire(windowId);
               lastOnFocusChangedWindowId = windowId;
             }
           });
         };
-        AllWindowEvents.addListener("focus", listener);
-        AllWindowEvents.addListener("blur", listener);
+        windowTracker.addListener("focus", listener);
+        windowTracker.addListener("blur", listener);
         return () => {
-          AllWindowEvents.removeListener("focus", listener);
-          AllWindowEvents.removeListener("blur", listener);
+          windowTracker.removeListener("focus", listener);
+          windowTracker.removeListener("blur", listener);
         };
       }).api(),
 
       get: function(windowId, getInfo) {
-        let window = WindowManager.getWindow(windowId, context);
+        let window = windowTracker.getWindow(windowId, context);
         if (!window) {
           return Promise.reject({message: `Invalid window ID: ${windowId}`});
         }
-        return Promise.resolve(WindowManager.convert(extension, window, getInfo));
+        return Promise.resolve(windowManager.convert(window, getInfo));
       },
 
       getCurrent: function(getInfo) {
-        let window = currentWindow(context);
-        return Promise.resolve(WindowManager.convert(extension, window, getInfo));
+        let window = context.currentWindow || windowTracker.topWindow;
+        return Promise.resolve(windowManager.convert(window, getInfo));
       },
 
       getLastFocused: function(getInfo) {
-        let window = WindowManager.topWindow;
-        return Promise.resolve(WindowManager.convert(extension, window, getInfo));
+        let window = windowTracker.topWindow;
+        return Promise.resolve(windowManager.convert(window, getInfo));
       },
 
       getAll: function(getInfo) {
-        let windows = Array.from(WindowListManager.browserWindows(),
-                                 window => WindowManager.convert(extension, window, getInfo));
+        let windows = Array.from(windowManager.getAll(), win => win.convert(getInfo));
+
         return Promise.resolve(windows);
       },
 
       create: function(createData) {
         let needResize = (createData.left !== null || createData.top !== null ||
                           createData.width !== null || createData.height !== null);
 
         if (needResize) {
@@ -105,17 +108,17 @@ extensions.registerSchemaAPI("windows", 
           if (createData.url !== null) {
             return Promise.reject({message: "`tabId` may not be used in conjunction with `url`"});
           }
 
           if (createData.allowScriptsToClose) {
             return Promise.reject({message: "`tabId` may not be used in conjunction with `allowScriptsToClose`"});
           }
 
-          let tab = TabManager.getTab(createData.tabId, context);
+          let tab = tabTracker.getTab(createData.tabId);
 
           // Private browsing tabs can only be moved to private browsing
           // windows.
           let incognito = PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser);
           if (createData.incognito !== null && createData.incognito != incognito) {
             return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
           }
           createData.incognito = incognito;
@@ -155,80 +158,81 @@ extensions.registerSchemaAPI("windows", 
         let {allowScriptsToClose, url} = createData;
         if (allowScriptsToClose === null) {
           allowScriptsToClose = typeof url === "string" && url.startsWith("moz-extension://");
         }
 
         let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
                                             features.join(","), args);
 
-        WindowManager.updateGeometry(window, createData);
+        let win = windowManager.getWrapper(window);
+        win.updateGeometry(createData);
 
         // TODO: focused, type
 
         return new Promise(resolve => {
           window.addEventListener("load", function listener() {
             window.removeEventListener("load", listener);
             if (["maximized", "normal"].includes(createData.state)) {
               window.document.documentElement.setAttribute("sizemode", createData.state);
             }
             resolve(promiseObserved("browser-delayed-startup-finished", win => win == window));
           });
         }).then(() => {
           // Some states only work after delayed-startup-finished
           if (["minimized", "fullscreen", "docked"].includes(createData.state)) {
-            WindowManager.setState(window, createData.state);
+            win.state = createData.state;
           }
           if (allowScriptsToClose) {
             for (let {linkedBrowser} of window.gBrowser.tabs) {
               onXULFrameLoaderCreated({target: linkedBrowser});
               linkedBrowser.addEventListener( // eslint-disable-line mozilla/balanced-listeners
                                              "XULFrameLoaderCreated", onXULFrameLoaderCreated);
             }
           }
-          return WindowManager.convert(extension, window, {populate: true});
+          return win.convert({populate: true});
         });
       },
 
       update: function(windowId, updateInfo) {
         if (updateInfo.state !== null && updateInfo.state != "normal") {
           if (updateInfo.left !== null || updateInfo.top !== null ||
               updateInfo.width !== null || updateInfo.height !== null) {
             return Promise.reject({message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`});
           }
         }
 
-        let window = WindowManager.getWindow(windowId, context);
+        let win = windowManager.get(windowId, context);
         if (updateInfo.focused) {
-          Services.focus.activeWindow = window;
+          Services.focus.activeWindow = win.window;
         }
 
         if (updateInfo.state !== null) {
-          WindowManager.setState(window, updateInfo.state);
+          win.state = updateInfo.state;
         }
 
         if (updateInfo.drawAttention) {
           // Bug 1257497 - Firefox can't cancel attention actions.
-          window.getAttention();
+          win.window.getAttention();
         }
 
-        WindowManager.updateGeometry(window, updateInfo);
+        win.updateGeometry(updateInfo);
 
         // TODO: All the other properties, focused=false...
 
-        return Promise.resolve(WindowManager.convert(extension, window));
+        return Promise.resolve(win.convert());
       },
 
       remove: function(windowId) {
-        let window = WindowManager.getWindow(windowId, context);
+        let window = windowTracker.getWindow(windowId, context);
         window.close();
 
         return new Promise(resolve => {
           let listener = () => {
-            AllWindowEvents.removeListener("domwindowclosed", listener);
+            windowTracker.removeListener("domwindowclosed", listener);
             resolve();
           };
-          AllWindowEvents.addListener("domwindowclosed", listener);
+          windowTracker.addListener("domwindowclosed", listener);
         });
       },
     },
   };
 });
--- a/browser/components/extensions/test/browser/browser_ext_currentWindow.js
+++ b/browser/components/extensions/test/browser/browser_ext_currentWindow.js
@@ -85,20 +85,20 @@ add_task(function* () {
       "popup.js": genericChecker,
     },
 
     background: genericChecker,
   });
 
   yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
 
-  let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+  let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
-  let winId1 = WindowManager.getId(win1);
-  let winId2 = WindowManager.getId(win2);
+  let winId1 = windowTracker.getId(win1);
+  let winId2 = windowTracker.getId(win2);
 
   function* checkWindow(kind, winId, name) {
     extension.sendMessage(kind + "-check-current1");
     is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 1) [${kind}]`);
     extension.sendMessage(kind + "-check-current2");
     is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 2) [${kind}]`);
     extension.sendMessage(kind + "-check-current3");
     is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 3) [${kind}]`);
--- a/browser/components/extensions/test/browser/browser_ext_getViews.js
+++ b/browser/components/extensions/test/browser/browser_ext_getViews.js
@@ -99,20 +99,20 @@ add_task(function* () {
 
     background: genericChecker,
   });
 
   yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
 
   info("started");
 
-  let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+  let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
-  let winId1 = WindowManager.getId(win1);
-  let winId2 = WindowManager.getId(win2);
+  let winId1 = windowTracker.getId(win1);
+  let winId2 = windowTracker.getId(win2);
 
   function* openTab(winId) {
     extension.sendMessage("background-open-tab", winId);
     yield extension.awaitMessage("tab-ready");
   }
 
   function* checkViews(kind, tabCount, popupCount, kindCount, windowId = undefined, windowCount = 0) {
     extension.sendMessage(kind + "-check-views", windowId);
--- a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
@@ -27,18 +27,18 @@ add_task(function* test_sessions_get_rec
     background,
   });
 
   // Open a private browsing window.
   let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true});
 
   yield extension.startup();
 
-  let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
-  let privateWinId = WindowManager.getId(privateWin);
+  let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+  let privateWinId = windowTracker.getId(privateWin);
 
   extension.sendMessage("check-sessions");
   let recentlyClosed = yield extension.awaitMessage("recentlyClosed");
   recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
 
   // Open and close two tabs in the private window
   let tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com");
   yield BrowserTestUtils.removeTab(tab);
--- a/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
@@ -50,20 +50,20 @@ add_task(function* test_sessions_restore
 
   function* assertNotificationCount(expected) {
     let notificationCount = yield extension.awaitMessage("notificationCount");
     is(notificationCount, expected, "the expected number of notifications was fired");
   }
 
   yield extension.startup();
 
-  let {Management: {global: {WindowManager, TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+  let {Management: {global: {windowTracker, tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
   function checkLocalTab(tab, expectedUrl) {
-    let realTab = TabManager.getTab(tab.id);
+    let realTab = tabTracker.getTab(tab.id);
     let tabState = JSON.parse(SessionStore.getTabState(realTab));
     is(tabState.entries[0].url, expectedUrl, "restored tab has the expected url");
   }
 
   yield extension.awaitMessage("ready");
 
   let win = yield BrowserTestUtils.openNewBrowserWindow();
   yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:config");
@@ -87,17 +87,17 @@ add_task(function* test_sessions_restore
 
   is(restored.length, 1, "restore returned the expected number of sessions");
   is(restored[0].window.tabs.length, 3, "restore returned a window with the expected number of tabs");
   checkLocalTab(restored[0].window.tabs[0], "about:config");
   checkLocalTab(restored[0].window.tabs[1], "about:robots");
   checkLocalTab(restored[0].window.tabs[2], "about:mozilla");
 
   // Close the window again.
-  let window = WindowManager.getWindow(restored[0].window.id);
+  let window = windowTracker.getWindow(restored[0].window.id);
   yield BrowserTestUtils.closeWindow(window);
   yield assertNotificationCount(3);
 
   // Restore the window using the sessionId.
   extension.sendMessage("check-sessions");
   recentlyClosed = yield extension.awaitMessage("recentlyClosed");
   extension.sendMessage("restore", recentlyClosed[0].window.sessionId);
   yield assertNotificationCount(4);
@@ -105,17 +105,17 @@ add_task(function* test_sessions_restore
 
   is(restored.length, 1, "restore returned the expected number of sessions");
   is(restored[0].window.tabs.length, 3, "restore returned a window with the expected number of tabs");
   checkLocalTab(restored[0].window.tabs[0], "about:config");
   checkLocalTab(restored[0].window.tabs[1], "about:robots");
   checkLocalTab(restored[0].window.tabs[2], "about:mozilla");
 
   // Close the window again.
-  window = WindowManager.getWindow(restored[0].window.id);
+  window = windowTracker.getWindow(restored[0].window.id);
   yield BrowserTestUtils.closeWindow(window);
   // notificationCount = yield extension.awaitMessage("notificationCount");
   yield assertNotificationCount(5);
 
   // Open and close a tab.
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots");
   yield TabStateFlusher.flush(tab.linkedBrowser);
   yield BrowserTestUtils.removeTab(tab);
@@ -127,34 +127,34 @@ add_task(function* test_sessions_restore
   restored = yield extension.awaitMessage("restored");
 
   is(restored.length, 1, "restore returned the expected number of sessions");
   tab = restored[0].tab;
   ok(tab, "restore returned a tab");
   checkLocalTab(tab, "about:robots");
 
   // Close the tab again.
-  let realTab = TabManager.getTab(tab.id);
+  let realTab = tabTracker.getTab(tab.id);
   yield BrowserTestUtils.removeTab(realTab);
   yield assertNotificationCount(8);
 
   // Restore the tab using the sessionId.
   extension.sendMessage("check-sessions");
   recentlyClosed = yield extension.awaitMessage("recentlyClosed");
   extension.sendMessage("restore", recentlyClosed[0].tab.sessionId);
   yield assertNotificationCount(9);
   restored = yield extension.awaitMessage("restored");
 
   is(restored.length, 1, "restore returned the expected number of sessions");
   tab = restored[0].tab;
   ok(tab, "restore returned a tab");
   checkLocalTab(tab, "about:robots");
 
   // Close the tab again.
-  realTab = TabManager.getTab(tab.id);
+  realTab = tabTracker.getTab(tab.id);
   yield BrowserTestUtils.removeTab(realTab);
   yield assertNotificationCount(10);
 
   // Try to restore something with an invalid sessionId.
   extension.sendMessage("restore-reject");
   restored = yield extension.awaitMessage("restore-rejected");
 
   yield extension.unload();
--- a/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
@@ -156,19 +156,19 @@ add_task(function* () {
     manifest: {
       "permissions": ["tabs"],
     },
 
     background,
   });
 
   extension.onMessage("change-tab", (tabId, attr, on) => {
-    let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+    let {Management: {global: {tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
-    let tab = TabManager.getTab(tabId);
+    let tab = tabTracker.getTab(tabId);
 
     if (attr == "muted") {
       // Ideally we'd simulate a click on the tab audio icon for this, but the
       // handler relies on CSS :hover states, which are complicated and fragile
       // to simulate.
       if (tab.muted != on) {
         tab.toggleMuteAudio();
       }
@@ -179,17 +179,17 @@ add_task(function* () {
       } else {
         browser.audioPlaybackStopped();
       }
     } else if (attr == "duplicate") {
       // This is a bit of a hack. It won't be necessary once we have
       // `tabs.duplicate`.
       let newTab = gBrowser.duplicateTab(tab);
       BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(() => {
-        extension.sendMessage("change-tab-done", tabId, TabManager.getId(newTab));
+        extension.sendMessage("change-tab-done", tabId, tabTracker.getId(newTab));
       });
       return;
     }
 
     extension.sendMessage("change-tab-done", tabId);
   });
 
   yield extension.startup();
--- a/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
@@ -89,24 +89,24 @@ add_task(function* testDuplicateTabLazil
     manifest: {
       "permissions": ["tabs"],
     },
 
     background,
   });
 
   extension.onMessage("duplicate-tab", tabId => {
-    let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+    let {Management: {global: {tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
-    let tab = TabManager.getTab(tabId);
+    let tab = tabTracker.getTab(tabId);
     // This is a bit of a hack to load a tab in the background.
     let newTab = gBrowser.duplicateTab(tab, true);
 
     BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(() => {
-      extension.sendMessage("duplicate-tab-done", TabManager.getId(newTab));
+      extension.sendMessage("duplicate-tab-done", tabTracker.getId(newTab));
     });
   });
 
   yield extension.startup();
   yield extension.awaitFinish("tabs.hasCorrectTabTitle");
   yield extension.unload();
 });
 
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
@@ -16,17 +16,17 @@ add_task(function* testExecuteScriptAtOn
 
   function background() {
     // Using variables to prevent listeners from running more than once, instead
     // of removing the listener. This is to minimize any IPC, since the bug that
     // is being tested is sensitive to timing.
     let ignore = false;
     let url;
     browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
-      if (changeInfo.status === "loading" && tab.url === url && !ignore) {
+      if (url && changeInfo.status === "loading" && tab.url === url && !ignore) {
         ignore = true;
         browser.tabs.executeScript(tabId, {
           code: "document.URL",
         }).then(results => {
           browser.test.assertEq(url, results[0], "Content script should run");
           browser.test.notifyPass("executeScript-at-onUpdated");
         }, error => {
           browser.test.fail(`Unexpected error: ${error} :: ${error.stack}`);
--- a/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
@@ -184,24 +184,24 @@ add_task(function* () {
     manifest: {
       "permissions": ["tabs"],
     },
 
     background,
   });
 
   extension.onMessage("msg", (id, msg, ...args) => {
-    let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+    let {Management: {global: {tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
     let resp;
     if (msg == "get-zoom") {
-      let tab = TabManager.getTab(args[0]);
+      let tab = tabTracker.getTab(args[0]);
       resp = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
     } else if (msg == "set-zoom") {
-      let tab = TabManager.getTab(args[0]);
+      let tab = tabTracker.getTab(args[0]);
       ZoomManager.setZoomForBrowser(tab.linkedBrowser);
     } else if (msg == "enlarge") {
       FullZoom.enlarge();
     } else if (msg == "site-specific") {
       if (args[0] == null) {
         SpecialPowers.clearUserPref(SITE_SPECIFIC_PREF);
       } else {
         SpecialPowers.setBoolPref(SITE_SPECIFIC_PREF, args[0]);
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
@@ -1,23 +1,23 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(function* testWebNavigationGetNonExistentTab() {
   let extension = ExtensionTestUtils.loadExtension({
     background: async function() {
-      // There is no "tabId = 0" because the id assigned by TabManager (defined in ext-utils.js)
+      // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-utils.js)
       // starts from 1.
       await browser.test.assertRejects(
         browser.webNavigation.getAllFrames({tabId: 0}),
         "Invalid tab ID: 0",
         "getAllFrames rejected Promise should pass the expected error");
 
-      // There is no "tabId = 0" because the id assigned by TabManager (defined in ext-utils.js)
+      // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-utils.js)
       // starts from 1, processId is currently marked as optional and it is ignored.
       await browser.test.assertRejects(
         browser.webNavigation.getFrame({tabId: 0, frameId: 15, processId: 20}),
         "Invalid tab ID: 0",
         "getFrame rejected Promise should pass the expected error");
 
       browser.test.sendMessage("getNonExistentTab.done");
     },
--- a/browser/components/extensions/test/browser/browser_ext_windows_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_windows_events.js
@@ -56,20 +56,20 @@ add_task(function* testWindowsEvents() {
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${background})()`,
   });
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
-  let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+  let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
   let currentWindow = window;
-  let currentWindowId = WindowManager.getId(currentWindow);
+  let currentWindowId = windowTracker.getId(currentWindow);
   info(`Current window ID: ${currentWindowId}`);
 
   info(`Create browser window 1`);
   let win1 = yield BrowserTestUtils.openNewBrowserWindow();
   let win1Id = yield extension.awaitMessage("window-created");
   info(`Window 1 ID: ${win1Id}`);
 
   // This shouldn't be necessary, but tests intermittently fail, so let's give
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -25,18 +25,17 @@ module.exports = { // eslint-disable-lin
     "isValidCookieStoreId": true,
     "NetUtil": true,
     "openOptionsPage": true,
     "require": false,
     "runSafe": true,
     "runSafeSync": true,
     "runSafeSyncWithoutClone": true,
     "Services": true,
-    "TabManager": true,
-    "WindowListManager": true,
+    "tabTracker": false,
     "XPCOMUtils": true,
   },
 
   "rules": {
     // Rules from the mozilla plugin
     "mozilla/balanced-listeners": "error",
     "mozilla/no-aArgs": "error",
     "mozilla/no-cpows-in-tests": "warn",
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -186,18 +186,18 @@ ProxyMessenger = {
    * @param {Extension} extension
    * @returns {object|null} The message manager matching the recipient if found.
    */
   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 `TabManager` exists.
-      let tab = apiManager.global.TabManager.getTab(tabId, null, null);
+      // need to check whether `tabTracker` exists.
+      let tab = apiManager.global.tabTracker.getTab(tabId, null);
       return tab && tab.linkedBrowser.messageManager;
     }
 
     // runtime.sendMessage / runtime.connect
     let extension = GlobalManager.extensionMap.get(recipient.extensionId);
     if (extension) {
       return extension.parentMessageManager;
     }
@@ -349,22 +349,29 @@ class ExtensionPageContextParent extends
     extension.emit("extension-proxy-context-load", this);
   }
 
   // The window that contains this context. This may change due to moving tabs.
   get xulWindow() {
     return this.xulBrowser.ownerGlobal;
   }
 
+  get currentWindow() {
+    if (this.viewType !== "background") {
+      return this.xulWindow;
+    }
+  }
+
   get windowId() {
-    if (!apiManager.global.WindowManager || this.viewType == "background") {
-      return;
+    let {currentWindow} = this;
+    let {windowTracker} = apiManager.global;
+
+    if (currentWindow && windowTracker) {
+      return windowTracker.getId(currentWindow);
     }
-    // viewType popup or tab:
-    return apiManager.global.WindowManager.getId(this.xulWindow);
   }
 
   get tabId() {
     let {getBrowserInfo} = apiManager.global;
 
     if (getBrowserInfo) {
       // This is currently only available on desktop Firefox.
       return getBrowserInfo(this.xulBrowser).tabId;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -0,0 +1,547 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* exported TabTrackerBase, TabManagerBase, TabBase, WindowTrackerBase, WindowManagerBase, WindowBase */
+
+var EXPORTED_SYMBOLS = ["TabTrackerBase", "TabManagerBase", "TabBase", "WindowTrackerBase", "WindowManagerBase", "WindowBase"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+const {
+  DefaultMap,
+  DefaultWeakMap,
+  EventEmitter,
+  ExtensionError,
+} = ExtensionUtils;
+
+class TabBase {
+  constructor(extension, tab, id) {
+    this.extension = extension;
+    this.tabManager = extension.tabManager;
+    this.id = id;
+    this.tab = tab;
+    this.activeTabWindowId = null;
+  }
+
+  get innerWindowId() {
+    return this.browser.innerWindowId;
+  }
+
+  get hasTabPermission() {
+    return this.extension.hasPermission("tabs") || this.hasActiveTabPermission;
+  }
+
+  get hasActiveTabPermission() {
+    return (this.extension.hasPermission("activeTab") &&
+            this.activeTabWindowId !== null &&
+            this.activeTabWindowId === this.innerWindowId);
+  }
+
+  get incognito() {
+    return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+  }
+
+  get _url() {
+    return this.browser.currentURI.spec;
+  }
+
+  get url() {
+    if (this.hasTabPermission) {
+      return this._url;
+    }
+  }
+
+  get uri() {
+    if (this.hasTabPermission) {
+      return this.browser.currentURI;
+    }
+  }
+
+  get _title() {
+    return this.browser.contentTitle || this.tab.label;
+  }
+
+
+  get title() {
+    if (this.hasTabPermission) {
+      return this._title;
+    }
+  }
+
+  get favIconUrl() {
+    if (this.hasTabPermission) {
+      return this._favIconUrl;
+    }
+  }
+
+  matches(queryInfo) {
+    const PROPS = ["active", "audible", "cookieStoreId", "highlighted", "index", "pinned", "status", "title"];
+
+    if (PROPS.some(prop => queryInfo[prop] !== null && queryInfo[prop] !== this[prop])) {
+      return false;
+    }
+
+    if (queryInfo.muted !== null) {
+      if (queryInfo.muted !== this.mutedInfo.muted) {
+        return false;
+      }
+    }
+
+    if (queryInfo.url && !queryInfo.url.matches(this.uri)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  convert() {
+    let result = {
+      id: this.id,
+      index: this.index,
+      windowId: this.windowId,
+      selected: this.selected,
+      highlighted: this.selected,
+      active: this.selected,
+      pinned: this.pinned,
+      status: this.status,
+      incognito: this.incognito,
+      width: this.width,
+      height: this.height,
+      audible: this.audible,
+      mutedInfo: this.mutedInfo,
+    };
+
+    if (this.extension.hasPermission("cookies")) {
+      result.cookieStoreId = this.cookieStoreId;
+    }
+
+    if (this.hasTabPermission) {
+      for (let prop of ["url", "title", "favIconUrl"]) {
+        let val = this[`_${prop}`];
+        if (val) {
+          result[prop] = val;
+        }
+      }
+    }
+
+    return result;
+  }
+}
+
+// Note: These must match the values in windows.json.
+const WINDOW_ID_NONE = -1;
+const WINDOW_ID_CURRENT = -2;
+
+class WindowBase {
+  constructor(extension, window, id) {
+    this.extension = extension;
+    this.window = window;
+    this.id = id;
+  }
+
+  get xulWindow() {
+    return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDocShell)
+               .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIXULWindow);
+  }
+
+  isCurrentFor(context) {
+    if (context && context.currentWindow) {
+      return this.window === context.currentWindow;
+    }
+    return this.isLastFocused;
+  }
+
+  get type() {
+    let {chromeFlags} = this.xulWindow;
+
+    if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) {
+      return "popup";
+    }
+
+    return "normal";
+  }
+
+  convert(getInfo) {
+    let result = {
+      id: this.id,
+      focused: this.focused,
+      top: this.top,
+      left: this.left,
+      width: this.width,
+      height: this.height,
+      incognito: this.incognito,
+      type: this.type,
+      state: this.state,
+      alwaysOnTop: this.alwaysOnTop,
+    };
+
+    if (getInfo && getInfo.populate) {
+      result.tabs = Array.from(this.getTabs(), tab => tab.convert());
+    }
+
+    return result;
+  }
+
+  matches(queryInfo, context) {
+    if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow !== this.isLastFocused) {
+      return false;
+    }
+
+    if (queryInfo.windowType !== null && queryInfo.windowType !== this.type) {
+      return false;
+    }
+
+    if (queryInfo.windowId !== null) {
+      if (queryInfo.windowId === WINDOW_ID_CURRENT) {
+        if (!this.isCurrentFor(context)) {
+          return false;
+        }
+      } else if (queryInfo.windowId !== this.id) {
+        return false;
+      }
+    }
+
+    if (queryInfo.currentWindow !== null && queryInfo.currentWindow !== this.isCurrentFor(context)) {
+      return false;
+    }
+
+    return true;
+  }
+}
+
+Object.assign(WindowBase, {WINDOW_ID_NONE, WINDOW_ID_CURRENT});
+
+class TabTrackerBase extends EventEmitter {
+  on(...args) {
+    if (!this.initialized) {
+      this.init();
+    }
+
+    return super.on(...args); // eslint-disable-line mozilla/balanced-listeners
+  }
+}
+
+class WindowTrackerBase extends EventEmitter {
+  constructor() {
+    super();
+
+    this.handleWindowOpened = this.handleWindowOpened.bind(this);
+
+    this._openListeners = new Set();
+    this._closeListeners = new Set();
+
+    this._listeners = new DefaultMap(() => new Set());
+
+    this._windowIds = new DefaultWeakMap(window => {
+      window.QueryInterface(Ci.nsIInterfaceRequestor);
+
+      return window.getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+    });
+  }
+
+  isBrowserWindow(window) {
+    let {documentElement} = window.document;
+
+    return documentElement.getAttribute("windowtype") === "navigator:browser";
+  }
+
+  // Returns an iterator for all browser windows. Unless |includeIncomplete| is
+  // true, only fully-loaded windows are returned.
+  * browserWindows(includeIncomplete = false) {
+    // The window type parameter is only available once the window's document
+    // element has been created. This means that, when looking for incomplete
+    // browser windows, we need to ignore the type entirely for windows which
+    // haven't finished loading, since we would otherwise skip browser windows
+    // in their early loading stages.
+    // This is particularly important given that the "domwindowcreated" event
+    // fires for browser windows when they're in that in-between state, and just
+    // before we register our own "domwindowcreated" listener.
+
+    let e = Services.wm.getEnumerator("");
+    while (e.hasMoreElements()) {
+      let window = e.getNext();
+
+      let ok = includeIncomplete;
+      if (window.document.readyState === "complete") {
+        ok = this.isBrowserWindow(window);
+      }
+
+      if (ok) {
+        yield window;
+      }
+    }
+  }
+
+  get topWindow() {
+    return Services.wm.getMostRecentWindow("navigator:browser");
+  }
+
+  getId(window) {
+    return this._windowIds.get(window);
+  }
+
+  getCurrentWindow(context) {
+    let {xulWindow} = context;
+    if (xulWindow && context.viewType !== "background") {
+      return xulWindow;
+    }
+    return this.topWindow;
+  }
+
+  getWindow(id, context) {
+    if (id === WINDOW_ID_CURRENT) {
+      return this.getCurrentWindow(context);
+    }
+
+    for (let window of this.browserWindows(true)) {
+      if (this.getId(window) === id) {
+        return window;
+      }
+    }
+    throw new ExtensionError(`Invalid window ID: ${id}`);
+  }
+
+  get haveListeners() {
+    return this._openListeners.size > 0 || this._closeListeners.size > 0;
+  }
+
+  addOpenListener(listener) {
+    if (!this.haveListeners) {
+      Services.ww.registerNotification(this);
+    }
+
+    this._openListeners.add(listener);
+
+    for (let window of this.browserWindows(true)) {
+      if (window.document.readyState !== "complete") {
+        window.addEventListener("load", this);
+      }
+    }
+  }
+
+  removeOpenListener(listener) {
+    this._openListeners.delete(listener);
+
+    if (!this.haveListeners) {
+      Services.ww.unregisterNotification(this);
+    }
+  }
+
+  addCloseListener(listener) {
+    if (!this.haveListeners) {
+      Services.ww.registerNotification(this);
+    }
+
+    this._closeListeners.add(listener);
+  }
+
+  removeCloseListener(listener) {
+    this._closeListeners.delete(listener);
+
+    if (!this.haveListeners) {
+      Services.ww.unregisterNotification(this);
+    }
+  }
+
+  handleEvent(event) {
+    if (event.type === "load") {
+      event.currentTarget.removeEventListener(event.type, this);
+
+      let window = event.target.defaultView;
+      if (!this.isBrowserWindow(window)) {
+        return;
+      }
+
+      for (let listener of this._openListeners) {
+        try {
+          listener(window);
+        } catch (e) {
+          Cu.reportError(e);
+        }
+      }
+    }
+  }
+
+  observe(window, topic, data) {
+    if (topic === "domwindowclosed") {
+      if (!this.isBrowserWindow(window)) {
+        return;
+      }
+
+      window.removeEventListener("load", this);
+      for (let listener of this._closeListeners) {
+        try {
+          listener(window);
+        } catch (e) {
+          Cu.reportError(e);
+        }
+      }
+    } else if (topic === "domwindowopened") {
+      window.addEventListener("load", this);
+    }
+  }
+
+  // If |type| is a normal event type, invoke |listener| each time
+  // that event fires in any open window. If |type| is "progress", add
+  // a web progress listener that covers all open windows.
+  addListener(type, listener) {
+    if (type === "domwindowopened") {
+      return this.addOpenListener(listener);
+    } else if (type === "domwindowclosed") {
+      return this.addCloseListener(listener);
+    }
+
+    if (this._listeners.size === 0) {
+      this.addOpenListener(this.handleWindowOpened);
+    }
+
+    this._listeners.get(type).add(listener);
+
+    // Register listener on all existing windows.
+    for (let window of this.browserWindows()) {
+      this.addWindowListener(window, type, listener);
+    }
+  }
+
+  removeListener(eventType, listener) {
+    if (eventType === "domwindowopened") {
+      return this.removeOpenListener(listener);
+    } else if (eventType === "domwindowclosed") {
+      return this.removeCloseListener(listener);
+    }
+
+    let listeners = this._listeners.get(eventType);
+    listeners.delete(listener);
+
+    if (listeners.size === 0) {
+      this._listeners.delete(eventType);
+      if (this._listeners.size === 0) {
+        this.removeOpenListener(this.handleWindowOpened);
+      }
+    }
+
+    // Unregister listener from all existing windows.
+    let useCapture = eventType === "focus" || eventType === "blur";
+    for (let window of this.browserWindows()) {
+      if (eventType === "progress") {
+        this.removeProgressListener(window, listener);
+      } else {
+        window.removeEventListener(eventType, listener, useCapture);
+      }
+    }
+  }
+
+  addWindowListener(window, eventType, listener) {
+    let useCapture = eventType === "focus" || eventType === "blur";
+
+    if (eventType === "progress") {
+      this.addProgressListener(window, listener);
+    } else {
+      window.addEventListener(eventType, listener, useCapture);
+    }
+  }
+
+  handleWindowOpened(window) {
+    for (let [eventType, listeners] of this._listeners) {
+      for (let listener of listeners) {
+        this.addWindowListener(window, eventType, listener);
+      }
+    }
+  }
+}
+
+class TabManagerBase {
+  constructor(extension) {
+    this.extension = extension;
+
+    this._tabs = new DefaultWeakMap(tab => this.wrapTab(tab));
+  }
+
+  addActiveTabPermission(tab) {
+    if (this.extension.hasPermission("activeTab")) {
+      // Note that, unlike Chrome, we don't currently clear this permission with
+      // the tab navigates. If the inner window is revived from BFCache before
+      // we've granted this permission to a new inner window, the extension
+      // maintains its permissions for it.
+      tab = this.getWrapper(tab);
+      tab.activeTabWindowId = tab.innerWindowId;
+    }
+  }
+
+  revokeActiveTabPermission(tab) {
+    this.getWrapper(tab).activeTabWindowId = null;
+  }
+
+  // Returns true if the extension has the "activeTab" permission for this tab.
+  // This is somewhat more permissive than the generic "tabs" permission, as
+  // checked by |hasTabPermission|, in that it also allows programmatic script
+  // injection without an explicit host permission.
+  hasActiveTabPermission(tab) {
+    return this.getWrapper(tab).hasActiveTabPermission;
+  }
+
+  hasTabPermission(tab) {
+    return this.getWrapper(tab).hasTabPermission;
+  }
+
+  getWrapper(tab) {
+    return this._tabs.get(tab);
+  }
+
+  * query(queryInfo = null, context = null) {
+    for (let window of this.extension.windowManager.query(queryInfo, context)) {
+      for (let tab of window.getTabs()) {
+        if (!queryInfo || tab.matches(queryInfo)) {
+          yield tab;
+        }
+      }
+    }
+  }
+
+  convert(tab) {
+    return this.getWrapper(tab).convert();
+  }
+
+  wrapTab(tab) {
+    throw new Error("Not implemented");
+  }
+}
+
+class WindowManagerBase {
+  constructor(extension) {
+    this.extension = extension;
+
+    this._windows = new DefaultWeakMap(window => this.wrapWindow(window));
+  }
+
+  convert(window, ...args) {
+    return this.getWrapper(window).convert(...args);
+  }
+
+  getWrapper(tab) {
+    return this._windows.get(tab);
+  }
+
+  * query(queryInfo = null, context = null) {
+    for (let window of this.getAll()) {
+      if (!queryInfo || window.matches(queryInfo, context)) {
+        yield window;
+      }
+    }
+  }
+}
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -416,24 +416,21 @@ extensions.registerSchemaAPI("cookies", 
           });
         }
 
         return Promise.resolve(null);
       },
 
       getAllCookieStores: function() {
         let data = {};
-        for (let window of WindowListManager.browserWindows()) {
-          let tabs = TabManager.for(extension).getTabs(window);
-          for (let tab of tabs) {
-            if (!(tab.cookieStoreId in data)) {
-              data[tab.cookieStoreId] = [];
-            }
-            data[tab.cookieStoreId].push(tab.id);
+        for (let tab of extension.tabManager.query()) {
+          if (!(tab.cookieStoreId in data)) {
+            data[tab.cookieStoreId] = [];
           }
+          data[tab.cookieStoreId].push(tab.id);
         }
 
         let result = [];
         for (let key in data) {
           result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE});
         }
         return Promise.resolve(result);
       },
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -147,44 +147,46 @@ function convertGetFrameResult(tabId, da
     url: data.url,
     tabId,
     frameId: ExtensionManagement.getFrameId(data.windowId),
     parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
   };
 }
 
 extensions.registerSchemaAPI("webNavigation", "addon_parent", context => {
+  let {tabManager} = context.extension;
+
   return {
     webNavigation: {
       onTabReplaced: ignoreEvent(context, "webNavigation.onTabReplaced"),
       onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
       onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
       onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
       onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
       onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
       onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
       onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
       onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
       getAllFrames(details) {
-        let tab = TabManager.getTab(details.tabId, context);
+        let tab = tabManager.get(details.tabId);
 
-        let {innerWindowID, messageManager} = tab.linkedBrowser;
+        let {innerWindowID, messageManager} = tab.browser;
         let recipient = {innerWindowID};
 
         return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient})
                       .then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
       },
       getFrame(details) {
-        let tab = TabManager.getTab(details.tabId, context);
+        let tab = tabManager.get(details.tabId);
 
         let recipient = {
-          innerWindowID: tab.linkedBrowser.innerWindowID,
+          innerWindowID: tab.browser.innerWindowID,
         };
 
-        let mm = tab.linkedBrowser.messageManager;
+        let mm = tab.browser.messageManager;
         return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, {recipient})
                       .then((result) => {
                         return result ?
                           convertGetFrameResult(details.tabId, result) :
                           Promise.reject({message: `No frame found with frameId: ${details.frameId}`});
                       });
       },
     },
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -9,16 +9,17 @@ EXTRA_JS_MODULES += [
     'ExtensionAPI.jsm',
     'ExtensionChild.jsm',
     'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
     'ExtensionManagement.jsm',
     'ExtensionParent.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageSync.jsm',
+    'ExtensionTabs.jsm',
     'ExtensionUtils.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'NativeMessaging.jsm',
     'Schemas.jsm',
 ]
 
 EXTRA_PP_COMPONENTS += [