Backed out changeset 8673477f5fc2 (bug 862563) for Windows mochitest-5 failures
authorWes Kocher <wkocher@mozilla.com>
Fri, 11 Jul 2014 17:00:08 -0700
changeset 215701 81dbb087ceffca2b1adbab3ab9fd7f79a7711bc1
parent 215700 4c74c20737387b117c3171b7d1e6e85d771a3ced
child 215702 4ecf47ef0ed2e7e57b4cd70c0798c083aa783e3c
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs862563
milestone33.0a1
backs out8673477f5fc29ced4fcd486e844796cd1ca7d42a
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
Backed out changeset 8673477f5fc2 (bug 862563) for Windows mochitest-5 failures
browser/base/content/browser-data-submission-info-bar.js
browser/base/content/test/general/browser_datareporting_notification.js
browser/base/content/test/general/head.js
browser/components/preferences/in-content/tests/browser_healthreport.js
browser/components/preferences/tests/browser_healthreport.js
browser/components/search/test/browser_healthreport.js
services/datareporting/DataReportingService.js
services/datareporting/datareporting-prefs.js
services/datareporting/modules-testing/mocks.jsm
services/datareporting/policy.jsm
services/datareporting/tests/xpcshell/test_policy.js
services/healthreport/healthreporter.jsm
services/healthreport/modules-testing/utils.jsm
services/healthreport/tests/xpcshell/test_healthreporter.js
testing/profiles/prefs_general.js
--- a/browser/base/content/browser-data-submission-info-bar.js
+++ b/browser/base/content/browser-data-submission-info-bar.js
@@ -57,59 +57,68 @@ let gDataNotificationInfoBar = {
 
     this._actionTaken = false;
 
     let buttons = [{
       label: gNavigatorBundle.getString("dataReportingNotification.button.label"),
       accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"),
       popup: null,
       callback: function () {
+        // Clicking the button to go to the preferences tab constitutes
+        // acceptance of the data upload policy for Firefox Health Report.
+        // This will ensure the checkbox is checked. The user has the option of
+        // unchecking it.
+        request.onUserAccept("info-bar-button-pressed");
         this._actionTaken = true;
         window.openAdvancedPreferences("dataChoicesTab");
       }.bind(this),
     }];
 
     this._log.info("Creating data reporting policy notification.");
     let notification = this._notificationBox.appendNotification(
       message,
       this._DATA_REPORTING_NOTIFICATION,
       null,
       this._notificationBox.PRIORITY_INFO_HIGH,
       buttons,
       function onEvent(event) {
         if (event == "removed") {
+          if (!this._actionTaken) {
+            request.onUserAccept("info-bar-dismissed");
+          }
+
           Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null);
         }
       }.bind(this)
     );
-    // It is important to defer calling onUserNotifyComplete() until we're
-    // actually sure the notification was displayed. If we ever called
-    // onUserNotifyComplete() without showing anything to the user, that
-    // would be very good for user choice. It may also have legal impact.
+
+    // Tell the notification request we have displayed the notification.
     request.onUserNotifyComplete();
   },
 
   _clearPolicyNotification: function () {
     let notification = this._getDataReportingNotification();
     if (notification) {
       this._log.debug("Closing notification.");
       notification.close();
     }
   },
 
+  onNotifyDataPolicy: function (request) {
+    try {
+      this._displayDataPolicyInfoBar(request);
+    } catch (ex) {
+      request.onUserNotifyFailed(ex);
+    }
+  },
+
   observe: function(subject, topic, data) {
     switch (topic) {
       case "datareporting:notify-data-policy:request":
-        let request = subject.wrappedJSObject.object;
-        try {
-          this._displayDataPolicyInfoBar(request);
-        } catch (ex) {
-          request.onUserNotifyFailed(ex);
-          return;
-        }
+        this.onNotifyDataPolicy(subject.wrappedJSObject.object);
         break;
 
       case "datareporting:notify-data-policy:close":
         // If this observer fires, it means something else took care of
         // responding. Therefore, we don't need to do anything. So, we
         // act like we took action and clear state.
         this._actionTaken = true;
         this._clearPolicyNotification();
--- a/browser/base/content/test/general/browser_datareporting_notification.js
+++ b/browser/base/content/test/general/browser_datareporting_notification.js
@@ -1,55 +1,36 @@
 /* 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 originalPolicy = null;
-
-/**
- * Display a datareporting notification to the user.
- *
- * @param  {String} name
- */
 function sendNotifyRequest(name) {
   let ns = {};
-  Cu.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
-  Cu.import("resource://gre/modules/Preferences.jsm", ns);
+  Components.utils.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
+  Components.utils.import("resource://gre/modules/Preferences.jsm", ns);
 
-  let service = Cc["@mozilla.org/datareporting/service;1"]
-                  .getService(Ci.nsISupports)
-                  .wrappedJSObject;
+  let service = Components.classes["@mozilla.org/datareporting/service;1"]
+                                  .getService(Components.interfaces.nsISupports)
+                                  .wrappedJSObject;
   ok(service.healthReporter, "Health Reporter instance is available.");
 
-  Cu.import("resource://gre/modules/Promise.jsm", ns);
-  let deferred = ns.Promise.defer();
-
-  if (!originalPolicy) {
-    originalPolicy = service.policy;
-  }
-
   let policyPrefs = new ns.Preferences("testing." + name + ".");
   ok(service._prefs, "Health Reporter prefs are available.");
   let hrPrefs = service._prefs;
 
   let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service);
-  policy.dataSubmissionPolicyBypassNotification = false;
-  service.policy = policy;
   policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
 
-  service.healthReporter.onInit().then(function onSuccess () {
-    is(policy.ensureUserNotified(), false, "User not notified about data policy on init.");
-    ok(policy._userNotifyPromise, "_userNotifyPromise defined.");
-    policy._userNotifyPromise.then(
-      deferred.resolve.bind(deferred),
-      deferred.reject.bind(deferred)
-    );
-  }.bind(this), deferred.reject.bind(deferred));
+  is(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED, "Policy is in unnotified state.");
 
-  return [policy, deferred.promise];
+  service.healthReporter.onInit().then(function onInit() {
+    is(policy.ensureNotifyResponse(new Date()), false, "User has not responded to policy.");
+  });
+
+  return policy;
 }
 
 /**
  * Wait for a <notification> to be closed then call the specified callback.
  */
 function waitForNotificationClose(notification, cb) {
   let parent = notification.parentNode;
 
@@ -69,73 +50,60 @@ function waitForNotificationClose(notifi
   });
 
   observer.observe(parent, {childList: true});
 }
 
 let dumpAppender, rootLogger;
 
 function test() {
-  registerCleanupFunction(cleanup);
   waitForExplicitFinish();
 
   let ns = {};
   Components.utils.import("resource://gre/modules/Log.jsm", ns);
   rootLogger = ns.Log.repository.rootLogger;
   dumpAppender = new ns.Log.DumpAppender();
   dumpAppender.level = ns.Log.Level.All;
   rootLogger.addAppender(dumpAppender);
 
-  closeAllNotifications().then(function onSuccess () {
-    let notification = document.getElementById("global-notificationbox");
-
-    notification.addEventListener("AlertActive", function active() {
-      notification.removeEventListener("AlertActive", active, true);
-      is(notification.allNotifications.length, 1, "Notification Displayed.");
+  let notification = document.getElementById("global-notificationbox");
+  let policy;
 
-      executeSoon(function afterNotification() {
-        waitForNotificationClose(notification.currentNotification, function onClose() {
-          is(notification.allNotifications.length, 0, "No notifications remain.");
-          is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Version pref set.");
-          ok(policy.dataSubmissionPolicyNotifiedDate.getTime() > -1, "Date pref set.");
-          test_multiple_windows();
-        });
-        notification.currentNotification.close();
-      });
-    }, true);
+  notification.addEventListener("AlertActive", function active() {
+    notification.removeEventListener("AlertActive", active, true);
+
+    executeSoon(function afterNotification() {
+      is(policy.notifyState, policy.STATE_NOTIFY_WAIT, "Policy is waiting for user response.");
+      ok(!policy.dataSubmissionPolicyAccepted, "Data submission policy not yet accepted.");
 
-    let [policy, promise] = sendNotifyRequest("single_window_notified");
-
-    is(policy.dataSubmissionPolicyAcceptedVersion, 0, "No version should be set on init.");
-    is(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0, "No date should be set on init.");
-    is(policy.userNotifiedOfCurrentPolicy, false, "User not notified about datareporting policy.");
+      waitForNotificationClose(notification.currentNotification, function onClose() {
+        is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE, "Closing info bar completes user notification.");
+        ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
+        is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-dismissed",
+           "Reason for acceptance was info bar dismissal.");
+        is(notification.allNotifications.length, 0, "No notifications remain.");
+        test_multiple_windows();
+      });
+      notification.currentNotification.close();
+    });
+  }, true);
 
-    promise.then(function () {
-      is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Policy version set.");
-      is(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0, true, "Policy date set.");
-      is(policy.userNotifiedOfCurrentPolicy, true, "User notified about datareporting policy.");
-    }.bind(this), function (err) {
-      throw err;
-    });
-
-  }.bind(this), function onError (err) {
-    throw err;
-  });
+  policy = sendNotifyRequest("single_window_notified");
 }
 
 function test_multiple_windows() {
   // Ensure we see the notification on all windows and that action on one window
   // results in dismiss on every window.
   let window2 = OpenBrowserWindow();
   whenDelayedStartupFinished(window2, function onWindow() {
     let notification1 = document.getElementById("global-notificationbox");
     let notification2 = window2.document.getElementById("global-notificationbox");
     ok(notification2, "2nd window has a global notification box.");
 
-    let [policy, promise] = sendNotifyRequest("multiple_window_behavior");
+    let policy;
     let displayCount = 0;
     let prefWindowClosed = false;
     let mutationObserversRemoved = false;
 
     function onAlertDisplayed() {
       displayCount++;
 
       if (displayCount != 2) {
@@ -156,32 +124,36 @@ function test_multiple_windows() {
           dump("Not finishing test yet because mutation observers haven't been removed yet.\n");
           return;
         }
 
         window2.close();
 
         dump("Finishing multiple window test.\n");
         rootLogger.removeAppender(dumpAppender);
-        dumpAppender = null;
-        rootLogger = null;
+        delete dumpAppender;
+        delete rootLogger;
         finish();
       }
       let closeCount = 0;
 
       function onAlertClose() {
         closeCount++;
 
         if (closeCount != 2) {
           return;
         }
 
         ok(true, "Closing info bar on one window closed them on all.");
-        is(policy.userNotifiedOfCurrentPolicy, true, "Data submission policy accepted.");
 
+        is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE,
+           "Closing info bar with multiple windows completes notification.");
+        ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
+        is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-button-pressed",
+           "Policy records reason for acceptance was button press.");
         is(notification1.allNotifications.length, 0, "No notifications remain on main window.");
         is(notification2.allNotifications.length, 0, "No notifications remain on 2nd window.");
 
         mutationObserversRemoved = true;
         maybeFinish();
       }
 
       waitForNotificationClose(notification1.currentNotification, onAlertClose);
@@ -215,25 +187,12 @@ function test_multiple_windows() {
       executeSoon(onAlertDisplayed);
     }, true);
 
     notification2.addEventListener("AlertActive", function active2() {
       notification2.removeEventListener("AlertActive", active2, true);
       executeSoon(onAlertDisplayed);
     }, true);
 
-    promise.then(null, function onError(err) {
-      throw err;
-    });
+    policy = sendNotifyRequest("multiple_window_behavior");
   });
 }
 
-function cleanup () {
-  // In case some test fails.
-  if (originalPolicy) {
-    let service = Cc["@mozilla.org/datareporting/service;1"]
-                    .getService(Ci.nsISupports)
-                    .wrappedJSObject;
-    service.policy = originalPolicy;
-  }
-
-  return closeAllNotifications();
-}
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -2,36 +2,16 @@ Components.utils.import("resource://gre/
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 
-function closeAllNotifications () {
-  let notificationBox = document.getElementById("global-notificationbox");
-
-  if (!notificationBox || !notificationBox.currentNotification) {
-    return Promise.resolve();
-  }
-
-  let deferred = Promise.defer();
-  for (let notification of notificationBox.allNotifications) {
-    waitForNotificationClose(notification, function () {
-      if (notificationBox.allNotifications.length === 0) {
-        deferred.resolve();
-      }
-    });
-    notification.close();
-  }
-
-  return deferred.promise;
-}
-
 function whenDelayedStartupFinished(aWindow, aCallback) {
   Services.obs.addObserver(function observer(aSubject, aTopic) {
     if (aWindow == aSubject) {
       Services.obs.removeObserver(observer, aTopic);
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
--- a/browser/components/preferences/in-content/tests/browser_healthreport.js
+++ b/browser/components/preferences/in-content/tests/browser_healthreport.js
@@ -24,16 +24,17 @@ function runPaneTest(fn) {
 function test() {
   waitForExplicitFinish();
   resetPreferences();
   registerCleanupFunction(resetPreferences);
   runPaneTest(testBasic);
 }
 
 function testBasic(win, doc, policy) {
+  is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
   is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
 
   let checkbox = doc.getElementById("submitHealthReportBox");
   ok(checkbox);
   is(checkbox.checked, true, "Health Report checkbox is checked on app first run.");
 
   checkbox.checked = false;
   checkbox.doCommand();
--- a/browser/components/preferences/tests/browser_healthreport.js
+++ b/browser/components/preferences/tests/browser_healthreport.js
@@ -8,50 +8,29 @@ function runPaneTest(fn) {
     Services.obs.removeObserver(observer, "advanced-pane-loaded");
 
     let policy = Components.classes["@mozilla.org/datareporting/service;1"]
                                    .getService(Components.interfaces.nsISupports)
                                    .wrappedJSObject
                                    .policy;
     ok(policy, "Policy object defined");
 
-    resetPreferences();
-
     fn(win, policy);
   }
 
   Services.obs.addObserver(observer, "advanced-pane-loaded", false);
   openDialog("chrome://browser/content/preferences/preferences.xul", "Preferences",
              "chrome,titlebar,toolbar,centerscreen,dialog=no", "paneAdvanced");
 }
 
-let logDetails = {
-  dumpAppender: null,
-  rootLogger: null,
-};
-
 function test() {
   waitForExplicitFinish();
   resetPreferences();
   registerCleanupFunction(resetPreferences);
 
-  let ld = logDetails;
-  registerCleanupFunction(() => {
-    ld.rootLogger.removeAppender(ld.dumpAppender);
-    delete ld.dumpAppender;
-    delete ld.rootLogger;
-  });
-
-  let ns = {};
-  Cu.import("resource://gre/modules/Log.jsm", ns);
-  ld.rootLogger = ns.Log.repository.rootLogger;
-  ld.dumpAppender = new ns.Log.DumpAppender();
-  ld.dumpAppender.level = ns.Log.Level.All;
-  ld.rootLogger.addAppender(ld.dumpAppender);
-
   Services.prefs.lockPref("datareporting.healthreport.uploadEnabled");
   runPaneTest(testUploadDisabled);
 }
 
 function testUploadDisabled(win, policy) {
   ok(policy.healthReportUploadLocked, "Upload enabled flag is locked.");
   let checkbox = win.document.getElementById("submitHealthReportBox");
   is(checkbox.getAttribute("disabled"), "true", "Checkbox is disabled if upload setting is locked.");
@@ -59,18 +38,17 @@ function testUploadDisabled(win, policy)
 
   win.close();
   runPaneTest(testBasic);
 }
 
 function testBasic(win, policy) {
   let doc = win.document;
 
-  resetPreferences();
-
+  is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
   is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
 
   let checkbox = doc.getElementById("submitHealthReportBox");
   ok(checkbox);
   is(checkbox.checked, true, "Health Report checkbox is checked on app first run.");
 
   checkbox.checked = false;
   checkbox.doCommand();
@@ -80,15 +58,11 @@ function testBasic(win, policy) {
   checkbox.doCommand();
   is(policy.healthReportUploadEnabled, true, "Checking checkbox allows FHR upload.");
 
   win.close();
   finish();
 }
 
 function resetPreferences() {
-  let service = Cc["@mozilla.org/datareporting/service;1"]
-                  .getService(Ci.nsISupports)
-                  .wrappedJSObject;
-  service.policy._prefs.resetBranch("datareporting.policy.");
-  service.policy.dataSubmissionPolicyBypassNotification = true;
+  Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
 }
 
--- a/browser/components/search/test/browser_healthreport.js
+++ b/browser/components/search/test/browser_healthreport.js
@@ -1,17 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 function test() {
   requestLongerTimeout(2);
   waitForExplicitFinish();
-  resetPreferences();
 
   try {
     let cm = Components.classes["@mozilla.org/categorymanager;1"]
                        .getService(Components.interfaces.nsICategoryManager);
     cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
   } catch (ex) {
     // Health Report disabled, or no SearchesProvider.
     // We need a test or else we'll be marked as failure.
@@ -97,15 +96,8 @@ function test() {
   Services.obs.addObserver(observer, "browser-search-engine-modified", false);
   Services.search.addEngine("http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml",
                             Ci.nsISearchEngine.DATA_XML,
                             "data:image/x-icon,%00",
                             false);
 
 }
 
-function resetPreferences() {
-  let service = Components.classes["@mozilla.org/datareporting/service;1"]
-                                  .getService(Components.interfaces.nsISupports)
-                                  .wrappedJSObject;
-  service.policy._prefs.resetBranch("datareporting.policy.");
-  service.policy.dataSubmissionPolicyBypassNotification = true;
-}
\ No newline at end of file
--- a/services/datareporting/DataReportingService.js
+++ b/services/datareporting/DataReportingService.js
@@ -170,17 +170,16 @@ DataReportingService.prototype = Object.
             }
 
             // Side effect: instantiates the reporter instance if not already
             // accessed.
             //
             // The instance installs its own shutdown observers. So, we just
             // fire and forget: it will clean itself up.
             let reporter = this.healthReporter;
-            this.policy.ensureUserNotified();
           }.bind(this),
         }, delayInterval, this.timer.TYPE_ONE_SHOT);
 
         break;
 
       case "quit-application":
         this._os.removeObserver(this, "quit-application");
         this._quitting = true;
--- a/services/datareporting/datareporting-prefs.js
+++ b/services/datareporting/datareporting-prefs.js
@@ -1,12 +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/. */
 
 pref("datareporting.policy.dataSubmissionEnabled", true);
-pref("datareporting.policy.firstRunTime", "0");
+pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
+pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", false);
 pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
-pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0);
-pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false);
+pref("datareporting.policy.dataSubmissionPolicyResponseType", "");
+pref("datareporting.policy.dataSubmissionPolicyResponseTime", "0");
+pref("datareporting.policy.firstRunTime", "0");
+
 pref("datareporting.policy.currentPolicyVersion", 2);
 pref("datareporting.policy.minimumPolicyVersion", 1);
 pref("datareporting.policy.minimumPolicyVersion.channel-beta", 2);
+
--- a/services/datareporting/modules-testing/mocks.jsm
+++ b/services/datareporting/modules-testing/mocks.jsm
@@ -21,32 +21,27 @@ this.MockPolicyListener = function MockP
   this.requestRemoteDeleteCount = 0;
   this.lastRemoteDeleteRequest = null;
 
   this.notifyUserCount = 0;
   this.lastNotifyRequest = null;
 }
 
 MockPolicyListener.prototype = {
-  onRequestDataUpload: function (request) {
+  onRequestDataUpload: function onRequestDataUpload(request) {
     this._log.info("onRequestDataUpload invoked.");
     this.requestDataUploadCount++;
     this.lastDataRequest = request;
   },
 
-  onRequestRemoteDelete: function (request) {
+  onRequestRemoteDelete: function onRequestRemoteDelete(request) {
     this._log.info("onRequestRemoteDelete invoked.");
     this.requestRemoteDeleteCount++;
     this.lastRemoteDeleteRequest = request;
   },
 
-  onNotifyDataPolicy: function (request, rejectMessage=null) {
-    this._log.info("onNotifyDataPolicy invoked.");
+  onNotifyDataPolicy: function onNotifyDataPolicy(request) {
+    this._log.info("onNotifyUser invoked.");
     this.notifyUserCount++;
     this.lastNotifyRequest = request;
-    if (rejectMessage) {
-      request.onUserNotifyFailed(rejectMessage);
-    } else {
-      request.onUserNotifyComplete();
-    }
   },
 };
 
--- a/services/datareporting/policy.jsm
+++ b/services/datareporting/policy.jsm
@@ -1,88 +1,132 @@
 /* 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. Most of its content needs to be moved under
- * /services/healthreport.
+ * 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";
 
 #ifndef MERGED_COMPARTMENT
 
 this.EXPORTED_SYMBOLS = [
   "DataSubmissionRequest", // For test use only.
   "DataReportingPolicy",
-  "DATAREPORTING_POLICY_VERSION",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 #endif
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/UpdateChannel.jsm");
 
-// The current policy version number. If the version number stored in the prefs
-// is smaller than this, data upload will be disabled until the user is re-notified
-// about the policy changes.
-const DATAREPORTING_POLICY_VERSION = 1;
-
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 // Used as a sanity lower bound for dates stored in prefs. This module was
 // implemented in 2012, so any earlier dates indicate an incorrect clock.
 const OLDEST_ALLOWED_YEAR = 2012;
 
 /**
  * Represents a request to display data policy.
  *
+ * Instances of this are created when the policy is requesting the user's
+ * approval to agree to the data submission policy.
+ *
  * Receivers of these instances are expected to call one or more of the on*
  * functions when events occur.
  *
  * When one of these requests is received, the first thing a callee should do
  * is present notification to the user of the data policy. When the notice
  * is displayed to the user, the callee should call `onUserNotifyComplete`.
+ * 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`.
  *
- * 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
+ * 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
  *        (DataReportingPolicy) The policy instance this request came from.
  * @param deferred
  *        (deferred) The promise that will be fulfilled when display occurs.
  */
 function NotifyPolicyRequest(policy, deferred) {
   this.policy = policy;
   this.deferred = deferred;
 }
-NotifyPolicyRequest.prototype = Object.freeze({
+NotifyPolicyRequest.prototype = {
   /**
    * Called when the user is notified of the policy.
+   *
+   * This starts a countdown timer that will eventually signify implicit
+   * acceptance of the data policy.
    */
-  onUserNotifyComplete: function () {
-    return this.deferred.resolve();
-   },
+  onUserNotifyComplete: function onUserNotified() {
+    this.deferred.resolve();
+    return this.deferred.promise;
+  },
 
   /**
    * Called when there was an error notifying the user about the policy.
    *
    * @param error
    *        (Error) Explains what went wrong.
    */
-  onUserNotifyFailed: function (error) {
-    return this.deferred.reject(error);
+  onUserNotifyFailed: function onUserNotifyFailed(error) {
+    this.deferred.reject(error);
+  },
+
+  /**
+   * Called when the user agreed to the data policy.
+   *
+   * @param reason
+   *        (string) How the user agreed to the policy.
+   */
+  onUserAccept: function onUserAccept(reason) {
+    this.policy.recordUserAcceptance(reason);
   },
-});
+
+  /**
+   * Called when the user rejected the data policy.
+   *
+   * @param reason
+   *        (string) How the user rejected the policy.
+   */
+  onUserReject: function onUserReject(reason) {
+    this.policy.recordUserRejection(reason);
+  },
+};
+
+Object.freeze(NotifyPolicyRequest.prototype);
 
 /**
  * Represents a request to submit data.
  *
  * Instances of this are created when the policy requests data upload or
  * deletion.
  *
  * Receivers are expected to call one of the provided on* functions to signal
@@ -189,17 +233,19 @@ this.DataSubmissionRequest.prototype = O
  *  1. Do not submit data more than once every 24 hours.
  *  2. Try to submit as close to 24 hours apart as possible.
  *  3. Do not submit too soon after application startup so as to not negatively
  *     impact performance at startup.
  *  4. Before first ever data submission, the user should be notified about
  *     data collection practices.
  *  5. User should have opportunity to react to this notification before
  *     data submission.
- *  6. If data submission fails, try at most 2 additional times before giving
+ *  6. Display of notification without any explicit user action constitutes
+ *     implicit consent after a certain duration of time.
+ *  7. If data submission fails, try at most 2 additional times before giving
  *     up on that day's submission.
  *
  * The listener passed into the instance must have the following properties
  * (which are callbacks that will be invoked at certain key events):
  *
  *   * onRequestDataUpload(request) - Called when the policy is requesting
  *     data to be submitted. The function is passed a `DataSubmissionRequest`.
  *     The listener should call one of the special resolving functions on that
@@ -238,19 +284,16 @@ this.DataReportingPolicy = function (pre
       throw new Error("Passed listener does not contain required handler: " +
                       handler);
     }
   }
 
   this._prefs = prefs;
   this._healthReportPrefs = healthReportPrefs;
   this._listener = listener;
-  this._userNotifyPromise = null;
-
-  this._migratePrefs();
 
   // If the policy version has changed, reset all preferences, so that
   // the notification reappears.
   let acceptedVersion = this._prefs.get("dataSubmissionPolicyAcceptedVersion");
   if (typeof(acceptedVersion) == "number" &&
       acceptedVersion < this.minimumPolicyVersion) {
     this._log.info("policy version has changed - resetting all prefs");
     // We don't want to delay the notification in this case.
@@ -281,23 +324,41 @@ this.DataReportingPolicy = function (pre
 
   healthReportPrefs.observe("uploadEnabled", this.uploadEnabledObserver);
 
   // Ensure we are scheduled to submit.
   if (!this.nextDataSubmissionDate.getTime()) {
     this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
   }
 
+  // Date at which we performed user notification of acceptance.
+  // This is an instance variable because implicit acceptance should only
+  // 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;
 };
 
 this.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.
+   *
+   * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
+   * Privacy and/or Legal before modifying.
+   */
+  IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
+
+  /**
    *  How often to poll to see if we need to do something.
    *
    * The interval needs to be short enough such that short-lived applications
    * have an opportunity to submit data. But, it also needs to be long enough
    * to not negatively impact performance.
    *
    * The random bit is to ensure that other systems scheduling around the same
    * interval don't all get scheduled together.
@@ -327,16 +388,23 @@ this.DataReportingPolicy.prototype = Obj
    * we run out of values in this array, we give up on that day's submission
    * and schedule for a day out.
    */
   FAILURE_BACKOFF_INTERVALS: [
     15 * 60 * 1000,
     60 * 60 * 1000,
   ],
 
+  /**
+   * State of user notification of data submission.
+   */
+  STATE_NOTIFY_UNNOTIFIED: "not-notified",
+  STATE_NOTIFY_WAIT: "waiting",
+  STATE_NOTIFY_COMPLETE: "ok",
+
   REQUIRED_LISTENERS: [
     "onRequestDataUpload",
     "onRequestRemoteDelete",
     "onNotifyDataPolicy",
   ],
 
   /**
    * The first time the health report policy came into existence.
@@ -349,34 +417,84 @@ this.DataReportingPolicy.prototype = Obj
   },
 
   set firstRunDate(value) {
     this._log.debug("Setting first-run date: " + value);
     CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
                             OLDEST_ALLOWED_YEAR);
   },
 
+  /**
+   * Short circuit policy checking and always assume acceptance.
+   *
+   * This shuld never be set by the user. Instead, it is a per-application or
+   * per-deployment default pref.
+   */
+  get dataSubmissionPolicyBypassAcceptance() {
+    return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
+  },
+
+  /**
+   * When the user was notified that data submission could occur.
+   *
+   * This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
+   * is what's used internally.
+   */
   get dataSubmissionPolicyNotifiedDate() {
     return CommonUtils.getDatePref(this._prefs,
                                    "dataSubmissionPolicyNotifiedTime", 0,
                                    this._log, OLDEST_ALLOWED_YEAR);
   },
 
   set dataSubmissionPolicyNotifiedDate(value) {
     this._log.debug("Setting user notified date: " + value);
     CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
                             value, OLDEST_ALLOWED_YEAR);
   },
 
-  get dataSubmissionPolicyBypassNotification() {
-    return this._prefs.get("dataSubmissionPolicyBypassNotification", false);
+  /**
+   * When the user accepted or rejected the data submission policy.
+   *
+   * If there was implicit acceptance, this will be set to the time of that.
+   */
+  get dataSubmissionPolicyResponseDate() {
+    return CommonUtils.getDatePref(this._prefs,
+                                   "dataSubmissionPolicyResponseTime",
+                                   0, this._log, OLDEST_ALLOWED_YEAR);
+  },
+
+  set dataSubmissionPolicyResponseDate(value) {
+    this._log.debug("Setting user notified reaction date: " + value);
+    CommonUtils.setDatePref(this._prefs,
+                            "dataSubmissionPolicyResponseTime",
+                            value, OLDEST_ALLOWED_YEAR);
   },
 
-  set dataSubmissionPolicyBypassNotification(value) {
-    return this._prefs.set("dataSubmissionPolicyBypassNotification", !!value);
+  /**
+   * Records the result of user notification of data submission policy.
+   *
+   * This is used for logging and diagnostics purposes. It can answer the
+   * question "how was data submission agreed to on this profile?"
+   *
+   * Not all values are defined by this type and can come from other systems.
+   *
+   * The value must be a string and should be something machine readable. e.g.
+   * "accept-user-clicked-ok-button-in-info-bar"
+   */
+  get dataSubmissionPolicyResponseType() {
+    return this._prefs.get("dataSubmissionPolicyResponseType",
+                           "none-recorded");
+  },
+
+  set dataSubmissionPolicyResponseType(value) {
+    if (typeof(value) != "string") {
+      throw new Error("Value must be a string. Got " + typeof(value));
+    }
+
+    this._prefs.set("dataSubmissionPolicyResponseType", value);
   },
 
   /**
    * Whether submission of data is allowed.
    *
    * This is the master switch for remote server communication. If it is
    * false, we never request upload or deletion.
    */
@@ -384,47 +502,70 @@ this.DataReportingPolicy.prototype = Obj
     // Default is true because we are opt-out.
     return this._prefs.get("dataSubmissionEnabled", true);
   },
 
   set dataSubmissionEnabled(value) {
     this._prefs.set("dataSubmissionEnabled", !!value);
   },
 
-  get currentPolicyVersion() {
-    return this._prefs.get("currentPolicyVersion", DATAREPORTING_POLICY_VERSION);
-  },
-
   /**
    * The minimum policy version which for dataSubmissionPolicyAccepted to
    * to be valid.
    */
   get minimumPolicyVersion() {
     // First check if the current channel has an ove
     let channel = UpdateChannel.get(false);
     let channelPref = this._prefs.get("minimumPolicyVersion.channel-" + channel);
     return channelPref !== undefined ?
            channelPref : this._prefs.get("minimumPolicyVersion", 1);
   },
 
-  get dataSubmissionPolicyAcceptedVersion() {
-    return this._prefs.get("dataSubmissionPolicyAcceptedVersion", 0);
+  /**
+   * 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 dataSubmissionPolicyAcceptedVersion(value) {
-    this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
+  set dataSubmissionPolicyAccepted(value) {
+    this._prefs.set("dataSubmissionPolicyAccepted", !!value);
+    if (!!value) {
+      let currentPolicyVersion = this._prefs.get("currentPolicyVersion", 1);
+      this._prefs.set("dataSubmissionPolicyAcceptedVersion", currentPolicyVersion);
+    } else {
+      this._prefs.reset("dataSubmissionPolicyAcceptedVersion");
+    }
   },
 
   /**
-   * Checks to see if the user has been notified about data submission
-   * @return {bool}
+   * The state of user notification of the data policy.
+   *
+   * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
+   * submission can occur.
+   *
+   * @return DataReportingPolicy.STATE_NOTIFY_* constant.
    */
-  get userNotifiedOfCurrentPolicy() {
-    return  this.dataSubmissionPolicyNotifiedDate.getTime() > 0 &&
-            this.dataSubmissionPolicyAcceptedVersion >= this.currentPolicyVersion;
+  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
+    // a scenario where notification occurs just before application shutdown and
+    // notification is displayed for shorter than the policy requires.
+    if (!this._dataSubmissionPolicyNotifiedDate) {
+      return this.STATE_NOTIFY_UNNOTIFIED;
+    }
+
+    return this.STATE_NOTIFY_WAIT;
   },
 
   /**
    * 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.
    */
@@ -548,16 +689,53 @@ this.DataReportingPolicy.prototype = Obj
   /**
    * Whether the FHR upload enabled setting is locked and can't be changed.
    */
   get healthReportUploadLocked() {
     return this._healthReportPrefs.locked("uploadEnabled");
   },
 
   /**
+   * 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
+   * the promise passed to `onUserNotify` in the policy listener. But, it can
+   * be called through other interfaces at any time and the call will have
+   * an impact on future data submissions.
+   *
+   * @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;
+  },
+
+  /**
+   * 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
+   * the promise passed to `onUserNotify` in the policy listener. But, it can
+   * be called through other interfaces at any time and the call will have an
+   * impact on future data submissions.
+   */
+  recordUserRejection: function recordUserRejection(reason="no-reason") {
+    this._log.info("User rejected data submission policy: " + reason);
+    this.dataSubmissionPolicyResponseDate = this.now();
+    this.dataSubmissionPolicyResponseType = "rejected-" + reason;
+    this.dataSubmissionPolicyAccepted = false;
+  },
+
+  /**
    * Record the user's intent for whether FHR should upload data.
    *
    * This is the preferred way for XUL applications to record a user's
    * preference on whether Firefox Health Report should upload data to
    * a server.
    *
    * If upload is disabled through this API, a request for remote data
    * deletion is initiated automatically.
@@ -699,90 +877,116 @@ this.DataReportingPolicy.prototype = Obj
       return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
     }
 
     if (!this.healthReportUploadEnabled) {
       this._log.debug("Data upload is disabled. Doing nothing.");
       return;
     }
 
-    if (!this.ensureUserNotified()) {
-      this._log.warn("The user has not been notified about the data submission " +
-                     "policy. Not attempting upload.");
+    // If the user hasn't responded to the data policy, don't do anything.
+    if (!this.ensureNotifyResponse(now)) {
       return;
     }
 
-    // Data submission is allowed to occur. Now comes the scheduling part.
+    // User has opted out of data submission.
+    if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) {
+      this._log.debug("Data submission has been disabled per user request.");
+      return;
+    }
+
+    // User has responded to data policy and data submission is enabled. Now
+    // comes the scheduling part.
 
     if (nowT < nextSubmissionDate.getTime()) {
       this._log.debug("Next data submission is scheduled in the future: " +
                      nextSubmissionDate);
       return;
     }
 
     return this._dispatchSubmissionRequest("onRequestDataUpload", false);
   },
 
   /**
-   * Ensure that the data policy notification has been displayed.
+   * Ensure user has responded to data submission policy.
    *
    * This must be called before data submission. If the policy has not been
-   * displayed, data submission must not occur.
+   * responded to, data submission must not occur.
    *
-   * @return bool Whether the notification has been displayed.
+   * @return bool Whether user has responded to data policy.
    */
-  ensureUserNotified: function () {
-    if (this.userNotifiedOfCurrentPolicy || this.dataSubmissionPolicyBypassNotification) {
+  ensureNotifyResponse: function ensureNotifyResponse(now) {
+    if (this.dataSubmissionPolicyBypassAcceptance) {
       return true;
     }
 
-    // The user has not been notified yet, but is in the process of being notified.
-    if (this._userNotifyPromise) {
+    let notifyState = this.notifyState;
+
+    if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) {
+      let notifyAt = new Date(this.firstRunDate.getTime() +
+                              this.SUBMISSION_NOTIFY_INTERVAL_MSEC);
+
+      if (now.getTime() < notifyAt.getTime()) {
+        this._log.debug("Don't have to notify about data submission yet.");
+        return false;
+      }
+
+      let onComplete = function onComplete() {
+        this._log.info("Data submission notification presented.");
+        let now = this.now();
+
+        this._dataSubmissionPolicyNotifiedDate = now;
+        this.dataSubmissionPolicyNotifiedDate = now;
+      }.bind(this);
+
+      let deferred = Promise.defer();
+
+      deferred.promise.then(onComplete, (error) => {
+        this._log.warn("Data policy notification presentation failed: " +
+                       CommonUtils.exceptionStr(error));
+      });
+
+      this._log.info("Requesting display of data policy.");
+      let request = new NotifyPolicyRequest(this, deferred);
+
+      try {
+        this._listener.onNotifyDataPolicy(request);
+      } catch (ex) {
+        this._log.warn("Exception when calling onNotifyDataPolicy: " +
+                       CommonUtils.exceptionStr(ex));
+      }
       return false;
     }
 
-    let deferred = Promise.defer();
-    deferred.promise.then((function onSuccess() {
-      this._recordDataPolicyNotification(this.now(), this.currentPolicyVersion);
-      this._userNotifyPromise = null;
-    }).bind(this), ((error) => {
-      this._log.warn("Data policy notification presentation failed: " +
-                     CommonUtils.exceptionStr(error));
-      this._userNotifyPromise = null;
-    }).bind(this));
+    // We're waiting for user action or implicit acceptance after display.
+    if (notifyState == this.STATE_NOTIFY_WAIT) {
+      // Check for implicit acceptance.
+      let implicitAcceptance =
+        this._dataSubmissionPolicyNotifiedDate.getTime() +
+        this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC;
 
-    this._log.info("Requesting display of data policy.");
-    let request = new NotifyPolicyRequest(this, deferred);
-    try {
-      this._listener.onNotifyDataPolicy(request);
-    } catch (ex) {
-      this._log.warn("Exception when calling onNotifyDataPolicy: " +
-                     CommonUtils.exceptionStr(ex));
+      this._log.debug("Now: " + now.getTime());
+      this._log.debug("Will accept: " + implicitAcceptance);
+      if (now.getTime() < implicitAcceptance) {
+        this._log.debug("Still waiting for reaction or implicit acceptance. " +
+                        "Now: " + now.getTime() + " < " +
+                        "Accept: " + implicitAcceptance);
+        return false;
+      }
+
+      this.recordUserAcceptance("implicit-time-elapsed");
+      return true;
     }
 
-    this._userNotifyPromise = deferred.promise;
-
-    return false;
-  },
+    // If this happens, we have a coding error in this file.
+    if (notifyState != this.STATE_NOTIFY_COMPLETE) {
+      throw new Error("Unknown notification state: " + notifyState);
+    }
 
-  _recordDataPolicyNotification: function (date, version) {
-    this._log.debug("Recording data policy notification to version " + version +
-                  " on date " + date);
-    this.dataSubmissionPolicyNotifiedDate = date;
-    this.dataSubmissionPolicyAcceptedVersion = version;
-  },
-
-  _migratePrefs: function () {
-    // Current prefs are mostly the same than the old ones, except for some deprecated ones.
-    this._prefs.reset([
-      "dataSubmissionPolicyAccepted",
-      "dataSubmissionPolicyBypassAcceptance",
-      "dataSubmissionPolicyResponseType",
-      "dataSubmissionPolicyResponseTime"
-    ]);
+    return true;
   },
 
   _processInProgressSubmission: function _processInProgressSubmission() {
     if (!this._inProgressSubmissionRequest) {
       return false;
     }
 
     let now = this.now().getTime();
--- a/services/datareporting/tests/xpcshell/test_policy.js
+++ b/services/datareporting/tests/xpcshell/test_policy.js
@@ -4,17 +4,16 @@
 "use strict";
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
 Cu.import("resource://gre/modules/UpdateChannel.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
 
 function getPolicy(name,
                    aCurrentPolicyVersion = 1,
                    aMinimumPolicyVersion = 1,
                    aBranchMinimumVersionOverride) {
   let branch = "testing.datareporting." + name;
 
   // The version prefs should not be removed on reset, so set them in the
@@ -33,32 +32,16 @@ function getPolicy(name,
   let healthReportPrefs = new Preferences(branch + ".healthreport.");
 
   let listener = new MockPolicyListener();
   let policy = new DataReportingPolicy(policyPrefs, healthReportPrefs, listener);
 
   return [policy, policyPrefs, healthReportPrefs, listener];
 }
 
-/**
- * Ensure that the notification has been displayed to the user therefore having
- * policy.ensureUserNotified() === true, which will allow for a successful
- * data upload and afterwards does a call to policy.checkStateAndTrigger()
- * @param  {Policy} policy
- * @return {Promise}
- */
-function ensureUserNotifiedAndTrigger(policy) {
-  return Task.spawn(function* ensureUserNotifiedAndTrigger () {
-    policy.ensureUserNotified();
-    yield policy._listener.lastNotifyRequest.deferred.promise;
-    do_check_true(policy.userNotifiedOfCurrentPolicy);
-    policy.checkStateAndTrigger();
-  });
-}
-
 function defineNow(policy, now) {
   print("Adjusting fake system clock to " + now);
   Object.defineProperty(policy, "now", {
     value: function customNow() {
       return now;
     },
     writable: true,
   });
@@ -78,49 +61,54 @@ add_test(function test_constructor() {
   };
 
   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.dataSubmissionPolicyAcceptedVersion, 0);
-  do_check_false(policy.userNotifiedOfCurrentPolicy);
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
 
   run_next_test();
 });
 
 add_test(function test_prefs() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("prefs");
 
   let now = new Date();
   let nowT = now.getTime();
 
   policy.firstRunDate = now;
   do_check_eq(policyPrefs.get("firstRunTime"), nowT);
   do_check_eq(policy.firstRunDate.getTime(), nowT);
 
-  policy.dataSubmissionPolicyNotifiedDate = now;
+  policy.dataSubmissionPolicyNotifiedDate= now;
   do_check_eq(policyPrefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
-  do_check_neq(policy.dataSubmissionPolicyNotifiedDate, null);
   do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), nowT);
 
+  policy.dataSubmissionPolicyResponseDate = now;
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseTime"), nowT);
+  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), nowT);
+
+  policy.dataSubmissionPolicyResponseType = "type-1";
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseType"), "type-1");
+  do_check_eq(policy.dataSubmissionPolicyResponseType, "type-1");
+
   policy.dataSubmissionEnabled = false;
   do_check_false(policyPrefs.get("dataSubmissionEnabled", true));
   do_check_false(policy.dataSubmissionEnabled);
 
-  let new_version = DATAREPORTING_POLICY_VERSION + 1;
-  policy.dataSubmissionPolicyAcceptedVersion = new_version;
-  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), new_version);
+  policy.dataSubmissionPolicyAccepted = false;
+  do_check_false(policyPrefs.get("dataSubmissionPolicyAccepted", true));
+  do_check_false(policy.dataSubmissionPolicyAccepted);
 
-  do_check_false(policy.dataSubmissionPolicyBypassNotification);
-  policy.dataSubmissionPolicyBypassNotification = true;
-  do_check_true(policy.dataSubmissionPolicyBypassNotification);
-  do_check_true(policyPrefs.get("dataSubmissionPolicyBypassNotification"));
+  do_check_false(policy.dataSubmissionPolicyBypassAcceptance);
+  policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
+  do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
 
   policy.lastDataSubmissionRequestedDate = now;
   do_check_eq(hrPrefs.get("lastDataSubmissionRequestedTime"), nowT);
   do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), nowT);
 
   policy.lastDataSubmissionSuccessfulDate = now;
   do_check_eq(hrPrefs.get("lastDataSubmissionSuccessfulTime"), nowT);
   do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), nowT);
@@ -149,172 +137,261 @@ add_test(function test_prefs() {
   hrPrefs.lock("uploadEnabled");
   do_check_true(policy.healthReportUploadLocked);
   hrPrefs.unlock("uploadEnabled");
   do_check_false(policy.healthReportUploadLocked);
 
   run_next_test();
 });
 
-add_task(function test_migratePrefs () {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("migratePrefs");
-  let outdated_prefs = {
-    dataSubmissionPolicyAccepted: true,
-    dataSubmissionPolicyBypassAcceptance: true,
-    dataSubmissionPolicyResponseType: "something",
-    dataSubmissionPolicyResponseTime: Date.now() + "",
-  };
+add_test(function test_notify_state_prefs() {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notify_state_prefs");
+
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
 
-  // Test removal of old prefs.
-  for (let name in outdated_prefs) {
-    policyPrefs.set(name, outdated_prefs[name]);
-  }
-  policy._migratePrefs();
-  for (let name in outdated_prefs) {
-    do_check_false(policyPrefs.has(name));
-  }
+  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_task(function test_userNotifiedOfCurrentPolicy () {
+add_task(function test_initial_submission_notification() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("initial_submission_notification");
 
-  do_check_false(policy.userNotifiedOfCurrentPolicy,
-                 "The initial state should be unnotified.");
+  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.
+  defineNow(policy, new Date(policy.firstRunDate.getTime() +
+                             policy.SUBMISSION_NOTIFY_INTERVAL_MSEC - 1));
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.notifyUserCount, 0);
+  do_check_null(policy._dataSubmissionPolicyNotifiedDate);
   do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
 
-  policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION;
-  do_check_false(policy.userNotifiedOfCurrentPolicy,
-                 "The default state of the date should have a time of 0 and it should therefore fail");
-  do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0,
-              "Updating the accepted version should not set a notified date.");
+  // We have crossed the threshold. We should see notification.
+  defineNow(policy, new Date(policy.firstRunDate.getTime() +
+                             policy.SUBMISSION_NOTIFY_INTERVAL_MSEC));
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.notifyUserCount, 1);
+  yield listener.lastNotifyRequest.onUserNotifyComplete();
+  do_check_true(policy._dataSubmissionPolicyNotifiedDate instanceof Date);
+  do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
+  do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(),
+              policy._dataSubmissionPolicyNotifiedDate.getTime());
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
+});
 
-  policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
-  do_check_true(policy.userNotifiedOfCurrentPolicy,
-                "Using the proper API causes user notification to report as true.");
+add_test(function test_bypass_acceptance() {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("bypass_acceptance");
+
+  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();
+});
 
-  // It is assumed that later versions of the policy will incorporate previous
-  // ones, therefore this should also return true.
-  policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
-  policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION + 1;
-  do_check_true(policy.userNotifiedOfCurrentPolicy, 'A future version of the policy should pass.');
+add_task(function test_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);
+  yield listener.lastNotifyRequest.onUserNotifyComplete();
+  do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
 
-  policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
-  policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION - 1;
-  do_check_false(policy.userNotifiedOfCurrentPolicy, 'A previous version of the policy should fail.');
+  do_check_true(5000 < policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC);
+  defineNow(policy, new Date(now.getTime() + 5000));
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.notifyUserCount, 1);
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
+  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), 0);
+  do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
+
+  defineNow(policy, new Date(now.getTime() + policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + 1));
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.notifyUserCount, 1);
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
+  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), policy.now().getTime());
+  do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-implicit-time-elapsed");
 });
 
-add_task(function* test_notification_displayed () {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accept_displayed");
+add_task(function test_notification_rejected() {
+  // User notification failed. We should not record it as being presented.
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_failed");
 
-  do_check_eq(listener.requestDataUploadCount, 0);
-  do_check_eq(listener.notifyUserCount, 0);
-  do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
-
-  // Uploads will trigger user notifications as needed.
+  let now = new Date(policy.nextDataSubmissionDate.getTime() -
+                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
+  defineNow(policy, now);
   policy.checkStateAndTrigger();
   do_check_eq(listener.notifyUserCount, 1);
-  do_check_eq(listener.requestDataUploadCount, 0);
+  yield 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);
+});
 
-  yield ensureUserNotifiedAndTrigger(policy);
+add_task(function test_notification_accepted() {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accepted");
 
-  do_check_eq(listener.notifyUserCount, 1);
-  do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
-  do_check_true(policy.userNotifiedOfCurrentPolicy);
+  let now = new Date(policy.nextDataSubmissionDate.getTime() -
+                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
+  defineNow(policy, now);
+  policy.checkStateAndTrigger();
+  yield listener.lastNotifyRequest.onUserNotifyComplete();
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
+  do_check_false(policy.dataSubmissionPolicyAccepted);
+  listener.lastNotifyRequest.onUserNotifyComplete();
+  listener.lastNotifyRequest.onUserAccept("foo-bar");
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
+  do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-foo-bar");
+  do_check_true(policy.dataSubmissionPolicyAccepted);
+  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), now.getTime());
 });
 
-add_task(function* test_submission_kill_switch() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
-  policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
+add_task(function test_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();
+  yield listener.lastNotifyRequest.onUserNotifyComplete();
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
+  do_check_false(policy.dataSubmissionPolicyAccepted);
+  listener.lastNotifyRequest.onUserReject();
+  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
+  do_check_eq(policy.dataSubmissionPolicyResponseType, "rejected-no-reason");
+  do_check_false(policy.dataSubmissionPolicyAccepted);
+
+  // No requests for submission should occur if user has rejected.
+  defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime() + 10000));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
-  yield ensureUserNotifiedAndTrigger(policy);
+});
+
+add_test(function test_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_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
+  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_task(function* test_upload_kill_switch() {
-   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
+add_test(function test_upload_kill_switch() {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
 
-  yield ensureUserNotifiedAndTrigger(policy);
+  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
+  policy.recordUserAcceptance();
   defineNow(policy, policy.nextDataSubmissionDate);
 
   // So that we don't trigger deletions, which cause uploads to be delayed.
   hrPrefs.ignore("uploadEnabled", policy.uploadEnabledObserver);
 
   policy.healthReportUploadEnabled = false;
-  yield policy.checkStateAndTrigger();
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
   policy.healthReportUploadEnabled = true;
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
+
+  run_next_test();
 });
 
-add_task(function* test_data_submission_no_data() {
+add_test(function test_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);
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   listener.lastDataRequest.onNoDataAvailable();
 
   // The next trigger should try again.
   defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
- });
 
-add_task(function* test_data_submission_submit_failure_hard() {
+  run_next_test();
+});
+
+add_task(function test_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);
 
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   yield listener.lastDataRequest.onSubmissionFailureHard();
   do_check_eq(listener.lastDataRequest.state,
               listener.lastDataRequest.SUBMISSION_FAILURE_HARD);
 
   let expected = new Date(now.getTime() + 24 * 60 * 60 * 1000);
   do_check_eq(policy.nextDataSubmissionDate.getTime(), expected.getTime());
 
   defineNow(policy, new Date(now.getTime() + 10));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 });
 
-add_task(function* test_data_submission_submit_try_again() {
+add_task(function test_data_submission_submit_try_again() {
   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);
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   yield listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               nextDataSubmissionDate.getTime() + 15 * 60 * 1000);
 });
 
-add_task(function* test_submission_daily_scheduling() {
+add_task(function test_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);
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), now.getTime());
 
   let finishedDate = new Date(now.getTime() + 250);
   defineNow(policy, new Date(finishedDate.getTime() + 50));
   yield listener.lastDataRequest.onSubmissionSuccess(finishedDate);
   do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), finishedDate.getTime());
 
@@ -332,46 +409,51 @@ add_task(function* test_submission_daily
   defineNow(policy, nextScheduled);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
   yield listener.lastDataRequest.onSubmissionSuccess(new Date(nextScheduled.getTime() + 200));
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
     new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
 });
 
-add_task(function* test_submission_far_future_scheduling() {
+add_test(function test_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);
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.recordUserAcceptance();
+  now = new Date();
+  defineNow(policy, now);
 
   let nextDate = policy._futureDate(3 * 24 * 60 * 60 * 1000 - 1);
   policy.nextDataSubmissionDate = nextDate;
   policy.checkStateAndTrigger();
-  do_check_true(policy.dataSubmissionPolicyAcceptedVersion >= DATAREPORTING_POLICY_VERSION);
   do_check_eq(listener.requestDataUploadCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(), nextDate.getTime());
 
   policy.nextDataSubmissionDate = new Date(nextDate.getTime() + 1);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               policy._futureDate(24 * 60 * 60 * 1000).getTime());
+
+  run_next_test();
 });
 
-add_task(function* test_submission_backoff() {
+add_task(function test_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);
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   do_check_eq(policy.currentDaySubmissionFailureCount, 0);
 
   now = new Date(now.getTime() + 5000);
   defineNow(policy, now);
 
   // On first soft failure we should back off by scheduled interval.
   yield listener.lastDataRequest.onSubmissionFailureSoft();
@@ -412,36 +494,40 @@ add_task(function* test_submission_backo
   // On 3rd failure we should back off by a whole day.
   yield listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.currentDaySubmissionFailureCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               new Date(now.getTime() + 24 * 60 * 60 * 1000).getTime());
 });
 
 // Ensure that only one submission request can be active at a time.
-add_task(function* test_submission_expiring() {
+add_test(function test_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);
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   defineNow(policy, new Date(now.getTime() + 500));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 
   defineNow(policy, new Date(policy.now().getTime() +
                              policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC));
 
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
+
+  run_next_test();
 });
 
-add_task(function* test_delete_remote_data() {
+add_task(function test_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);
 
@@ -455,37 +541,43 @@ add_task(function* test_delete_remote_da
   do_check_true(listener.lastRemoteDeleteRequest.isDelete);
   defineNow(policy, policy._futureDate(1000));
 
   yield listener.lastRemoteDeleteRequest.onSubmissionSuccess(policy.now());
   do_check_false(policy.pendingDeleteRemoteData);
 });
 
 // Ensure that deletion requests take priority over regular data submission.
-add_task(function* test_delete_remote_data_priority() {
+add_test(function test_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));
 
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   policy._inProgressSubmissionRequest = null;
 
   policy.deleteRemoteData();
   policy.checkStateAndTrigger();
 
   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, 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();
 
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestRemoteDeleteCount, 1);
   defineNow(policy, policy._futureDate(1000));
   policy.checkStateAndTrigger();
@@ -503,22 +595,25 @@ add_test(function test_delete_remote_dat
   policy.checkStateAndTrigger();
   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_task(function* test_delete_remote_data_in_progress_upload() {
+add_task(function test_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);
 
-  yield ensureUserNotifiedAndTrigger(policy);
+  policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   defineNow(policy, policy._futureDate(50 * 1000));
 
   // If we request a delete during a pending request, nothing should be done.
   policy.deleteRemoteData();
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   do_check_eq(listener.requestRemoteDeleteCount, 0);
@@ -554,16 +649,17 @@ add_test(function test_polling() {
 
       print("Polled at " + now + " after " + after + "ms, intended " + intended);
       do_check_true(after >= acceptable);
       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();
       }
 
       // "Specified timer period will be at least the time between when
       // processing for last firing the callback completes and when the next
       // firing occurs."
@@ -571,17 +667,89 @@ add_test(function test_polling() {
       // That means we should set 'then' at the *end* of our handler, not
       // earlier.
       then = Date.now();
     }
   });
   policy.startPolling();
 });
 
-add_task(function* test_record_health_report_upload_enabled() {
+// 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, 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: 700,
+  });
+
+  let count = 0;
+
+  // Track JS elapsed time, so we can decide if we've waited for enough ticks.
+  let start;
+  Object.defineProperty(policy, "checkStateAndTrigger", {
+    value: function CheckStateAndTriggerProxy() {
+      count++;
+      let now = Date.now();
+      let delta = now - start;
+      print("checkStateAndTrigger count: " + count + ", now " + now +
+            ", delta " + delta);
+
+      // Account for some slack.
+      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.
+      //
+      // Note that, due to the inaccuracy of timers, 4 might not happen until 5
+      // firings have occurred. Yay. So we watch times, not just counts.
+
+      do_check_eq(listener.notifyUserCount, 1);
+
+      if (count == 1) {
+        listener.lastNotifyRequest.onUserNotifyComplete();
+      }
+
+      if (delta <= (policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + policy.POLL_INTERVAL_MSEC)) {
+        do_check_false(policy.dataSubmissionPolicyAccepted);
+        do_check_eq(listener.requestDataUploadCount, 0);
+      } else if (count > 3) {
+        do_check_true(policy.dataSubmissionPolicyAccepted);
+        do_check_eq(policy.dataSubmissionPolicyResponseType,
+                    "accepted-implicit-time-elapsed");
+        do_check_eq(listener.requestDataUploadCount, 1);
+      }
+
+      if ((count > 4) && policy.dataSubmissionPolicyAccepted) {
+        do_check_eq(listener.requestDataUploadCount, 1);
+        policy.stopPolling();
+        run_next_test();
+      }
+    }
+  });
+
+  policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);
+  policy.nextDataSubmissionDate = new Date(Date.now());
+  start = Date.now();
+  policy.startPolling();
+});
+
+add_task(function test_record_health_report_upload_enabled() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("record_health_report_upload_enabled");
 
   // Preconditions.
   do_check_false(policy.pendingDeleteRemoteData);
   do_check_true(policy.healthReportUploadEnabled);
   do_check_eq(listener.requestRemoteDeleteCount, 0);
 
   // User intent to disable should immediately result in a pending
@@ -618,72 +786,78 @@ add_test(function test_pref_change_initi
       do_check_false(policy.pendingDeleteRemoteData);     // Just called.
 
       run_next_test();
     },
   });
 
   hrPrefs.set("uploadEnabled", false);
 });
-
+ 
 add_task(function* test_policy_version() {
   let policy, policyPrefs, hrPrefs, listener, now, firstRunTime;
-  function createPolicy(shouldBeNotified = false,
+  function createPolicy(shouldBeAccepted = false,
                         currentPolicyVersion = 1, minimumPolicyVersion = 1,
                         branchMinimumVersionOverride) {
     [policy, policyPrefs, hrPrefs, listener] =
       getPolicy("policy_version_test", currentPolicyVersion,
                 minimumPolicyVersion, branchMinimumVersionOverride);
     let firstRun = now === undefined;
     if (firstRun) {
       firstRunTime = policy.firstRunDate.getTime();
       do_check_true(firstRunTime > 0);
-      now = new Date(policy.firstRunDate.getTime());
+      now = new Date(policy.firstRunDate.getTime() +
+                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC);
     }
     else {
       // The first-run time should not be reset even after policy-version
       // upgrades.
       do_check_eq(policy.firstRunDate.getTime(), firstRunTime);
     }
     defineNow(policy, now);
-    do_check_eq(policy.userNotifiedOfCurrentPolicy, shouldBeNotified);
+    do_check_eq(policy.dataSubmissionPolicyAccepted, shouldBeAccepted);
   }
 
-  function* triggerPolicyCheckAndEnsureNotified(notified = true) {
+  function* triggerPolicyCheckAndEnsureNotified(notified = true, accept = true) {
     policy.checkStateAndTrigger();
     do_check_eq(listener.notifyUserCount, Number(notified));
     if (notified) {
-      policy.ensureUserNotified();
-      yield listener.lastNotifyRequest.deferred.promise;
-      do_check_true(policy.userNotifiedOfCurrentPolicy);
-      do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
-                  policyPrefs.get("currentPolicyVersion"));
+      yield listener.lastNotifyRequest.onUserNotifyComplete();
+      if (accept) {
+        listener.lastNotifyRequest.onUserAccept("because,");
+        do_check_true(policy.dataSubmissionPolicyAccepted);
+        do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
+                    policyPrefs.get("currentPolicyVersion"));
+      }
+      else {
+        do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
+      }
     }
   }
 
   createPolicy();
   yield triggerPolicyCheckAndEnsureNotified();
 
   // We shouldn't be notified again if the current version is still valid;
   createPolicy(true);
   yield triggerPolicyCheckAndEnsureNotified(false);
 
   // Just increasing the current version isn't enough. The minimum
   // version must be changed.
   let currentPolicyVersion = policyPrefs.get("currentPolicyVersion");
   let minimumPolicyVersion = policyPrefs.get("minimumPolicyVersion");
-  createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion);
-  yield triggerPolicyCheckAndEnsureNotified(true);
-  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), currentPolicyVersion);
+  createPolicy(true, ++currentPolicyVersion, minimumPolicyVersion);
+  yield triggerPolicyCheckAndEnsureNotified(false);
+  do_check_true(policy.dataSubmissionPolicyAccepted);
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
+              minimumPolicyVersion);
 
   // Increase the minimum policy version and check if we're notified.
-
-  createPolicy(true, currentPolicyVersion, ++minimumPolicyVersion);
-  do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
-  yield triggerPolicyCheckAndEnsureNotified(false);
-
+  createPolicy(false, currentPolicyVersion, ++minimumPolicyVersion);
+  do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
+  yield triggerPolicyCheckAndEnsureNotified();
 
   // Test increasing the minimum version just on the current channel.
   createPolicy(true, currentPolicyVersion, minimumPolicyVersion);
   yield triggerPolicyCheckAndEnsureNotified(false);
   createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion, minimumPolicyVersion + 1);
   yield triggerPolicyCheckAndEnsureNotified(true);
 });
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -1255,18 +1255,18 @@ this.HealthReporter.prototype = Object.f
 
     this._prefs.set("documentServerNamespace", value);
   },
 
   /**
    * Whether this instance will upload data to a server.
    */
   get willUploadData() {
-    return  this._policy.userNotifiedOfCurrentPolicy &&
-            this._policy.healthReportUploadEnabled;
+    return this._policy.dataSubmissionPolicyAccepted &&
+           this._policy.healthReportUploadEnabled;
   },
 
   /**
    * Whether remote data is currently stored.
    *
    * @return bool
    */
   haveRemoteData: function () {
@@ -1316,32 +1316,32 @@ this.HealthReporter.prototype = Object.f
 
   /**
    * Override default handler to incur an upload describing the error.
    */
   _onInitError: function (error) {
     // Need to capture this before we call the parent else it's always
     // set.
     let inShutdown = this._shutdownRequested;
+
     let result;
-
     try {
       result = AbstractHealthReporter.prototype._onInitError.call(this, error);
     } catch (ex) {
       this._log.error("Error when calling _onInitError: " +
                       CommonUtils.exceptionStr(ex));
     }
 
     // This bypasses a lot of the checks in policy, such as respect for
     // backoff. We should arguably not do this. However, reporting
     // startup errors is important. And, they should not occur with much
     // frequency in the wild. So, it shouldn't be too big of a deal.
     if (!inShutdown &&
-        this._policy.healthReportUploadEnabled &&
-        this._policy.ensureUserNotified()) {
+        this._policy.ensureNotifyResponse(new Date()) &&
+        this._policy.healthReportUploadEnabled) {
       // We don't care about what happens to this request. It's best
       // effort.
       let request = {
         onNoDataAvailable: function () {},
         onSubmissionSuccess: function () {},
         onSubmissionFailureSoft: function () {},
         onSubmissionFailureHard: function () {},
         onUploadInProgress: function () {},
--- a/services/healthreport/modules-testing/utils.jsm
+++ b/services/healthreport/modules-testing/utils.jsm
@@ -19,17 +19,16 @@ Cu.import("resource://gre/modules/Prefer
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services-common/utils.js");
 Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
-Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
 
 
 let APP_INFO = {
   vendor: "Mozilla",
   name: "xpcshell",
   ID: "xpcshell@tests.mozilla.org",
   version: "1",
   appBuildID: "20121107",
@@ -187,24 +186,26 @@ this.getHealthReporter = function (name,
 
   let prefs = new Preferences(branch + "healthreport.");
   prefs.set("documentServerURI", uri);
   prefs.set("dbName", name);
 
   let reporter;
 
   let policyPrefs = new Preferences(branch + "policy.");
-  let listener = new MockPolicyListener();
-  listener.onRequestDataUpload = function (request) {
-    reporter.requestDataUpload(request);
-    MockPolicyListener.prototype.onRequestDataUpload.call(this, request);
-  }
-  listener.onRequestRemoteDelete = function (request) {
-    reporter.deleteRemoteData(request);
-    MockPolicyListener.prototype.onRequestRemoteDelete.call(this, request);
-  }
-  let policy = new DataReportingPolicy(policyPrefs, prefs, listener);
+  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;
   reporter = new type(branch + "healthreport.", policy, null,
                       "state-" + name + ".json");
 
   return reporter;
 };
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -87,31 +87,16 @@ function getHealthReportProviderValues(r
     let serializer = m.serializer(m.SERIALIZE_JSON)
     let json = serializer.daily(data.days.getDay(day));
     do_check_eq(json._v, 2);
 
     throw new Task.Result(json);
   });
 }
 
-/*
- * Ensure that the notification has been displayed to the user therefore having
- * reporter._policy.userNotifiedOfCurrentPolicy === true, which will allow for a
- * successful data upload.
- * @param  {HealthReporter} reporter
- * @return {Promise}
- */
-function ensureUserNotified (reporter) {
-  return Task.spawn(function* ensureUserNotified () {
-    reporter._policy.ensureUserNotified();
-    yield reporter._policy._listener.lastNotifyRequest.deferred.promise;
-    do_check_true(reporter._policy.userNotifiedOfCurrentPolicy);
-  });
-}
-
 function run_test() {
   run_next_test();
 }
 
 // run_test() needs to finish synchronously, so we do async init here.
 add_task(function test_init() {
   yield makeFakeAppDir();
 });
@@ -683,18 +668,19 @@ add_task(function test_data_submission_s
 
 add_task(function test_recurring_daily_pings() {
   let [reporter, server] = yield getReporterAndServer("recurring_daily_pings");
   try {
     reporter._providerManager.registerProvider(new DummyProvider());
 
     let policy = reporter._policy;
 
+    defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000));
+    policy.recordUserAcceptance();
     defineNow(policy, policy.nextDataSubmissionDate);
-    yield ensureUserNotified(reporter);
     let promise = policy.checkStateAndTrigger();
     do_check_neq(promise, null);
     yield promise;
 
     let lastID = reporter.lastSubmitID;
     do_check_neq(lastID, null);
     do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
 
@@ -721,18 +707,18 @@ add_task(function test_recurring_daily_p
 });
 
 add_task(function test_request_remote_data_deletion() {
   let [reporter, server] = yield getReporterAndServer("request_remote_data_deletion");
 
   try {
     let policy = reporter._policy;
     defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
+    policy.recordUserAcceptance();
     defineNow(policy, policy.nextDataSubmissionDate);
-    yield ensureUserNotified(reporter);
     yield policy.checkStateAndTrigger();
     let id = reporter.lastSubmitID;
     do_check_neq(id, null);
     do_check_true(server.hasDocument(reporter.serverNamespace, id));
 
     let clientID = reporter._state.clientID;
     do_check_neq(clientID, null);
 
@@ -809,22 +795,26 @@ add_task(function test_multiple_simultan
 });
 
 add_task(function test_policy_accept_reject() {
   let [reporter, server] = yield getReporterAndServer("policy_accept_reject");
 
   try {
     let policy = reporter._policy;
 
-    do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
-    do_check_true(policy.dataSubmissionPolicyAcceptedVersion < DATAREPORTING_POLICY_VERSION);
+    do_check_false(policy.dataSubmissionPolicyAccepted);
     do_check_false(reporter.willUploadData);
 
-    yield ensureUserNotified(reporter);
+    policy.recordUserAcceptance();
+    do_check_true(policy.dataSubmissionPolicyAccepted);
     do_check_true(reporter.willUploadData);
+
+    policy.recordUserRejection();
+    do_check_false(policy.dataSubmissionPolicyAccepted);
+    do_check_false(reporter.willUploadData);
   } finally {
     yield reporter._shutdown();
     yield shutdownServer(server);
   }
 });
 
 add_task(function test_error_message_scrubbing() {
   let reporter = yield getReporter("error_message_scrubbing");
@@ -945,19 +935,19 @@ add_task(function test_upload_on_init_fa
       do_check_true(result.transportSuccess);
       do_check_true(result.serverSuccess);
 
       oldOnResult.call(reporter, request, isDelete, new Date(), result);
       deferred.resolve();
     },
   });
 
+  reporter._policy.recordUserAcceptance();
   let error = false;
   try {
-    yield ensureUserNotified(reporter);
     yield reporter.init();
   } catch (ex) {
     error = true;
   } finally {
     do_check_true(error);
   }
 
   // At this point the emergency upload should have been initiated. We
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -112,20 +112,18 @@ user_pref("security.turn_off_all_securit
 // to run our XBL tests in automation, in which case we really want to be testing
 // the configuration that we ship to users without special whitelisting. So we
 // use an additional pref here to allow automation to use the "normal" behavior.
 user_pref("dom.use_xbl_scopes_for_remote_xul", true);
 
 // Get network events.
 user_pref("network.activity.blipIntervalMilliseconds", 250);
 
-// We do not wish to display datareporting policy notifications as it might
-// cause other tests to fail. Tests that wish to test the notification functionality
-// should explicitly disable this pref.
-user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
+// Don't allow the Data Reporting service to prompt for policy acceptance.
+user_pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", true);
 
 // Point Firefox Health Report at a local server. We don't care if it actually
 // works. It just can't hit the default production endpoint.
 user_pref("datareporting.healthreport.documentServerURI", "http://%(server)s/healthreport/");
 user_pref("datareporting.healthreport.about.reportUrl", "http://%(server)s/abouthealthreport/");
 
 // Make sure CSS error reporting is enabled for tests
 user_pref("layout.css.report_errors", true);