Bug 1493193 - Add an initial UrlbarProvidersManager implementation. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 28 Sep 2018 14:18:04 +0000
changeset 494434 fb7be104d610f09af0d150465c93121491a945fd
parent 494433 2cdac78b6bcd17cae2ca079a9bf995b00b6c888b
child 494435 892a0ef879980cd26764f7a6c96501356c35ea14
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1493193
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1493193 - Add an initial UrlbarProvidersManager implementation. r=adw Differential Revision: https://phabricator.services.mozilla.com/D6508
browser/base/content/tabbrowser.js
browser/base/content/test/static/browser_all_files_referenced.js
browser/components/urlbar/UrlbarMatch.jsm
browser/components/urlbar/UrlbarPrefs.jsm
browser/components/urlbar/UrlbarProviderOpenTabs.jsm
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/UrlbarUtils.jsm
browser/components/urlbar/moz.build
browser/components/urlbar/tests/unit/head.js
browser/components/urlbar/tests/unit/test_providerOpenTabs.js
browser/components/urlbar/tests/unit/test_providersManager.js
browser/components/urlbar/tests/unit/xpcshell.ini
toolkit/components/places/SQLFunctions.h
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/mozIPlacesAutoComplete.idl
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
tools/lint/eslint/modules.json
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -15,19 +15,20 @@ const FAVICON_DEFAULTS = {
   "about:welcome": "chrome://branding/content/icon32.png",
   "about:privatebrowsing": "chrome://browser/skin/privatebrowsing/favicon.svg",
 };
 
 window._gBrowser = {
   init() {
     ChromeUtils.defineModuleGetter(this, "AsyncTabSwitcher",
       "resource:///modules/AsyncTabSwitcher.jsm");
+    ChromeUtils.defineModuleGetter(this, "UrlbarProviderOpenTabs",
+      "resource:///modules/UrlbarProviderOpenTabs.jsm");
 
     XPCOMUtils.defineLazyServiceGetters(this, {
-      _unifiedComplete: ["@mozilla.org/autocomplete/search;1?name=unifiedcomplete", "mozIPlacesAutoComplete"],
       serializationHelper: ["@mozilla.org/network/serialization-helper;1", "nsISerializationHelper"],
       mURIFixup: ["@mozilla.org/docshell/urifixup;1", "nsIURIFixup"],
     });
 
     Services.obs.addObserver(this, "contextual-identity-updated");
 
     Services.els.addSystemEventListener(document, "keydown", this, false);
     if (AppConstants.platform == "macosx") {
@@ -2464,17 +2465,18 @@ window._gBrowser = {
 
       // If the caller opts in, create a lazy browser.
       if (createLazyBrowser) {
         this._createLazyBrowser(t);
 
         if (lazyBrowserURI) {
           // Lazy browser must be explicitly registered so tab will appear as
           // a switch-to-tab candidate in autocomplete.
-          this._unifiedComplete.registerOpenPage(lazyBrowserURI, userContextId);
+          this.UrlbarProviderOpenTabs.registerOpenTab(lazyBrowserURI.spec,
+                                                      userContextId);
           b.registeredOpenURI = lazyBrowserURI;
         }
       } else {
         this._insertBrowser(t, true);
       }
     } catch (e) {
       Cu.reportError("Failed to create tab");
       Cu.reportError(e);
@@ -2932,18 +2934,19 @@ window._gBrowser = {
       browser.webProgress.removeProgressListener(filter);
 
       const listener = this._tabListeners.get(aTab);
       filter.removeProgressListener(listener);
       listener.destroy();
     }
 
     if (browser.registeredOpenURI && !aAdoptedByTab) {
-      this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
-        browser.getAttribute("usercontextid") || 0);
+      let userContextId = browser.getAttribute("usercontextid") || 0;
+      this.UrlbarProviderOpenTabs.unregisterOpenTab(browser.registeredOpenURI.spec,
+                                                    userContextId);
       delete browser.registeredOpenURI;
     }
 
     // We are no longer the primary content area.
     browser.removeAttribute("primary");
 
     // Remove this tab as the owner of any other tabs, since it's going away.
     for (let tab of this.tabs) {
@@ -3219,18 +3222,19 @@ window._gBrowser = {
           this._isBusy = true;
       }
 
       this._swapBrowserDocShells(aOurTab, otherBrowser, Ci.nsIBrowser.SWAP_DEFAULT, stateFlags);
     }
 
     // Unregister the previously opened URI
     if (otherBrowser.registeredOpenURI) {
-      this._unifiedComplete.unregisterOpenPage(otherBrowser.registeredOpenURI,
-        otherBrowser.getAttribute("usercontextid") || 0);
+      let userContextId = otherBrowser.getAttribute("usercontextid") || 0;
+      this.UrlbarProviderOpenTabs.unregisterOpenTab(otherBrowser.registeredOpenURI.spec,
+                                                    userContextId);
       delete otherBrowser.registeredOpenURI;
     }
 
     // Handle findbar data (if any)
     let otherFindBar = aOtherTab._findBar;
     if (otherFindBar &&
         otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
       let oldValue = otherFindBar._findField.value;
@@ -4353,18 +4357,19 @@ window._gBrowser = {
   },
 
   destroy() {
     Services.obs.removeObserver(this, "contextual-identity-updated");
 
     for (let tab of this.tabs) {
       let browser = tab.linkedBrowser;
       if (browser.registeredOpenURI) {
-        this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
-          browser.getAttribute("usercontextid") || 0);
+        let userContextId = browser.getAttribute("usercontextid") || 0;
+        this.UrlbarProviderOpenTabs.unregisterOpenTab(browser.registeredOpenURI.spec,
+                                                      userContextId);
         delete browser.registeredOpenURI;
       }
 
       let filter = this._tabFilters.get(tab);
       if (filter) {
         browser.webProgress.removeProgressListener(filter);
 
         let listener = this._tabListeners.get(tab);
@@ -5012,26 +5017,27 @@ class TabProgressListener {
           !isSameDocument) {
         // Removing the tab's image here causes flickering, wait until the load
         // is complete.
         this.mBrowser.mIconURL = null;
       }
 
       let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
       if (this.mBrowser.registeredOpenURI) {
-        gBrowser._unifiedComplete
-          .unregisterOpenPage(this.mBrowser.registeredOpenURI, userContextId);
+        let uri = this.mBrowser.registeredOpenURI;
+        gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab(uri.spec, userContextId);
         delete this.mBrowser.registeredOpenURI;
       }
       // Tabs in private windows aren't registered as "Open" so
       // that they don't appear as switch-to-tab candidates.
       if (!isBlankPageURL(aLocation.spec) &&
           (!PrivateBrowsingUtils.isWindowPrivate(window) ||
             PrivateBrowsingUtils.permanentPrivateBrowsing)) {
-        gBrowser._unifiedComplete.registerOpenPage(aLocation, userContextId);
+        gBrowser.UrlbarProviderOpenTabs.registerOpenTab(aLocation.spec,
+                                                        userContextId);
         this.mBrowser.registeredOpenURI = aLocation;
       }
 
       if (this.mTab != gBrowser.selectedTab) {
         let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this.mTab);
         if (tabCacheIndex != -1) {
           gBrowser._tabLayerCache.splice(tabCacheIndex, 1);
           gBrowser._getSwitcher().cleanUpTabAfterEviction(this.mTab);
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -118,20 +118,16 @@ var whitelist = [
   // browser/extensions/pdfjs/content/web/viewer.js#7450
   {file: "resource://pdf.js/web/debugger.js"},
 
   // resource://app/modules/translation/TranslationContentHandler.jsm
   {file: "resource://app/modules/translation/BingTranslator.jsm"},
   {file: "resource://app/modules/translation/GoogleTranslator.jsm"},
   {file: "resource://app/modules/translation/YandexTranslator.jsm"},
 
-  // The Quantum Bar files are not in use yet, but we plan to start using them
-  // soon in parallel to the old implementation.
-  {file: "resource://app/modules/UrlbarTokenizer.jsm"},
-
   // Starting from here, files in the whitelist are bugs that need fixing.
   // Bug 1339424 (wontfix?)
   {file: "chrome://browser/locale/taskbar.properties",
    platforms: ["linux", "macosx"]},
   // Bug 1356031 (only used by devtools)
   {file: "chrome://global/skin/icons/error-16.png"},
   // Bug 1348526
   {file: "chrome://global/skin/tree/sort-asc-classic.png", platforms: ["linux"]},
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/UrlbarMatch.jsm
@@ -0,0 +1,59 @@
+/* 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";
+
+/**
+ * This module exports a urlbar match class, each representing a single match.
+ * A match is a single search result found by a provider, that can be passed
+ * from the model to the view, through the controller. It is mainly defined by
+ * a type of the match, and a payload, containing the data. A few getters allow
+ * to retrieve information common to all the match types.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarMatch"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Class used to create a match.
+ */
+class UrlbarMatch {
+  /**
+   * Creates a match.
+   * @param {integer} matchType one of UrlbarUtils.MATCHTYPE.* values
+   * @param {object} payload data for this match. A payload should always
+   *        contain a way to extract a final url to visit. The url getter
+   *        should have a case for each of the types.
+   */
+  constructor(matchType, payload) {
+    this.type = matchType;
+    this.payload = payload;
+  }
+
+  /**
+   * Returns a final destination for this match.
+   * Different kind of matches may have different ways to express this value,
+   * and this is a common getter for all of them.
+   * @returns {string} a url to load when this match is confirmed byt the user.
+   */
+  get url() {
+    switch (this.type) {
+      case UrlbarUtils.MATCH_TYPE.TAB_SWITCH:
+        return this.payload.url;
+    }
+    return "";
+  }
+
+  /**
+   * Returns a title that could be used as a label for this match.
+   * @returns {string} The label to show in a simplified title / url view.
+   */
+  get title() {
+    return "";
+  }
+}
--- a/browser/components/urlbar/UrlbarPrefs.jsm
+++ b/browser/components/urlbar/UrlbarPrefs.jsm
@@ -133,25 +133,25 @@ const PREF_TYPES = new Map([
 // Each bucket is an array containing the following indices:
 //   0: The match type of the acceptable entries.
 //   1: available number of slots in this bucket.
 // There are different matchBuckets definition for different contexts, currently
 // a general one (matchBuckets) and a search one (matchBucketsSearch).
 //
 // First buckets. Anything with an Infinity frecency ends up here.
 const DEFAULT_BUCKETS_BEFORE = [
-  [UrlbarUtils.MATCHTYPE.HEURISTIC, 1],
-  [UrlbarUtils.MATCHTYPE.EXTENSION, UrlbarUtils.MAXIMUM_ALLOWED_EXTENSION_MATCHES - 1],
+  [UrlbarUtils.MATCH_GROUP.HEURISTIC, 1],
+  [UrlbarUtils.MATCH_GROUP.EXTENSION, UrlbarUtils.MAXIMUM_ALLOWED_EXTENSION_MATCHES - 1],
 ];
 // => USER DEFINED BUCKETS WILL BE INSERTED HERE <=
 //
 // Catch-all buckets. Anything remaining ends up here.
 const DEFAULT_BUCKETS_AFTER = [
-  [UrlbarUtils.MATCHTYPE.SUGGESTION, Infinity],
-  [UrlbarUtils.MATCHTYPE.GENERAL, Infinity],
+  [UrlbarUtils.MATCH_GROUP.SUGGESTION, Infinity],
+  [UrlbarUtils.MATCH_GROUP.GENERAL, Infinity],
 ];
 
 /**
  * Preferences class.  The exported object is a singleton instance.
  */
 class Preferences {
   /**
    * Constructor
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -0,0 +1,213 @@
+/* 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";
+
+/**
+ * This module exports a provider, returning open tabs matches for the urlbar.
+ * It is also used to register and unregister open tabs.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderOpenTabs"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Log: "resource://gre/modules/Log.jsm",
+  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+  UrlbarMatch: "resource:///modules/UrlbarMatch.jsm",
+  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger",
+  () => Log.repository.getLogger("Places.Urlbar.Provider.OpenTabs"));
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderOpenTabs {
+  constructor() {
+    // Maps the open tabs by userContextId.
+    this.openTabs = new Map();
+    // Maps the running queries by queryContext.
+    this.queries = new Map();
+  }
+
+  /**
+   * Database handle. For performance reasons the temp tables are created and
+   * populated only when a query starts, rather than when a tab is added.
+   * @returns {object} the Sqlite database handle.
+   */
+  async promiseDb() {
+    if (this._db) {
+      return this._db;
+    }
+    let conn = await PlacesUtils.promiseLargeCacheDBConnection();
+    // Create the temp tables to store open pages.
+    UrlbarProvidersManager.runInCriticalSection(async () => {
+      // These should be kept up-to-date with the definition in nsPlacesTables.h.
+      await conn.execute(`
+        CREATE TEMP TABLE IF NOT EXISTS moz_openpages_temp (
+          url TEXT,
+          userContextId INTEGER,
+          open_count INTEGER,
+          PRIMARY KEY (url, userContextId)
+        )
+      `);
+      await conn.execute(`
+        CREATE TEMP TRIGGER IF NOT EXISTS moz_openpages_temp_afterupdate_trigger
+        AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW
+        WHEN NEW.open_count = 0
+        BEGIN
+          DELETE FROM moz_openpages_temp
+          WHERE url = NEW.url
+            AND userContextId = NEW.userContextId;
+        END
+      `);
+    }).catch(Cu.reportError);
+
+    // Populate the table with the current cache contents...
+    for (let [userContextId, urls] of this.openTabs) {
+      for (let url of urls) {
+        await addToMemoryTable(conn, url, userContextId).catch(Cu.reportError);
+      }
+    }
+    return this._db = conn;
+  }
+
+  /**
+   * Returns the name of this provider.
+   * @returns {string} the name of this provider.
+   */
+  get name() {
+    return "OpenTabs";
+  }
+
+  /**
+   * Returns the type of this provider.
+   * @returns {integer} one of the types from UrlbarProvidersManager.TYPE.*
+   */
+  get type() {
+    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+  }
+
+  /**
+   * Registers a tab as open.
+   * @param {string} url Address of the tab
+   * @param {integer} userContextId Containers user context id
+   */
+  registerOpenTab(url, userContextId = 0) {
+    if (!this.openTabs.has(userContextId)) {
+      this.openTabs.set(userContextId, []);
+    }
+    this.openTabs.get(userContextId).push(url);
+    if (this._db) {
+      addToMemoryTable(this._db, url, userContextId);
+    }
+  }
+
+  /**
+   * Unregisters a previously registered open tab.
+   * @param {string} url Address of the tab
+   * @param {integer} userContextId Containers user context id
+   */
+  unregisterOpenTab(url, userContextId = 0) {
+    let openTabs = this.openTabs.get(userContextId);
+    if (openTabs) {
+      let index = openTabs.indexOf(url);
+      if (index != -1) {
+        openTabs.splice(index, 1);
+        if (this._db) {
+          removeFromMemoryTable(this._db, url, userContextId);
+        }
+      }
+    }
+  }
+
+  /**
+   * Starts querying.
+   * @param {object} queryContext The query context object
+   * @param {function} addCallback Callback invoked by the provider to add a new
+   *        match.
+   * @returns {Promise} resolved when the query stops.
+   */
+  async startQuery(queryContext, addCallback) {
+    // TODO:
+    //  * properly search and handle tokens, this is just a mock for now.
+    //  * we won't search openTabs like this usually, the history search will
+    //  * coalesce the temp table with its data.
+    logger.info(`Starting query for ${queryContext.searchString}`);
+    let instance = {};
+    this.queries.set(queryContext, instance);
+    let conn = await this.promiseDb();
+    await conn.executeCached(`
+      SELECT url, userContextId
+      FROM moz_openpages_temp
+    `, {}, (row, cancel) => {
+      if (!this.queries.has(queryContext)) {
+        cancel();
+        return;
+      }
+      addCallback(this, new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {
+        url: row.getResultByName("url"),
+        userContextId: row.getResultByName("userContextId"),
+      }));
+    });
+    // We are done.
+    this.queries.delete(queryContext);
+  }
+
+  /**
+   * Cancels a running query.
+   * @param {object} queryContext The query context object
+   */
+  cancelQuery(queryContext) {
+    logger.info(`Canceling query for ${queryContext.searchString}`);
+    this.queries.delete(queryContext);
+  }
+}
+
+var UrlbarProviderOpenTabs = new ProviderOpenTabs();
+
+/**
+ * Adds an open page to the memory table.
+ * @param {object} conn A Sqlite.jsm database handle
+ * @param {string} url Address of the page
+ * @param {number} userContextId Containers user context id
+ * @returns {Promise} resolved after the addition.
+ */
+async function addToMemoryTable(conn, url, userContextId) {
+  return UrlbarProvidersManager.runInCriticalSection(async () => {
+    await conn.executeCached(`
+      INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
+      VALUES ( :url,
+                :userContextId,
+                IFNULL( ( SELECT open_count + 1
+                          FROM moz_openpages_temp
+                          WHERE url = :url
+                          AND userContextId = :userContextId ),
+                        1
+                      )
+              )
+    `, { url, userContextId });
+  });
+}
+
+/**
+ * Removes an open page from the memory table.
+ * @param {object} conn A Sqlite.jsm database handle
+ * @param {string} url Address of the page
+ * @param {number} userContextId Containers user context id
+ * @returns {Promise} resolved after the removal.
+ */
+async function removeFromMemoryTable(conn, url, userContextId) {
+  return UrlbarProvidersManager.runInCriticalSection(async () => {
+    await conn.executeCached(`
+      UPDATE moz_openpages_temp
+      SET open_count = open_count - 1
+      WHERE url = :url
+        AND userContextId = :userContextId
+    `, { url, userContextId });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -0,0 +1,237 @@
+/* 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";
+
+/**
+ * This module exports a component used to register search providers and manage
+ * the connection between such providers and a UrlbarController.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProvidersManager"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Log: "resource://gre/modules/Log.jsm",
+  PlacesUtils: "resource://modules/PlacesUtils.jsm",
+  UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+  UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () =>
+  Log.repository.getLogger("Places.Urlbar.ProvidersManager"));
+
+// List of available local providers, each is implemented in its own jsm module
+// and will track different queries internally by queryContext.
+var localProviderModules = {
+  UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
+};
+
+/**
+ * Class used to create a manager.
+ * The manager is responsible to keep a list of providers, instantiate query
+ * objects and pass those to the providers.
+ */
+class ProvidersManager {
+  constructor() {
+    // Tracks the available providers.
+    // This is a double map, first it maps by PROVIDER_TYPE, then
+    // registerProvider maps by provider.name: { type: { name: provider }}
+    this.providers = new Map();
+    for (let type of Object.values(UrlbarUtils.PROVIDER_TYPE)) {
+      this.providers.set(type, new Map());
+    }
+    for (let [symbol, module] of Object.entries(localProviderModules)) {
+      let {[symbol]: provider} = ChromeUtils.import(module, {});
+      this.registerProvider(provider);
+    }
+    // Tracks ongoing Query instances by queryContext.
+    this.queries = new Map();
+
+    // Interrupt() allows to stop any running SQL query, some provider may be
+    // running a query that shouldn't be interrupted, and if so it should
+    // bump this through disableInterrupt and enableInterrupt.
+    this.interruptLevel = 0;
+  }
+
+  /**
+   * Registers a provider object with the manager.
+   * @param {object} provider
+   */
+  registerProvider(provider) {
+    logger.info(`Registering provider ${provider.name}`);
+    if (!Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type)) {
+      throw new Error(`Unknown provider type ${provider.type}`);
+    }
+    this.providers.get(provider.type).set(provider.name, provider);
+  }
+
+  /**
+   * Unregisters a previously registered provider object.
+   * @param {object} provider
+   */
+  unregisterProvider(provider) {
+    logger.info(`Unregistering provider ${provider.name}`);
+    this.providers.get(provider.type).delete(provider.name);
+  }
+
+  /**
+   * Starts querying.
+   * @param {object} queryContext The query context object
+   * @param {object} controller a UrlbarController instance
+   */
+  async startQuery(queryContext, controller) {
+    logger.info(`Query start ${queryContext.searchString}`);
+    let query = Object.seal(new Query(queryContext, controller, this.providers));
+    this.queries.set(queryContext, query);
+    await query.start();
+  }
+
+  /**
+   * Cancels a running query.
+   * @param {object} queryContext
+   */
+  cancelQuery(queryContext) {
+    logger.info(`Query cancel ${queryContext.searchString}`);
+    let query = this.queries.get(queryContext);
+    if (!query) {
+      throw new Error("Couldn't find a matching query for the given context");
+    }
+    query.cancel();
+    if (!this.interruptLevel) {
+      try {
+        let db = PlacesUtils.promiseLargeCacheDBConnection();
+        db.interrupt();
+      } catch (ex) {}
+    }
+    this.queries.delete(queryContext);
+  }
+
+  /**
+   * A provider can use this util when it needs to run a SQL query that can't
+   * be interrupted. Otherwise, when a query is canceled any running SQL query
+   * is interrupted abruptly.
+   * @param {function} taskFn a Task to execute in the critical section.
+   */
+  async runInCriticalSection(taskFn) {
+    this.interruptLevel++;
+    try {
+      await taskFn();
+    } finally {
+      this.interruptLevel--;
+    }
+  }
+}
+
+var UrlbarProvidersManager = new ProvidersManager();
+
+/**
+ * Tracks a query status.
+ * Multiple queries can potentially be executed at the same time by different
+ * controllers. Each query has to track its own status and delays separately,
+ * to avoid conflicting with other ones.
+ */
+class Query {
+  /**
+   * Initializes the query object.
+   * @param {object} queryContext
+   *        The query context
+   * @param {object} controller
+   *        The controller to be notified
+   * @param {object} providers
+   *        Map of all the providers by type and name
+   */
+  constructor(queryContext, controller, providers) {
+    this.context = queryContext;
+    this.context.results = [];
+    this.controller = controller;
+    this.providers = providers;
+    // Track the delay timer.
+    this.sleepResolve = Promise.resolve();
+    this.sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    this.started = false;
+    this.canceled = false;
+    this.complete = false;
+  }
+
+  /**
+   * Starts querying.
+   */
+  async start() {
+    if (this.started) {
+      throw new Error("This Query has been started already");
+    }
+    this.started = true;
+    UrlbarTokenizer.tokenize(this.context);
+
+    let promises = [];
+    for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
+      if (this.canceled) {
+        break;
+      }
+      promises.push(provider.startQuery(this.context, this.add));
+    }
+
+    await new Promise(resolve => {
+      let time = UrlbarPrefs.get("delay");
+      this.sleepResolve = resolve;
+      this.sleepTimer.initWithCallback(resolve, time, Ci.nsITimer.TYPE_ONE_SHOT);
+    });
+
+    for (let providerType of [UrlbarUtils.PROVIDER_TYPE.NETWORK,
+                              UrlbarUtils.PROVIDER_TYPE.PROFILE,
+                              UrlbarUtils.PROVIDER_TYPE.EXTENSION]) {
+      for (let provider of this.providers.get(providerType).values()) {
+        if (this.canceled) {
+          break;
+        }
+        promises.push(provider.startQuery(this.context, this.add.bind(this)));
+      }
+    }
+
+    await Promise.all(promises.map(p => p.catch(Cu.reportError)));
+
+    // Nothing should be failing above, since we catch all the promises, thus
+    // this is not in a finally for now.
+    this.complete = true;
+  }
+
+  /**
+   * Cancels this query.
+   * @note Invoking cancel multiple times is a no-op.
+   */
+  cancel() {
+    if (this.canceled) {
+      return;
+    }
+    this.canceled = true;
+    this.sleepTimer.cancel();
+    for (let providers of this.providers.values()) {
+      for (let provider of providers.values()) {
+        provider.cancelQuery(this.context);
+      }
+    }
+    this.sleepResolve();
+  }
+
+  /**
+   * Adds a match returned from a provider to the results set.
+   * @param {object} provider
+   * @param {object} match
+   */
+  add(provider, match) {
+    // Stop returning results as soon as we've been canceled.
+    if (this.canceled) {
+      return;
+    }
+    // TODO:
+    //  * coalesce results in timed chunks: we don't want to notify every single
+    //    result as soon as it arrives, we'll rather collect results for a few
+    //    ms, then send them
+    //  * pass results to a muxer before sending them back to the controller.
+    this.context.results.push(match);
+    this.controller.receiveResults(this.context);
+  }
+}
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -17,21 +17,43 @@ var UrlbarUtils = {
     // Just append new results.
     APPEND: 0,
     // Merge previous and current results if search strings are related.
     MERGE_RELATED: 1,
     // Always merge previous and current results.
     MERGE: 2,
   },
 
-  MATCHTYPE: {
+  // Extensions are allowed to add suggestions if they have registered a keyword
+  // with the omnibox API. This is the maximum number of suggestions an extension
+  // is allowed to add for a given search string.
+  // This value includes the heuristic result.
+  MAXIMUM_ALLOWED_EXTENSION_MATCHES: 6,
+
+  // This is used by UnifiedComplete, the new implementation will use
+  // PROVIDER_TYPE and MATCH_TYPE
+  MATCH_GROUP: {
     HEURISTIC: "heuristic",
     GENERAL: "general",
     SUGGESTION: "suggestion",
     EXTENSION: "extension",
   },
 
-  // Extensions are allowed to add suggestions if they have registered a keyword
-  // with the omnibox API. This is the maximum number of suggestions an extension
-  // is allowed to add for a given search string.
-  // This value includes the heuristic result.
-  MAXIMUM_ALLOWED_EXTENSION_MATCHES: 6,
+  // Defines provider types.
+  PROVIDER_TYPE: {
+    // Should be executed immediately, because it returns heuristic results
+    // that must be handled to the user asap.
+    IMMEDIATE: 1,
+    // Can be delayed, contains results coming from the session or the profile.
+    PROFILE: 2,
+    // Can be delayed, contains results coming from the network.
+    NETWORK: 3,
+    // Can be delayed, contains results coming from unknown sources.
+    EXTENSION: 4,
+  },
+
+  // Defines UrlbarMatch types.
+  MATCH_TYPE: {
+    // Indicates an open tab.
+    // The payload is: { url, userContextId }
+    TAB_SWITCH: 1,
+  },
 };
--- a/browser/components/urlbar/moz.build
+++ b/browser/components/urlbar/moz.build
@@ -3,16 +3,19 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Address Bar")
 
 EXTRA_JS_MODULES += [
     'UrlbarController.jsm',
     'UrlbarInput.jsm',
+    'UrlbarMatch.jsm',
     'UrlbarPrefs.jsm',
+    'UrlbarProviderOpenTabs.jsm',
+    'UrlbarProvidersManager.jsm',
     'UrlbarTokenizer.jsm',
     'UrlbarUtils.jsm',
     'UrlbarView.jsm',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -9,22 +9,28 @@ var commonFile = do_get_file("../../../.
 if (commonFile) {
   let uri = Services.io.newFileURI(commonFile);
   Services.scriptloader.loadSubScript(uri.spec, this);
 }
 
 // Put any other stuff relative to this test folder below.
 
 ChromeUtils.import("resource:///modules/UrlbarController.jsm");
-ChromeUtils.defineModuleGetter(this, "UrlbarInput",
-                               "resource:///modules/UrlbarInput.jsm");
-ChromeUtils.defineModuleGetter(this, "UrlbarTokenizer",
-                               "resource:///modules/UrlbarTokenizer.jsm");
-ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
-                               "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+  UrlbarController: "resource:///modules/UrlbarController.jsm",
+  UrlbarInput: "resource:///modules/UrlbarInput.jsm",
+  UrlbarMatch: "resource:///modules/UrlbarMatch.jsm",
+  UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+  UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
+  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+  UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
 
 // ================================================
 // Load mocking/stubbing library, sinon
 // docs: http://sinonjs.org/releases/v2.3.2/
 // Sinon needs Timer.jsm for setTimeout etc.
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
 Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js", this);
 /* globals sinon */
copy from browser/components/urlbar/tests/unit/test_QueryContext.js
copy to browser/components/urlbar/tests/unit/test_providerOpenTabs.js
--- a/browser/components/urlbar/tests/unit/test_QueryContext.js
+++ b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
@@ -1,53 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-ChromeUtils.defineModuleGetter(this, "QueryContext",
-  "resource:///modules/UrlbarController.jsm");
-
-add_task(function test_constructor() {
-  Assert.throws(() => new QueryContext(),
-    /Missing or empty searchString provided to QueryContext/,
-    "Should throw with no arguments");
-
-  Assert.throws(() => new QueryContext({
-    searchString: "foo",
-    maxResults: 1,
-    isPrivate: false,
-  }), /Missing or empty lastKey provided to QueryContext/,
-    "Should throw with a missing lastKey parameter");
-
-  Assert.throws(() => new QueryContext({
-    searchString: "foo",
-    lastKey: "b",
-    isPrivate: false,
-  }), /Missing or empty maxResults provided to QueryContext/,
-    "Should throw with a missing maxResults parameter");
+add_task(async function test_openTabs() {
+  const userContextId = 5;
+  const url = "http://foo.mozilla.org/";
+  UrlbarProviderOpenTabs.registerOpenTab(url, userContextId);
+  UrlbarProviderOpenTabs.registerOpenTab(url, userContextId);
+  Assert.equal(UrlbarProviderOpenTabs.openTabs.get(userContextId).length, 2,
+               "Found all the expected tabs");
+  UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId);
+  Assert.equal(UrlbarProviderOpenTabs.openTabs.get(userContextId).length, 1,
+               "Found all the expected tabs");
 
-  Assert.throws(() => new QueryContext({
-    searchString: "foo",
-    lastKey: "b",
-    maxResults: 1,
-  }), /Missing or empty isPrivate provided to QueryContext/,
-    "Should throw with a missing isPrivate parameter");
+  let context = createContext();
+  let matchCount = 0;
+  let callback = function(provider, match) {
+    matchCount++;
+    Assert.equal(provider, UrlbarProviderOpenTabs, "Got the expected provider");
+    Assert.equal(match.type, UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                 "Got the expected match type");
+    Assert.equal(match.url, url, "Got the expected url");
+    Assert.equal(match.title, "", "Got the expected title");
+  };
 
-  let qc = new QueryContext({
-    searchString: "foo",
-    lastKey: "b",
-    maxResults: 1,
-    isPrivate: true,
-    autoFill: false,
-  });
-
-  Assert.equal(qc.searchString, "foo",
-    "Should have saved the correct value for searchString");
-  Assert.equal(qc.lastKey, "b",
-    "Should have saved the correct value for lastKey");
-  Assert.equal(qc.maxResults, 1,
-    "Should have saved the correct value for maxResults");
-  Assert.strictEqual(qc.isPrivate, true,
-    "Should have saved the correct value for isPrivate");
-  Assert.strictEqual(qc.autoFill, false,
-    "Should have saved the correct value for autoFill");
+  await UrlbarProviderOpenTabs.startQuery(context, callback);
+  Assert.equal(matchCount, 1, "Found the expected number of matches");
+  // Sanity check that this doesn't throw.
+  UrlbarProviderOpenTabs.cancelQuery(context);
+  Assert.equal(UrlbarProviderOpenTabs.queries.size, 0,
+    "All the queries have been removed");
 });
copy from browser/components/urlbar/tests/unit/test_QueryContext.js
copy to browser/components/urlbar/tests/unit/test_providersManager.js
--- a/browser/components/urlbar/tests/unit/test_QueryContext.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -1,53 +1,55 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-ChromeUtils.defineModuleGetter(this, "QueryContext",
-  "resource:///modules/UrlbarController.jsm");
-
-add_task(function test_constructor() {
-  Assert.throws(() => new QueryContext(),
-    /Missing or empty searchString provided to QueryContext/,
-    "Should throw with no arguments");
-
-  Assert.throws(() => new QueryContext({
-    searchString: "foo",
-    maxResults: 1,
-    isPrivate: false,
-  }), /Missing or empty lastKey provided to QueryContext/,
-    "Should throw with a missing lastKey parameter");
-
-  Assert.throws(() => new QueryContext({
-    searchString: "foo",
-    lastKey: "b",
-    isPrivate: false,
-  }), /Missing or empty maxResults provided to QueryContext/,
-    "Should throw with a missing maxResults parameter");
-
-  Assert.throws(() => new QueryContext({
-    searchString: "foo",
-    lastKey: "b",
-    maxResults: 1,
-  }), /Missing or empty isPrivate provided to QueryContext/,
-    "Should throw with a missing isPrivate parameter");
-
-  let qc = new QueryContext({
-    searchString: "foo",
-    lastKey: "b",
-    maxResults: 1,
-    isPrivate: true,
-    autoFill: false,
+add_task(async function test_providers() {
+  // First unregister all the existing providers.
+  for (let providers of UrlbarProvidersManager.providers.values()) {
+    for (let provider of providers.values()) {
+      // While here check all providers have name and type.
+      Assert.ok(Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type),
+        `The provider "${provider.name}" should have a valid type`);
+      Assert.ok(provider.name, "All providers should have a name");
+      UrlbarProvidersManager.unregisterProvider(provider);
+    }
+  }
+  let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, { url: "http://mozilla.org/foo/" });
+  UrlbarProvidersManager.registerProvider({
+    get name() {
+      return "TestProvider";
+    },
+    get type() {
+      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    },
+    async startQuery(context, add) {
+      Assert.ok(context, "context is passed-in");
+      Assert.equal(typeof add, "function", "add is a callback");
+      this._context = context;
+      add(this, match);
+    },
+    cancelQuery(context) {
+      Assert.equal(this._context, context, "context is the same");
+    },
   });
 
-  Assert.equal(qc.searchString, "foo",
-    "Should have saved the correct value for searchString");
-  Assert.equal(qc.lastKey, "b",
-    "Should have saved the correct value for lastKey");
-  Assert.equal(qc.maxResults, 1,
-    "Should have saved the correct value for maxResults");
-  Assert.strictEqual(qc.isPrivate, true,
-    "Should have saved the correct value for isPrivate");
-  Assert.strictEqual(qc.autoFill, false,
-    "Should have saved the correct value for autoFill");
+  let context = createContext();
+  let controller = new UrlbarController();
+  let resultsPromise = promiseControllerNotification(controller, "onQueryResults");
+
+  await UrlbarProvidersManager.startQuery(context, controller);
+  // Sanity check that this doesn't throw. It should be a no-op since we await
+  // for startQuery.
+  UrlbarProvidersManager.cancelQuery(context);
+
+  let params = await resultsPromise;
+  Assert.deepEqual(params[0].results, [match]);
 });
+
+add_task(async function test_criticalSection() {
+  // Just a sanity check, this shouldn't throw.
+  await UrlbarProvidersManager.runInCriticalSection(async () => {
+    let db = await PlacesUtils.promiseLargeCacheDBConnection();
+    await db.execute(`PRAGMA page_cache`);
+  });
+});
--- a/browser/components/urlbar/tests/unit/xpcshell.ini
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -1,8 +1,10 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 
+[test_providerOpenTabs.js]
+[test_providersManager.js]
 [test_QueryContext.js]
 [test_tokenizer.js]
 [test_UrlbarController_unit.js]
 [test_UrlbarController_integration.js]
--- a/toolkit/components/places/SQLFunctions.h
+++ b/toolkit/components/places/SQLFunctions.h
@@ -45,17 +45,17 @@ namespace places {
  * @param aVisitCount
  *        The number of visits aURL has.
  * @param aTyped
  *        Indicates if aURL is a typed URL or not.  Treated as a boolean.
  * @param aBookmark
  *        Indicates if aURL is a bookmark or not.  Treated as a boolean.
  * @param aOpenPageCount
  *        The number of times aURL has been registered as being open.  (See
- *        mozIPlacesAutoComplete::registerOpenPage.)
+ *        UrlbarProviderOpenTabs::registerOpenTab.)
  * @param aMatchBehavior
  *        The match behavior to use for this search.
  * @param aSearchBehavior
  *        A bitfield dictating the search behavior.
  */
 class MatchAutoCompleteFunction final : public mozIStorageFunction
 {
 public:
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -340,16 +340,18 @@ XPCOMUtils.defineLazyModuleGetters(this,
   OS: "resource://gre/modules/osfile.jsm",
   PlacesRemoteTabsAutocompleteProvider: "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm",
   PlacesSearchAutocompleteProvider: "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   ProfileAge: "resource://gre/modules/ProfileAge.jsm",
   Sqlite: "resource://gre/modules/Sqlite.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+  UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
+  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "syncUsernamePref",
                                       "services.sync.username");
 
 function setTimeout(callback, ms) {
   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@@ -364,131 +366,16 @@ function iconHelper(url) {
       "page-icon:" + url : PlacesUtils.favicons.defaultFavicon.spec;
   }
   if (url && url instanceof URL && kProtocolsWithIcons.includes(url.protocol)) {
     return "page-icon:" + url.href;
   }
   return PlacesUtils.favicons.defaultFavicon.spec;
 }
 
-/**
- * Storage object for switch-to-tab entries.
- * This takes care of caching and registering open pages, that will be reused
- * by switch-to-tab queries.  It has an internal cache, so that the Sqlite
- * store is lazy initialized only on first use.
- * It has a simple API:
- *   initDatabase(conn): initializes the temporary Sqlite entities to store data
- *   add(uri): adds a given nsIURI to the store
- *   delete(uri): removes a given nsIURI from the store
- *   shutdown(): stops storing data to Sqlite
- */
-XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({
-  _conn: null,
-  // Temporary queue used while the database connection is not available.
-  _queue: new Map(),
-  // Whether we are in the process of updating the temp table.
-  _updatingLevel: 0,
-  get updating() {
-    return this._updatingLevel > 0;
-  },
-  async initDatabase(conn) {
-    // To reduce IO use an in-memory table for switch-to-tab tracking.
-    // Note: this should be kept up-to-date with the definition in
-    //       nsPlacesTables.h.
-    await conn.execute(
-      `CREATE TEMP TABLE moz_openpages_temp (
-         url TEXT,
-         userContextId INTEGER,
-         open_count INTEGER,
-         PRIMARY KEY (url, userContextId)
-       )`);
-
-    // Note: this should be kept up-to-date with the definition in
-    //       nsPlacesTriggers.h.
-    await conn.execute(
-      `CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger
-       AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW
-       WHEN NEW.open_count = 0
-       BEGIN
-         DELETE FROM moz_openpages_temp
-         WHERE url = NEW.url
-           AND userContextId = NEW.userContextId;
-       END`);
-
-    this._conn = conn;
-
-    // Populate the table with the current cache contents...
-    for (let [userContextId, uris] of this._queue) {
-      for (let uri of uris) {
-        this.add(uri, userContextId).catch(Cu.reportError);
-      }
-    }
-
-    // ...then clear it to avoid double additions.
-    this._queue.clear();
-  },
-
-  async add(uri, userContextId) {
-    if (!this._conn) {
-      if (!this._queue.has(userContextId)) {
-        this._queue.set(userContextId, new Set());
-      }
-      this._queue.get(userContextId).add(uri);
-      return;
-    }
-    try {
-      this._updatingLevel++;
-      await this._conn.executeCached(
-        `INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
-          VALUES ( :url,
-                    :userContextId,
-                    IFNULL( ( SELECT open_count + 1
-                              FROM moz_openpages_temp
-                              WHERE url = :url
-                              AND userContextId = :userContextId ),
-                            1
-                          )
-                  )
-        `, { url: uri.spec, userContextId });
-    } finally {
-      this._updatingLevel--;
-    }
-  },
-
-  async delete(uri, userContextId) {
-    if (!this._conn) {
-      if (!this._queue.has(userContextId)) {
-        throw new Error("Unknown userContextId!");
-      }
-
-      this._queue.get(userContextId).delete(uri);
-      if (this._queue.get(userContextId).size == 0) {
-        this._queue.delete(userContextId);
-      }
-      return;
-    }
-    try {
-      this._updatingLevel++;
-      await this._conn.executeCached(
-        `UPDATE moz_openpages_temp
-         SET open_count = open_count - 1
-         WHERE url = :url
-           AND userContextId = :userContextId
-        `, { url: uri.spec, userContextId });
-    } finally {
-      this._updatingLevel--;
-    }
-  },
-
-  shutdown() {
-    this._conn = null;
-    this._queue.clear();
-  },
-}));
-
 // Preloaded Sites related
 
 function PreloadedSite(url, title) {
   this.uri = Services.io.newURI(url);
   this.title = title;
   this._matchTitle = title.toLowerCase();
   this._hasWWW = this.uri.host.startsWith("www.");
   this._hostWithoutWWW = this._hasWWW ? this.uri.host.slice(4)
@@ -702,23 +589,23 @@ function Search(searchString, searchPara
   // Will be set later, if needed.
   result.setDefaultIndex(-1);
   this._result = result;
 
   this._previousSearchMatchTypes = [];
   for (let i = 0; previousResult && i < previousResult.matchCount; ++i) {
     let style = previousResult.getStyleAt(i);
     if (style.includes("heuristic")) {
-      this._previousSearchMatchTypes.push(UrlbarUtils.MATCHTYPE.HEURISTIC);
+      this._previousSearchMatchTypes.push(UrlbarUtils.MATCH_GROUP.HEURISTIC);
     } else if (style.includes("suggestion")) {
-      this._previousSearchMatchTypes.push(UrlbarUtils.MATCHTYPE.SUGGESTION);
+      this._previousSearchMatchTypes.push(UrlbarUtils.MATCH_GROUP.SUGGESTION);
     } else if (style.includes("extension")) {
-      this._previousSearchMatchTypes.push(UrlbarUtils.MATCHTYPE.EXTENSION);
+      this._previousSearchMatchTypes.push(UrlbarUtils.MATCH_GROUP.EXTENSION);
     } else {
-      this._previousSearchMatchTypes.push(UrlbarUtils.MATCHTYPE.GENERAL);
+      this._previousSearchMatchTypes.push(UrlbarUtils.MATCH_GROUP.GENERAL);
     }
   }
 
   // Used to limit the number of adaptive results.
   this._adaptiveCount = 0;
   this._extraAdaptiveRows = [];
 
   // Used to limit the number of remote tab results.
@@ -728,18 +615,18 @@ function Search(searchString, searchPara
   // to check how many "current" matches have been inserted.
   // Indeed this._result.matchCount may include matches from the previous search.
   this._currentMatchCount = 0;
 
   // These are used to avoid adding duplicate entries to the results.
   this._usedURLs = [];
   this._usedPlaceIds = new Set();
 
-  // Counters for the number of matches per MATCHTYPE.
-  this._counts = Object.values(UrlbarUtils.MATCHTYPE)
+  // Counters for the number of matches per MATCH_GROUP.
+  this._counts = Object.values(UrlbarUtils.MATCH_GROUP)
                        .reduce((o, p) => { o[p] = 0; return o; }, {});
 }
 
 Search.prototype = {
   /**
    * Enables the desired AutoComplete behavior.
    *
    * @param type
@@ -871,17 +758,17 @@ Search.prototype = {
   async execute(conn) {
     // A search might be canceled before it starts.
     if (!this.pending)
       return;
 
     // Used by stop() to interrupt an eventual running statement.
     this.interrupt = () => {
       // Interrupt any ongoing statement to run the search sooner.
-      if (!SwitchToTabStorage.updating) {
+      if (!UrlbarProvidersManager.interruptLevel) {
         conn.interrupt();
       }
     };
 
     TelemetryStopwatch.start(TELEMETRY_1ST_RESULT, this);
     TelemetryStopwatch.start(TELEMETRY_6_FIRST_RESULTS, this);
 
     // Since we call the synchronous parseSubmissionURL function later, we must
@@ -916,17 +803,17 @@ Search.prototype = {
     await this._checkPreloadedSitesExpiry();
 
     // Add the first heuristic result, if any.  Set _addingHeuristicFirstMatch
     // to true so that when the result is added, "heuristic" can be included in
     // its style.
     this._addingHeuristicFirstMatch = true;
     let hasHeuristic = await this._matchFirstHeuristicResult(conn);
     this._addingHeuristicFirstMatch = false;
-    this._cleanUpNonCurrentMatches(UrlbarUtils.MATCHTYPE.HEURISTIC);
+    this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.HEURISTIC);
     if (!this.pending)
       return;
 
     // We sleep a little between adding the heuristicFirstMatch and matching
     // any other searches so we aren't kicking off potentially expensive
     // searches on every keystroke.
     // Though, if there's no heuristic result, we start searching immediately,
     // since autocomplete may be waiting for us.
@@ -957,27 +844,27 @@ Search.prototype = {
       // Avoid fetching suggestions if they are not required, private browsing
       // mode is enabled, or the search string may expose sensitive information.
       if (this.hasBehavior("searches") && !this._inPrivateWindow &&
           !this._prohibitSearchSuggestionsFor(searchString)) {
         searchSuggestionsCompletePromise = this._matchSearchSuggestions(searchString);
         if (this.hasBehavior("restrict")) {
           // Wait for the suggestions to be added.
           await searchSuggestionsCompletePromise;
-          this._cleanUpNonCurrentMatches(UrlbarUtils.MATCHTYPE.SUGGESTION);
+          this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.SUGGESTION);
           // We're done if we're restricting to search suggestions.
           // Notify the result completion then stop the search.
           this._autocompleteSearch.finishSearch(true);
           return;
         }
       }
     }
     // In any case, clear previous suggestions.
     searchSuggestionsCompletePromise.then(() => {
-      this._cleanUpNonCurrentMatches(UrlbarUtils.MATCHTYPE.SUGGESTION);
+      this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.SUGGESTION);
     });
 
     // Run the adaptive query first.
     await conn.executeCached(this._adaptiveQuery[0], this._adaptiveQuery[1],
                              this._onResultRow.bind(this));
     if (!this.pending)
       return;
 
@@ -1014,25 +901,25 @@ Search.prototype = {
     // If we have some unused remote tab matches, add them now.
     while (this._extraRemoteTabRows.length &&
           this._currentMatchCount < UrlbarPrefs.get("maxRichResults")) {
       this._addMatch(this._extraRemoteTabRows.shift());
     }
 
     // Ideally we should wait until MATCH_BOUNDARY_ANYWHERE, but that query
     // may be really slow and we may end up showing old results for too long.
-    this._cleanUpNonCurrentMatches(UrlbarUtils.MATCHTYPE.GENERAL);
+    this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.GENERAL);
 
     this._matchAboutPages();
 
     // If we do not have enough results, and our match type is
     // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
     // results.
-    let count = this._counts[UrlbarUtils.MATCHTYPE.GENERAL] +
-                this._counts[UrlbarUtils.MATCHTYPE.HEURISTIC];
+    let count = this._counts[UrlbarUtils.MATCH_GROUP.GENERAL] +
+                this._counts[UrlbarUtils.MATCH_GROUP.HEURISTIC];
     if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
         count < UrlbarPrefs.get("maxRichResults")) {
       this._matchBehavior = MATCH_ANYWHERE;
       for (let [query, params] of [ this._adaptiveQuery,
                                     this._searchQuery ]) {
         await conn.executeCached(query, params, this._onResultRow.bind(this));
         if (!this.pending)
           return;
@@ -1476,32 +1363,32 @@ Search.prototype = {
       return false;
 
     let query = this._originalSearchString;
     this._addSearchEngineMatch(match, query);
     return true;
   },
 
   _addExtensionMatch(content, comment) {
-    let count = this._counts[UrlbarUtils.MATCHTYPE.EXTENSION] +
-                this._counts[UrlbarUtils.MATCHTYPE.HEURISTIC];
+    let count = this._counts[UrlbarUtils.MATCH_GROUP.EXTENSION] +
+                this._counts[UrlbarUtils.MATCH_GROUP.HEURISTIC];
     if (count >= UrlbarUtils.MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
       return;
     }
 
     this._addMatch({
       value: PlacesUtils.mozActionURI("extension", {
         content,
         keyword: this._searchTokens[0],
       }),
       comment,
       icon: "chrome://browser/content/extension.svg",
       style: "action extension",
       frecency: Infinity,
-      type: UrlbarUtils.MATCHTYPE.EXTENSION,
+      type: UrlbarUtils.MATCH_GROUP.EXTENSION,
     });
   },
 
   _addSearchEngineMatch(searchMatch, query, suggestion = "", historical = false) {
     let actionURLParams = {
       engineName: searchMatch.engineName,
       input: suggestion || this._originalSearchString,
       searchQuery: query,
@@ -1516,17 +1403,17 @@ Search.prototype = {
       value,
       comment: searchMatch.engineName,
       icon: searchMatch.iconUrl,
       style: "action searchengine",
       frecency: FRECENCY_DEFAULT,
     };
     if (suggestion) {
       match.style += " suggestion";
-      match.type = UrlbarUtils.MATCHTYPE.SUGGESTION;
+      match.type = UrlbarUtils.MATCH_GROUP.SUGGESTION;
     }
 
     this._addMatch(match);
   },
 
   _matchExtensionSuggestions() {
     let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
       suggestions => {
@@ -1535,17 +1422,17 @@ Search.prototype = {
           this._addExtensionMatch(content, suggestion.description);
         }
       }
     );
     // Remove previous search matches sooner than the maximum timeout, otherwise
     // matches may appear stale for a long time.
     // This is necessary because WebExtensions don't have a method to notify
     // that they are done providing results, so they could be pending forever.
-    setTimeout(() => this._cleanUpNonCurrentMatches(UrlbarUtils.MATCHTYPE.EXTENSION), 100);
+    setTimeout(() => this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.EXTENSION), 100);
 
     // Since the extension has no way to signale when it's done pushing
     // results, we add a timeout racing with the addition.
     let timeoutPromise = new Promise(resolve => {
       setTimeout(resolve, MAXIMUM_ALLOWED_EXTENSION_TIME_MS);
     });
     return Promise.race([timeoutPromise, promise]).catch(Cu.reportError);
   },
@@ -1681,18 +1568,18 @@ Search.prototype = {
         this._addAdaptiveQueryMatch(row);
         break;
       case QUERYTYPE_FILTERED:
         this._addFilteredQueryMatch(row);
         break;
     }
     // If the search has been canceled by the user or by _addMatch, or we
     // fetched enough results, we can stop the underlying Sqlite query.
-    let count = this._counts[UrlbarUtils.MATCHTYPE.GENERAL] +
-                this._counts[UrlbarUtils.MATCHTYPE.HEURISTIC];
+    let count = this._counts[UrlbarUtils.MATCH_GROUP.GENERAL] +
+                this._counts[UrlbarUtils.MATCH_GROUP.HEURISTIC];
     if (!this.pending || count >= UrlbarPrefs.get("maxRichResults")) {
       cancel();
     }
   },
 
   _maybeRestyleSearchMatch(match) {
     // Return if the URL does not represent a search result.
     let parseResult =
@@ -1722,19 +1609,19 @@ Search.prototype = {
     match.style = "action searchengine favicon";
   },
 
   _addMatch(match) {
     if (typeof match.frecency != "number")
       throw new Error("Frecency not provided");
 
     if (this._addingHeuristicFirstMatch)
-      match.type = UrlbarUtils.MATCHTYPE.HEURISTIC;
+      match.type = UrlbarUtils.MATCH_GROUP.HEURISTIC;
     else if (typeof match.type != "string")
-      match.type = UrlbarUtils.MATCHTYPE.GENERAL;
+      match.type = UrlbarUtils.MATCH_GROUP.GENERAL;
 
     // A search could be canceled between a query start and its completion,
     // in such a case ensure we won't notify any result for it.
     if (!this.pending)
       return;
 
     match.style = match.style || "favicon";
 
@@ -1764,17 +1651,17 @@ Search.prototype = {
                                match.finalCompleteValue);
     this._currentMatchCount++;
     this._counts[match.type]++;
 
     if (this._currentMatchCount == 1)
       TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT, this);
     if (this._currentMatchCount == 6)
       TelemetryStopwatch.finish(TELEMETRY_6_FIRST_RESULTS, this);
-    this.notifyResult(true, match.type == UrlbarUtils.MATCHTYPE.HEURISTIC);
+    this.notifyResult(true, match.type == UrlbarUtils.MATCH_GROUP.HEURISTIC);
   },
 
   _getInsertIndexForMatch(match) {
     // Check for duplicates and either discard (by returning -1) the duplicate
     // or suggest to replace the original match, in case the new one is more
     // specific (for example a Remote Tab wins over History, and a Switch to Tab
     // wins over a Remote Tab).
     // Must check both id and url, cause keywords dynamically modify the url.
@@ -1789,17 +1676,17 @@ Search.prototype = {
         // The new entry is a switch/remote tab entry, look for the duplicate
         // among current matches.
         for (let i = 0; i < this._usedURLs.length; ++i) {
           let {key: matchKey, action: matchAction, type: matchType} = this._usedURLs[i];
           if (matchKey == urlMapKey) {
             isDupe = true;
             // Don't replace the match if the existing one is heuristic and the
             // new one is a switchtab, instead also add the switchtab match.
-            if (matchType == UrlbarUtils.MATCHTYPE.HEURISTIC &&
+            if (matchType == UrlbarUtils.MATCH_GROUP.HEURISTIC &&
                 action.type == "switchtab") {
               isDupe = false;
               // Since we allow to insert a dupe in this case, we must continue
               // checking the next matches to be sure we won't insert more than
               // one dupe. For this same reason we must reset isDupe = true for
               // each found dupe.
               continue;
             }
@@ -1825,17 +1712,17 @@ Search.prototype = {
     if (match.placeId)
       this._usedPlaceIds.add(match.placeId);
 
     let index = 0;
     // The buckets change depending on the context, that is currently decided by
     // the first added match (the heuristic one).
     if (!this._buckets) {
       // Convert the buckets to readable objects with a count property.
-      let buckets = match.type == UrlbarUtils.MATCHTYPE.HEURISTIC &&
+      let buckets = match.type == UrlbarUtils.MATCH_GROUP.HEURISTIC &&
                     match.style.includes("searchengine") ? UrlbarPrefs.get("matchBucketsSearch")
                                                          : UrlbarPrefs.get("matchBuckets");
       // - available is the number of available slots in the bucket
       // - insertIndex is the index of the first available slot in the bucket
       // - count is the number of matches in the bucket, note that it also
       //   account for matches from the previous search, while available and
       //   insertIndex don't.
       this._buckets = buckets.map(([type, available]) => ({ type,
@@ -1883,17 +1770,17 @@ Search.prototype = {
     this._usedURLs[index] = {key: urlMapKey, action, type: match.type};
     return { index, replace };
   },
 
   /**
    * Removes matches from a previous search, that are no more returned by the
    * current search
    * @param type
-   *        The UrlbarUtils.MATCHTYPE to clean up.
+   *        The UrlbarUtils.MATCH_GROUP to clean up.
    * @param [optional] notify
    *        Whether to notify a result change.
    */
   _cleanUpNonCurrentMatches(type, notify = true) {
     if (this._previousSearchMatchTypes.length == 0 || !this.pending)
       return;
 
     let index = 0;
@@ -2410,42 +2297,33 @@ UnifiedComplete.prototype = {
 
         try {
            Sqlite.shutdown.addBlocker("Places UnifiedComplete.js closing",
                                       () => {
                                         // Break a possible cycle through the
                                         // previous result, the controller and
                                         // ourselves.
                                         this._currentSearch = null;
-                                        SwitchToTabStorage.shutdown();
                                       });
         } catch (ex) {
           // It's too late to block shutdown.
           throw ex;
         }
-        await SwitchToTabStorage.initDatabase(conn);
+        await UrlbarProviderOpenTabs.promiseDb();
         return conn;
       })().catch(ex => {
         dump("Couldn't get database handle: " + ex + "\n");
         Cu.reportError(ex);
       });
     }
     return this._promiseDatabase;
   },
 
   // mozIPlacesAutoComplete
 
-  registerOpenPage(uri, userContextId) {
-    SwitchToTabStorage.add(uri, userContextId).catch(Cu.reportError);
-  },
-
-  unregisterOpenPage(uri, userContextId) {
-    SwitchToTabStorage.delete(uri, userContextId).catch(Cu.reportError);
-  },
-
   populatePreloadedSiteStorage(json) {
     PreloadedSiteStorage.populate(json);
   },
 
   // nsIAutoCompleteSearch
 
   startSearch(searchString, searchParam, acPreviousResult, listener) {
     // Stop the search in case the controller has not taken care of it.
--- a/toolkit/components/places/mozIPlacesAutoComplete.idl
+++ b/toolkit/components/places/mozIPlacesAutoComplete.idl
@@ -103,44 +103,15 @@ interface mozIPlacesAutoComplete : nsISu
   const long BEHAVIOR_RESTRICT = 1 << 8;
 
   /**
    * Include search suggestions from the currently selected search provider.
    */
   const long BEHAVIOR_SEARCHES = 1 << 9;
 
   /**
-   * Mark a page as being currently open.
-   *
-   * @note Pages will not be automatically unregistered when Private Browsing
-   *       mode is entered or exited.  Therefore, consumers MUST unregister or
-   *       register themselves.
-   *
-   * @param aURI
-   *        The URI to register as an open page.
-   * @param aUserContextId
-   *        The Container Id of the tab.
-   */
-  void registerOpenPage(in nsIURI aURI, in uint32_t aUserContextId);
-
-  /**
-   * Mark a page as no longer being open (either by closing the window or tab,
-   * or by navigating away from that page).
-   *
-   * @note Pages will not be automatically unregistered when Private Browsing
-   *       mode is entered or exited.  Therefore, consumers MUST unregister or
-   *       register themselves.
-   *
-   * @param aURI
-   *        The URI to unregister as an open page.
-   * @param aUserContextId
-   *        The Container Id of the tab.
-   */
-  void unregisterOpenPage(in nsIURI aURI, in uint32_t aUserContextId);
-
-  /**
    * Populate list of Preloaded Sites from JSON.
    *
    * @param sites
    *        Array of [url,title] to populate from.
    */
   void populatePreloadedSiteStorage(in jsval sites);
 };
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -20,16 +20,19 @@ ChromeUtils.import("resource://testing-c
   /* import-globals-from ./autofill_tasks.js */
   let file = do_get_file("autofill_tasks.js", false);
   let uri = Services.io.newFileURI(file);
   XPCOMUtils.defineLazyScriptGetter(this, "addAutofillTasks", uri.spec);
 }
 
 // Put any other stuff relative to this test folder below.
 
+ChromeUtils.defineModuleGetter(this, "UrlbarProviderOpenTabs",
+  "resource:///modules/UrlbarProviderOpenTabs.jsm");
+
 const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 ";
 
 async function cleanup() {
   Services.prefs.clearUserPref("browser.urlbar.autocomplete.enabled");
   Services.prefs.clearUserPref("browser.urlbar.autoFill");
   Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
   let suggestPrefs = [
     "history",
@@ -308,28 +311,24 @@ var addBookmark = async function(aBookma
   }
 
   if (aBookmarkObj.tags) {
     PlacesUtils.tagging.tagURI(aBookmarkObj.uri, aBookmarkObj.tags);
   }
 };
 
 function addOpenPages(aUri, aCount = 1, aUserContextId = 0) {
-  let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
-             .getService(Ci.mozIPlacesAutoComplete);
   for (let i = 0; i < aCount; i++) {
-    ac.registerOpenPage(aUri, aUserContextId);
+    UrlbarProviderOpenTabs.registerOpenTab(aUri.spec, aUserContextId);
   }
 }
 
 function removeOpenPages(aUri, aCount = 1, aUserContextId = 0) {
-  let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
-             .getService(Ci.mozIPlacesAutoComplete);
   for (let i = 0; i < aCount; i++) {
-    ac.unregisterOpenPage(aUri, aUserContextId);
+    UrlbarProviderOpenTabs.unregisterOpenTab(aUri.spec, aUserContextId);
   }
 }
 
 /**
  * Strip prefixes from the URI that we don't care about for searching.
  *
  * @param {String} spec
  *        The text to modify.
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -205,19 +205,16 @@
   "tokenserverclient.js": ["TokenServerClient", "TokenServerClientError", "TokenServerClientNetworkError", "TokenServerClientServerError"],
   "ToolboxProcess.jsm": ["BrowserToolboxProcess"],
   "tps.jsm": ["ACTIONS", "Addons", "Addresses", "Bookmarks", "CreditCards", "Formdata", "History", "Passwords", "Prefs", "Tabs", "TPS", "Windows"],
   "Translation.jsm": ["Translation", "TranslationTelemetry"],
   "Traversal.jsm": ["TraversalRules", "TraversalHelper"],
   "UpdateTelemetry.jsm": ["AUSTLMY"],
   "UpdateTopLevelContentWindowIDHelper.jsm": ["trackBrowserWindow"],
   "UrlbarController.jsm": ["QueryContext", "UrlbarController"],
-  "UrlbarPrefs.jsm": ["UrlbarPrefs"],
-  "UrlbarTokenizer.jsm": ["UrlbarTokenizer"],
-  "UrlbarUtils.jsm": ["UrlbarUtils"],
   "util.js": ["getChromeWindow", "Utils", "Svc", "SerializableSet"],
   "utils.js": ["btoa", "encryptPayload", "makeIdentityConfig", "makeFxAccountsInternalMock", "configureFxAccountIdentity", "configureIdentity", "SyncTestingInfrastructure", "waitForZeroTimer", "Promise", "MockFxaStorageManager", "AccountState", "sumHistogram", "CommonUtils", "CryptoUtils", "TestingUtils", "promiseZeroTimer", "promiseNamedTimer", "getLoginTelemetryScalar", "syncTestLogging"],
   "Utils.jsm": ["Utils", "Logger", "PivotContext", "PrefCache"],
   "VariablesView.jsm": ["VariablesView", "escapeHTML"],
   "VariablesViewController.jsm": ["VariablesViewController", "StackFrameUtils"],
   "version.jsm": ["VERSION"],
   "vtt.jsm": ["WebVTT"],
   "WebChannel.jsm": ["WebChannel", "WebChannelBroker"],