Bug 699859 - Create nsIContentPrefService2, an async version of nsIContentPrefService. r=mak, sr=mossop
authorDrew Willcoxon <adw@mozilla.com>
Thu, 20 Dec 2012 17:37:56 -0800
changeset 125844 cca7f05e2c053daeedd7a95e843fe29f740db8e4
parent 125843 c928f50fe4fcf2922c613256e0afa2e04ad7315c
child 125845 cf585138ff25010a27e6cc1120242b29dcf9ac64
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, mossop
bugs699859
milestone20.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 699859 - Create nsIContentPrefService2, an async version of nsIContentPrefService. r=mak, sr=mossop
dom/interfaces/base/Makefile.in
dom/interfaces/base/nsIContentPrefService2.idl
testing/xpcshell/xpcshell.ini
toolkit/components/contentprefs/ContentPrefService2.jsm
toolkit/components/contentprefs/ContentPrefStore.jsm
toolkit/components/contentprefs/Makefile.in
toolkit/components/contentprefs/nsContentPrefService.js
toolkit/components/contentprefs/tests/Makefile.in
toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm
toolkit/components/contentprefs/tests/unit_cps2/head.js
toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js
toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js
toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js
toolkit/components/contentprefs/tests/unit_cps2/test_observers.js
toolkit/components/contentprefs/tests/unit_cps2/test_remove.js
toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js
toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js
toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js
toolkit/components/contentprefs/tests/unit_cps2/test_service.js
toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js
toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
--- a/dom/interfaces/base/Makefile.in
+++ b/dom/interfaces/base/Makefile.in
@@ -22,16 +22,17 @@ SDK_XPIDLSRCS =                         
 	nsIDOMWindowUtils.idl			\
 	$(NULL)
 
 XPIDLSRCS =					\
 	nsIFrameRequestCallback.idl             \
 	nsIBrowserDOMWindow.idl			\
 	nsIContentPermissionPrompt.idl  \
 	nsIContentPrefService.idl		\
+	nsIContentPrefService2.idl		\
 	nsIContentURIGrouper.idl		\
 	nsIDOMClientInformation.idl		\
 	nsIDOMConstructor.idl			\
 	nsIDOMCRMFObject.idl			\
 	nsIDOMCrypto.idl			\
 	nsIDOMHistory.idl			\
 	nsIDOMLocation.idl			\
 	nsIDOMMediaQueryList.idl		\
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/base/nsIContentPrefService2.idl
@@ -0,0 +1,365 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIVariant;
+interface nsIContentPrefObserver;
+interface nsIContentPrefCallback2;
+interface nsILoadContext;
+interface nsIContentPref;
+
+/**
+ * Content Preferences
+ *
+ * Content preferences allow the application to associate arbitrary data, or
+ * "preferences", with specific domains, or web "content".  Specifically, a
+ * content preference is a structure with three values: a domain with which the
+ * preference is associated, a name that identifies the preference within its
+ * domain, and a value.  (See nsIContentPref below.)
+ *
+ * For example, if you want to remember the user's preference for a certain zoom
+ * level on www.mozilla.org pages, you might store a preference whose domain is
+ * "www.mozilla.org", whose name is "zoomLevel", and whose value is the numeric
+ * zoom level.
+ *
+ * A preference need not have a domain, and in that case the preference is
+ * called a "global" preference.  This interface doesn't impart any special
+ * significance to global preferences; they're simply name-value pairs that
+ * aren't associated with any particular domain.  As a consumer of this
+ * interface, you might choose to let a global preference override all non-
+ * global preferences of the same name, for example, for whatever definition of
+ * "override" is appropriate for your use case.
+ *
+ *
+ * Domain Parameters
+ *
+ * Many methods of this interface accept a "domain" parameter.  Domains may be
+ * specified either exactly, like "example.com", or as full URLs, like
+ * "http://example.com/foo/bar".  In the latter case the API extracts the full
+ * domain from the URL, so if you specify "http://foo.bar.example.com/baz", the
+ * domain is taken to be "foo.bar.example.com", not "example.com".
+ *
+ *
+ * Private-Browsing Context Parameters
+ *
+ * Many methods also accept a "context" parameter.  This parameter relates to
+ * private browsing and determines the kind of storage that a method uses,
+ * either the usual permanent storage or temporary storage set aside for private
+ * browsing sessions.
+ *
+ * Pass null to unconditionally use permanent storage.  Pass an nsILoadContext
+ * to use storage appropriate to the context's usePrivateBrowsing attribute: if
+ * usePrivateBrowsing is true, temporary private-browsing storage is used, and
+ * otherwise permanent storage is used.  A context can be obtained from the
+ * window or channel whose content pertains to the preferences being modified or
+ * retrieved.
+ *
+ *
+ * Callbacks
+ *
+ * The methods of callback objects are always called asynchronously.  See
+ * nsIContentPrefCallback2 below for more information about callbacks.
+ */
+
+[scriptable, uuid(51e1d34a-5e9d-4b77-b14c-0f8346e264ca)]
+interface nsIContentPrefService2 : nsISupports
+{
+  /**
+   * Gets the preference with the given domain and name.
+   *
+   * @param domain    The preference's domain.
+   * @param name      The preference's name.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleResult is called once unless no such preference
+   *                  exists, in which case handleResult is not called at all.
+   */
+  void getByDomainAndName(in AString domain,
+                          in AString name,
+                          in nsILoadContext context,
+                          in nsIContentPrefCallback2 callback);
+
+  /**
+   * Gets all preferences with the given name whose domains are either the same
+   * as or subdomains of the given domain.
+   *
+   * @param domain    The preferences' domain.
+   * @param name      The preferences' name.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleResult is called once for each preference.  If no
+   *                  such preferences exist, handleResult is not called at all.
+   */
+  void getBySubdomainAndName(in AString domain,
+                             in AString name,
+                             in nsILoadContext context,
+                             in nsIContentPrefCallback2 callback);
+
+  /**
+   * Gets the preference with no domain and the given name.
+   *
+   * @param name      The preference's name.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleResult is called once unless no such preference
+   *                  exists, in which case handleResult is not called at all.
+   */
+  void getGlobal(in AString name,
+                 in nsILoadContext context,
+                 in nsIContentPrefCallback2 callback);
+
+  /**
+   * Synchronously retrieves from the in-memory cache the preference with the
+   * given domain and name.
+   *
+   * In addition to caching preference values, the cache also keeps track of
+   * preferences that are known not to exist.  If the preference is known not to
+   * exist, the value attribute of the returned object will be undefined
+   * (nsIDataType::VTYPE_VOID).
+   *
+   * If the preference is neither cached nor known not to exist, then null is
+   * returned, and get() must be called to determine whether the preference
+   * exists.
+   *
+   * @param domain   The preference's domain.
+   * @param name     The preference's name.
+   * @param context  The private-browsing context, if any.
+   * @return         The preference, or null if no such preference is known to
+   *                 exist.
+   */
+  nsIContentPref getCachedByDomainAndName(in AString domain,
+                                          in AString name,
+                                          in nsILoadContext context);
+
+  /**
+   * Synchronously retrieves from the in-memory cache all preferences with the
+   * given name whose domains are either the same as or subdomains of the given
+   * domain.
+   *
+   * The preferences are returned in an array through the out-parameter.  If a
+   * preference for a particular subdomain is known not to exist, then an object
+   * corresponding to that preference will be present in the array, and, as with
+   * getCachedByDomainAndName, its value attribute will be undefined.
+   *
+   * @param domain   The preferences' domain.
+   * @param name     The preferences' name.
+   * @param context  The private-browsing context, if any.
+   * @param len      The length of the returned array.
+   * @param prefs    The array of preferences.
+   */
+  void getCachedBySubdomainAndName(in AString domain,
+                                   in AString name,
+                                   in nsILoadContext context,
+                                   out unsigned long len,
+                                   [retval,array,size_is(len)] out nsIContentPref prefs);
+
+  /**
+   * Synchronously retrieves from the in-memory cache the preference with no
+   * domain and the given name.
+   *
+   * As with getCachedByDomainAndName, if the preference is cached then it is
+   * returned; if the preference is known not to exist, then the value attribute
+   * of the returned object will be undefined; if the preference is neither
+   * cached nor known not to exist, then null is returned.
+   *
+   * @param name     The preference's name.
+   * @param context  The private-browsing context, if any.
+   * @return         The preference, or null if no such preference is known to
+   *                 exist.
+   */
+  nsIContentPref getCachedGlobal(in AString name,
+                                 in nsILoadContext context);
+
+  /**
+   * Sets a preference.
+   *
+   * @param domain    The preference's domain.
+   * @param name      The preference's name.
+   * @param value     The preference's value.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the preference has been
+   *                  stored.
+   */
+  void set(in AString domain,
+           in AString name,
+           in nsIVariant value,
+           in nsILoadContext context,
+           [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Sets a preference with no domain.
+   *
+   * @param name      The preference's name.
+   * @param value     The preference's value.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the preference has been
+   *                  stored.
+   */
+  void setGlobal(in AString name,
+                 in nsIVariant value,
+                 in nsILoadContext context,
+                 [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes the preference with the given domain and name.
+   *
+   * @param domain    The preference's domain.
+   * @param name      The preference's name.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeByDomainAndName(in AString domain,
+                             in AString name,
+                             in nsILoadContext context,
+                             [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes all the preferences with the given name whose domains are either
+   * the same as or subdomains of the given domain.
+   *
+   * @param domain    The preferences' domain.
+   * @param name      The preferences' name.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeBySubdomainAndName(in AString domain,
+                                in AString name,
+                                in nsILoadContext context,
+                                [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes the preference with no domain and the given name.
+   *
+   * @param name      The preference's name.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeGlobal(in AString name,
+                    in nsILoadContext context,
+                    [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes all preferences with the given domain.
+   *
+   * @param domain    The preferences' domain.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeByDomain(in AString domain,
+                      in nsILoadContext context,
+                      [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes all preferences whose domains are either the same as or subdomains
+   * of the given domain.
+   *
+   * @param domain    The preferences' domain.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeBySubdomain(in AString domain,
+                         in nsILoadContext context,
+                         [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes all preferences with the given name regardless of domain, including
+   * global preferences with the given name.
+   *
+   * @param name      The preferences' name.
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeByName(in AString name,
+                    in nsILoadContext context,
+                    [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes all non-global preferences -- in other words, all preferences that
+   * have a domain.
+   *
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeAllDomains(in nsILoadContext context,
+                        [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Removes all global preferences -- in other words, all preferences that have
+   * no domain.
+   *
+   * @param context   The private-browsing context, if any.
+   * @param callback  handleCompletion is called when the operation completes.
+   */
+  void removeAllGlobals(in nsILoadContext context,
+                        [optional] in nsIContentPrefCallback2 callback);
+
+  /**
+   * Registers an observer that will be notified whenever a preference with the
+   * given name is set or removed.
+   *
+   * When a set or remove method is called, observers are notified after the set
+   * or removal completes but before method's callback is called.
+   *
+   * The service holds a strong reference to the observer, so the observer must
+   * be removed later to avoid leaking it.
+   *
+   * @param name      The name of the preferences to observe.  Pass null to
+   *                  observe all preference changes regardless of name.
+   * @param observer  The observer.
+   */
+  void addObserverForName(in AString name,
+                          in nsIContentPrefObserver observer);
+
+  /**
+   * Unregisters an observer for the given name.
+   *
+   * @param name      The name for which the observer was registered.  Pass null
+   *                  if the observer was added with a null name.
+   * @param observer  The observer.
+   */
+  void removeObserverForName(in AString name,
+                             in nsIContentPrefObserver observer);
+};
+
+/**
+ * The callback used by the above methods.
+ */
+[scriptable, uuid(1a12cf41-79e8-4d0f-9899-2f7b27c5d9a1)]
+interface nsIContentPrefCallback2 : nsISupports
+{
+  /**
+   * For the retrieval methods, this is called once for each retrieved
+   * preference.  It is not called for other methods.
+   *
+   * @param pref  The retrieved preference.
+   */
+  void handleResult(in nsIContentPref pref);
+
+  /**
+   * Called when an error occurs.  This may be called multiple times before
+   * onComplete is called.
+   *
+   * @param error  A number in Components.results describing the error.
+   */
+  void handleError(in nsresult error);
+
+  /**
+   * Called when the method finishes.  This will be called exactly once for
+   * each method invocation, and afterward no other callback methods will be
+   * called.
+   *
+   * @param reason  One of the COMPLETE_* values indicating the manner in which
+   *                the method completed.
+   */
+  void handleCompletion(in unsigned short reason);
+
+  const unsigned short COMPLETE_OK = 0;
+  const unsigned short COMPLETE_ERROR = 1;
+};
+
+[scriptable, function, uuid(9f24948d-24b5-4b1b-b554-7dbd58c1d792)]
+interface nsIContentPref : nsISupports
+{
+  readonly attribute AString domain;
+  readonly attribute AString name;
+  readonly attribute nsIVariant value;
+};
--- a/testing/xpcshell/xpcshell.ini
+++ b/testing/xpcshell/xpcshell.ini
@@ -21,16 +21,17 @@
 [include:dom/system/gonk/tests/xpcshell.ini]
 [include:dom/tests/unit/xpcshell.ini]
 [include:dom/indexedDB/test/unit/xpcshell.ini]
 [include:docshell/test/unit/xpcshell.ini]
 [include:docshell/test/unit_ipc/xpcshell.ini]
 [include:embedding/tests/unit/xpcshell.ini]
 [include:toolkit/components/commandlines/test/unit/xpcshell.ini]
 [include:toolkit/components/contentprefs/tests/unit/xpcshell.ini]
+[include:toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini]
 [include:toolkit/devtools/debugger/tests/unit/xpcshell.ini]
 [include:toolkit/devtools/sourcemap/tests/unit/xpcshell.ini]
 [include:toolkit/components/passwordmgr/test/unit/xpcshell.ini]
 # Bug 676989: tests hang on Android
 skip-if = os == "android"
 [include:toolkit/components/places/tests/migration/xpcshell.ini]
 [include:toolkit/components/places/tests/autocomplete/xpcshell.ini]
 [include:toolkit/components/places/tests/inline/xpcshell.ini]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefService2.jsm
@@ -0,0 +1,798 @@
+/* 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/. */
+
+// This file is an XPCOM component that implements nsIContentPrefService2.
+// Although it's a JSM, it's not intended to be imported by consumers like JSMs
+// are usually imported.  It's only a JSM so that nsContentPrefService.js can
+// easily use it.  Consumers should access this component with the usual XPCOM
+// rigmarole:
+//
+//   Cc["@mozilla.org/content-pref/service;1"].
+//   getService(Ci.nsIContentPrefService2);
+//
+// That contract ID actually belongs to nsContentPrefService.js, which, when
+// QI'ed to nsIContentPrefService2, returns an instance of this component.
+//
+// The plan is to eventually remove nsIContentPrefService and its
+// implementation, nsContentPrefService.js.  At such time this file can stop
+// being a JSM, and the "_cps" parts that ContentPrefService2 relies on and
+// NSGetFactory and all the other XPCOM initialization goop in
+// nsContentPrefService.js can be moved here.
+//
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=699859
+
+let EXPORTED_SYMBOLS = [
+  "ContentPrefService2",
+];
+
+const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+
+function ContentPrefService2(cps) {
+  this._cps = cps;
+  this._cache = cps._cache;
+  this._pbStore = cps._privModeStorage;
+}
+
+ContentPrefService2.prototype = {
+
+  getByDomainAndName: function CPS2_getByDomainAndName(group, name, context,
+                                                       callback) {
+    checkGroupArg(group);
+    this._get(group, name, false, context, callback);
+  },
+
+  getBySubdomainAndName: function CPS2_getBySubdomainAndName(group, name,
+                                                             context,
+                                                             callback) {
+    checkGroupArg(group);
+    this._get(group, name, true, context, callback);
+  },
+
+  getGlobal: function CPS2_getGlobal(name, context, callback) {
+    this._get(null, name, false, context, callback);
+  },
+
+  _get: function CPS2__get(group, name, includeSubdomains, context, callback) {
+    group = this._parseGroup(group);
+    checkNameArg(name);
+    checkCallbackArg(callback, true);
+
+    // Some prefs may be in both the database and the private browsing store.
+    // Notify the caller of such prefs only once, using the values from private
+    // browsing.
+    let pbPrefs = new ContentPrefStore();
+    if (context && context.usePrivateBrowsing) {
+      for (let [sgroup, val] in
+             this._pbStore.match(group, name, includeSubdomains)) {
+        pbPrefs.set(sgroup, name, val);
+      }
+    }
+
+    this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
+      onRow: function onRow(row) {
+        let grp = row.getResultByName("grp");
+        let val = row.getResultByName("value");
+        this._cache.set(grp, name, val);
+        if (!pbPrefs.has(group, name))
+          cbHandleResult(callback, new ContentPref(grp, name, val));
+      },
+      onDone: function onDone(reason, ok, gotRow) {
+        if (ok) {
+          if (!gotRow)
+            this._cache.set(group, name, undefined);
+          for (let [pbGroup, pbName, pbVal] in pbPrefs) {
+            cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
+          }
+        }
+        cbHandleCompletion(callback, reason);
+      },
+      onError: function onError(nsresult) {
+        cbHandleError(callback, nsresult);
+      }
+    });
+  },
+
+  _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
+    let stmt = group ?
+      this._stmtWithGroupClause(group, includeSubdomains,
+        "SELECT groups.name AS grp, prefs.value AS value",
+        "FROM prefs",
+        "JOIN settings ON settings.id = prefs.settingID",
+        "JOIN groups ON groups.id = prefs.groupID",
+        "WHERE settings.name = :name AND prefs.groupID IN ($)"
+      ) :
+      this._stmt(
+        "SELECT NULL AS grp, prefs.value AS value",
+        "FROM prefs",
+        "JOIN settings ON settings.id = prefs.settingID",
+        "WHERE settings.name = :name AND prefs.groupID ISNULL"
+      );
+    stmt.params.name = name;
+    return stmt;
+  },
+
+  _stmtWithGroupClause: function CPS2__stmtWithGroupClause(group,
+                                                           includeSubdomains) {
+    let stmt = this._stmt(joinArgs(Array.slice(arguments, 2)).replace("$",
+      "SELECT id " +
+      "FROM groups " +
+      "WHERE name = :group OR " +
+            "(:includeSubdomains AND name LIKE :pattern ESCAPE '/')"
+    ));
+    stmt.params.group = group;
+    stmt.params.includeSubdomains = includeSubdomains || false;
+    stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/");
+    return stmt;
+  },
+
+  getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group,
+                                                                   name,
+                                                                   context) {
+    checkGroupArg(group);
+    let prefs = this._getCached(group, name, false, context);
+    return prefs[0] || null;
+  },
+
+  getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(group,
+                                                                         name,
+                                                                         context,
+                                                                         len) {
+    checkGroupArg(group);
+    let prefs = this._getCached(group, name, true, context);
+    if (len)
+      len.value = prefs.length;
+    return prefs;
+  },
+
+  getCachedGlobal: function CPS2_getCachedGlobal(name, context) {
+    let prefs = this._getCached(null, name, false, context);
+    return prefs[0] || null;
+  },
+
+  _getCached: function CPS2__getCached(group, name, includeSubdomains,
+                                       context) {
+    group = this._parseGroup(group);
+    checkNameArg(name);
+
+    let storesToCheck = [this._cache];
+    if (context && context.usePrivateBrowsing)
+      storesToCheck.push(this._pbStore);
+
+    let outStore = new ContentPrefStore();
+    storesToCheck.forEach(function (store) {
+      for (let [sgroup, val] in store.match(group, name, includeSubdomains)) {
+        outStore.set(sgroup, name, val);
+      }
+    });
+
+    let prefs = [];
+    for (let [sgroup, sname, val] in outStore) {
+      prefs.push(new ContentPref(sgroup, sname, val));
+    }
+    return prefs;
+  },
+
+  set: function CPS2_set(group, name, value, context, callback) {
+    checkGroupArg(group);
+    this._set(group, name, value, context, callback);
+  },
+
+  setGlobal: function CPS2_setGlobal(name, value, context, callback) {
+    this._set(null, name, value, context, callback);
+  },
+
+  _set: function CPS2__set(group, name, value, context, callback) {
+    group = this._parseGroup(group);
+    checkNameArg(name);
+    checkValueArg(value);
+    checkCallbackArg(callback, false);
+
+    if (context && context.usePrivateBrowsing) {
+      this._pbStore.set(group, name, value);
+      this._schedule(function () {
+        this._cps._notifyPrefSet(group, name, value);
+        cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK);
+      });
+      return;
+    }
+
+    let stmts = [];
+
+    // Create the setting if it doesn't exist.
+    let stmt = this._stmt(
+      "INSERT OR IGNORE INTO settings (id, name)",
+      "VALUES((SELECT id FROM settings WHERE name = :name), :name)"
+    );
+    stmt.params.name = name;
+    stmts.push(stmt);
+
+    // Create the group if it doesn't exist.
+    if (group) {
+      stmt = this._stmt(
+        "INSERT OR IGNORE INTO groups (id, name)",
+        "VALUES((SELECT id FROM groups WHERE name = :group), :group)"
+      );
+      stmt.params.group = group;
+      stmts.push(stmt);
+    }
+
+    // Finally create or update the pref.
+    if (group) {
+      stmt = this._stmt(
+        "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)",
+        "VALUES(",
+          "(SELECT prefs.id",
+           "FROM prefs",
+           "JOIN groups ON groups.id = prefs.groupID",
+           "JOIN settings ON settings.id = prefs.settingID",
+           "WHERE groups.name = :group AND settings.name = :name),",
+          "(SELECT id FROM groups WHERE name = :group),",
+          "(SELECT id FROM settings WHERE name = :name),",
+          ":value",
+        ")"
+      );
+      stmt.params.group = group;
+    }
+    else {
+      stmt = this._stmt(
+        "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)",
+        "VALUES(",
+          "(SELECT prefs.id",
+           "FROM prefs",
+           "JOIN settings ON settings.id = prefs.settingID",
+           "WHERE prefs.groupID IS NULL AND settings.name = :name),",
+          "NULL,",
+          "(SELECT id FROM settings WHERE name = :name),",
+          ":value",
+        ")"
+      );
+    }
+    stmt.params.name = name;
+    stmt.params.value = value;
+    stmts.push(stmt);
+
+    this._execStmts(stmts, {
+      onDone: function onDone(reason, ok) {
+        if (ok) {
+          this._cache.setWithCast(group, name, value);
+          this._cps._notifyPrefSet(group, name, value);
+        }
+        cbHandleCompletion(callback, reason);
+      },
+      onError: function onError(nsresult) {
+        cbHandleError(callback, nsresult);
+      }
+    });
+  },
+
+  removeByDomainAndName: function CPS2_removeByDomainAndName(group, name,
+                                                             context,
+                                                             callback) {
+    checkGroupArg(group);
+    this._remove(group, name, false, context, callback);
+  },
+
+  removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(group, name,
+                                                                   context,
+                                                                   callback) {
+    checkGroupArg(group);
+    this._remove(group, name, true, context, callback);
+  },
+
+  removeGlobal: function CPS2_removeGlobal(name, context,callback) {
+    this._remove(null, name, false, context, callback);
+  },
+
+  _remove: function CPS2__remove(group, name, includeSubdomains, context,
+                                 callback) {
+    group = this._parseGroup(group);
+    checkNameArg(name);
+    checkCallbackArg(callback, false);
+
+    let stmts = [];
+
+    // First get the matching prefs.
+    stmts.push(this._commonGetStmt(group, name, includeSubdomains));
+
+    // Delete the matching prefs.
+    let stmt = this._stmtWithGroupClause(group, includeSubdomains,
+      "DELETE FROM prefs",
+      "WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND",
+            "CASE typeof(:group)",
+            "WHEN 'null' THEN prefs.groupID IS NULL",
+            "ELSE prefs.groupID IN ($)",
+            "END"
+    );
+    stmt.params.name = name;
+    stmts.push(stmt);
+
+    // Delete settings and groups that are no longer used.  The NOTNULL term in
+    // the subquery of the second statment is needed because of SQLite's weird
+    // IN behavior vis-a-vis NULLs.  See http://sqlite.org/lang_expr.html.
+    stmts.push(this._stmt(
+      "DELETE FROM settings",
+      "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)"
+    ));
+    stmts.push(this._stmt(
+      "DELETE FROM groups WHERE id NOT IN (",
+        "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL",
+      ")"
+    ));
+
+    let prefs = new ContentPrefStore();
+
+    this._execStmts(stmts, {
+      onRow: function onRow(row) {
+        let grp = row.getResultByName("grp");
+        prefs.set(grp, name);
+        this._cache.set(grp, name, undefined);
+      },
+      onDone: function onDone(reason, ok) {
+        if (ok) {
+          this._cache.set(group, name, undefined);
+          if (context && context.usePrivateBrowsing) {
+            for (let [sgroup, ] in
+                   this._pbStore.match(group, name, includeSubdomains)) {
+              prefs.set(sgroup, name);
+              this._pbStore.remove(sgroup, name);
+            }
+          }
+          for (let [sgroup, , ] in prefs) {
+            this._cps._notifyPrefRemoved(sgroup, name);
+          }
+        }
+        cbHandleCompletion(callback, reason);
+      },
+      onError: function onError(nsresult) {
+        cbHandleError(callback, nsresult);
+      }
+    });
+  },
+
+  removeByDomain: function CPS2_removeByDomain(group, context, callback) {
+    checkGroupArg(group);
+    this._removeByDomain(group, false, context, callback);
+  },
+
+  removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) {
+    checkGroupArg(group);
+    this._removeByDomain(group, true, context, callback);
+  },
+
+  removeAllGlobals: function CPS2_removeAllGlobals(context, callback) {
+    this._removeByDomain(null, false, context, callback);
+  },
+
+  _removeByDomain: function CPS2__removeByDomain(group, includeSubdomains,
+                                                 context, callback) {
+    group = this._parseGroup(group);
+    checkCallbackArg(callback, false);
+
+    let stmts = [];
+
+    // First get the matching prefs, then delete groups and prefs that reference
+    // deleted groups.
+    if (group) {
+      stmts.push(this._stmtWithGroupClause(group, includeSubdomains,
+        "SELECT groups.name AS grp, settings.name AS name",
+        "FROM prefs",
+        "JOIN settings ON settings.id = prefs.settingID",
+        "JOIN groups ON groups.id = prefs.groupID",
+        "WHERE prefs.groupID IN ($)"
+      ));
+      stmts.push(this._stmtWithGroupClause(group, includeSubdomains,
+        "DELETE FROM groups WHERE id IN ($)"
+      ));
+      stmts.push(this._stmt(
+        "DELETE FROM prefs",
+        "WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)"
+      ));
+    }
+    else {
+      stmts.push(this._stmt(
+        "SELECT NULL AS grp, settings.name AS name",
+        "FROM prefs",
+        "JOIN settings ON settings.id = prefs.settingID",
+        "WHERE prefs.groupID IS NULL"
+      ));
+      stmts.push(this._stmt(
+        "DELETE FROM prefs WHERE groupID IS NULL"
+      ));
+    }
+
+    // Finally delete settings that are no longer referenced.
+    stmts.push(this._stmt(
+      "DELETE FROM settings",
+      "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)"
+    ));
+
+    let prefs = new ContentPrefStore();
+
+    this._execStmts(stmts, {
+      onRow: function onRow(row) {
+        let grp = row.getResultByName("grp");
+        let name = row.getResultByName("name");
+        prefs.set(grp, name);
+        this._cache.set(grp, name, undefined);
+      },
+      onDone: function onDone(reason, ok) {
+        if (ok) {
+          if (context && context.usePrivateBrowsing) {
+            for (let [sgroup, sname, ] in this._pbStore) {
+              prefs.set(sgroup, sname);
+              this._pbStore.remove(sgroup, sname);
+            }
+          }
+          for (let [sgroup, sname, ] in prefs) {
+            this._cps._notifyPrefRemoved(sgroup, sname);
+          }
+        }
+        cbHandleCompletion(callback, reason);
+      },
+      onError: function onError(nsresult) {
+        cbHandleError(callback, nsresult);
+      }
+    });
+  },
+
+  removeAllDomains: function CPS2_removeAllDomains(context, callback) {
+    checkCallbackArg(callback, false);
+    let stmts = [];
+
+    // First get the matching prefs.
+    stmts.push(this._stmt(
+      "SELECT groups.name AS grp, settings.name AS name",
+      "FROM prefs",
+      "JOIN settings ON settings.id = prefs.settingID",
+      "JOIN groups ON groups.id = prefs.groupID"
+    ));
+
+    stmts.push(this._stmt(
+      "DELETE FROM prefs WHERE groupID NOTNULL"
+    ));
+    stmts.push(this._stmt(
+      "DELETE FROM groups"
+    ));
+    stmts.push(this._stmt(
+      "DELETE FROM settings",
+      "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)"
+    ));
+
+    let prefs = new ContentPrefStore();
+
+    this._execStmts(stmts, {
+      onRow: function onRow(row) {
+        let grp = row.getResultByName("grp");
+        let name = row.getResultByName("name");
+        prefs.set(grp, name);
+        this._cache.set(grp, name, undefined);
+      },
+      onDone: function onDone(reason, ok) {
+        if (ok) {
+          if (context && context.usePrivateBrowsing) {
+            for (let [sgroup, sname, ] in this._pbStore) {
+              prefs.set(sgroup, sname);
+            }
+            this._pbStore.removeGrouped();
+          }
+          for (let [sgroup, sname, ] in prefs) {
+            this._cps._notifyPrefRemoved(sgroup, sname);
+          }
+        }
+        cbHandleCompletion(callback, reason);
+      },
+      onError: function onError(nsresult) {
+        cbHandleError(callback, nsresult);
+      }
+    });
+  },
+
+  removeByName: function CPS2_removeByName(name, context, callback) {
+    checkNameArg(name);
+    checkCallbackArg(callback, false);
+
+    let stmts = [];
+
+    // First get the matching prefs.  Include null if any of those prefs are
+    // global.
+    let stmt = this._stmt(
+      "SELECT groups.name AS grp",
+      "FROM prefs",
+      "JOIN settings ON settings.id = prefs.settingID",
+      "JOIN groups ON groups.id = prefs.groupID",
+      "WHERE settings.name = :name",
+      "UNION",
+      "SELECT NULL AS grp",
+      "WHERE EXISTS (",
+        "SELECT prefs.id",
+        "FROM prefs",
+        "JOIN settings ON settings.id = prefs.settingID",
+        "WHERE settings.name = :name AND prefs.groupID IS NULL",
+      ")"
+    );
+    stmt.params.name = name;
+    stmts.push(stmt);
+
+    // Delete the target settings.
+    stmt = this._stmt(
+      "DELETE FROM settings WHERE name = :name"
+    );
+    stmt.params.name = name;
+    stmts.push(stmt);
+
+    // Delete prefs and groups that are no longer used.
+    stmts.push(this._stmt(
+      "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)"
+    ));
+    stmts.push(this._stmt(
+      "DELETE FROM groups WHERE id NOT IN (",
+        "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL",
+      ")"
+    ));
+
+    let prefs = new ContentPrefStore();
+
+    this._execStmts(stmts, {
+      onRow: function onRow(row) {
+        let grp = row.getResultByName("grp");
+        prefs.set(grp, name);
+        this._cache.set(grp, name, undefined);
+      },
+      onDone: function onDone(reason, ok) {
+        if (ok) {
+          if (context && context.usePrivateBrowsing) {
+            for (let [sgroup, sname, ] in this._pbStore) {
+              if (sname === name) {
+                prefs.set(sgroup, name);
+                this._pbStore.remove(sgroup, name);
+              }
+            }
+          }
+          for (let [sgroup, , ] in prefs) {
+            this._cps._notifyPrefRemoved(sgroup, name);
+          }
+        }
+        cbHandleCompletion(callback, reason);
+      },
+      onError: function onError(nsresult) {
+        cbHandleError(callback, nsresult);
+      }
+    });
+  },
+
+  destroy: function CPS2_destroy() {
+    for each (let stmt in this._statements) {
+      stmt.finalize();
+    }
+  },
+
+  /**
+   * Returns the cached mozIStorageAsyncStatement for the given SQL.  If no such
+   * statement is cached, one is created and cached.
+   *
+   * @param sql  The SQL query string.  If more than one string is given, then
+   *             all are concatenated.  The concatenation process inserts
+   *             spaces where appropriate and removes unnecessary contiguous
+   *             spaces.  Call like _stmt("SELECT *", "FROM foo").
+   * @return     The cached, possibly new, statement.
+   */
+  _stmt: function CPS2__stmt(sql /*, sql2, sql3, ... */) {
+    let sql = joinArgs(arguments);
+    if (!this._statements)
+      this._statements = {};
+    if (!this._statements[sql])
+      this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql);
+    return this._statements[sql];
+  },
+
+  /**
+   * Executes some async statements.
+   *
+   * @param stmts      An array of mozIStorageAsyncStatements.
+   * @param callbacks  An object with the following methods:
+   *                   onRow(row) (optional)
+   *                     Called once for each result row.
+   *                     row: A mozIStorageRow.
+   *                   onDone(reason, reasonOK, didGetRow) (required)
+   *                     Called when done.
+   *                     reason: A nsIContentPrefService2.COMPLETE_* value.
+   *                     reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
+   *                     didGetRow: True if onRow was ever called.
+   *                   onError(nsresult) (optional)
+   *                     Called on error.
+   *                     nsresult: The error code.
+   */
+  _execStmts: function CPS2__execStmts(stmts, callbacks) {
+    let self = this;
+    let gotRow = false;
+    this._cps._dbConnection.executeAsync(stmts, stmts.length, {
+      handleResult: function handleResult(results) {
+        try {
+          let row = null;
+          while ((row = results.getNextRow())) {
+            gotRow = true;
+            if (callbacks.onRow)
+              callbacks.onRow.call(self, row);
+          }
+        }
+        catch (err) {
+          Cu.reportError(err);
+        }
+      },
+      handleCompletion: function handleCompletion(reason) {
+        try {
+          let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED;
+          callbacks.onDone.call(self,
+                                ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK :
+                                  Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
+                                ok, gotRow);
+        }
+        catch (err) {
+          Cu.reportError(err);
+        }
+      },
+      handleError: function handleError(error) {
+        try {
+          if (callbacks.onError)
+            callbacks.onError.call(self, Cr.NS_ERROR_FAILURE);
+        }
+        catch (err) {
+          Cu.reportError(err);
+        }
+      }
+    });
+  },
+
+  /**
+   * Parses the domain (the "group", to use the database's term) from the given
+   * string.
+   *
+   * @param groupStr  Assumed to be either a string or falsey.
+   * @return          If groupStr is a valid URL string, returns the domain of
+   *                  that URL.  If groupStr is some other nonempty string,
+   *                  returns groupStr itself.  Otherwise returns null.
+   */
+  _parseGroup: function CPS2__parseGroup(groupStr) {
+    if (!groupStr)
+      return null;
+    try {
+      var groupURI = Services.io.newURI(groupStr, null, null);
+    }
+    catch (err) {
+      return groupStr;
+    }
+    return this._cps.grouper.group(groupURI);
+  },
+
+  _schedule: function CPS2__schedule(fn) {
+    Services.tm.mainThread.dispatch(fn.bind(this),
+                                    Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  addObserverForName: function CPS2_addObserverForName(name, observer) {
+    this._cps.addObserver(name, observer);
+  },
+
+  removeObserverForName: function CPS2_removeObserverForName(name, observer) {
+    this._cps.removeObserver(name, observer);
+  },
+
+  /**
+   * Tests use this as a backchannel by calling it directly.
+   *
+   * @param subj   This value depends on topic.
+   * @param topic  The backchannel "method" name.
+   * @param data   This value depends on topic.
+   */
+  observe: function CPS2_observe(subj, topic, data) {
+    switch (topic) {
+    case "test:reset":
+      let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
+      this._reset(fn);
+      break;
+    case "test:db":
+      let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
+      obj.value = this._cps._dbConnection;
+      break;
+    }
+  },
+
+  /**
+   * Removes all state from the service.  Used by tests.
+   *
+   * @param callback  A function that will be called when done.
+   */
+  _reset: function CPS2__reset(callback) {
+    this._pbStore.removeAll();
+    this._cache.removeAll();
+
+    let cps = this._cps;
+    cps._observers = {};
+    cps._genericObservers = [];
+
+    let tables = ["prefs", "groups", "settings"];
+    let stmts = tables.map(function (t) this._stmt("DELETE FROM", t), this);
+    this._execStmts(stmts, { onDone: function () callback() });
+  },
+
+  QueryInterface: function CPS2_QueryInterface(iid) {
+    let supportedIIDs = [
+      Ci.nsIContentPrefService,
+      Ci.nsIObserver,
+      Ci.nsISupports,
+    ];
+    if (supportedIIDs.some(function (i) iid.equals(i)))
+      return this;
+    if (iid.equals(Ci.nsIContentPrefService))
+      return this._cps;
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+};
+
+function ContentPref(domain, name, value) {
+  this.domain = domain;
+  this.name = name;
+  this.value = value;
+}
+
+ContentPref.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]),
+};
+
+function cbHandleResult(callback, pref) {
+  safeCallback(callback, "handleResult", [pref]);
+}
+
+function cbHandleCompletion(callback, reason) {
+  safeCallback(callback, "handleCompletion", [reason]);
+}
+
+function cbHandleError(callback, nsresult) {
+  safeCallback(callback, "handleError", [nsresult]);
+}
+
+function safeCallback(callbackObj, methodName, args) {
+  if (!callbackObj || typeof(callbackObj[methodName]) != "function")
+    return;
+  try {
+    callbackObj[methodName].apply(callbackObj, args);
+  }
+  catch (err) {
+    Cu.reportError(err);
+  }
+}
+
+function checkGroupArg(group) {
+  if (!group || typeof(group) != "string")
+    throw invalidArg("domain must be nonempty string.");
+}
+
+function checkNameArg(name) {
+  if (!name || typeof(name) != "string")
+    throw invalidArg("name must be nonempty string.");
+}
+
+function checkValueArg(value) {
+  if (value === undefined)
+    throw invalidArg("value must not be undefined.");
+}
+
+function checkCallbackArg(callback, required) {
+  if (callback && !(callback instanceof Ci.nsIContentPrefCallback2))
+    throw invalidArg("callback must be an nsIContentPrefCallback2.");
+  if (!callback && required)
+    throw invalidArg("callback must be given.");
+}
+
+function invalidArg(msg) {
+  return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG);
+}
+
+function joinArgs(args) {
+  return Array.join(args, " ").trim().replace(/\s{2,}/g, " ");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefStore.jsm
@@ -0,0 +1,115 @@
+/* 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/. */
+
+let EXPORTED_SYMBOLS = [
+  "ContentPrefStore",
+];
+
+function ContentPrefStore() {
+  this._groups = {};
+  this._globalNames = {};
+}
+
+ContentPrefStore.prototype = {
+
+  set: function CPS_set(group, name, val) {
+    if (group) {
+      if (!this._groups.hasOwnProperty(group))
+        this._groups[group] = {};
+      this._groups[group][name] = val;
+    }
+    else {
+      this._globalNames[name] = val;
+    }
+  },
+
+  setWithCast: function CPS_setWithCast(group, name, val) {
+    if (typeof(val) == "boolean")
+      val = val ? 1 : 0;
+    else if (val === undefined)
+      val = null;
+    this.set(group, name, val);
+  },
+
+  has: function CPS_has(group, name) {
+    return (group &&
+            this._groups.hasOwnProperty(group) &&
+            this._groups[group].hasOwnProperty(name)) ||
+           (!group &&
+            this._globalNames.hasOwnProperty(name));
+  },
+
+  get: function CPS_get(group, name) {
+    if (group) {
+      if (this._groups.hasOwnProperty(group) &&
+          this._groups[group].hasOwnProperty(name))
+        return this._groups[group][name];
+    }
+    else if (this._globalNames.hasOwnProperty(name)) {
+      return this._globalNames[name];
+    }
+    return undefined;
+  },
+
+  remove: function CPS_remove(group, name) {
+    if (group) {
+      if (this._groups.hasOwnProperty(group)) {
+        delete this._groups[group][name];
+        if (!Object.keys(this._groups[group]).length)
+          delete this._groups[group];
+      }
+    }
+    else {
+      delete this._globalNames[name];
+    }
+  },
+
+  removeGrouped: function CPS_removeGrouped() {
+    this._groups = {};
+  },
+
+  removeAll: function CPS_removeAll() {
+    this.removeGrouped();
+    this._globalNames = {};
+  },
+
+  __iterator__: function CPS___iterator__() {
+    for (let [group, names] in Iterator(this._groups)) {
+      for (let [name, val] in Iterator(names)) {
+        yield [group, name, val];
+      }
+    }
+    for (let [name, val] in Iterator(this._globalNames)) {
+      yield [null, name, val];
+    }
+  },
+
+  match: function CPS_match(group, name, includeSubdomains) {
+    for (let sgroup in this.matchGroups(group, includeSubdomains)) {
+      if (this.has(sgroup, name))
+        yield [sgroup, this.get(sgroup, name)];
+    }
+  },
+
+  matchGroups: function CPS_matchGroups(group, includeSubdomains) {
+    if (group) {
+      if (includeSubdomains) {
+        for (let [sgroup, , ] in this) {
+          if (sgroup) {
+            let idx = sgroup.indexOf(group);
+            if (idx == sgroup.length - group.length &&
+                (idx == 0 || sgroup[idx - 1] == "."))
+              yield sgroup;
+          }
+        }
+      }
+      else if (this._groups.hasOwnProperty(group)) {
+        yield group;
+      }
+    }
+    else if (Object.keys(this._globalNames).length) {
+      yield null;
+    }
+  },
+};
--- a/toolkit/components/contentprefs/Makefile.in
+++ b/toolkit/components/contentprefs/Makefile.in
@@ -6,15 +6,22 @@ DEPTH = @DEPTH@
 topsrcdir = @top_srcdir@
 srcdir = @srcdir@
 VPATH = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = contentprefs
 
-EXTRA_COMPONENTS = nsContentPrefService.js nsContentPrefService.manifest
+EXTRA_COMPONENTS = \
+  nsContentPrefService.js \
+  nsContentPrefService.manifest \
+  $(NULL)
 
-EXTRA_JS_MODULES = ContentPrefInstance.jsm
+EXTRA_JS_MODULES = \
+  ContentPrefInstance.jsm \
+  ContentPrefService2.jsm \
+  ContentPrefStore.jsm \
+  $(NULL)
 
 TEST_DIRS += tests
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/components/contentprefs/nsContentPrefService.js
+++ b/toolkit/components/contentprefs/nsContentPrefService.js
@@ -101,71 +101,58 @@ function ContentPrefService() {
   this._dbInit();
 
   this._observerSvc.addObserver(this, "last-pb-context-exited", false);
 
   // Observe shutdown so we can shut down the database connection.
   this._observerSvc.addObserver(this, "xpcom-shutdown", false);
 }
 
-var inMemoryPrefsProto = {
-  getPref: function(aName, aGroup) {
-    aGroup = aGroup || "__GlobalPrefs__";
-
-    if (this._prefCache[aGroup] && this._prefCache[aGroup].has(aName)) {
-      let value = this._prefCache[aGroup].get(aName);
-      return [true, value];
-    }
-    return [false, undefined];
-  },
-
-  setPref: function(aName, aValue, aGroup) {
-    if (typeof aValue == "boolean")
-      aValue = aValue ? 1 : 0;
-    else if (aValue === undefined)
-      aValue = null;
-
-    this.cachePref(aName, aValue, aGroup);
-  },
-
-  removePref: function(aName, aGroup) {
-    aGroup = aGroup || "__GlobalPrefs__";
-
-    if (this._prefCache[aGroup].has(aName)) {
-      this._prefCache[aGroup].delete(aName);
-      if (this._prefCache[aGroup].size == 0) {
-        // remove empty group
-        delete this._prefCache[aGroup];
-      }
-    }
-  },
-
-  invalidate: function(aKeepGlobal) {
-    if (!aKeepGlobal) {
-      this._prefCache = {};
-      return;
-    }
-
-    if (this._prefCache.hasOwnProperty("__GlobalPrefs__")) {
-      let globals = this._prefCache["__GlobalPrefs__"];
-      this._prefCache = {"__GlobalPrefs__": globals};
-    } else {
-      this._prefCache = {};
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+const cache = new ContentPrefStore();
+cache.set = function CPS_cache_set(group, name, val) {
+  Object.getPrototypeOf(this).set.apply(this, arguments);
+  let groupCount = Object.keys(this._groups).length;
+  if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
+    // Clean half of the entries
+    for (let [group, name, ] in this) {
+      this.remove(group, name);
+      groupCount--;
+      if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2)
+        break;
     }
   }
 };
 
+const privModeStorage = new ContentPrefStore();
+
 ContentPrefService.prototype = {
   //**************************************************************************//
   // XPCOM Plumbing
 
-  classID:          Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
-  QueryInterface:   XPCOMUtils.generateQI([Ci.nsIContentPrefService,
-                                           Ci.nsIMessageListener]),
+  classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
 
+  QueryInterface: function CPS_QueryInterface(iid) {
+    let supportedIIDs = [
+      Ci.nsIContentPrefService,
+      Ci.nsIFrameMessageListener,
+      Ci.nsISupports,
+    ];
+    if (supportedIIDs.some(function (i) iid.equals(i)))
+      return this;
+    if (iid.equals(Ci.nsIContentPrefService2)) {
+      if (!this._contentPrefService2) {
+        let s = {};
+        Cu.import("resource://gre/modules/ContentPrefService2.jsm", s);
+        this._contentPrefService2 = new s.ContentPrefService2(this);
+      }
+      return this._contentPrefService2;
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
 
   //**************************************************************************//
   // Convenience Getters
 
   // Observer Service
   __observerSvc: null,
   get _observerSvc() {
     if (!this.__observerSvc)
@@ -258,16 +245,19 @@ ContentPrefService.prototype = {
       this.__stmtDeletePref.finalize();
       this.__stmtDeletePref = null;
     }
     if (this.__stmtUpdatePref) {
       this.__stmtUpdatePref.finalize();
       this.__stmtUpdatePref = null;
     }
 
+    if (this._contentPrefService2)
+      this._contentPrefService2.destroy();
+
     this._dbConnection.asyncClose();
 
     // Delete references to XPCOM components to make sure we don't leak them
     // (although we haven't observed leakage in tests).  Also delete references
     // in _observers and _genericObservers to avoid cycles with those that
     // refer to us and don't remove themselves from those observer pools.
     for (var i in this) {
       try { this[i] = null }
@@ -281,118 +271,41 @@ ContentPrefService.prototype = {
   // nsIObserver
 
   observe: function ContentPrefService_observe(subject, topic, data) {
     switch (topic) {
       case "xpcom-shutdown":
         this._destroy();
         break;
       case "last-pb-context-exited":
-        this._privModeStorage.invalidate();
+        this._privModeStorage.removeAll();
         break;
     }
   },
 
 
   //**************************************************************************//
-  // Prefs cache
-  _cache: Object.create(inMemoryPrefsProto, {
-    _prefCache: { 
-      value: {}, configurable: true, writable: true, enumerable: true
-    },
-
-    cachePref: { value:
-      function(aName, aValue, aGroup) {
-        aGroup = aGroup || "__GlobalPrefs__";
-
-        if (!this._prefCache[aGroup]) {
-          this._possiblyCleanCache();
-          this._prefCache[aGroup] = new Map();
-        }
-
-        this._prefCache[aGroup].set(aName, aValue);
-      }
-    },
-
-    _possiblyCleanCache: { value:
-      function() {
-        let groupCount = Object.keys(this._prefCache).length;
-
-        if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
-          // Clean half of the entries
-          for (let entry in this._prefCache) {
-            delete this._prefCache[entry];
-            groupCount--;
-
-            if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2)
-              break;
-          }
-        }
-      }
-    }
-  }),
+  // in-memory cache and private-browsing stores
 
-  //**************************************************************************//
-  // Private mode storage
-  _privModeStorage: Object.create(inMemoryPrefsProto, {
-    _prefCache: { 
-      value: {}, configurable: true, writable: true, enumerable: true
-    },
-
-    cachePref: { value: 
-      function(aName, aValue, aGroup) {
-        aGroup = aGroup || "__GlobalPrefs__";
-
-        if (!this._prefCache[aGroup]) {
-          this._prefCache[aGroup] = new Map();
-        }
-
-        this._prefCache[aGroup].set(aName, aValue);
-      }
-    },
-
-    getPrefs: { value: 
-      function(aGroup) {
-        aGroup = aGroup || "__GlobalPrefs__";
-        if (this._prefCache[aGroup]) {
-          return [true, this._prefCache[aGroup]];
-        }
-        return [false, undefined];
-      }
-    },
-
-    groupsForName: { value: 
-      function(aName) {
-        var res = [];
-        for (let entry in this._prefCache) {
-          if (this._prefCache[entry]) {
-            if (entry === "__GlobalPrefs__") {
-              entry = null;
-            }
-            res.push(entry);
-          }
-        }
-        return res;
-      }
-    }
-  }),
+  _cache: cache,
+  _privModeStorage: privModeStorage,
 
   //**************************************************************************//
   // nsIContentPrefService
 
   getPref: function ContentPrefService_getPref(aGroup, aName, aContext, aCallback) {
     if (!aName)
       throw Components.Exception("aName cannot be null or an empty string",
                                  Cr.NS_ERROR_ILLEGAL_VALUE);
 
     var group = this._parseGroupParam(aGroup);
 
     if (aContext && aContext.usePrivateBrowsing) {
-      let [haspref, value] = this._privModeStorage.getPref(aName, group);
-      if (haspref) {
+      if (this._privModeStorage.has(group, aName)) {
+        let value = this._privModeStorage.get(group, aName);
         if (aCallback) {
           this._scheduleCallback(function(){aCallback.onResult(value);});
           return;
         }
         return value;
       }
       // if we don't have a pref specific to this private mode browsing
       // session, to try to get one from normal mode
@@ -409,17 +322,17 @@ ContentPrefService.prototype = {
     if (typeof currentValue != "undefined") {
       if (currentValue == aValue)
         return;
     }
 
     var group = this._parseGroupParam(aGroup);
 
     if (aContext && aContext.usePrivateBrowsing) {
-      this._privModeStorage.setPref(aName, aValue, group);
+      this._privModeStorage.setWithCast(group, aName, aValue);
       this._notifyPrefSet(group, aName, aValue);
       return;
     }
 
     var settingID = this._selectSettingID(aName) || this._insertSetting(aName);
     var groupID, prefID;
     if (group == null) {
       groupID = null;
@@ -431,17 +344,17 @@ ContentPrefService.prototype = {
     }
 
     // Update the existing record, if any, or create a new one.
     if (prefID)
       this._updatePref(prefID, aValue);
     else
       this._insertPref(groupID, settingID, aValue);
 
-    this._cache.setPref(aName, aValue, group);
+    this._cache.setWithCast(group, aName, aValue);
 
     this._notifyPrefSet(group, aName, aValue);
   },
 
   hasPref: function ContentPrefService_hasPref(aGroup, aName, aContext) {
     // XXX If consumers end up calling this method regularly, then we should
     // optimize this to query the database directly.
     return (typeof this.getPref(aGroup, aName, aContext) != "undefined");
@@ -449,29 +362,28 @@ ContentPrefService.prototype = {
 
   hasCachedPref: function ContentPrefService_hasCachedPref(aGroup, aName, aContext) {
     if (!aName)
       throw Components.Exception("aName cannot be null or an empty string",
                                  Cr.NS_ERROR_ILLEGAL_VALUE);
 
     let group = this._parseGroupParam(aGroup);
     let storage = aContext && aContext.usePrivateBrowsing ? this._privModeStorage: this._cache;
-    let [cached,] = storage.getPref(aName, group);
-    return cached;
+    return storage.has(group, aName);
   },
 
   removePref: function ContentPrefService_removePref(aGroup, aName, aContext) {
     // If there's no old value, then there's nothing to remove.
     if (!this.hasPref(aGroup, aName, aContext))
       return;
 
     var group = this._parseGroupParam(aGroup);
 
     if (aContext && aContext.usePrivateBrowsing) {
-      this._privModeStorage.removePref(aName, group);
+      this._privModeStorage.remove(group, aName);
       this._notifyPrefRemoved(group, aName);
       return;
     }
 
     var settingID = this._selectSettingID(aName);
     var groupID, prefID;
     if (group == null) {
       groupID = null;
@@ -484,27 +396,27 @@ ContentPrefService.prototype = {
 
     this._deletePref(prefID);
 
     // Get rid of extraneous records that are no longer being used.
     this._deleteSettingIfUnused(settingID);
     if (groupID)
       this._deleteGroupIfUnused(groupID);
 
-    this._cache.removePref(aName, group);
+    this._cache.remove(group, aName);
     this._notifyPrefRemoved(group, aName);
   },
 
   removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs(aContext) {
     // will not delete global preferences
     if (aContext && aContext.usePrivateBrowsing) {
         // keep only global prefs
-        this._privModeStorage.invalidate(true);
+        this._privModeStorage.removeGrouped();
     }
-    this._cache.invalidate(true);
+    this._cache.removeGrouped();
     this._dbConnection.beginTransaction();
     try {
       this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE groupID IS NOT NULL");
       this._dbConnection.executeSimpleSQL("DELETE FROM groups");
       this._dbConnection.executeSimpleSQL(
         "DELETE FROM settings " +
         "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)"
       );
@@ -517,21 +429,21 @@ ContentPrefService.prototype = {
   },
 
   removePrefsByName: function ContentPrefService_removePrefsByName(aName, aContext) {
     if (!aName)
       throw Components.Exception("aName cannot be null or an empty string",
                                  Cr.NS_ERROR_ILLEGAL_VALUE);
 
     if (aContext && aContext.usePrivateBrowsing) {
-      let groupNames = this._privModeStorage.groupsForName(aName);
-      for (var i = 0; i < groupNames.length; i++) {
-        let groupName = groupNames[i];
-        this._privModeStorage.removePref(aName, groupName);
-        this._notifyPrefRemoved(groupName, aName);
+      for (let [group, name, ] in this._privModeStorage) {
+        if (name === aName) {
+          this._privModeStorage.remove(group, aName);
+          this._notifyPrefRemoved(group, aName);
+        }
       }
     }
 
     var settingID = this._selectSettingID(aName);
     if (!settingID)
       return;
     
     var selectGroupsStmt = this._dbCreateStatement(
@@ -558,33 +470,33 @@ ContentPrefService.prototype = {
     if (this.hasPref(null, aName)) {
       groupNames.push(null);
     }
 
     this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE settingID = " + settingID);
     this._dbConnection.executeSimpleSQL("DELETE FROM settings WHERE id = " + settingID);
 
     for (var i = 0; i < groupNames.length; i++) {
-      this._cache.removePref(aName, groupNames[i]);
+      this._cache.remove(groupNames[i], aName);
       if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length)
         this._deleteGroupIfUnused(groupIDs[i]);
       if (!aContext || !aContext.usePrivateBrowsing) {
         this._notifyPrefRemoved(groupNames[i], aName);
       }
     }
   },
 
   getPrefs: function ContentPrefService_getPrefs(aGroup, aContext) {
     var group = this._parseGroupParam(aGroup);
     if (aContext && aContext.usePrivateBrowsing) {
         let prefs = Cc["@mozilla.org/hash-property-bag;1"].
                     createInstance(Ci.nsIWritablePropertyBag);
-        let [hasbranch,properties] = this._privModeStorage.getPrefs(group);
-        for (let [entry, value] of properties) {
-          prefs.setProperty(entry, value);
+        for (let [sgroup, sname, sval] in this._privModeStorage) {
+          if (sgroup === group)
+            prefs.setProperty(sname, sval);
         }
         return prefs;
     }
 
     if (group == null)
       return this._selectGlobalPrefs();
     return this._selectPrefs(group);
   },
@@ -592,21 +504,19 @@ ContentPrefService.prototype = {
   getPrefsByName: function ContentPrefService_getPrefsByName(aName, aContext) {
     if (!aName)
       throw Components.Exception("aName cannot be null or an empty string",
                                  Cr.NS_ERROR_ILLEGAL_VALUE);
 
     if (aContext && aContext.usePrivateBrowsing) {
       let prefs = Cc["@mozilla.org/hash-property-bag;1"].
                   createInstance(Ci.nsIWritablePropertyBag);
-      let groupNames = this._privModeStorage.groupsForName(aName);
-      for (var i = 0; i < groupNames.length; i++) {
-        let groupName = groupNames[i];
-        prefs.setProperty(groupName,
-                          this._privModeStorage.getPref(aName, groupName)[1]);
+      for (let [sgroup, sname, sval] in this._privModeStorage) {
+        if (sname === aName)
+          prefs.setProperty(sgroup, sval);
       }
       return prefs;
     }
 
     return this._selectPrefsByName(aName);
   },
 
   // A hash of arrays of observers, indexed by setting name.
@@ -714,41 +624,42 @@ ContentPrefService.prototype = {
   },
 
   _scheduleCallback: function(func) {
     let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
     tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
   },
 
   _selectPref: function ContentPrefService__selectPref(aGroup, aSetting, aCallback) {
-    let [cached, value] = this._cache.getPref(aSetting, aGroup);
-    if (cached) {
+    let value = undefined;
+    if (this._cache.has(aGroup, aSetting)) {
+      value = this._cache.get(aGroup, aSetting);
       if (aCallback) {
         this._scheduleCallback(function(){aCallback.onResult(value);});
         return;
       }
       return value;
     }
 
     try {
       this._stmtSelectPref.params.group = aGroup;
       this._stmtSelectPref.params.setting = aSetting;
 
       if (aCallback) {
         let cache = this._cache;
         new AsyncStatement(this._stmtSelectPref).execute({onResult: function(aResult) {
-          cache.cachePref(aSetting, aResult, aGroup);
+          cache.set(aGroup, aSetting, aResult);
           aCallback.onResult(aResult);
         }});
       }
       else {
         if (this._stmtSelectPref.executeStep()) {
           value = this._stmtSelectPref.row["value"];
         }
-        this._cache.cachePref(aSetting, value, aGroup);
+        this._cache.set(aGroup, aSetting, value);
       }
     }
     finally {
       this._stmtSelectPref.reset();
     }
 
     return value;
   },
@@ -763,40 +674,41 @@ ContentPrefService.prototype = {
         "WHERE prefs.groupID IS NULL " +
         "AND settings.name = :name"
       );
 
     return this.__stmtSelectGlobalPref;
   },
 
   _selectGlobalPref: function ContentPrefService__selectGlobalPref(aName, aCallback) {
-    let [cached, value] = this._cache.getPref(aName, null);
-    if (cached) {
+    let value = undefined;
+    if (this._cache.has(null, aName)) {
+      value = this._cache.get(null, aName);
       if (aCallback) {
         this._scheduleCallback(function(){aCallback.onResult(value);});
         return;
       }
       return value;
     }
 
     try {
       this._stmtSelectGlobalPref.params.name = aName;
 
       if (aCallback) {
         let cache = this._cache;
         new AsyncStatement(this._stmtSelectGlobalPref).execute({onResult: function(aResult) {
-          cache.cachePref(aName, aResult);
+          cache.set(null, aName, aResult);
           aCallback.onResult(aResult);
         }});
       }
       else {
         if (this._stmtSelectGlobalPref.executeStep()) {
           value = this._stmtSelectGlobalPref.row["value"];
         }
-        this._cache.cachePref(aName, value);
+        this._cache.set(null, aName, value);
       }
     }
     finally {
       this._stmtSelectGlobalPref.reset();
     }
 
     return value;
   },
--- a/toolkit/components/contentprefs/tests/Makefile.in
+++ b/toolkit/components/contentprefs/tests/Makefile.in
@@ -8,19 +8,16 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE		= test_toolkit_contentprefs
 
-ifdef MOZ_PHOENIX
-XPCSHELL_TESTS = unit
+XPCSHELL_TESTS = unit unit_cps2
 
 # FIXME/bug 575918: out-of-process xpcshell is broken on OS X
 ifneq ($(OS_ARCH),Darwin)
 XPCSHELL_TESTS += unit_ipc
 endif
 
-endif
-
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm
@@ -0,0 +1,69 @@
+/* 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/. */
+
+let EXPORTED_SYMBOLS = [
+  "AsyncRunner",
+];
+
+const { interfaces: Ci, classes: Cc } = Components;
+
+function AsyncRunner(callbacks) {
+  this._callbacks = callbacks;
+  this._iteratorQueue = [];
+
+  // This catches errors reported to the console, e.g., via Cu.reportError.
+  Cc["@mozilla.org/consoleservice;1"].
+    getService(Ci.nsIConsoleService).
+    registerListener(this);
+}
+
+AsyncRunner.prototype = {
+
+  appendIterator: function AR_appendIterator(iter) {
+    this._iteratorQueue.push(iter);
+  },
+
+  next: function AR_next(/* ... */) {
+    if (!this._iteratorQueue.length) {
+      this.destroy();
+      this._callbacks.done();
+      return;
+    }
+
+    // send() discards all arguments after the first, so there's no choice here
+    // but to send only one argument to the yielder.
+    let args = [arguments.length <= 1 ? arguments[0] : Array.slice(arguments)];
+    try {
+      var val = this._iteratorQueue[0].send.apply(this._iteratorQueue[0], args);
+    }
+    catch (err if err instanceof StopIteration) {
+      this._iteratorQueue.shift();
+      this.next();
+      return;
+    }
+    catch (err) {
+      this._callbacks.error(err);
+    }
+
+    // val is truthy => call next
+    // val is an iterator => prepend it to the queue and start on it
+    if (val) {
+      if (typeof(val) != "boolean")
+        this._iteratorQueue.unshift(val);
+      this.next();
+    }
+  },
+
+  destroy: function AR_destroy() {
+    Cc["@mozilla.org/consoleservice;1"].
+      getService(Ci.nsIConsoleService).
+      unregisterListener(this);
+    this.destroy = function AR_alreadyDestroyed() {};
+  },
+
+  observe: function AR_consoleServiceListener(msg) {
+    if (msg instanceof Ci.nsIScriptError)
+      this._callbacks.consoleError(msg);
+  },
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/head.js
@@ -0,0 +1,311 @@
+/* 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/. */
+
+const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var cps;
+var asyncRunner;
+var next;
+
+(function init() {
+  // There has to be a profile directory before the CPS service is gotten.
+  do_get_profile();
+})();
+
+function runAsyncTests(tests) {
+  do_test_pending();
+
+  cps = Cc["@mozilla.org/content-pref/service;1"].
+        getService(Ci.nsIContentPrefService2);
+
+  // Without this the private-browsing service tries to open a dialog when you
+  // change its enabled state.
+  Services.prefs.setBoolPref("browser.privatebrowsing.keep_current_session",
+                             true);
+
+  let s = {};
+  Cu.import("resource://test/AsyncRunner.jsm", s);
+  asyncRunner = new s.AsyncRunner({
+    done: do_test_finished,
+    error: function (err) {
+      // xpcshell test functions like do_check_eq throw NS_ERROR_ABORT on
+      // failure.  Ignore those and catch only uncaught exceptions.
+      if (err !== Cr.NS_ERROR_ABORT) {
+        if (err.stack) {
+          err = err + "\n\nTraceback (most recent call first):\n" + err.stack +
+                      "\nUseless do_throw stack:";
+        }
+        do_throw(err);
+      }
+    },
+    consoleError: function (scriptErr) {
+      // As much as possible make sure the error is related to the test.  On the
+      // other hand if this fails to catch a test-related error, we'll hang.
+      let filename = scriptErr.sourceName || scriptErr.toString() || "";
+      if (/contentpref/i.test(filename))
+        do_throw(scriptErr);
+    }
+  });
+
+  next = asyncRunner.next.bind(asyncRunner);
+
+  do_register_cleanup(function () {
+    asyncRunner.destroy();
+    asyncRunner = null;
+  });
+
+  tests.forEach(function (test) {
+    function gen() {
+      do_print("Running " + test.name);
+      yield test();
+      yield reset();
+    }
+    asyncRunner.appendIterator(gen());
+  });
+
+  // reset() ends up calling asyncRunner.next(), starting the tests.
+  reset();
+}
+
+function makeCallback(callbacks) {
+  callbacks = callbacks || {};
+  ["handleResult", "handleError"].forEach(function (meth) {
+    if (!callbacks[meth])
+      callbacks[meth] = function () {
+        do_throw(meth + " shouldn't be called.");
+      };
+  });
+  if (!callbacks.handleCompletion)
+    callbacks.handleCompletion = function (reason) {
+      do_check_eq(reason, Ci.nsIContentPrefCallback2.COMPLETE_OK);
+      next();
+    };
+  return callbacks;
+}
+
+function do_check_throws(fn) {
+  let threw = false;
+  try {
+    fn();
+  }
+  catch (err) {
+    threw = true;
+  }
+  do_check_true(threw);
+}
+
+function sendMessage(msg, callback) {
+  let obj = callback || {};
+  let ref = Cu.getWeakReference(obj);
+  cps.QueryInterface(Ci.nsIObserver).observe(ref, "test:" + msg, null);
+  return "value" in obj ? obj.value : undefined;
+}
+
+function reset() {
+  sendMessage("reset", next);
+}
+
+function set(group, name, val, context) {
+  cps.set(group, name, val, context, makeCallback());
+}
+
+function setGlobal(name, val, context) {
+  cps.setGlobal(name, val, context, makeCallback());
+}
+
+function prefOK(actual, expected, strict) {
+  do_check_true(actual instanceof Ci.nsIContentPref);
+  do_check_eq(actual.domain, expected.domain);
+  do_check_eq(actual.name, expected.name);
+  if (strict)
+    do_check_true(actual.value === expected.value);
+  else
+    do_check_eq(actual.value, expected.value);
+}
+
+function getOK(args, expectedVal, expectedGroup, strict) {
+  if (args.length == 2)
+    args.push(undefined);
+  let expectedPrefs = expectedVal === undefined ? [] :
+                      [{ domain: expectedGroup || args[0],
+                         name: args[1],
+                         value: expectedVal }];
+  yield getOKEx("getByDomainAndName", args, expectedPrefs, strict);
+}
+
+function getSubdomainsOK(args, expectedGroupValPairs) {
+  if (args.length == 2)
+    args.push(undefined);
+  let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) {
+    return { domain: group, name: args[1], value: val };
+  });
+  yield getOKEx("getBySubdomainAndName", args, expectedPrefs);
+}
+
+function getGlobalOK(args, expectedVal) {
+  if (args.length == 1)
+    args.push(undefined);
+  let expectedPrefs = expectedVal === undefined ? [] :
+                      [{ domain: null, name: args[0], value: expectedVal }];
+  yield getOKEx("getGlobal", args, expectedPrefs);
+}
+
+function getOKEx(methodName, args, expectedPrefs, strict, context) {
+  let actualPrefs = [];
+  args.push(makeCallback({
+    handleResult: function (pref) actualPrefs.push(pref)
+  }));
+  yield cps[methodName].apply(cps, args);
+  arraysOfArraysOK([actualPrefs], [expectedPrefs], function (actual, expected) {
+    prefOK(actual, expected, strict);
+  });
+}
+
+function getCachedOK(args, expectedIsCached, expectedVal, expectedGroup,
+                     strict) {
+  if (args.length == 2)
+    args.push(undefined);
+  let expectedPref = !expectedIsCached ? null : {
+    domain: expectedGroup || args[0],
+    name: args[1],
+    value: expectedVal
+  };
+  getCachedOKEx("getCachedByDomainAndName", args, expectedPref, strict);
+}
+
+function getCachedSubdomainsOK(args, expectedGroupValPairs) {
+  if (args.length == 2)
+    args.push(undefined);
+  let len = {};
+  args.push(len);
+  let actualPrefs = cps.getCachedBySubdomainAndName.apply(cps, args);
+  do_check_eq(actualPrefs.length, len.value);
+  let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) {
+    return { domain: group, name: args[1], value: val };
+  });
+  arraysOfArraysOK([actualPrefs], [expectedPrefs], prefOK);
+}
+
+function getCachedGlobalOK(args, expectedIsCached, expectedVal) {
+  if (args.length == 1)
+    args.push(undefined);
+  let expectedPref = !expectedIsCached ? null : {
+    domain: null,
+    name: args[0],
+    value: expectedVal
+  };
+  getCachedOKEx("getCachedGlobal", args, expectedPref);
+}
+
+function getCachedOKEx(methodName, args, expectedPref, strict) {
+  let actualPref = cps[methodName].apply(cps, args);
+  if (expectedPref)
+    prefOK(actualPref, expectedPref, strict);
+  else
+    do_check_true(actualPref === null);
+}
+
+function arraysOfArraysOK(actual, expected, cmp) {
+  cmp = cmp || function (a, b) do_check_eq(a, b);
+  do_check_eq(actual.length, expected.length);
+  actual.forEach(function (actualChildArr, i) {
+    let expectedChildArr = expected[i];
+    do_check_eq(actualChildArr.length, expectedChildArr.length);
+    actualChildArr.forEach(function (actualElt, j) {
+      let expectedElt = expectedChildArr[j];
+      cmp(actualElt, expectedElt);
+    });
+  });
+}
+
+function dbOK(expectedRows) {
+  let db = sendMessage("db");
+  let stmt = db.createAsyncStatement(
+    "SELECT groups.name AS grp, settings.name AS name, prefs.value AS value " +
+    "FROM prefs " +
+    "LEFT JOIN groups ON groups.id = prefs.groupID " +
+    "LEFT JOIN settings ON settings.id = prefs.settingID " +
+    "UNION " +
+
+    // These second two SELECTs get the rows of the groups and settings tables
+    // that aren't referenced by the prefs table.  Neither should return any
+    // rows if the component is working properly.
+    "SELECT groups.name AS grp, NULL AS name, NULL AS value " +
+    "FROM groups " +
+    "WHERE id NOT IN (" +
+      "SELECT DISTINCT groupID " +
+      "FROM prefs " +
+      "WHERE groupID NOTNULL" +
+    ") " +
+    "UNION " +
+    "SELECT NULL AS grp, settings.name AS name, NULL AS value " +
+    "FROM settings " +
+    "WHERE id NOT IN (" +
+      "SELECT DISTINCT settingID " +
+      "FROM prefs " +
+      "WHERE settingID NOTNULL" +
+    ") " +
+
+    "ORDER BY value ASC, grp ASC, name ASC"
+  );
+
+  let actualRows = [];
+  let cols = ["grp", "name", "value"];
+
+  db.executeAsync([stmt], 1, {
+    handleCompletion: function (reason) {
+      arraysOfArraysOK(actualRows, expectedRows);
+      next();
+    },
+    handleResult: function (results) {
+      let row = null;
+      while (row = results.getNextRow()) {
+        actualRows.push(cols.map(function (c) row.getResultByName(c)));
+      }
+    },
+    handleError: function (err) {
+      do_throw(err);
+    }
+  });
+  stmt.finalize();
+}
+
+function on(event, names) {
+  let args = {
+    reset: function () {
+      for (let prop in this) {
+        if (Array.isArray(this[prop]))
+          this[prop].splice(0, this[prop].length);
+      }
+    },
+    destroy: function () {
+      names.forEach(function (n) cps.removeObserverForName(n, observers[n]));
+    },
+  };
+
+  let observers = {};
+
+  names.forEach(function (name) {
+    let obs = {};
+    ["onContentPrefSet", "onContentPrefRemoved"].forEach(function (meth) {
+      obs[meth] = function () do_throw(meth + " should not be called");
+    });
+    obs["onContentPref" + event] = function () {
+      args[name].push(Array.slice(arguments));
+    };
+    observers[name] = obs;
+    args[name] = [];
+    args[name].observer = obs;
+    cps.addObserverForName(name, obs);
+  });
+
+  return args;
+}
+
+function observerArgsOK(actualArgs, expectedArgs) {
+  do_check_neq(actualArgs, undefined);
+  arraysOfArraysOK(actualArgs, expectedArgs);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js
@@ -0,0 +1,99 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function nonexistent() {
+    getCachedOK(["a.com", "foo"], false, undefined);
+    getCachedGlobalOK(["foo"], false, undefined);
+    yield true;
+  },
+
+  function isomorphicDomains() {
+    yield set("a.com", "foo", 1);
+    getCachedOK(["a.com", "foo"], true, 1);
+    getCachedOK(["http://a.com/huh", "foo"], true, 1, "a.com");
+  },
+
+  function names() {
+    yield set("a.com", "foo", 1);
+    getCachedOK(["a.com", "foo"], true, 1);
+
+    yield set("a.com", "bar", 2);
+    getCachedOK(["a.com", "foo"], true, 1);
+    getCachedOK(["a.com", "bar"], true, 2);
+
+    yield setGlobal("foo", 3);
+    getCachedOK(["a.com", "foo"], true, 1);
+    getCachedOK(["a.com", "bar"], true, 2);
+    getCachedGlobalOK(["foo"], true, 3);
+
+    yield setGlobal("bar", 4);
+    getCachedOK(["a.com", "foo"], true, 1);
+    getCachedOK(["a.com", "bar"], true, 2);
+    getCachedGlobalOK(["foo"], true, 3);
+    getCachedGlobalOK(["bar"], true, 4);
+  },
+
+  function subdomains() {
+    yield set("a.com", "foo", 1);
+    yield set("b.a.com", "foo", 2);
+    getCachedOK(["a.com", "foo"], true, 1);
+    getCachedOK(["b.a.com", "foo"], true, 2);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 6, context);
+    yield setGlobal("foo", 7, context);
+    getCachedOK(["a.com", "foo", context], true, 6);
+    getCachedOK(["a.com", "bar", context], true, 2);
+    getCachedGlobalOK(["foo", context], true, 7);
+    getCachedGlobalOK(["bar", context], true, 4);
+    getCachedOK(["b.com", "foo", context], true, 5);
+
+    getCachedOK(["a.com", "foo"], true, 1);
+    getCachedOK(["a.com", "bar"], true, 2);
+    getCachedGlobalOK(["foo"], true, 3);
+    getCachedGlobalOK(["bar"], true, 4);
+    getCachedOK(["b.com", "foo"], true, 5);
+  },
+
+  function erroneous() {
+    do_check_throws(function ()
+                    cps.getCachedByDomainAndName(null, "foo", null));
+    do_check_throws(function ()
+                    cps.getCachedByDomainAndName("", "foo", null));
+    do_check_throws(function ()
+                    cps.getCachedByDomainAndName("a.com", "", null));
+    do_check_throws(function ()
+                    cps.getCachedByDomainAndName("a.com", null, null));
+    do_check_throws(function () cps.getCachedGlobal("", null));
+    do_check_throws(function () cps.getCachedGlobal(null, null));
+    yield true;
+  },
+
+  function casts() {
+    // SQLite casts booleans to integers.  This makes sure the values stored in
+    // the cache are the same as the casted values in the database.
+
+    yield set("a.com", "foo", false);
+    yield getOK(["a.com", "foo"], 0, "a.com", true);
+    getCachedOK(["a.com", "foo"], true, 0, "a.com", true);
+
+    yield set("a.com", "bar", true);
+    yield getOK(["a.com", "bar"], 1, "a.com", true);
+    getCachedOK(["a.com", "bar"], true, 1, "a.com", true);
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js
@@ -0,0 +1,190 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function nonexistent() {
+    getCachedSubdomainsOK(["a.com", "foo"], []);
+    yield true;
+  },
+
+  function isomorphicDomains() {
+    yield set("a.com", "foo", 1);
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    getCachedSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+  },
+
+  function names() {
+    yield set("a.com", "foo", 1);
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+    yield set("a.com", "bar", 2);
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+    yield setGlobal("foo", 3);
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+    getCachedGlobalOK(["foo"], true, 3);
+
+    yield setGlobal("bar", 4);
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+    getCachedGlobalOK(["foo"], true, 3);
+    getCachedGlobalOK(["bar"], true, 4);
+  },
+
+  function subdomains() {
+    yield set("a.com", "foo", 1);
+    yield set("b.a.com", "foo", 2);
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1], ["b.a.com", 2]]);
+    getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+  },
+
+  function populateViaGet() {
+    yield cps.getByDomainAndName("a.com", "foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+    yield cps.getGlobal("foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+    getCachedGlobalOK(["foo"], true, undefined);
+  },
+
+  function populateViaGetSubdomains() {
+    yield cps.getBySubdomainAndName("a.com", "foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+  },
+
+  function populateViaRemove() {
+    yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+    yield cps.removeBySubdomainAndName("b.com", "foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+    getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+
+    yield cps.removeGlobal("foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+    getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+    getCachedGlobalOK(["foo"], true, undefined);
+
+    yield set("a.com", "foo", 1);
+    yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+    getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+    getCachedGlobalOK(["foo"], true, undefined);
+
+    yield set("a.com", "foo", 2);
+    yield set("b.a.com", "foo", 3);
+    yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"],
+                          [["a.com", undefined], ["b.a.com", undefined]]);
+    getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+    getCachedGlobalOK(["foo"], true, undefined);
+    getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]);
+
+    yield setGlobal("foo", 4);
+    yield cps.removeGlobal("foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"],
+                          [["a.com", undefined], ["b.a.com", undefined]]);
+    getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+    getCachedGlobalOK(["foo"], true, undefined);
+    getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]);
+  },
+
+  function populateViaRemoveByDomain() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield set("b.a.com", "foo", 3);
+    yield set("b.a.com", "bar", 4);
+    yield cps.removeByDomain("a.com", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"],
+                          [["a.com", undefined], ["b.a.com", 3]]);
+    getCachedSubdomainsOK(["a.com", "bar"],
+                          [["a.com", undefined], ["b.a.com", 4]]);
+
+    yield set("a.com", "foo", 5);
+    yield set("a.com", "bar", 6);
+    yield cps.removeBySubdomain("a.com", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"],
+                          [["a.com", undefined], ["b.a.com", undefined]]);
+    getCachedSubdomainsOK(["a.com", "bar"],
+                          [["a.com", undefined], ["b.a.com", undefined]]);
+
+    yield setGlobal("foo", 7);
+    yield setGlobal("bar", 8);
+    yield cps.removeAllGlobals(null, makeCallback());
+    getCachedGlobalOK(["foo"], true, undefined);
+    getCachedGlobalOK(["bar"], true, undefined);
+  },
+
+  function populateViaRemoveAllDomains() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield set("b.com", "foo", 3);
+    yield set("b.com", "bar", 4);
+    yield cps.removeAllDomains(null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+    getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]);
+    getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+    getCachedSubdomainsOK(["b.com", "bar"], [["b.com", undefined]]);
+  },
+
+  function populateViaRemoveByName() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield cps.removeByName("foo", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+    getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+    getCachedGlobalOK(["foo"], true, undefined);
+    getCachedGlobalOK(["bar"], true, 4);
+
+    yield cps.removeByName("bar", null, makeCallback());
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+    getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]);
+    getCachedGlobalOK(["foo"], true, undefined);
+    getCachedGlobalOK(["bar"], true, undefined);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 6, context);
+    yield setGlobal("foo", 7, context);
+    getCachedSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]);
+    getCachedSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]);
+    getCachedGlobalOK(["foo", context], true, 7);
+    getCachedGlobalOK(["bar", context], true, 4);
+    getCachedSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]);
+
+    getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+    getCachedGlobalOK(["foo"], true, 3);
+    getCachedGlobalOK(["bar"], true, 4);
+    getCachedSubdomainsOK(["b.com", "foo"], [["b.com", 5]]);
+  },
+
+  function erroneous() {
+    do_check_throws(function ()
+                    cps.getCachedBySubdomainAndName(null, "foo", null));
+    do_check_throws(function ()
+                    cps.getCachedBySubdomainAndName("", "foo", null));
+    do_check_throws(function ()
+                    cps.getCachedBySubdomainAndName("a.com", "", null));
+    do_check_throws(function ()
+                    cps.getCachedBySubdomainAndName("a.com", null, null));
+    yield true;
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js
@@ -0,0 +1,73 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function get_nonexistent() {
+    yield getSubdomainsOK(["a.com", "foo"], []);
+  },
+
+  function isomorphicDomains() {
+    yield set("a.com", "foo", 1);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    yield getSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+  },
+
+  function names() {
+    yield set("a.com", "foo", 1);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+    yield set("a.com", "bar", 2);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+    yield setGlobal("foo", 3);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+  },
+
+  function subdomains() {
+    yield set("a.com", "foo", 1);
+    yield set("b.a.com", "foo", 2);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1], ["b.a.com", 2]]);
+    yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 6, context);
+    yield setGlobal("foo", 7, context);
+    yield getSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]);
+    yield getSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]);
+    yield getSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]);
+
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+    yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+    yield getSubdomainsOK(["b.com", "foo"], [["b.com", 5]]);
+  },
+
+  function erroneous() {
+    do_check_throws(function ()
+                    cps.getBySubdomainAndName(null, "foo", null, {}));
+    do_check_throws(function ()
+                    cps.getBySubdomainAndName("", "foo", null, {}));
+    do_check_throws(function ()
+                    cps.getBySubdomainAndName("a.com", "", null, {}));
+    do_check_throws(function ()
+                    cps.getBySubdomainAndName("a.com", null, null, {}));
+    do_check_throws(function ()
+                    cps.getBySubdomainAndName("a.com", "foo", null, null));
+    yield true;
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js
@@ -0,0 +1,125 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function observerForName_set() {
+    let args = on("Set", ["foo", null, "bar"]);
+
+    yield set("a.com", "foo", 1);
+    observerArgsOK(args.foo, [["a.com", "foo", 1]]);
+    observerArgsOK(args.null, [["a.com", "foo", 1]]);
+    observerArgsOK(args.bar, []);
+    args.reset();
+
+    yield setGlobal("foo", 2);
+    observerArgsOK(args.foo, [[null, "foo", 2]]);
+    observerArgsOK(args.null, [[null, "foo", 2]]);
+    observerArgsOK(args.bar, []);
+    args.reset();
+  },
+
+  function observerForName_remove() {
+    yield set("a.com", "foo", 1);
+    yield setGlobal("foo", 2);
+
+    let args = on("Removed", ["foo", null, "bar"]);
+    yield cps.removeByDomainAndName("a.com", "bogus", null, makeCallback());
+    observerArgsOK(args.foo, []);
+    observerArgsOK(args.null, []);
+    observerArgsOK(args.bar, []);
+    args.reset();
+
+    yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+    observerArgsOK(args.foo, [["a.com", "foo"]]);
+    observerArgsOK(args.null, [["a.com", "foo"]]);
+    observerArgsOK(args.bar, []);
+    args.reset();
+
+    yield cps.removeGlobal("foo", null, makeCallback());
+    observerArgsOK(args.foo, [[null, "foo"]]);
+    observerArgsOK(args.null, [[null, "foo"]]);
+    observerArgsOK(args.bar, []);
+    args.reset();
+  },
+
+  function observerForName_removeByDomain() {
+    yield set("a.com", "foo", 1);
+    yield set("b.a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+
+    let args = on("Removed", ["foo", null, "bar"]);
+    yield cps.removeByDomain("bogus", null, makeCallback());
+    observerArgsOK(args.foo, []);
+    observerArgsOK(args.null, []);
+    observerArgsOK(args.bar, []);
+    args.reset();
+
+    yield cps.removeBySubdomain("a.com", null, makeCallback());
+    observerArgsOK(args.foo, [["a.com", "foo"]]);
+    observerArgsOK(args.null, [["a.com", "foo"], ["b.a.com", "bar"]]);
+    observerArgsOK(args.bar, [["b.a.com", "bar"]]);
+    args.reset();
+
+    yield cps.removeAllGlobals(null, makeCallback());
+    observerArgsOK(args.foo, [[null, "foo"]]);
+    observerArgsOK(args.null, [[null, "foo"]]);
+    observerArgsOK(args.bar, []);
+    args.reset();
+  },
+
+  function observerForName_removeAllDomains() {
+    yield set("a.com", "foo", 1);
+    yield setGlobal("foo", 2);
+    yield set("b.com", "bar", 3);
+
+    let args = on("Removed", ["foo", null, "bar"]);
+    yield cps.removeAllDomains(null, makeCallback());
+    observerArgsOK(args.foo, [["a.com", "foo"]]);
+    observerArgsOK(args.null, [["a.com", "foo"], ["b.com", "bar"]]);
+    observerArgsOK(args.bar, [["b.com", "bar"]]);
+    args.reset();
+  },
+
+  function observerForName_removeByName() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+
+    let args = on("Removed", ["foo", null, "bar"]);
+    yield cps.removeByName("bogus", null, makeCallback());
+    observerArgsOK(args.foo, []);
+    observerArgsOK(args.null, []);
+    observerArgsOK(args.bar, []);
+    args.reset();
+
+    yield cps.removeByName("foo", null, makeCallback());
+    observerArgsOK(args.foo, [["a.com", "foo"], [null, "foo"]]);
+    observerArgsOK(args.null, [["a.com", "foo"], [null, "foo"]]);
+    observerArgsOK(args.bar, []);
+    args.reset();
+  },
+
+  function removeObserverForName() {
+    let args = on("Set", ["foo", null, "bar"]);
+
+    cps.removeObserverForName("foo", args.foo.observer);
+    yield set("a.com", "foo", 1);
+    observerArgsOK(args.foo, []);
+    observerArgsOK(args.null, [["a.com", "foo", 1]]);
+    observerArgsOK(args.bar, []);
+    args.reset();
+
+    cps.removeObserverForName(null, args.null.observer);
+    yield set("a.com", "foo", 2);
+    observerArgsOK(args.foo, []);
+    observerArgsOK(args.null, []);
+    observerArgsOK(args.bar, []);
+    args.reset();
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js
@@ -0,0 +1,194 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function nonexistent() {
+    yield set("a.com", "foo", 1);
+    yield setGlobal("foo", 2);
+
+    yield cps.removeByDomainAndName("a.com", "bogus", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getGlobalOK(["foo"], 2);
+
+    yield cps.removeBySubdomainAndName("a.com", "bogus", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getGlobalOK(["foo"], 2);
+
+    yield cps.removeGlobal("bogus", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getGlobalOK(["foo"], 2);
+
+    yield cps.removeByDomainAndName("bogus", "bogus", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getGlobalOK(["foo"], 2);
+  },
+
+  function isomorphicDomains() {
+    yield set("a.com", "foo", 1);
+    yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+    yield dbOK([]);
+    yield getOK(["a.com", "foo"], undefined);
+
+    yield set("a.com", "foo", 2);
+    yield cps.removeByDomainAndName("http://a.com/huh", "foo", null,
+                                    makeCallback());
+    yield dbOK([]);
+    yield getOK(["a.com", "foo"], undefined);
+  },
+
+  function names() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+
+    yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+    yield dbOK([
+      ["a.com", "bar", 2],
+      [null, "foo", 3],
+      [null, "bar", 4],
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], 2);
+    yield getGlobalOK(["foo"], 3);
+    yield getGlobalOK(["bar"], 4);
+
+    yield cps.removeGlobal("foo", null, makeCallback());
+    yield dbOK([
+      ["a.com", "bar", 2],
+      [null, "bar", 4],
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], 2);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], 4);
+
+    yield cps.removeByDomainAndName("a.com", "bar", null, makeCallback());
+    yield dbOK([
+      [null, "bar", 4],
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], 4);
+
+    yield cps.removeGlobal("bar", null, makeCallback());
+    yield dbOK([
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], undefined);
+  },
+
+  function subdomains() {
+    yield set("a.com", "foo", 1);
+    yield set("b.a.com", "foo", 2);
+    yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+    yield dbOK([
+      ["b.a.com", "foo", 2],
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+    yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+    yield set("a.com", "foo", 3);
+    yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+    yield dbOK([
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], []);
+    yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+    yield set("a.com", "foo", 4);
+    yield set("b.a.com", "foo", 5);
+    yield cps.removeByDomainAndName("b.a.com", "foo", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 4],
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+    yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+    yield set("b.a.com", "foo", 6);
+    yield cps.removeBySubdomainAndName("b.a.com", "foo", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 4],
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+    yield getSubdomainsOK(["b.a.com", "foo"], []);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield setGlobal("qux", 5);
+    yield set("b.com", "foo", 6);
+    yield set("b.com", "bar", 7);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 8, context);
+    yield setGlobal("foo", 9, context);
+    yield cps.removeByDomainAndName("a.com", "foo", context, makeCallback());
+    yield cps.removeGlobal("foo", context, makeCallback());
+    yield cps.removeGlobal("qux", context, makeCallback());
+    yield cps.removeByDomainAndName("b.com", "foo", context, makeCallback());
+    yield dbOK([
+      ["a.com", "bar", 2],
+      [null, "bar", 4],
+      ["b.com", "bar", 7],
+    ]);
+    yield getOK(["a.com", "foo", context], undefined);
+    yield getOK(["a.com", "bar", context], 2);
+    yield getGlobalOK(["foo", context], undefined);
+    yield getGlobalOK(["bar", context], 4);
+    yield getGlobalOK(["qux", context], undefined);
+    yield getOK(["b.com", "foo", context], undefined);
+    yield getOK(["b.com", "bar", context], 7);
+
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], 2);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], 4);
+    yield getGlobalOK(["qux"], undefined);
+    yield getOK(["b.com", "foo"], undefined);
+    yield getOK(["b.com", "bar"], 7);
+  },
+
+  function erroneous() {
+    do_check_throws(function () cps.removeByDomainAndName(null, "foo", null));
+    do_check_throws(function () cps.removeByDomainAndName("", "foo", null));
+    do_check_throws(function () cps.removeByDomainAndName("a.com", "foo", null,
+                                                          "bogus"));
+    do_check_throws(function () cps.removeBySubdomainAndName(null, "foo",
+                                                             null));
+    do_check_throws(function () cps.removeBySubdomainAndName("", "foo", null));
+    do_check_throws(function () cps.removeBySubdomainAndName("a.com", "foo",
+                                                             null, "bogus"));
+    do_check_throws(function () cps.removeGlobal("", null));
+    do_check_throws(function () cps.removeGlobal(null, null));
+    do_check_throws(function () cps.removeGlobal("foo", null, "bogus"));
+    yield true;
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js
@@ -0,0 +1,73 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function nonexistent() {
+    yield setGlobal("foo", 1);
+    yield cps.removeAllDomains(null, makeCallback());
+    yield dbOK([
+      [null, "foo", 1],
+    ]);
+    yield getGlobalOK(["foo"], 1);
+  },
+
+  function domains() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+    yield set("b.com", "bar", 6);
+
+    yield cps.removeAllDomains(null, makeCallback());
+    yield dbOK([
+      [null, "foo", 3],
+      [null, "bar", 4],
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], 3);
+    yield getGlobalOK(["bar"], 4);
+    yield getOK(["b.com", "foo"], undefined);
+    yield getOK(["b.com", "bar"], undefined);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 6, context);
+    yield setGlobal("foo", 7, context);
+    yield cps.removeAllDomains(context, makeCallback());
+    yield dbOK([
+      [null, "foo", 3],
+      [null, "bar", 4],
+    ]);
+    yield getOK(["a.com", "foo", context], undefined);
+    yield getOK(["a.com", "bar", context], undefined);
+    yield getGlobalOK(["foo", context], 7);
+    yield getGlobalOK(["bar", context], 4);
+    yield getOK(["b.com", "foo", context], undefined);
+
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], 3);
+    yield getGlobalOK(["bar"], 4);
+    yield getOK(["b.com", "foo"], undefined);
+  },
+
+  function erroneous() {
+    do_check_throws(function () cps.removeAllDomains(null, "bogus"));
+    yield true;
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js
@@ -0,0 +1,162 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function nonexistent() {
+    yield set("a.com", "foo", 1);
+    yield setGlobal("foo", 2);
+
+    yield cps.removeByDomain("bogus", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getGlobalOK(["foo"], 2);
+
+    yield cps.removeBySubdomain("bogus", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getGlobalOK(["foo"], 2);
+  },
+
+  function isomorphicDomains() {
+    yield set("a.com", "foo", 1);
+    yield cps.removeByDomain("a.com", null, makeCallback());
+    yield dbOK([]);
+    yield getOK(["a.com", "foo"], undefined);
+
+    yield set("a.com", "foo", 2);
+    yield cps.removeByDomain("http://a.com/huh", null, makeCallback());
+    yield dbOK([]);
+    yield getOK(["a.com", "foo"], undefined);
+  },
+
+  function domains() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+    yield set("b.com", "bar", 6);
+
+    yield cps.removeByDomain("a.com", null, makeCallback());
+    yield dbOK([
+      [null, "foo", 3],
+      [null, "bar", 4],
+      ["b.com", "foo", 5],
+      ["b.com", "bar", 6],
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], 3);
+    yield getGlobalOK(["bar"], 4);
+    yield getOK(["b.com", "foo"], 5);
+    yield getOK(["b.com", "bar"], 6);
+
+    yield cps.removeAllGlobals(null, makeCallback());
+    yield dbOK([
+      ["b.com", "foo", 5],
+      ["b.com", "bar", 6],
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], undefined);
+    yield getOK(["b.com", "foo"], 5);
+    yield getOK(["b.com", "bar"], 6);
+
+    yield cps.removeByDomain("b.com", null, makeCallback());
+    yield dbOK([
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], undefined);
+    yield getOK(["b.com", "foo"], undefined);
+    yield getOK(["b.com", "bar"], undefined);
+  },
+
+  function subdomains() {
+    yield set("a.com", "foo", 1);
+    yield set("b.a.com", "foo", 2);
+    yield cps.removeByDomain("a.com", null, makeCallback());
+    yield dbOK([
+      ["b.a.com", "foo", 2],
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+    yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+    yield set("a.com", "foo", 3);
+    yield cps.removeBySubdomain("a.com", null, makeCallback());
+    yield dbOK([
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], []);
+    yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+    yield set("a.com", "foo", 4);
+    yield set("b.a.com", "foo", 5);
+    yield cps.removeByDomain("b.a.com", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 4],
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+    yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+    yield set("b.a.com", "foo", 6);
+    yield cps.removeBySubdomain("b.a.com", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 4],
+    ]);
+    yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+    yield getSubdomainsOK(["b.a.com", "foo"], []);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 6, context);
+    yield setGlobal("foo", 7, context);
+    yield cps.removeByDomain("a.com", context, makeCallback());
+    yield cps.removeAllGlobals(context, makeCallback());
+    yield dbOK([
+      ["b.com", "foo", 5],
+    ]);
+    yield getOK(["a.com", "foo", context], undefined);
+    yield getOK(["a.com", "bar", context], undefined);
+    yield getGlobalOK(["foo", context], undefined);
+    yield getGlobalOK(["bar", context], undefined);
+    yield getOK(["b.com", "foo", context], 5);
+
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], undefined);
+    yield getOK(["b.com", "foo"], 5);
+  },
+
+  function erroneous() {
+    do_check_throws(function () cps.removeByDomain(null, null));
+    do_check_throws(function () cps.removeByDomain("", null));
+    do_check_throws(function () cps.removeByDomain("a.com", null, "bogus"));
+    do_check_throws(function () cps.removeBySubdomain(null, null));
+    do_check_throws(function () cps.removeBySubdomain("", null));
+    do_check_throws(function () cps.removeBySubdomain("a.com", null, "bogus"));
+    do_check_throws(function () cps.removeAllGlobals(null, "bogus"));
+    yield true;
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function nonexistent() {
+    yield set("a.com", "foo", 1);
+    yield setGlobal("foo", 2);
+
+    yield cps.removeByName("bogus", null, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getGlobalOK(["foo"], 2);
+  },
+
+  function names() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+    yield set("b.com", "bar", 6);
+
+    yield cps.removeByName("foo", null, makeCallback());
+    yield dbOK([
+      ["a.com", "bar", 2],
+      [null, "bar", 4],
+      ["b.com", "bar", 6],
+    ]);
+    yield getOK(["a.com", "foo"], undefined);
+    yield getOK(["a.com", "bar"], 2);
+    yield getGlobalOK(["foo"], undefined);
+    yield getGlobalOK(["bar"], 4);
+    yield getOK(["b.com", "foo"], undefined);
+    yield getOK(["b.com", "bar"], 6);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+    yield set("b.com", "bar", 6);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 7, context);
+    yield setGlobal("foo", 8, context);
+    yield set("b.com", "bar", 9, context);
+    yield cps.removeByName("bar", context, makeCallback());
+    yield dbOK([
+      ["a.com", "foo", 1],
+      [null, "foo", 3],
+      ["b.com", "foo", 5],
+    ]);
+    yield getOK(["a.com", "foo", context], 7);
+    yield getOK(["a.com", "bar", context], undefined);
+    yield getGlobalOK(["foo", context], 8);
+    yield getGlobalOK(["bar", context], undefined);
+    yield getOK(["b.com", "foo", context], 5);
+    yield getOK(["b.com", "bar", context], undefined);
+
+    yield getOK(["a.com", "foo"], 1);
+    yield getOK(["a.com", "bar"], undefined);
+    yield getGlobalOK(["foo"], 3);
+    yield getGlobalOK(["bar"], undefined);
+    yield getOK(["b.com", "foo"], 5);
+    yield getOK(["b.com", "bar"], undefined);
+  },
+
+  function erroneous() {
+    do_check_throws(function () cps.removeByName("", null));
+    do_check_throws(function () cps.removeByName(null, null));
+    do_check_throws(function () cps.removeByName("foo", null, "bogus"));
+    yield true;
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js
@@ -0,0 +1,12 @@
+/* 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/. */
+
+function run_test() {
+  let serv = Cc["@mozilla.org/content-pref/service;1"].
+             getService(Ci.nsIContentPrefService2);
+  do_check_eq(serv.QueryInterface(Ci.nsIContentPrefService2), serv);
+  do_check_eq(serv.QueryInterface(Ci.nsISupports), serv);
+  let val = serv.QueryInterface(Ci.nsIContentPrefService);
+  do_check_true(val instanceof Ci.nsIContentPrefService);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js
@@ -0,0 +1,138 @@
+/* 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/. */
+
+function run_test() {
+  runAsyncTests(tests);
+}
+
+let tests = [
+
+  function get_nonexistent() {
+    yield getOK(["a.com", "foo"], undefined);
+    yield getGlobalOK(["foo"], undefined);
+  },
+
+  function isomorphicDomains() {
+    yield set("a.com", "foo", 1);
+    yield dbOK([
+      ["a.com", "foo", 1],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getOK(["http://a.com/huh", "foo"], 1, "a.com");
+
+    yield set("http://a.com/huh", "foo", 2);
+    yield dbOK([
+      ["a.com", "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 2);
+    yield getOK(["http://a.com/yeah", "foo"], 2, "a.com");
+  },
+
+  function names() {
+    yield set("a.com", "foo", 1);
+    yield dbOK([
+      ["a.com", "foo", 1],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+
+    yield set("a.com", "bar", 2);
+    yield dbOK([
+      ["a.com", "foo", 1],
+      ["a.com", "bar", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getOK(["a.com", "bar"], 2);
+
+    yield setGlobal("foo", 3);
+    yield dbOK([
+      ["a.com", "foo", 1],
+      ["a.com", "bar", 2],
+      [null, "foo", 3],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getOK(["a.com", "bar"], 2);
+    yield getGlobalOK(["foo"], 3);
+
+    yield setGlobal("bar", 4);
+    yield dbOK([
+      ["a.com", "foo", 1],
+      ["a.com", "bar", 2],
+      [null, "foo", 3],
+      [null, "bar", 4],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getOK(["a.com", "bar"], 2);
+    yield getGlobalOK(["foo"], 3);
+    yield getGlobalOK(["bar"], 4);
+  },
+
+  function subdomains() {
+    yield set("a.com", "foo", 1);
+    yield set("b.a.com", "foo", 2);
+    yield dbOK([
+      ["a.com", "foo", 1],
+      ["b.a.com", "foo", 2],
+    ]);
+    yield getOK(["a.com", "foo"], 1);
+    yield getOK(["b.a.com", "foo"], 2);
+  },
+
+  function privateBrowsing() {
+    yield set("a.com", "foo", 1);
+    yield set("a.com", "bar", 2);
+    yield setGlobal("foo", 3);
+    yield setGlobal("bar", 4);
+    yield set("b.com", "foo", 5);
+
+    let context = { usePrivateBrowsing: true };
+    yield set("a.com", "foo", 6, context);
+    yield setGlobal("foo", 7, context);
+    yield dbOK([
+      ["a.com", "foo", 1],
+      ["a.com", "bar", 2],
+      [null, "foo", 3],
+      [null, "bar", 4],
+      ["b.com", "foo", 5],
+    ]);
+    yield getOK(["a.com", "foo", context], 6, "a.com");
+    yield getOK(["a.com", "bar", context], 2);
+    yield getGlobalOK(["foo", context], 7);
+    yield getGlobalOK(["bar", context], 4);
+    yield getOK(["b.com", "foo", context], 5);
+
+    yield getOK(["a.com", "foo"], 1);
+    yield getOK(["a.com", "bar"], 2);
+    yield getGlobalOK(["foo"], 3);
+    yield getGlobalOK(["bar"], 4);
+    yield getOK(["b.com", "foo"], 5);
+  },
+
+  function set_erroneous() {
+    do_check_throws(function () cps.set(null, "foo", 1, null));
+    do_check_throws(function () cps.set("", "foo", 1, null));
+    do_check_throws(function () cps.set("a.com", "", 1, null));
+    do_check_throws(function () cps.set("a.com", null, 1, null));
+    do_check_throws(function () cps.set("a.com", "foo", undefined, null));
+    do_check_throws(function () cps.set("a.com", "foo", 1, null, "bogus"));
+    do_check_throws(function () cps.setGlobal("", 1, null));
+    do_check_throws(function () cps.setGlobal(null, 1, null));
+    do_check_throws(function () cps.setGlobal("foo", undefined, null));
+    do_check_throws(function () cps.setGlobal("foo", 1, null, "bogus"));
+    yield true;
+  },
+
+  function get_erroneous() {
+    do_check_throws(function () cps.getByDomainAndName(null, "foo", null, {}));
+    do_check_throws(function () cps.getByDomainAndName("", "foo", null, {}));
+    do_check_throws(function () cps.getByDomainAndName("a.com", "", null, {}));
+    do_check_throws(function ()
+                    cps.getByDomainAndName("a.com", null, null, {}));
+    do_check_throws(function ()
+                    cps.getByDomainAndName("a.com", "foo", null, null));
+    do_check_throws(function () cps.getGlobal("", null, {}));
+    do_check_throws(function () cps.getGlobal(null, null, {}));
+    do_check_throws(function () cps.getGlobal("foo", null, null));
+    yield true;
+  },
+];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+head = head.js
+tail =
+
+[test_service.js]
+[test_setGet.js]
+[test_getSubdomains.js]
+[test_remove.js]
+[test_removeByDomain.js]
+[test_removeAllDomains.js]
+[test_removeByName.js]
+[test_getCached.js]
+[test_getCachedSubdomains.js]
+[test_observers.js]