Bug 828829 - Refactor Health Report policy out of services/healthreport; r=rnewman, a=akeybl
authorGregory Szorc <gps@mozilla.com>
Fri, 11 Jan 2013 13:45:22 -0800
changeset 127230 3a9faed791c821c017c6130a6ee157aa350febcf
parent 127229 50db9b596a5dade0f225b4eaa1a59b3cebbbd153
child 127231 695dd2dc9fe8073132597402ec82c1931c429ae8
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)
reviewersrnewman, akeybl
bugs828829
milestone20.0a2
Bug 828829 - Refactor Health Report policy out of services/healthreport; r=rnewman, a=akeybl
browser/installer/package-manifest.in
browser/installer/removed-files.in
configure.in
modules/libpref/src/Makefile.in
services/Makefile.in
services/datareporting/DataReporting.manifest
services/datareporting/DataReportingService.js
services/datareporting/Makefile.in
services/datareporting/datareporting-prefs.js
services/datareporting/modules-testing/mocks.jsm
services/datareporting/policy.jsm
services/datareporting/tests/Makefile.in
services/datareporting/tests/xpcshell/test_policy.js
services/datareporting/tests/xpcshell/xpcshell.ini
services/healthreport/HealthReportComponents.manifest
services/healthreport/HealthReportService.js
services/healthreport/Makefile.in
services/healthreport/healthreport-prefs.js
services/healthreport/healthreporter.jsm
services/healthreport/modules-testing/mocks.jsm
services/healthreport/modules-testing/utils.jsm
services/healthreport/policy.jsm
services/healthreport/tests/xpcshell/test_healthreporter.js
services/healthreport/tests/xpcshell/test_load_modules.js
services/healthreport/tests/xpcshell/test_policy.js
services/healthreport/tests/xpcshell/xpcshell.ini
services/makefiles.sh
testing/xpcshell/xpcshell.ini
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -464,19 +464,19 @@
 @BINPATH@/components/nsINIProcessor.manifest
 @BINPATH@/components/nsINIProcessor.js
 @BINPATH@/components/nsPrompter.manifest
 @BINPATH@/components/nsPrompter.js
 #ifdef MOZ_SERVICES_AITC
 @BINPATH@/components/AitcComponents.manifest
 @BINPATH@/components/Aitc.js
 #endif
-#ifdef MOZ_SERVICES_HEALTHREPORT
-@BINPATH@/components/HealthReportComponents.manifest
-@BINPATH@/components/HealthReportService.js
+#ifdef MOZ_DATA_REPORTING
+@BINPATH@/components/DataReporting.manifest
+@BINPATH@/components/DataReportingService.js
 #endif
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/components/SyncComponents.manifest
 @BINPATH@/components/Weave.js
 #endif
 @BINPATH@/components/servicesComponents.manifest
 @BINPATH@/components/cryptoComponents.manifest
 @BINPATH@/components/TelemetryPing.js
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -894,16 +894,17 @@ xpicleanup@BIN_SUFFIX@
   components/crypto-SDR.js
   components/FeedConverter.js
   components/FeedProcessor.js
   components/FeedWriter.js
   components/fuelApplication.js
   components/GPSDGeolocationProvider.js
   components/interfaces.manifest
   components/jsconsole-clhandler.js
+  components/MetricsCollectionService.js
   components/NetworkGeolocationProvider.js
   components/NotificationsComponents.manifest
   components/nsBadCertHandler.js
   components/nsBlocklistService.js
   components/nsBrowserContentHandler.js
   components/nsBrowserGlue.js
   components/nsContentDispatchChooser.js
   components/nsContentPrefService.js
--- a/configure.in
+++ b/configure.in
@@ -8714,16 +8714,25 @@ if test "$MOZ_TELEMETRY_REPORTING"; then
 
     # Enable Telemetry by default for nightly and aurora channels
     if test "$MOZ_UPDATE_CHANNEL" = "nightly" -o \
         "$MOZ_UPDATE_CHANNEL" = "aurora"; then
         AC_DEFINE(MOZ_TELEMETRY_ON_BY_DEFAULT)
     fi
 fi
 
+dnl If we have any service that uploads data (and requires data submission
+dnl policy alert), set MOZ_DATA_REPORTING.
+dnl We need SUBST for build system and DEFINE for xul preprocessor.
+if test -n "$MOZ_TELEMETRY_REPORTING" || test -n "$MOZ_SERVICES_HEALTHREPORT" || test -n "MOZ_CRASHREPORTER"; then
+  MOZ_DATA_REPORTING=1
+  AC_DEFINE(MOZ_DATA_REPORTING)
+  AC_SUBST(MOZ_DATA_REPORTING)
+fi
+
 dnl win32 options
 AC_SUBST(MOZ_MAPINFO)
 AC_SUBST(MOZ_BROWSE_INFO)
 AC_SUBST(MOZ_TOOLS_DIR)
 AC_SUBST(WIN32_REDIST_DIR)
 AC_SUBST(MAKENSISU)
 
 dnl Echo the CFLAGS to remove extra whitespace.
--- a/modules/libpref/src/Makefile.in
+++ b/modules/libpref/src/Makefile.in
@@ -38,16 +38,20 @@ GARBAGE		+= $(addprefix $(DIST)/bin/defa
 			mailnews.js editor.js \
 			aix.js unix.js winpref.js os2prefs.js)
 
 GARBAGE		+= greprefs.js
 
 # TODO bug 813259 external files should be defined near their location in the source tree.
 grepref_files = $(topsrcdir)/netwerk/base/public/security-prefs.js $(srcdir)/init/all.js
 
+ifdef MOZ_DATA_REPORTING
+grepref_files += $(topsrcdir)/services/datareporting/datareporting-prefs.js
+endif
+
 ifdef MOZ_SERVICES_HEALTHREPORT
 grepref_files += $(topsrcdir)/services/healthreport/healthreport-prefs.js
 endif
 
 # Optimizer bug with GCC 3.2.2 on OS/2
 ifeq ($(OS_ARCH), OS2)
 nsPrefService.$(OBJ_SUFFIX): nsPrefService.cpp
 	$(REPORT_BUILD)
--- a/services/Makefile.in
+++ b/services/Makefile.in
@@ -4,28 +4,33 @@
 
 DEPTH     = @DEPTH@
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
-PARALLEL_DIRS += common
-
-PARALLEL_DIRS += crypto
+PARALLEL_DIRS += \
+  common \
+  crypto \
+  $(NULL)
 
 ifdef MOZ_SERVICES_AITC
 PARALLEL_DIRS += aitc
 endif
 
 ifdef MOZ_SERVICES_HEALTHREPORT
 PARALLEL_DIRS += healthreport
 endif
 
+ifdef MOZ_DATA_REPORTING
+PARALLEL_DIRS += datareporting
+endif
+
 ifdef MOZ_SERVICES_METRICS
 PARALLEL_DIRS += metrics
 endif
 
 ifdef MOZ_SERVICES_SYNC
 PARALLEL_DIRS += sync
 endif
 
new file mode 100644
--- /dev/null
+++ b/services/datareporting/DataReporting.manifest
@@ -0,0 +1,16 @@
+#   b2g:            {3c2e2abc-06d4-11e1-ac3b-374f68613e61}
+#   browser:        {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+#   mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110}
+#   mobile/xul:     {a23983c0-fd0e-11dc-95ff-0800200c9a66}
+#   suite (comm):   {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+#   metro browser:  {99bceaaa-e3c6-48c1-b981-ef9b46b67d60}
+
+# The Data Reporting Service drives collection and submission of metrics
+# and other useful data to Mozilla. It drives the display of the data
+# submission notification info bar and thus is required by Firefox Health
+# Report and Telemetry.
+
+component {41f6ae36-a79f-4613-9ac3-915e70f83789} DataReportingService.js
+contract @mozilla.org/datareporting/service;1 {41f6ae36-a79f-4613-9ac3-915e70f83789}
+category app-startup DataReportingService service,@mozilla.org/datareporting/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66}
+
rename from services/healthreport/HealthReportService.js
rename to services/datareporting/DataReportingService.js
--- a/services/healthreport/HealthReportService.js
+++ b/services/datareporting/DataReportingService.js
@@ -1,38 +1,43 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 
 
-const BRANCH = "healthreport.";
+const ROOT_BRANCH = "datareporting.";
+const POLICY_BRANCH = ROOT_BRANCH + "policy.";
+const HEALTHREPORT_BRANCH = ROOT_BRANCH + "healthreport.";
+const HEALTHREPORT_LOGGING_BRANCH = HEALTHREPORT_BRANCH + "logging.";
 const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
 
 /**
  * The Firefox Health Report XPCOM service.
  *
  * External consumers will be interested in the "reporter" property of this
  * service. This property is a `HealthReporter` instance that powers the
  * service. The property may be null if the Health Report service is not
  * enabled.
  *
  * EXAMPLE USAGE
  * =============
  *
  * let reporter = Cc["@mozilla.org/healthreport/service;1"]
  *                  .getService(Ci.nsISupports)
  *                  .wrappedJSObject
- *                  .reporter;
+ *                  .healthReporter;
  *
  * if (reporter.haveRemoteData) {
  *   // ...
  * }
  *
  * IMPLEMENTATION NOTES
  * ====================
  *
@@ -40,112 +45,168 @@ const DEFAULT_LOAD_DELAY_MSEC = 10 * 100
  * instance is not initialized until a few seconds after "final-ui-startup."
  * The exact delay is configurable via preferences so it can be adjusted with
  * a hotfix extension if the default value is ever problematic.
  *
  * Shutdown of the `HealthReporter` instance is handled completely within the
  * instance (it registers observers on initialization). See the notes on that
  * type for more.
  */
-this.HealthReportService = function HealthReportService() {
+this.DataReportingService = function () {
   this.wrappedJSObject = this;
 
-  this._prefs = new Preferences(BRANCH);
-
-  this._reporter = null;
+  this._os = Cc["@mozilla.org/observer-service;1"]
+               .getService(Ci.nsIObserverService);
 }
 
-HealthReportService.prototype = {
-  classID: Components.ID("{e354c59b-b252-4040-b6dd-b71864e3e35c}"),
+DataReportingService.prototype = Object.freeze({
+  classID: Components.ID("{41f6ae36-a79f-4613-9ac3-915e70f83789}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
-  observe: function observe(subject, topic, data) {
-    // If the background service is disabled, don't do anything.
-    if (!this._prefs.get("service.enabled", true)) {
+  //---------------------------------------------
+  // Start of policy listeners.
+  //---------------------------------------------
+
+  /**
+   * Called when policy requests data upload.
+   */
+  onRequestDataUpload: function (request) {
+    if (!this.healthReporter) {
+      return;
+    }
+
+    this.healthReporter.requestDataUpload(request);
+  },
+
+  onNotifyDataPolicy: function (request) {
+    Observers.notify("datareporting:notify-data-policy:request", request);
+  },
+
+  onRequestRemoteDelete: function (request) {
+    if (!this.healthReporter) {
       return;
     }
 
-    let os = Cc["@mozilla.org/observer-service;1"]
-               .getService(Ci.nsIObserverService);
+    this.healthReporter.deleteRemoteData(request);
+  },
 
+  //---------------------------------------------
+  // End of policy listeners.
+  //---------------------------------------------
+
+  observe: function observe(subject, topic, data) {
     switch (topic) {
       case "app-startup":
-        os.addObserver(this, "sessionstore-windows-restored", true);
+        this._os.addObserver(this, "profile-after-change", true);
+        break;
+
+      case "profile-after-change":
+        this._os.removeObserver(this, "profile-after-change");
+        this._os.addObserver(this, "sessionstore-windows-restored", true);
+
+        // We can't interact with prefs until after the profile is present.
+        let policyPrefs = new Preferences(POLICY_BRANCH);
+        this._prefs = new Preferences(HEALTHREPORT_BRANCH);
+        this.policy = new DataReportingPolicy(policyPrefs, this._prefs, this);
         break;
 
       case "sessionstore-windows-restored":
-        os.removeObserver(this, "sessionstore-windows-restored");
+        this._os.removeObserver(this, "sessionstore-windows-restored");
+        this._os.addObserver(this, "quit-application", false);
+
+        this.policy.startPolling();
+
+        // Don't initialize Firefox Health Reporter collection and submission
+        // service unless it is enabled.
+        if (!this._prefs.get("service.enabled", true)) {
+          return;
+        }
 
         let delayInterval = this._prefs.get("service.loadDelayMsec") ||
                             DEFAULT_LOAD_DELAY_MSEC;
 
         // Delay service loading a little more so things have an opportunity
         // to cool down first.
         this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         this.timer.initWithCallback({
           notify: function notify() {
             // Side effect: instantiates the reporter instance if not already
             // accessed.
-            let reporter = this.reporter;
+            let reporter = this.healthReporter;
             delete this.timer;
           }.bind(this),
         }, delayInterval, this.timer.TYPE_ONE_SHOT);
 
         break;
+
+      case "quit-application":
+        this._os.removeObserver(this, "quit-application");
+        this.policy.stopPolling();
+        break;
     }
   },
 
   /**
    * The HealthReporter instance associated with this service.
    *
    * If the service is disabled, this will return null.
    *
    * The obtained instance may not be fully initialized.
    */
-  get reporter() {
+  get healthReporter() {
     if (!this._prefs.get("service.enabled", true)) {
       return null;
     }
 
-    if (this._reporter) {
-      return this._reporter;
+    if ("_healthReporter" in this) {
+      return this._healthReporter;
     }
 
+    try {
+      this._loadHealthReporter();
+    } catch (ex) {
+      dump("Error loading health reporter: " + ex);
+      this._healthReporter = null;
+    }
+
+    return this._healthReporter;
+  },
+
+  _loadHealthReporter: function () {
     let ns = {};
     // Lazy import so application startup isn't adversely affected.
+
     Cu.import("resource://gre/modules/Task.jsm", ns);
     Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns);
     Cu.import("resource://services-common/log4moz.js", ns);
 
     // How many times will we rewrite this code before rolling it up into a
     // generic module? See also bug 451283.
     const LOGGERS = [
+      "Services.DataReporting",
       "Services.HealthReport",
       "Services.Metrics",
       "Services.BagheeraClient",
       "Sqlite.Connection.healthreport",
     ];
 
-    let prefs = new Preferences(BRANCH + "logging.");
-    if (prefs.get("consoleEnabled", true)) {
-      let level = prefs.get("consoleLevel", "Warn");
+    let loggingPrefs = new Preferences(HEALTHREPORT_LOGGING_BRANCH);
+    if (loggingPrefs.get("consoleEnabled", true)) {
+      let level = loggingPrefs.get("consoleLevel", "Warn");
       let appender = new ns.Log4Moz.ConsoleAppender();
       appender.level = ns.Log4Moz.Level[level] || ns.Log4Moz.Level.Warn;
 
       for (let name of LOGGERS) {
         let logger = ns.Log4Moz.repository.getLogger(name);
         logger.addAppender(appender);
       }
     }
 
     // The reporter initializes in the background.
-    this._reporter = new ns.HealthReporter(BRANCH);
-
-    return this._reporter;
+    this._healthReporter = new ns.HealthReporter(HEALTHREPORT_BRANCH,
+                                                 this.policy);
   },
-};
+});
 
-Object.freeze(HealthReportService.prototype);
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataReportingService]);
 
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HealthReportService]);
-
new file mode 100644
--- /dev/null
+++ b/services/datareporting/Makefile.in
@@ -0,0 +1,26 @@
+# 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/.
+
+DEPTH     = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+TEST_DIRS += tests
+
+MODULES_FILES := policy.jsm
+MODULES_DEST = $(FINAL_TARGET)/modules/services/datareporting
+INSTALL_TARGETS += MODULES
+
+TESTING_JS_MODULES := $(addprefix modules-testing/,mocks.jsm)
+TESTING_JS_MODULE_DIR := services/datareporting
+
+EXTRA_COMPONENTS := \
+  DataReporting.manifest \
+  DataReportingService.js \
+  $(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/datareporting/datareporting-prefs.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/. */
+
+pref("datareporting.policy.dataSubmissionEnabled", true);
+pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
+pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", false);
+pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
+pref("datareporting.policy.dataSubmissionPolicyResponseType", "");
+pref("datareporting.policy.dataSubmissionPolicyResponseTime", "0");
+pref("datareporting.policy.firstRunTime", "0");
+
rename from services/healthreport/modules-testing/mocks.jsm
rename to services/datareporting/modules-testing/mocks.jsm
--- a/services/healthreport/modules-testing/mocks.jsm
+++ b/services/datareporting/modules-testing/mocks.jsm
@@ -7,17 +7,17 @@
 this.EXPORTED_SYMBOLS = ["MockPolicyListener"];
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://services-common/log4moz.js");
 
 
 this.MockPolicyListener = function MockPolicyListener() {
-  this._log = Log4Moz.repository.getLogger("HealthReport.Testing.MockPolicyListener");
+  this._log = Log4Moz.repository.getLogger("Services.DataReporting.Testing.MockPolicyListener");
   this._log.level = Log4Moz.Level["Debug"];
 
   this.requestDataUploadCount = 0;
   this.lastDataRequest = null;
 
   this.requestRemoteDeleteCount = 0;
   this.lastRemoteDeleteRequest = null;
 
@@ -39,8 +39,9 @@ MockPolicyListener.prototype = {
   },
 
   onNotifyDataPolicy: function onNotifyDataPolicy(request) {
     this._log.info("onNotifyUser invoked.");
     this.notifyUserCount++;
     this.lastNotifyRequest = request;
   },
 };
+
rename from services/healthreport/policy.jsm
rename to services/datareporting/policy.jsm
--- a/services/healthreport/policy.jsm
+++ b/services/datareporting/policy.jsm
@@ -1,17 +1,28 @@
 /* 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 in transition. It was originally conceived to fulfill the
+ * needs of only Firefox Health Report. It is slowly being morphed into
+ * fulfilling the needs of all data reporting facilities in Gecko applications.
+ * As a result, some things feel a bit weird.
+ *
+ * DataReportingPolicy is both a driver for data reporting notification
+ * (a true policy) and the driver for FHR data submission. The latter should
+ * eventually be split into its own type and module.
+ */
+
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "DataSubmissionRequest", // For test use only.
-  "HealthReportPolicy",
+  "DataReportingPolicy",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/utils.js");
 
@@ -36,29 +47,29 @@ const OLDEST_ALLOWED_YEAR = 2012;
  * This begins a countdown timer that upon completion will signal implicit
  * acceptance of the policy. If for whatever reason the callee could not
  * display a notice, it should call `onUserNotifyFailed`.
  *
  * Once the user is notified of the policy, the callee has the option of
  * signaling explicit user acceptance or rejection of the policy. They do this
  * by calling `onUserAccept` or `onUserReject`, respectively. These functions
  * are essentially proxies to
- * HealthReportPolicy.{recordUserAcceptance,recordUserRejection}.
+ * DataReportingPolicy.{recordUserAcceptance,recordUserRejection}.
  *
  * If the user never explicitly accepts or rejects the policy, it will be
  * implicitly accepted after a specified duration of time. The notice is
  * expected to remain displayed even after implicit acceptance (in case the
  * user is away from the device). So, no event signaling implicit acceptance
  * is exposed.
  *
  * Receivers of instances of this type should treat it as a black box with
  * the exception of the on* functions.
  *
  * @param policy
- *        (HealthReportPolicy) The policy instance this request came from.
+ *        (DataReportingPolicy) The policy instance this request came from.
  * @param promise
  *        (deferred) The promise that will be fulfilled when display occurs.
  */
 function NotifyPolicyRequest(policy, promise) {
   this.policy = policy;
   this.promise = promise;
 }
 NotifyPolicyRequest.prototype = {
@@ -230,35 +241,38 @@ Object.freeze(DataSubmissionRequest.prot
  *     receives a `NotifyPolicyRequest` instance. The callee should call one or
  *     more of the functions on that instance when specific events occur. See
  *     the documentation for that type for more.
  *
  * Note that the notification method is abstracted. Different applications
  * can have different mechanisms by which they notify the user of data
  * submission practices.
  *
- * @param prefs
+ * @param policyPrefs
  *        (Preferences) Handle on preferences branch on which state will be
  *        queried and stored.
+ * @param healthReportPrefs
+ *        (Preferences) Handle on preferences branch hold Health Report state.
  * @param listener
  *        (object) Object with callbacks that will be invoked at certain key
  *        events.
  */
-this.HealthReportPolicy = function HealthReportPolicy(prefs, listener) {
-  this._log = Log4Moz.repository.getLogger("Services.HealthReport.Policy");
+this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
+  this._log = Log4Moz.repository.getLogger("Services.DataReporting.Policy");
   this._log.level = Log4Moz.Level["Debug"];
 
   for (let handler of this.REQUIRED_LISTENERS) {
     if (!listener[handler]) {
       throw new Error("Passed listener does not contain required handler: " +
                       handler);
     }
   }
 
   this._prefs = prefs;
+  this._healthReportPrefs = healthReportPrefs;
   this._listener = listener;
 
   // If we've never run before, record the current time.
   if (!this.firstRunDate.getTime()) {
     this.firstRunDate = this.now();
   }
 
   // Ensure we are scheduled to submit.
@@ -271,17 +285,17 @@ this.HealthReportPolicy = function Healt
   // carry forward through a single application instance.
   this._dataSubmissionPolicyNotifiedDate = null;
 
   // Record when we last requested for submitted data to be sent. This is
   // to avoid having multiple outstanding requests.
   this._inProgressSubmissionRequest = null;
 }
 
-HealthReportPolicy.prototype = {
+DataReportingPolicy.prototype = Object.freeze({
   /**
    * How long after first run we should notify about data submission.
    */
   SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
 
   /**
    * Time that must elapse with no user action for implicit acceptance.
    *
@@ -440,52 +454,40 @@ HealthReportPolicy.prototype = {
     return this._prefs.get("dataSubmissionEnabled", true);
   },
 
   set dataSubmissionEnabled(value) {
     this._prefs.set("dataSubmissionEnabled", !!value);
   },
 
   /**
-   * Whether upload of data is allowed.
-   *
-   * This is a kill switch for upload. It is meant to reflect a system or
-   * deployment policy decision. User intent should be reflected in the
-   * "dataSubmissionPolicy" prefs.
-   */
-  get dataUploadEnabled() {
-    // Default is true because we are opt-out.
-    return this._prefs.get("dataUploadEnabled", true);
-  },
-
-  set dataUploadEnabled(value) {
-    this._prefs.set("dataUploadEnabled", !!value);
-  },
-
-  /**
    * Whether the user has accepted that data submission can occur.
    *
    * This overrides dataSubmissionEnabled.
    */
   get dataSubmissionPolicyAccepted() {
     // Be conservative and default to false.
     return this._prefs.get("dataSubmissionPolicyAccepted", false);
   },
 
   set dataSubmissionPolicyAccepted(value) {
     this._prefs.set("dataSubmissionPolicyAccepted", !!value);
   },
 
+  set dataSubmissionPolicyAcceptedVersion(value) {
+    this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
+  },
+
   /**
    * The state of user notification of the data policy.
    *
-   * This must be HealthReportPolicy.STATE_NOTIFY_COMPLETE before data
+   * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
    * submission can occur.
    *
-   * @return HealthReportPolicy.STATE_NOTIFY_* constant.
+   * @return DataReportingPolicy.STATE_NOTIFY_* constant.
    */
   get notifyState() {
     if (this.dataSubmissionPolicyResponseDate.getTime()) {
       return this.STATE_NOTIFY_COMPLETE;
     }
 
     // We get the local state - not the state from prefs - because we don't want
     // a value from a previous application run to interfere. This prevents
@@ -500,111 +502,128 @@ HealthReportPolicy.prototype = {
 
   /**
    * When this policy last requested data submission.
    *
    * This is used mainly for forensics purposes and should have no bearing
    * on scheduling or run-time behavior.
    */
   get lastDataSubmissionRequestedDate() {
-    return CommonUtils.getDatePref(this._prefs,
+    return CommonUtils.getDatePref(this._healthReportPrefs,
                                    "lastDataSubmissionRequestedTime", 0,
                                    this._log, OLDEST_ALLOWED_YEAR);
   },
 
   set lastDataSubmissionRequestedDate(value) {
-    CommonUtils.setDatePref(this._prefs, "lastDataSubmissionRequestedTime",
+    CommonUtils.setDatePref(this._healthReportPrefs,
+                            "lastDataSubmissionRequestedTime",
                             value, OLDEST_ALLOWED_YEAR);
   },
 
   /**
    * When the last data submission actually occurred.
    *
    * This is used mainly for forensics purposes and should have no bearing on
    * actual scheduling.
    */
   get lastDataSubmissionSuccessfulDate() {
-    return CommonUtils.getDatePref(this._prefs,
+    return CommonUtils.getDatePref(this._healthReportPrefs,
                                    "lastDataSubmissionSuccessfulTime", 0,
                                    this._log, OLDEST_ALLOWED_YEAR);
   },
 
   set lastDataSubmissionSuccessfulDate(value) {
-    CommonUtils.setDatePref(this._prefs, "lastDataSubmissionSuccessfulTime",
+    CommonUtils.setDatePref(this._healthReportPrefs,
+                            "lastDataSubmissionSuccessfulTime",
                             value, OLDEST_ALLOWED_YEAR);
   },
 
   /**
    * When we last encountered a submission failure.
    *
    * This is used for forensics purposes and should have no bearing on
    * scheduling.
    */
   get lastDataSubmissionFailureDate() {
-    return CommonUtils.getDatePref(this._prefs, "lastDataSubmissionFailureTime",
+    return CommonUtils.getDatePref(this._healthReportPrefs,
+                                   "lastDataSubmissionFailureTime",
                                    0, this._log, OLDEST_ALLOWED_YEAR);
   },
 
   set lastDataSubmissionFailureDate(value) {
-    CommonUtils.setDatePref(this._prefs, "lastDataSubmissionFailureTime", value,
-                            OLDEST_ALLOWED_YEAR);
+    CommonUtils.setDatePref(this._healthReportPrefs,
+                            "lastDataSubmissionFailureTime",
+                            value, OLDEST_ALLOWED_YEAR);
   },
 
   /**
    * When the next data submission is scheduled to occur.
    *
    * This is maintained internally by this type. External users should not
    * mutate this value.
    */
   get nextDataSubmissionDate() {
-    return CommonUtils.getDatePref(this._prefs, "nextDataSubmissionTime", 0,
+    return CommonUtils.getDatePref(this._healthReportPrefs,
+                                   "nextDataSubmissionTime", 0,
                                    this._log, OLDEST_ALLOWED_YEAR);
   },
 
   set nextDataSubmissionDate(value) {
-    CommonUtils.setDatePref(this._prefs, "nextDataSubmissionTime", value,
+    CommonUtils.setDatePref(this._healthReportPrefs,
+                            "nextDataSubmissionTime", value,
                             OLDEST_ALLOWED_YEAR);
   },
 
   /**
    * The number of submission failures for this day's upload.
    *
    * This is used to drive backoff and scheduling.
    */
   get currentDaySubmissionFailureCount() {
-    let v = this._prefs.get("currentDaySubmissionFailureCount", 0);
+    let v = this._healthReportPrefs.get("currentDaySubmissionFailureCount", 0);
 
     if (!Number.isInteger(v)) {
       v = 0;
     }
 
     return v;
   },
 
   set currentDaySubmissionFailureCount(value) {
     if (!Number.isInteger(value)) {
       throw new Error("Value must be integer: " + value);
     }
 
-    this._prefs.set("currentDaySubmissionFailureCount", value);
+    this._healthReportPrefs.set("currentDaySubmissionFailureCount", value);
   },
 
   /**
    * Whether a request to delete remote data is awaiting completion.
    *
    * If this is true, the policy will request that remote data be deleted.
    * Furthermore, no new data will be uploaded (if it's even allowed) until
    * the remote deletion is fulfilled.
    */
   get pendingDeleteRemoteData() {
-    return !!this._prefs.get("pendingDeleteRemoteData", false);
+    return !!this._healthReportPrefs.get("pendingDeleteRemoteData", false);
   },
 
   set pendingDeleteRemoteData(value) {
-    this._prefs.set("pendingDeleteRemoteData", !!value);
+    this._healthReportPrefs.set("pendingDeleteRemoteData", !!value);
+  },
+
+  /**
+   * Whether upload of Firefox Health Report data is enabled.
+   */
+  get healthReportUploadEnabled() {
+    return !!this._healthReportPrefs.get("uploadEnabled", true);
+  },
+
+  set healthReportUploadEnabled(value) {
+    this._healthReportPrefs.set("uploadEnabled", !!value);
   },
 
   /**
    * Record user acceptance of data submission policy.
    *
    * Data submission will not be allowed to occur until this is called.
    *
    * This is typically called through the `onUserAccept` property attached to
@@ -615,16 +634,17 @@ HealthReportPolicy.prototype = {
    * @param reason
    *        (string) How the user accepted the data submission policy.
    */
   recordUserAcceptance: function recordUserAcceptance(reason="no-reason") {
     this._log.info("User accepted data submission policy: " + reason);
     this.dataSubmissionPolicyResponseDate = this.now();
     this.dataSubmissionPolicyResponseType = "accepted-" + reason;
     this.dataSubmissionPolicyAccepted = true;
+    this.dataSubmissionPolicyAcceptedVersion = 1;
   },
 
   /**
    * Record user rejection of submission policy.
    *
    * Data submission will not be allowed to occur if this is called.
    *
    * This is typically called through the `onUserReject` property attached to
@@ -748,17 +768,17 @@ HealthReportPolicy.prototype = {
         this._log.debug("Deletion request is scheduled for the future: " +
                         nextSubmissionDate);
         return;
       }
 
       return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
     }
 
-    if (!this.dataUploadEnabled) {
+    if (!this.healthReportUploadEnabled) {
       this._log.debug("Data upload is disabled. Doing nothing.");
       return;
     }
 
     // If the user hasn't responded to the data policy, don't do anything.
     if (!this.ensureNotifyResponse(now)) {
       return;
     }
@@ -998,12 +1018,10 @@ HealthReportPolicy.prototype = {
 
     this.nextDataSubmissionDate = d;
     this.currentDaySubmissionFailureCount = 0;
   },
 
   _futureDate: function _futureDate(offset) {
     return new Date(this.now().getTime() + offset);
   },
-};
+});
 
-Object.freeze(HealthReportPolicy.prototype);
-
new file mode 100644
--- /dev/null
+++ b/services/datareporting/tests/Makefile.in
@@ -0,0 +1,16 @@
+# 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/.
+
+DEPTH          = @DEPTH@
+topsrcdir      = @top_srcdir@
+srcdir         = @srcdir@
+VPATH          = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+XPCSHELL_TESTS = xpcshell
+
+include $(topsrcdir)/config/rules.mk
+
rename from services/healthreport/tests/xpcshell/test_policy.js
rename to services/datareporting/tests/xpcshell/test_policy.js
--- a/services/healthreport/tests/xpcshell/test_policy.js
+++ b/services/datareporting/tests/xpcshell/test_policy.js
@@ -1,25 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://services-common/preferences.js");
-Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
-Cu.import("resource://testing-common/services/healthreport/mocks.jsm");
+Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
+Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
 
 
 function getPolicy(name) {
-  let prefs = new Preferences(name);
+  let branch = "testing.datareporting." + name;
+  let policyPrefs = new Preferences(branch + ".policy.");
+  let healthReportPrefs = new Preferences(branch + ".healthreport.");
+
   let listener = new MockPolicyListener();
+  let policy = new DataReportingPolicy(policyPrefs, healthReportPrefs, listener);
 
-  return [new HealthReportPolicy(prefs, listener), prefs, listener];
+  return [policy, policyPrefs, healthReportPrefs, listener];
 }
 
 function defineNow(policy, now) {
   print("Adjusting fake system clock to " + now);
   Object.defineProperty(policy, "now", {
     value: function customNow() {
       return now;
     },
@@ -27,112 +31,120 @@ function defineNow(policy, now) {
   });
 }
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_constructor() {
-  let prefs = new Preferences("foo.bar");
+  let policyPrefs = new Preferences("foo.bar.policy.");
+  let hrPrefs = new Preferences("foo.bar.healthreport.");
   let listener = {
     onRequestDataUpload: function() {},
     onRequestRemoteDelete: function() {},
     onNotifyDataPolicy: function() {},
   };
 
-  let policy = new HealthReportPolicy(prefs, listener);
+  let policy = new DataReportingPolicy(policyPrefs, hrPrefs, listener);
   do_check_true(Date.now() - policy.firstRunDate.getTime() < 1000);
 
   let tomorrow = Date.now() + 24 * 60 * 60 * 1000;
   do_check_true(tomorrow - policy.nextDataSubmissionDate.getTime() < 1000);
 
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
 
   run_next_test();
 });
 
 add_test(function test_prefs() {
-  let [policy, prefs, listener] = getPolicy("prefs");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("prefs");
 
   let now = new Date();
   let nowT = now.getTime();
 
   policy.firstRunDate = now;
-  do_check_eq(prefs.get("firstRunTime"), nowT);
+  do_check_eq(policyPrefs.get("firstRunTime"), nowT);
   do_check_eq(policy.firstRunDate.getTime(), nowT);
 
   policy.dataSubmissionPolicyNotifiedDate= now;
-  do_check_eq(prefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
   do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), nowT);
 
   policy.dataSubmissionPolicyResponseDate = now;
-  do_check_eq(prefs.get("dataSubmissionPolicyResponseTime"), nowT);
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseTime"), nowT);
   do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), nowT);
 
   policy.dataSubmissionPolicyResponseType = "type-1";
-  do_check_eq(prefs.get("dataSubmissionPolicyResponseType"), "type-1");
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseType"), "type-1");
   do_check_eq(policy.dataSubmissionPolicyResponseType, "type-1");
 
   policy.dataSubmissionEnabled = false;
-  do_check_false(prefs.get("dataSubmissionEnabled", true));
+  do_check_false(policyPrefs.get("dataSubmissionEnabled", true));
   do_check_false(policy.dataSubmissionEnabled);
 
   policy.dataSubmissionPolicyAccepted = false;
-  do_check_false(prefs.get("dataSubmissionPolicyAccepted", true));
+  do_check_false(policyPrefs.get("dataSubmissionPolicyAccepted", true));
   do_check_false(policy.dataSubmissionPolicyAccepted);
 
+  policy.dataSubmissionPolicyAcceptedVersion = 2;
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), 2);
+
   do_check_false(policy.dataSubmissionPolicyBypassAcceptance);
-  prefs.set("dataSubmissionPolicyBypassAcceptance", true);
+  policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
   do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
 
   policy.lastDataSubmissionRequestedDate = now;
-  do_check_eq(prefs.get("lastDataSubmissionRequestedTime"), nowT);
+  do_check_eq(hrPrefs.get("lastDataSubmissionRequestedTime"), nowT);
   do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), nowT);
 
   policy.lastDataSubmissionSuccessfulDate = now;
-  do_check_eq(prefs.get("lastDataSubmissionSuccessfulTime"), nowT);
+  do_check_eq(hrPrefs.get("lastDataSubmissionSuccessfulTime"), nowT);
   do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), nowT);
 
   policy.lastDataSubmissionFailureDate = now;
-  do_check_eq(prefs.get("lastDataSubmissionFailureTime"), nowT);
+  do_check_eq(hrPrefs.get("lastDataSubmissionFailureTime"), nowT);
   do_check_eq(policy.lastDataSubmissionFailureDate.getTime(), nowT);
 
   policy.nextDataSubmissionDate = now;
-  do_check_eq(prefs.get("nextDataSubmissionTime"), nowT);
+  do_check_eq(hrPrefs.get("nextDataSubmissionTime"), nowT);
   do_check_eq(policy.nextDataSubmissionDate.getTime(), nowT);
 
   policy.currentDaySubmissionFailureCount = 2;
-  do_check_eq(prefs.get("currentDaySubmissionFailureCount", 0), 2);
+  do_check_eq(hrPrefs.get("currentDaySubmissionFailureCount", 0), 2);
   do_check_eq(policy.currentDaySubmissionFailureCount, 2);
 
   policy.pendingDeleteRemoteData = true;
-  do_check_true(prefs.get("pendingDeleteRemoteData"));
+  do_check_true(hrPrefs.get("pendingDeleteRemoteData"));
   do_check_true(policy.pendingDeleteRemoteData);
 
+  policy.healthReportUploadEnabled = false;
+  do_check_false(hrPrefs.get("uploadEnabled"));
+  do_check_false(policy.healthReportUploadEnabled);
+
   run_next_test();
 });
 
 add_test(function test_notify_state_prefs() {
-  let [policy, prefs, listener] = getPolicy("notify_state_prefs");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notify_state_prefs");
 
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
 
   policy._dataSubmissionPolicyNotifiedDate = new Date();
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
 
   policy.dataSubmissionPolicyResponseDate = new Date();
   policy._dataSubmissionPolicyNotifiedDate = null;
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
 
   run_next_test();
 });
 
 add_test(function test_initial_submission_notification() {
-  let [policy, prefs, listener] = getPolicy("initial_submission_notification");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("initial_submission_notification");
 
   do_check_eq(listener.notifyUserCount, 0);
 
   // Fresh instances should not do anything initially.
   policy.checkStateAndTrigger();
   do_check_eq(listener.notifyUserCount, 0);
 
   // We still shouldn't notify up to the millisecond before the barrier.
@@ -154,30 +166,30 @@ add_test(function test_initial_submissio
   do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(),
               policy._dataSubmissionPolicyNotifiedDate.getTime());
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
 
   run_next_test();
 });
 
 add_test(function test_bypass_acceptance() {
-  let [policy, prefs, listener] = getPolicy("bypass_acceptance");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("bypass_acceptance");
 
-  prefs.set("dataSubmissionPolicyBypassAcceptance", true);
+  policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
   do_check_false(policy.dataSubmissionPolicyAccepted);
   do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
   defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime()));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_notification_implicit_acceptance() {
-  let [policy, prefs, listener] = getPolicy("notification_implicit_acceptance");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_implicit_acceptance");
 
   let now = new Date(policy.nextDataSubmissionDate.getTime() -
                      policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
   defineNow(policy, now);
   policy.checkStateAndTrigger();
   do_check_eq(listener.notifyUserCount, 1);
   listener.lastNotifyRequest.onUserNotifyComplete();
   do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
@@ -197,33 +209,33 @@ add_test(function test_notification_impl
   do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), policy.now().getTime());
   do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-implicit-time-elapsed");
 
   run_next_test();
 });
 
 add_test(function test_notification_rejected() {
   // User notification failed. We should not record it as being presented.
-  let [policy, prefs, listener] = getPolicy("notification_failed");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_failed");
 
   let now = new Date(policy.nextDataSubmissionDate.getTime() -
                      policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
   defineNow(policy, now);
   policy.checkStateAndTrigger();
   do_check_eq(listener.notifyUserCount, 1);
   listener.lastNotifyRequest.onUserNotifyFailed(new Error("testing failed."));
   do_check_null(policy._dataSubmissionPolicyNotifiedDate);
   do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
 
   run_next_test();
 });
 
 add_test(function test_notification_accepted() {
-  let [policy, prefs, listener] = getPolicy("notification_accepted");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accepted");
 
   let now = new Date(policy.nextDataSubmissionDate.getTime() -
                      policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
   defineNow(policy, now);
   policy.checkStateAndTrigger();
   listener.lastNotifyRequest.onUserNotifyComplete();
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
   do_check_false(policy.dataSubmissionPolicyAccepted);
@@ -233,17 +245,17 @@ add_test(function test_notification_acce
   do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-foo-bar");
   do_check_true(policy.dataSubmissionPolicyAccepted);
   do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), now.getTime());
 
   run_next_test();
 });
 
 add_test(function test_notification_rejected() {
-  let [policy, prefs, listener] = getPolicy("notification_rejected");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_rejected");
 
   let now = new Date(policy.nextDataSubmissionDate.getTime() -
                      policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
   defineNow(policy, now);
   policy.checkStateAndTrigger();
   listener.lastNotifyRequest.onUserNotifyComplete();
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
   do_check_false(policy.dataSubmissionPolicyAccepted);
@@ -256,52 +268,53 @@ add_test(function test_notification_reje
   defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime() + 10000));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
 
   run_next_test();
 });
 
 add_test(function test_submission_kill_switch() {
-  let [policy, prefs, listener] = getPolicy("submission_kill_switch");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
 
   policy.firstRunDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
   policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.recordUserAcceptance("accept-old-ack");
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), 1);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 
   defineNow(policy,
     new Date(Date.now() + policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC + 100));
   policy.dataSubmissionEnabled = false;
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_upload_kill_switch() {
-  let [policy, prefs, listener] = getPolicy("upload_kill_switch");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
 
   defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
   policy.recordUserAcceptance();
   defineNow(policy, policy.nextDataSubmissionDate);
 
-  policy.dataUploadEnabled = false;
+  policy.healthReportUploadEnabled = false;
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
-  policy.dataUploadEnabled = true;
+  policy.healthReportUploadEnabled = true;
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_data_submission_no_data() {
-  let [policy, prefs, listener] = getPolicy("data_submission_no_data");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_no_data");
 
   policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.dataSubmissionPolicyAccepted = true;
   let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
   defineNow(policy, now);
   do_check_eq(listener.requestDataUploadCount, 0);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
@@ -311,17 +324,17 @@ add_test(function test_data_submission_n
   defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
 
   run_next_test();
 });
 
 add_test(function test_data_submission_submit_failure_hard() {
-  let [policy, prefs, listener] = getPolicy("data_submission_submit_failure_hard");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_submit_failure_hard");
 
   policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmissionDate = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
   defineNow(policy, now);
 
   policy.checkStateAndTrigger();
@@ -336,32 +349,32 @@ add_test(function test_data_submission_s
   defineNow(policy, new Date(now.getTime() + 10));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_data_submission_submit_try_again() {
-  let [policy, prefs, listener] = getPolicy("data_submission_failure_soft");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_failure_soft");
 
   policy.recordUserAcceptance();
   let nextDataSubmissionDate = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
   policy.checkStateAndTrigger();
   listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               nextDataSubmissionDate.getTime() + 15 * 60 * 1000);
 
   run_next_test();
 });
 
 add_test(function test_submission_daily_scheduling() {
-  let [policy, prefs, listener] = getPolicy("submission_daily_scheduling");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_daily_scheduling");
 
   policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmissionDate = policy.nextDataSubmissionDate;
 
   // Skip ahead to next submission date. We should get a submission request.
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
@@ -391,17 +404,17 @@ add_test(function test_submission_daily_
   listener.lastDataRequest.onSubmissionSuccess(new Date(nextScheduled.getTime() + 200));
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
     new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
 
   run_next_test();
 });
 
 add_test(function test_submission_far_future_scheduling() {
-  let [policy, prefs, listener] = getPolicy("submission_far_future_scheduling");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_far_future_scheduling");
 
   let now = new Date(Date.now() - 24 * 60 * 60 * 1000);
   defineNow(policy, now);
   policy.recordUserAcceptance();
   now = new Date();
   defineNow(policy, now);
 
   let nextDate = policy._futureDate(3 * 24 * 60 * 60 * 1000 - 1);
@@ -415,17 +428,17 @@ add_test(function test_submission_far_fu
   do_check_eq(listener.requestDataUploadCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               policy._futureDate(24 * 60 * 60 * 1000).getTime());
 
   run_next_test();
 });
 
 add_test(function test_submission_backoff() {
-  let [policy, prefs, listener] = getPolicy("submission_backoff");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_backoff");
 
   do_check_eq(policy.FAILURE_BACKOFF_INTERVALS.length, 2);
 
   policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.dataSubmissionPolicyAccepted = true;
 
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
@@ -478,17 +491,17 @@ add_test(function test_submission_backof
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               new Date(now.getTime() + 24 * 60 * 60 * 1000).getTime());
 
   run_next_test();
 });
 
 // Ensure that only one submission request can be active at a time.
 add_test(function test_submission_expiring() {
-  let [policy, prefs, listener] = getPolicy("submission_expiring");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_expiring");
 
   policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmission = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
@@ -501,17 +514,17 @@ add_test(function test_submission_expiri
 
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
 
   run_next_test();
 });
 
 add_test(function test_delete_remote_data() {
-  let [policy, prefs, listener] = getPolicy("delete_remote_data");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data");
 
   do_check_false(policy.pendingDeleteRemoteData);
   let nextSubmissionDate = policy.nextDataSubmissionDate;
 
   let now = new Date();
   defineNow(policy, now);
 
   policy.deleteRemoteData();
@@ -527,17 +540,17 @@ add_test(function test_delete_remote_dat
   listener.lastRemoteDeleteRequest.onSubmissionSuccess(policy.now());
   do_check_false(policy.pendingDeleteRemoteData);
 
   run_next_test();
 });
 
 // Ensure that deletion requests take priority over regular data submission.
 add_test(function test_delete_remote_data_priority() {
-  let [policy, prefs, listener] = getPolicy("delete_remote_data_priority");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_priority");
 
   let now = new Date();
   defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
   policy.recordUserAcceptance();
   defineNow(policy, new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000));
 
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
@@ -548,17 +561,17 @@ add_test(function test_delete_remote_dat
 
   do_check_eq(listener.requestRemoteDeleteCount, 1);
   do_check_eq(listener.requestDataUploadCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_delete_remote_data_backoff() {
-  let [policy, prefs, listener] = getPolicy("delete_remote_data_backoff");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_backoff");
 
   let now = new Date();
   defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
   policy.recordUserAcceptance();
   defineNow(policy, now);
   policy.nextDataSubmissionDate = now;
   policy.deleteRemoteData();
 
@@ -581,17 +594,17 @@ add_test(function test_delete_remote_dat
   do_check_eq(listener.requestRemoteDeleteCount, 2);
 
   run_next_test();
 });
 
 // If we request delete while an upload is in progress, delete should be
 // scheduled immediately after upload.
 add_test(function test_delete_remote_data_in_progress_upload() {
-  let [policy, prefs, listener] = getPolicy("delete_remote_data_in_progress_upload");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_in_progress_upload");
 
   let now = new Date();
   defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
   policy.recordUserAcceptance();
   defineNow(policy, policy.nextDataSubmissionDate);
 
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
@@ -611,33 +624,33 @@ add_test(function test_delete_remote_dat
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   do_check_eq(listener.requestRemoteDeleteCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_polling() {
-  let [policy, prefs, listener] = getPolicy("polling");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("polling");
 
   // Ensure checkStateAndTrigger is called at a regular interval.
   let now = new Date();
   Object.defineProperty(policy, "POLL_INTERVAL_MSEC", {
     value: 500,
   });
   let count = 0;
 
   Object.defineProperty(policy, "checkStateAndTrigger", {
     value: function fakeCheckStateAndTrigger() {
       let now2 = new Date();
       count++;
 
       do_check_true(now2.getTime() - now.getTime() >= 500);
       now = now2;
-      HealthReportPolicy.prototype.checkStateAndTrigger.call(policy);
+      DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
 
       if (count >= 2) {
         policy.stopPolling();
 
         do_check_eq(listener.notifyUserCount, 0);
         do_check_eq(listener.requestDataUploadCount, 0);
 
         run_next_test();
@@ -647,17 +660,17 @@ add_test(function test_polling() {
   policy.startPolling();
 });
 
 // Ensure that implicit acceptance of policy is resolved through polling.
 //
 // This is probably covered by other tests. But, it's best to have explicit
 // coverage from a higher-level.
 add_test(function test_polling_implicit_acceptance() {
-  let [policy, prefs, listener] = getPolicy("polling_implicit_acceptance");
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("polling_implicit_acceptance");
 
   // Redefine intervals with shorter, test-friendly values.
   Object.defineProperty(policy, "POLL_INTERVAL_MSEC", {
     value: 250,
   });
 
   Object.defineProperty(policy, "IMPLICIT_ACCEPTANCE_INTERVAL_MSEC", {
     value: 750,
@@ -665,17 +678,17 @@ add_test(function test_polling_implicit_
 
   let count = 0;
   Object.defineProperty(policy, "checkStateAndTrigger", {
     value: function CheckStateAndTriggerProxy() {
       count++;
       print("checkStateAndTrigger count: " + count);
 
       // Account for some slack.
-      HealthReportPolicy.prototype.checkStateAndTrigger.call(policy);
+      DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
 
       // What should happen on different invocations:
       //
       //   1) We are inside the prompt interval so user gets prompted.
       //   2) still ~300ms away from implicit acceptance
       //   3) still ~50ms away from implicit acceptance
       //   4) Implicit acceptance recorded. Data submission requested.
       //   5) Request still pending. No new submission requested.
new file mode 100644
--- /dev/null
+++ b/services/datareporting/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_policy.js]
--- a/services/healthreport/HealthReportComponents.manifest
+++ b/services/healthreport/HealthReportComponents.manifest
@@ -1,18 +1,8 @@
-#   b2g:            {3c2e2abc-06d4-11e1-ac3b-374f68613e61}
-#   browser:        {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
-#   mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110}
-#   mobile/xul:     {a23983c0-fd0e-11dc-95ff-0800200c9a66}
-#   suite (comm):   {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
-#   metro browser:  {99bceaaa-e3c6-48c1-b981-ef9b46b67d60}
-
-component {e354c59b-b252-4040-b6dd-b71864e3e35c} HealthReportService.js
-contract @mozilla.org/healthreport/service;1 {e354c59b-b252-4040-b6dd-b71864e3e35c}
-category app-startup HealthReportService service,@mozilla.org/healthreport/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66}
-
+# Register Firefox Health Report providers.
 category healthreport-js-provider AddonsProvider resource://gre/modules/services/healthreport/providers.jsm
 category healthreport-js-provider AppInfoProvider resource://gre/modules/services/healthreport/providers.jsm
 category healthreport-js-provider CrashesProvider resource://gre/modules/services/healthreport/providers.jsm
 category healthreport-js-provider SysInfoProvider resource://gre/modules/services/healthreport/providers.jsm
 category healthreport-js-provider ProfileMetadataProvider resource://gre/modules/services/healthreport/profile.jsm
 category healthreport-js-provider SessionsProvider resource://gre/modules/services/healthreport/providers.jsm
 
--- a/services/healthreport/Makefile.in
+++ b/services/healthreport/Makefile.in
@@ -6,33 +6,30 @@ DEPTH     = @DEPTH@
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 modules := \
   healthreporter.jsm \
-  policy.jsm \
   profile.jsm \
   providers.jsm \
   $(NULL)
 
 testing_modules := \
-  mocks.jsm \
   utils.jsm \
   $(NULL)
 
 TEST_DIRS += tests
 
 MODULES_FILES := $(modules)
 MODULES_DEST = $(FINAL_TARGET)/modules/services/healthreport
 INSTALL_TARGETS += MODULES
 
 TESTING_JS_MODULES := $(addprefix modules-testing/,$(testing_modules))
 TESTING_JS_MODULE_DIR := services/healthreport
 
 EXTRA_COMPONENTS := \
   HealthReportComponents.manifest \
-  HealthReportService.js \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/services/healthreport/healthreport-prefs.js
+++ b/services/healthreport/healthreport-prefs.js
@@ -1,24 +1,23 @@
 /* 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/. */
 
-pref("healthreport.documentServerURI", "https://data.mozilla.com/");
-pref("healthreport.documentServerNamespace", "metrics");
-pref("healthreport.logging.consoleEnabled", true);
-pref("healthreport.logging.consoleLevel", "Warn");
-pref("healthreport.policy.currentDaySubmissionFailureCount", 0);
-pref("healthreport.policy.dataSubmissionEnabled", true);
-pref("healthreport.policy.dataSubmissionPolicyAccepted", false);
-pref("healthreport.policy.dataSubmissionPolicyBypassAcceptance", false);
-pref("healthreport.policy.dataSubmissionPolicyNotifiedTime", "0");
-pref("healthreport.policy.dataSubmissionPolicyResponseType", "");
-pref("healthreport.policy.dataSubmissionPolicyResponseTime", "0");
-pref("healthreport.policy.firstRunTime", "0");
-pref("healthreport.policy.lastDataSubmissionFailureTime", "0");
-pref("healthreport.policy.lastDataSubmissionRequestedTime", "0");
-pref("healthreport.policy.lastDataSubmissionSuccessfulTime", "0");
-pref("healthreport.policy.nextDataSubmissionTime", "0");
-pref("healthreport.service.enabled", true);
-pref("healthreport.service.loadDelayMsec", 10000);
-pref("healthreport.service.providerCategories", "healthreport-js-provider");
-pref("healthreport.infoURL", "http://www.mozilla.org/legal/privacy/firefox.html#health-report");
+pref("datareporting.healthreport.currentDaySubmissionFailureCount", 0);
+pref("datareporting.healthreport.documentServerURI", "https://data.mozilla.com/");
+pref("datareporting.healthreport.documentServerNamespace", "metrics");
+pref("datareporting.healthreport.infoURL", "http://www.mozilla.org/legal/privacy/firefox.html#health-report");
+pref("datareporting.healthreport.logging.consoleEnabled", true);
+pref("datareporting.healthreport.logging.consoleLevel", "Warn");
+pref("datareporting.healthreport.lastDataSubmissionFailureTime", "0");
+pref("datareporting.healthreport.lastDataSubmissionRequestedTime", "0");
+pref("datareporting.healthreport.lastDataSubmissionSuccessfulTime", "0");
+pref("datareporting.healthreport.nextDataSubmissionTime", "0");
+pref("datareporting.healthreport.pendingDeleteRemoteData", false);
+
+// Health Report is enabled by default on all channels.
+pref("datareporting.healthreport.uploadEnabled", true);
+
+pref("datareporting.healthreport.service.enabled", true);
+pref("datareporting.healthreport.service.loadDelayMsec", 10000);
+pref("datareporting.healthreport.service.providerCategories", "healthreport-js-provider");
+
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -6,26 +6,24 @@
 
 this.EXPORTED_SYMBOLS = ["HealthReporter"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-common/bagheeraclient.js");
 Cu.import("resource://services-common/log4moz.js");
-Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
 Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
 
 
 // Oldest year to allow in date preferences. This module was implemented in
 // 2012 and no dates older than that should be encountered.
 const OLDEST_ALLOWED_YEAR = 2012;
 
 const DAYS_IN_PAYLOAD = 180;
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
@@ -76,39 +74,45 @@ const DEFAULT_DATABASE_NAME = "healthrep
  * initiate storage early in the shutdown cycle ("quit-application").
  * Hopefully all the async operations have completed by the time we reach
  * "profile-do-change." If so, great. If not, we spin the event loop until
  * they have completed, avoiding potential race conditions.
  *
  * @param branch
  *        (string) The preferences branch to use for state storage. The value
  *        must end with a period (.).
+ *
+ * @param policy
+ *        (HealthReportPolicy) Policy driving execution of HealthReporter.
  */
-function HealthReporter(branch) {
+function HealthReporter(branch, policy) {
   if (!branch.endsWith(".")) {
     throw new Error("Branch must end with a period (.): " + branch);
   }
 
+  if (!policy) {
+    throw new Error("Must provide policy to HealthReporter constructor.");
+  }
+
   this._log = Log4Moz.repository.getLogger("Services.HealthReport.HealthReporter");
   this._log.info("Initializing health reporter instance against " + branch);
 
   this._prefs = new Preferences(branch);
 
   if (!this.serverURI) {
     throw new Error("No server URI defined. Did you forget to define the pref?");
   }
 
   if (!this.serverNamespace) {
     throw new Error("No server namespace defined. Did you forget a pref?");
   }
 
-  this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME;
+  this._policy = policy;
 
-  let policyBranch = new Preferences(branch + "policy.");
-  this._policy = new HealthReportPolicy(policyBranch, this);
+  this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME;
 
   this._storage = null;
   this._storageInProgress = false;
   this._collector = null;
   this._collectorInProgress = false;
   this._initialized = false;
   this._initializeHadError = false;
   this._initializedDeferred = Promise.defer();
@@ -206,16 +210,24 @@ HealthReporter.prototype = Object.freeze
     return this._prefs.get("lastSubmitID", null) || null;
   },
 
   set lastSubmitID(value) {
     this._prefs.set("lastSubmitID", value || "");
   },
 
   /**
+   * Whether this instance will upload data to a server.
+   */
+  get willUploadData() {
+    return this._policy.dataSubmissionPolicyAccepted &&
+           this._policy.healthReportUploadEnabled;
+  },
+
+  /**
    * Whether remote data is currently stored.
    *
    * @return bool
    */
   haveRemoteData: function () {
     return !!this.lastSubmitID;
   },
 
@@ -284,17 +296,16 @@ HealthReporter.prototype = Object.freeze
     this._log.debug("Collector initialized.");
     this._collectorInProgress = false;
 
     if (this._shutdownRequested) {
       this._initiateShutdown();
       return;
     }
 
-    this._policy.startPolling();
     this._log.info("HealthReporter started.");
     this._initialized = true;
     Services.obs.addObserver(this, "idle-daily", false);
     this._initializedDeferred.resolve(this);
   },
 
   // nsIObserver to handle shutdown.
   observe: function (subject, topic, data) {
@@ -322,19 +333,16 @@ HealthReporter.prototype = Object.freeze
       return;
     }
 
     this._log.info("Request to shut down.");
 
     this._initialized = false;
     this._shutdownRequested = true;
 
-    // Safe to call multiple times.
-    this._policy.stopPolling();
-
     if (this._collectorInProgress) {
       this._log.warn("Collector is in progress of initializing. Waiting to finish.");
       return;
     }
 
     // If storage is in the process of initializing, we need to wait for it
     // to finish before continuing. The initialization process will call us
     // again once storage has initialized.
@@ -547,55 +555,24 @@ HealthReporter.prototype = Object.freeze
   /**
    * Collect all measurements for all registered providers.
    */
   collectMeasurements: function () {
     return this._collector.collectConstantData();
   },
 
   /**
-   * Record the user's rejection of the data submission policy.
-   *
-   * This should be what everything uses to disable data submission.
+   * Called to initiate a data upload.
    *
-   * @param reason
-   *        (string) Why data submission is being disabled.
-   */
-  recordPolicyRejection: function (reason) {
-    this._policy.recordUserRejection(reason);
-  },
-
-  /**
-   * Record the user's acceptance of the data submission policy.
-   *
-   * This should be what everything uses to enable data submission.
-   *
-   * @param reason
-   *        (string) Why data submission is being enabled.
+   * The passed argument is a `DataSubmissionRequest` from policy.jsm.
    */
-  recordPolicyAcceptance: function (reason) {
-    this._policy.recordUserAcceptance(reason);
-  },
-
-  /**
-   * Whether the data submission policy has been accepted.
-   *
-   * If this is true, health data will be submitted unless one of the kill
-   * switches is active.
-   */
-  get dataSubmissionPolicyAccepted() {
-    return this._policy.dataSubmissionPolicyAccepted;
-  },
-
-  /**
-   * Whether this health reporter will upload data to a server.
-   */
-  get willUploadData() {
-    return this._policy.dataSubmissionPolicyAccepted &&
-           this._policy.dataUploadEnabled;
+  requestDataUpload: function (request) {
+    this.collectMeasurements()
+        .then(this._uploadData.bind(this, request),
+              this._onSubmitDataRequestFailure.bind(this));
   },
 
   /**
    * Request that server data be deleted.
    *
    * If deletion is scheduled to occur immediately, a promise will be returned
    * that will be fulfilled when the deletion attempt finishes. Otherwise,
    * callers should poll haveRemoteData() to determine when remote data is
@@ -763,17 +740,17 @@ HealthReporter.prototype = Object.freeze
       let payload = yield this.getJSONPayload();
       yield this._saveLastPayload(payload);
       let result = yield client.uploadJSON(this.serverNamespace, id, payload,
                                            this.lastSubmitID);
       yield this._onBagheeraResult(request, false, result);
     }.bind(this));
   },
 
-  _deleteRemoteData: function (request) {
+  deleteRemoteData: function (request) {
     if (!this.lastSubmitID) {
       this._log.info("Received request to delete remote data but no data stored.");
       request.onNoDataAvailable();
       return;
     }
 
     this._log.warn("Deleting remote data.");
     let client = new BagheeraClient(this.serverURI);
@@ -854,33 +831,10 @@ HealthReporter.prototype = Object.freeze
       }
     );
   },
 
   _now: function _now() {
     return new Date();
   },
 
-  //-----------------------------
-  // HealthReportPolicy listeners
-  //-----------------------------
-
-  onRequestDataUpload: function (request) {
-    this.collectMeasurements()
-        .then(this._uploadData.bind(this, request),
-              this._onSubmitDataRequestFailure.bind(this));
-  },
-
-  onNotifyDataPolicy: function (request) {
-    // This isn't very loosely coupled. We may want to have this call
-    // registered listeners instead.
-    Observers.notify("healthreport:notify-data-policy:request", request);
-  },
-
-  onRequestRemoteDelete: function (request) {
-    this._deleteRemoteData(request);
-  },
-
-  //------------------------------------
-  // End of HealthReportPolicy listeners
-  //------------------------------------
 });
 
--- a/services/healthreport/modules-testing/utils.jsm
+++ b/services/healthreport/modules-testing/utils.jsm
@@ -211,18 +211,18 @@ this.createFakeCrash = function (submitt
 };
 
 
 /**
  * A HealthReporter that is probed with various callbacks and counters.
  *
  * The purpose of this type is to aid testing of startup and shutdown.
  */
-this.InspectedHealthReporter = function (branch) {
-  HealthReporter.call(this, branch);
+this.InspectedHealthReporter = function (branch, policy) {
+  HealthReporter.call(this, branch, policy);
 
   this.onStorageCreated = null;
   this.onCollectorInitialized = null;
   this.collectorShutdownCount = 0;
   this.storageCloseCount = 0;
 }
 
 InspectedHealthReporter.prototype = {
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -4,17 +4,17 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
 Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
-Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
+Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://testing-common/services-common/bagheeraserver.js");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
 Cu.import("resource://testing-common/services/healthreport/utils.jsm");
 
 
 const SERVER_HOSTNAME = "localhost";
@@ -31,22 +31,39 @@ function defineNow(policy, now) {
     },
     writable: true,
   });
 }
 
 function getJustReporter(name, uri=SERVER_URI, inspected=false) {
   let branch = "healthreport.testing. " + name + ".";
 
-  let prefs = new Preferences(branch);
+  let prefs = new Preferences(branch + "healthreport.");
   prefs.set("documentServerURI", uri);
   prefs.set("dbName", name);
 
+  let reporter;
+
+  let policyPrefs = new Preferences(branch + "policy.");
+  let policy = new DataReportingPolicy(policyPrefs, prefs, {
+    onRequestDataUpload: function (request) {
+      reporter.requestDataUpload(request);
+    },
+
+    onNotifyDataPolicy: function (request) { },
+
+    onRequestRemoteDelete: function (request) {
+      reporter.deleteRemoteData(request);
+    },
+  });
+
   let type = inspected ? InspectedHealthReporter : HealthReporter;
-  return new type(branch);
+  reporter = new type(branch + "healthreport.", policy);
+
+  return reporter;
 }
 
 function getReporter(name, uri, inspected) {
   let reporter = getJustReporter(name, uri, inspected);
   return reporter.onInit();
 }
 
 function getReporterAndServer(name, namespace="test") {
@@ -246,34 +263,34 @@ add_task(function test_idle_daily() {
 
 add_task(function test_data_submission_transport_failure() {
   let reporter = yield getReporter("data_submission_transport_failure");
   reporter.serverURI = "http://localhost:8080/";
   reporter.serverNamespace = "test00";
 
   let deferred = Promise.defer();
   let request = new DataSubmissionRequest(deferred, new Date(Date.now + 30000));
-  reporter.onRequestDataUpload(request);
+  reporter.requestDataUpload(request);
 
   yield deferred.promise;
   do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
 
   reporter._shutdown();
 });
 
 add_task(function test_data_submission_success() {
   let [reporter, server] = yield getReporterAndServer("data_submission_success");
 
   do_check_eq(reporter.lastPingDate.getTime(), 0);
   do_check_false(reporter.haveRemoteData());
 
   let deferred = Promise.defer();
 
   let request = new DataSubmissionRequest(deferred, new Date());
-  reporter.onRequestDataUpload(request);
+  reporter.requestDataUpload(request);
   yield deferred.promise;
   do_check_eq(request.state, request.SUBMISSION_SUCCESS);
   do_check_true(reporter.lastPingDate.getTime() > 0);
   do_check_true(reporter.haveRemoteData());
 
   reporter._shutdown();
   yield shutdownServer(server);
 });
@@ -331,32 +348,33 @@ add_task(function test_request_remote_da
 
   reporter._shutdown();
   yield shutdownServer(server);
 });
 
 add_task(function test_policy_accept_reject() {
   let [reporter, server] = yield getReporterAndServer("policy_accept_reject");
 
-  do_check_false(reporter.dataSubmissionPolicyAccepted);
+  let policy = reporter._policy;
+
+  do_check_false(policy.dataSubmissionPolicyAccepted);
   do_check_false(reporter.willUploadData);
 
-  reporter.recordPolicyAcceptance();
-  do_check_true(reporter.dataSubmissionPolicyAccepted);
+  policy.recordUserAcceptance();
+  do_check_true(policy.dataSubmissionPolicyAccepted);
   do_check_true(reporter.willUploadData);
 
-  reporter.recordPolicyRejection();
-  do_check_false(reporter.dataSubmissionPolicyAccepted);
+  policy.recordUserRejection();
+  do_check_false(policy.dataSubmissionPolicyAccepted);
   do_check_false(reporter.willUploadData);
 
   reporter._shutdown();
   yield shutdownServer(server);
 });
 
-
 add_task(function test_upload_save_payload() {
   let [reporter, server] = yield getReporterAndServer("upload_save_payload");
 
   let deferred = Promise.defer();
   let request = new DataSubmissionRequest(deferred, new Date(), false);
 
   yield reporter._uploadData(request);
   let json = yield reporter.getLastPayload();
--- a/services/healthreport/tests/xpcshell/test_load_modules.js
+++ b/services/healthreport/tests/xpcshell/test_load_modules.js
@@ -1,28 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const modules = [
   "healthreporter.jsm",
-  "policy.jsm",
   "profile.jsm",
   "providers.jsm",
 ];
 
-const test_modules = [
-  "mocks.jsm",
-];
-
 function run_test() {
   for (let m of modules) {
     let resource = "resource://gre/modules/services/healthreport/" + m;
     Components.utils.import(resource, {});
   }
-
-  for (let m of test_modules) {
-    let resource = "resource://testing-common/services/healthreport/" + m;
-    Components.utils.import(resource, {});
-  }
 }
 
--- a/services/healthreport/tests/xpcshell/xpcshell.ini
+++ b/services/healthreport/tests/xpcshell/xpcshell.ini
@@ -1,14 +1,13 @@
 [DEFAULT]
 head = head.js
 tail =
 
 [test_load_modules.js]
 [test_profile.js]
-[test_policy.js]
 [test_healthreporter.js]
 [test_provider_addons.js]
 [test_provider_appinfo.js]
 [test_provider_crashes.js]
 [test_provider_sysinfo.js]
 [test_provider_sessions.js]
 
--- a/services/makefiles.sh
+++ b/services/makefiles.sh
@@ -4,23 +4,25 @@
 
 add_makefiles "
   services/Makefile
   services/aitc/Makefile
   services/common/Makefile
   services/crypto/Makefile
   services/crypto/component/Makefile
   services/healthreport/Makefile
+  services/datareporting/Makefile
   services/metrics/Makefile
   services/sync/Makefile
   services/sync/locales/Makefile
 "
 
 if [ "$ENABLE_TESTS" ]; then
   add_makefiles "
     services/aitc/tests/Makefile
     services/common/tests/Makefile
     services/crypto/tests/Makefile
     services/healthreport/tests/Makefile
+    services/datareporting/tests/Makefile
     services/metrics/tests/Makefile
     services/sync/tests/Makefile
   "
 fi
--- a/testing/xpcshell/xpcshell.ini
+++ b/testing/xpcshell/xpcshell.ini
@@ -83,16 +83,17 @@ skip-if = os == "android"
 [include:widget/tests/unit/xpcshell.ini]
 [include:content/base/test/unit/xpcshell.ini]
 [include:content/test/unit/xpcshell.ini]
 [include:toolkit/components/url-classifier/tests/unit/xpcshell.ini]
 [include:services/aitc/tests/unit/xpcshell.ini]
 [include:services/common/tests/unit/xpcshell.ini]
 [include:services/crypto/tests/unit/xpcshell.ini]
 [include:services/crypto/components/tests/unit/xpcshell.ini]
+[include:services/datareporting/tests/xpcshell/xpcshell.ini]
 [include:services/healthreport/tests/xpcshell/xpcshell.ini]
 [include:services/metrics/tests/xpcshell/xpcshell.ini]
 [include:services/sync/tests/unit/xpcshell.ini]
 # Bug 676978: tests hang on Android
 skip-if = os == "android"
 [include:browser/components/dirprovider/tests/unit/xpcshell.ini]
 [include:browser/components/downloads/test/unit/xpcshell.ini]
 [include:browser/components/feeds/test/unit/xpcshell.ini]