Bug 810132 - Add remote deletion requests to policy; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Fri, 09 Nov 2012 13:59:40 -0800
changeset 113828 525e8539150a138f18783e461b22cfebc2bb8582
parent 113827 6b2eb103766a470894c40f432acd567ee9f0a884
child 113829 6f544baffff0389ece855da3fa9cdc45eea922e6
push id23890
push userryanvm@gmail.com
push dateWed, 21 Nov 2012 02:43:32 +0000
treeherdermozilla-central@4f19e7fd8bea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs810132
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 810132 - Add remote deletion requests to policy; r=rnewman
services/healthreport/modules-testing/mocks.jsm
services/healthreport/policy.jsm
services/healthreport/tests/xpcshell/test_policy.js
--- a/services/healthreport/modules-testing/mocks.jsm
+++ b/services/healthreport/modules-testing/mocks.jsm
@@ -10,28 +10,37 @@ const {utils: Cu} = Components;
 
 Cu.import("resource://services-common/log4moz.js");
 
 
 this.MockPolicyListener = function MockPolicyListener() {
   this._log = Log4Moz.repository.getLogger("HealthReport.Testing.MockPolicyListener");
   this._log.level = Log4Moz.Level["Debug"];
 
-  this.requestDataSubmissionCount = 0;
+  this.requestDataUploadCount = 0;
   this.lastDataRequest = null;
 
+  this.requestRemoteDeleteCount = 0;
+  this.lastRemoteDeleteRequest = null;
+
   this.notifyUserCount = 0;
   this.lastNotifyRequest = null;
 }
 
 MockPolicyListener.prototype = {
-  onRequestDataSubmission: function onRequestDataSubmission(request) {
-    this._log.info("onRequestDataSubmission invoked.");
-    this.requestDataSubmissionCount++;
+  onRequestDataUpload: function onRequestDataUpload(request) {
+    this._log.info("onRequestDataUpload invoked.");
+    this.requestDataUploadCount++;
     this.lastDataRequest = request;
   },
 
+  onRequestRemoteDelete: function onRequestRemoteDelete(request) {
+    this._log.info("onRequestRemoteDelete invoked.");
+    this.requestRemoteDeleteCount++;
+    this.lastRemoteDeleteRequest = request;
+  },
+
   onNotifyDataPolicy: function onNotifyDataPolicy(request) {
     this._log.info("onNotifyUser invoked.");
     this.notifyUserCount++;
     this.lastNotifyRequest = request;
   },
 };
--- a/services/healthreport/policy.jsm
+++ b/services/healthreport/policy.jsm
@@ -102,49 +102,59 @@ NotifyPolicyRequest.prototype = {
   },
 };
 
 Object.freeze(NotifyPolicyRequest.prototype);
 
 /**
  * Represents a request to submit data.
  *
- * Instances of this are created when the policy requests data submission.
+ * 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
  * completion of the request.
  *
  * Instances of this type should not be instantiated outside of this file.
  * Receivers of instances of this type should not attempt to do anything with
  * the instance except call one of the on* methods.
  */
-function DataSubmissionRequest(promise, expiresDate) {
+function DataSubmissionRequest(promise, expiresDate, isDelete) {
   this.promise = promise;
   this.expiresDate = expiresDate;
+  this.isDelete = isDelete;
 
   this.state = null;
   this.reason = null;
 }
 
 DataSubmissionRequest.prototype = {
   NO_DATA_AVAILABLE: "no-data-available",
   SUBMISSION_SUCCESS: "success",
   SUBMISSION_FAILURE_SOFT: "failure-soft",
   SUBMISSION_FAILURE_HARD: "failure-hard",
 
   /**
    * No submission was attempted because no data was available.
+   *
+   * In the case of upload, this means there is no data to upload (perhaps
+   * it isn't available yet). In case of remote deletion, it means that there
+   * is no remote data to delete.
    */
   onNoDataAvailable: function onNoDataAvailable() {
     this.state = this.NO_DATA_AVAILABLE;
     this.promise.resolve(this);
   },
 
   /**
    * Data submission has completed successfully.
    *
+   * In case of upload, this means the upload completed successfully. In case
+   * of deletion, the data was deleted successfully.
+   *
    * @param date
    *        (Date) When data submission occurred.
    */
   onSubmissionSuccess: function onSubmissionSuccess(date) {
     this.state = this.SUBMISSION_SUCCESS;
     this.submissionDate = date;
     this.promise.resolve(this);
   },
@@ -199,21 +209,26 @@ Object.freeze(DataSubmissionRequest.prot
  *  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):
  *
- *   * onRequestDataSubmission(request) - Called when the policy is requesting
+ *   * 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
  *     instance (see the documentation for that type).
  *
+ *   * onRequestRemoteDelete(request) - Called when the policy is requesting
+ *     deletion of remotely stored data. The function is passed a
+ *     `DataSubmissionRequest`. The listener should call one of the special
+ *     resolving functions on that instance (just like `onRequestDataUpload`).
+ *
  *   * onNotifyDataPolicy(request) - Called when the policy is requesting the
  *     user to be notified that data submission will occur. The function
  *     receives a `NotifyPolicyRequest` instance. The callee should call one or
  *     more of the functions on that instance when specific events occur. See
  *     the documentation for that type for more.
  *
  * Note that the notification method is abstracted. Different applications
  * can have different mechanisms by which they notify the user of data
@@ -316,17 +331,21 @@ HealthReportPolicy.prototype = {
 
   /**
    * State of user notification of data submission.
    */
   STATE_NOTIFY_UNNOTIFIED: "not-notified",
   STATE_NOTIFY_WAIT: "waiting",
   STATE_NOTIFY_COMPLETE: "ok",
 
-  REQUIRED_LISTENERS: ["onRequestDataSubmission", "onNotifyDataPolicy"],
+  REQUIRED_LISTENERS: [
+    "onRequestDataUpload",
+    "onRequestRemoteDelete",
+    "onNotifyDataPolicy",
+  ],
 
   /**
    * The first time the health report policy came into existence.
    *
    * This is used for scheduling of the initial submission.
    */
   get firstRunDate() {
     return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log,
@@ -397,29 +416,45 @@ HealthReportPolicy.prototype = {
     }
 
     this._prefs.set("dataSubmissionPolicyResponseType", value);
   },
 
   /**
    * Whether submission of data is allowed.
    *
-   * This is the master switch for data submission. If it is off, we will
-   * never submit data, even if the user has agreed to it.
+   * This is the master switch for remote server communication. If it is
+   * false, we never request upload or deletion.
    */
   get dataSubmissionEnabled() {
     // Default is true because we are opt-out.
     return this._prefs.get("dataSubmissionEnabled", true);
   },
 
   set dataSubmissionEnabled(value) {
     this._prefs.set("dataSubmissionEnabled", !!value);
   },
 
   /**
+   * Whether upload of data is allowed.
+   *
+   * This is a kill switch for upload. It is meant to reflect a system or
+   * deployment policy decision. User intent should be reflected in the
+   * "dataSubmissionPolicy" prefs.
+   */
+  get dataUploadEnabled() {
+    // Default is true because we are opt-out.
+    return this._prefs.get("dataUploadEnabled", true);
+  },
+
+  set dataUploadEnabled(value) {
+    this._prefs.set("dataUploadEnabled", !!value);
+  },
+
+  /**
    * Whether the user has accepted that data submission can occur.
    *
    * This overrides dataSubmissionEnabled.
    */
   get dataSubmissionPolicyAccepted() {
     // Be conservative and default to false.
     return this._prefs.get("dataSubmissionPolicyAccepted", false);
   },
@@ -537,16 +572,31 @@ HealthReportPolicy.prototype = {
     if (!Number.isInteger(value)) {
       throw new Error("Value must be integer: " + value);
     }
 
     this._prefs.set("currentDaySubmissionFailureCount", value);
   },
 
   /**
+   * Whether a request to delete remote data is awaiting completion.
+   *
+   * If this is true, the policy will request that remote data be deleted.
+   * Furthermore, no new data will be uploaded (if it's even allowed) until
+   * the remote deletion is fulfilled.
+   */
+  get pendingDeleteRemoteData() {
+    return !!this._prefs.get("pendingDeleteRemoteData", false);
+  },
+
+  set pendingDeleteRemoteData(value) {
+    this._prefs.set("pendingDeleteRemoteData", !!value);
+  },
+
+  /**
    * Record user acceptance of data submission policy.
    *
    * Data submission will not be allowed to occur until this is called.
    *
    * This is typically called through the `onUserAccept` property attached to
    * 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.
@@ -574,16 +624,35 @@ HealthReportPolicy.prototype = {
   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;
   },
 
   /**
+   * Request that remote data be deleted.
+   *
+   * This will record an intent that previously uploaded data is to be deleted.
+   * The policy will eventually issue a request to the listener for data
+   * deletion. It will keep asking for deletion until the listener acknowledges
+   * that data has been deleted.
+   */
+  deleteRemoteData: function deleteRemoteData(reason="no-reason") {
+    this._log.info("Remote data deletion requested: " + reason);
+
+    this.pendingDeleteRemoteData = true;
+
+    // We want delete deletion to occur as soon as possible. Move up any
+    // pending scheduled data submission and try to trigger.
+    this.nextDataSubmissionDate = this.now();
+    this.checkStateAndTrigger();
+  },
+
+  /**
    * Start background polling for activity.
    *
    * This will set up a recurring timer that will periodically check if
    * activity is warranted.
    *
    * You typically call this function for each constructed instance.
    */
   startPolling: function startPolling() {
@@ -649,17 +718,39 @@ HealthReportPolicy.prototype = {
     if (nextSubmissionDate.getTime() >= nowT + 3 * MILLISECONDS_PER_DAY) {
       this._log.warn("Next data submission time is far away. Was the system " +
                      "clock recently readjusted? " + nextSubmissionDate);
 
       // It shouldn't really matter what we set this to. 1 day in the future
       // should be pretty safe.
       this._moveScheduleForward24h();
 
-      // Fall through and prompt for user notification, if necessary.
+      // Fall through since we may have other actions.
+    }
+
+    // Tend to any in progress work.
+    if (this._processInProgressSubmission()) {
+      return;
+    }
+
+    // Requests to delete remote data take priority above everything else.
+    if (this.pendingDeleteRemoteData) {
+      if (nowT < nextSubmissionDate.getTime()) {
+        this._log.debug("Deletion request is scheduled for the future: " +
+                        nextSubmissionDate);
+        return;
+      }
+
+      this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
+      return;
+    }
+
+    if (!this.dataUploadEnabled) {
+      this._log.debug("Data upload is disabled. Doing nothing.");
+      return;
     }
 
     // If the user hasn't responded to the data policy, don't do anything.
     if (!this.ensureNotifyResponse(now)) {
       return;
     }
 
     // User has opted out of data submission.
@@ -672,63 +763,17 @@ HealthReportPolicy.prototype = {
     // comes the scheduling part.
 
     if (nowT < nextSubmissionDate.getTime()) {
       this._log.debug("Next data submission is scheduled in the future: " +
                      nextSubmissionDate);
       return;
     }
 
-    if (this._inProgressSubmissionRequest) {
-      if (this._inProgressSubmissionRequest.expiresDate.getTime() > nowT) {
-        this._log.info("Waiting on in-progress submission request to finish.");
-        return;
-      }
-
-      this._log.warn("Old submission request has expired from no activity.");
-      this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired."));
-      this._inProgressSubmissionRequest = null;
-      if (!this._handleSubmissionFailure()) {
-        return;
-      }
-    }
-
-    // We're past our scheduled next data submission date, so let's do it!
-    this.lastDataSubmissionRequestedDate = now;
-    let deferred = Promise.defer();
-    let requestExpiresDate =
-      this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC);
-    this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred,
-                                                                  requestExpiresDate);
-
-    let onSuccess = function onSuccess(result) {
-      this._inProgressSubmissionRequest = null;
-      this._handleSubmissionResult(result);
-    }.bind(this);
-
-    let onError = function onError(error) {
-      this._log.error("Error when handling data submission result: " +
-                      CommonUtils.exceptionStr(result));
-      this._inProgressSubmissionRequest = null;
-      this._handleSubmissionFailure();
-    }.bind(this);
-
-    deferred.promise.then(onSuccess, onError);
-
-    this._log.info("Requesting data submission. Will expire at " +
-                   requestExpiresDate);
-    try {
-      this._listener.onRequestDataSubmission(this._inProgressSubmissionRequest);
-    } catch (ex) {
-      this._log.warn("Exception when calling onRequestDataSubmission: " +
-                     CommonUtils.exceptionStr(ex));
-      this._inProgressSubmissionRequest = null;
-      this._handleSubmissionFailure();
-      return;
-    }
+    this._dispatchSubmissionRequest("onRequestDataUpload", false);
   },
 
   /**
    * Ensure user has responded to data submission policy.
    *
    * This must be called before data submission. If the policy has not been
    * responded to, data submission must not occur.
    *
@@ -792,36 +837,119 @@ HealthReportPolicy.prototype = {
     // If this happens, we have a coding error in this file.
     if (notifyState != this.STATE_NOTIFY_COMPLETE) {
       throw new Error("Unknown notification state: " + notifyState);
     }
 
     return true;
   },
 
+  _processInProgressSubmission: function _processInProgressSubmission() {
+    if (!this._inProgressSubmissionRequest) {
+      return false;
+    }
+
+    let now = this.now().getTime();
+    if (this._inProgressSubmissionRequest.expiresDate.getTime() > now) {
+      this._log.info("Waiting on in-progress submission request to finish.");
+      return true;
+    }
+
+    this._log.warn("Old submission request has expired from no activity.");
+    this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired."));
+    this._inProgressSubmissionRequest = null;
+    this._handleSubmissionFailure();
+
+    return false;
+  },
+
+  _dispatchSubmissionRequest: function _dispatchSubmissionRequest(handler, isDelete) {
+    let now = this.now();
+
+    // We're past our scheduled next data submission date, so let's do it!
+    this.lastDataSubmissionRequestedDate = now;
+    let deferred = Promise.defer();
+    let requestExpiresDate =
+      this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC);
+    this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred,
+                                                                  requestExpiresDate,
+                                                                  isDelete);
+
+    let onSuccess = function onSuccess(result) {
+      this._inProgressSubmissionRequest = null;
+      this._handleSubmissionResult(result);
+    }.bind(this);
+
+    let onError = function onError(error) {
+      this._log.error("Error when handling data submission result: " +
+                      CommonUtils.exceptionStr(result));
+      this._inProgressSubmissionRequest = null;
+      this._handleSubmissionFailure();
+    }.bind(this);
+
+    deferred.promise.then(onSuccess, onError);
+
+    this._log.info("Requesting data submission. Will expire at " +
+                   requestExpiresDate);
+    try {
+      this._listener[handler](this._inProgressSubmissionRequest);
+    } catch (ex) {
+      this._log.warn("Exception when calling " + handler + ": " +
+                     CommonUtils.exceptionStr(ex));
+      this._inProgressSubmissionRequest = null;
+      this._handleSubmissionFailure();
+      return;
+    }
+  },
+
   _handleSubmissionResult: function _handleSubmissionResult(request) {
     let state = request.state;
     let reason = request.reason || "no reason";
     this._log.info("Got submission request result: " + state);
 
     if (state == request.SUBMISSION_SUCCESS) {
-      this._log.info("Successful data submission reported.");
+      if (request.isDelete) {
+        this.pendingDeleteRemoteData = false;
+        this._log.info("Successful data delete reported.");
+      } else {
+        this._log.info("Successful data upload reported.");
+      }
+
       this.lastDataSubmissionSuccessfulDate = request.submissionDate;
-      this.nextDataSubmissionDate =
+
+      let nextSubmissionDate =
         new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY);
+
+      // Schedule pending deletes immediately. This has potential to overload
+      // the server. However, the frequency of delete requests across all
+      // clients should be low, so this shouldn't pose a problem.
+      if (this.pendingDeleteRemoteData) {
+        nextSubmissionDate = this.now();
+      }
+
+      this.nextDataSubmissionDate = nextSubmissionDate;
       this.currentDaySubmissionFailureCount = 0;
       return;
     }
 
     if (state == request.NO_DATA_AVAILABLE) {
+      if (request.isDelete) {
+        this._log.info("Remote data delete requested but no remote data was stored.");
+        this.pendingDeleteRemoteData = false;
+        return;
+      }
+
       this._log.info("No data was available to submit. May try later.");
       this._handleSubmissionFailure();
       return;
     }
 
+    // We don't special case request.isDelete for these failures because it
+    // likely means there was a server error.
+
     if (state == request.SUBMISSION_FAILURE_SOFT) {
       this._log.warn("Soft error submitting data: " + reason);
       this.lastDataSubmissionFailureDate = this.now();
       this._handleSubmissionFailure();
       return;
     }
 
     if (state == request.SUBMISSION_FAILURE_HARD) {
@@ -857,8 +985,9 @@ HealthReportPolicy.prototype = {
   },
 
   _futureDate: function _futureDate(offset) {
     return new Date(this.now().getTime() + offset);
   },
 };
 
 Object.freeze(HealthReportPolicy.prototype);
+
--- a/services/healthreport/tests/xpcshell/test_policy.js
+++ b/services/healthreport/tests/xpcshell/test_policy.js
@@ -29,17 +29,18 @@ function defineNow(policy, now) {
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_constructor() {
   let prefs = new Preferences("foo.bar");
   let listener = {
-    onRequestDataSubmission: function() {},
+    onRequestDataUpload: function() {},
+    onRequestRemoteDelete: function() {},
     onNotifyDataPolicy: function() {},
   };
 
   let policy = new HealthReportPolicy(prefs, 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);
@@ -94,16 +95,20 @@ add_test(function test_prefs() {
   policy.nextDataSubmissionDate = now;
   do_check_eq(prefs.get("nextDataSubmissionTime"), nowT);
   do_check_eq(policy.nextDataSubmissionDate.getTime(), nowT);
 
   policy.currentDaySubmissionFailureCount = 2;
   do_check_eq(prefs.get("currentDaySubmissionFailureCount", 0), 2);
   do_check_eq(policy.currentDaySubmissionFailureCount, 2);
 
+  policy.pendingDeleteRemoteData = true;
+  do_check_true(prefs.get("pendingDeleteRemoteData"));
+  do_check_true(policy.pendingDeleteRemoteData);
+
   run_next_test();
 });
 
 add_test(function test_notify_state_prefs() {
   let [policy, prefs, listener] = getPolicy("notify_state_prefs");
 
   do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
 
@@ -228,80 +233,97 @@ add_test(function test_notification_reje
   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.requestDataSubmissionCount, 0);
+  do_check_eq(listener.requestDataUploadCount, 0);
 
   run_next_test();
 });
 
 add_test(function test_submission_kill_switch() {
   let [policy, prefs, 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");
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  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.requestDataSubmissionCount, 1);
+  do_check_eq(listener.requestDataUploadCount, 1);
+
+  run_next_test();
+});
+
+add_test(function test_upload_kill_switch() {
+  let [policy, prefs, listener] = getPolicy("upload_kill_switch");
+
+  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
+  policy.recordUserAcceptance();
+  defineNow(policy, policy.nextDataSubmissionDate);
+
+  policy.dataUploadEnabled = false;
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.requestDataUploadCount, 0);
+  policy.dataUploadEnabled = true;
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.requestDataUploadCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_data_submission_no_data() {
   let [policy, prefs, listener] = getPolicy("data_submission_no_data");
 
   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.requestDataSubmissionCount, 0);
+  do_check_eq(listener.requestDataUploadCount, 0);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  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.requestDataSubmissionCount, 2);
+  do_check_eq(listener.requestDataUploadCount, 2);
 
   run_next_test();
 });
 
 add_test(function test_data_submission_submit_failure_hard() {
   let [policy, prefs, listener] = getPolicy("data_submission_submit_failure_hard");
 
   policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmissionDate = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
   defineNow(policy, now);
 
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  do_check_eq(listener.requestDataUploadCount, 1);
   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.requestDataSubmissionCount, 1);
+  do_check_eq(listener.requestDataUploadCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_data_submission_submit_try_again() {
   let [policy, prefs, listener] = getPolicy("data_submission_failure_soft");
 
   policy.recordUserAcceptance();
@@ -322,38 +344,38 @@ add_test(function test_submission_daily_
   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);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  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));
   listener.lastDataRequest.onSubmissionSuccess(finishedDate);
   do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), finishedDate.getTime());
 
   // Next scheduled submission should be exactly 1 day after the reported
   // submission success.
 
   let nextScheduled = new Date(finishedDate.getTime() + 24 * 60 * 60 * 1000);
   do_check_eq(policy.nextDataSubmissionDate.getTime(), nextScheduled.getTime());
 
   // Fast forward some arbitrary time. We shouldn't do any work yet.
   defineNow(policy, new Date(now.getTime() + 40000));
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  do_check_eq(listener.requestDataUploadCount, 1);
 
   defineNow(policy, nextScheduled);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 2);
+  do_check_eq(listener.requestDataUploadCount, 2);
   listener.lastDataRequest.onSubmissionSuccess(new Date(nextScheduled.getTime() + 200));
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
     new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
 
   run_next_test();
 });
 
 add_test(function test_submission_far_future_scheduling() {
@@ -363,22 +385,22 @@ add_test(function test_submission_far_fu
   defineNow(policy, now);
   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_eq(listener.requestDataSubmissionCount, 0);
+  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.requestDataSubmissionCount, 0);
+  do_check_eq(listener.requestDataUploadCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               policy._futureDate(24 * 60 * 60 * 1000).getTime());
 
   run_next_test();
 });
 
 add_test(function test_submission_backoff() {
   let [policy, prefs, listener] = getPolicy("submission_backoff");
@@ -386,54 +408,54 @@ add_test(function test_submission_backof
   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);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  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.
   listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.currentDaySubmissionFailureCount, 1);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[0]).getTime());
   do_check_eq(policy.lastDataSubmissionFailureDate.getTime(), now.getTime());
 
   // Should not request submission until scheduled.
   now = new Date(policy.nextDataSubmissionDate.getTime() - 1);
   defineNow(policy, now);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  do_check_eq(listener.requestDataUploadCount, 1);
 
   // 2nd request for submission.
   now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 2);
+  do_check_eq(listener.requestDataUploadCount, 2);
 
   now = new Date(now.getTime() + 5000);
   defineNow(policy, now);
 
   // On second failure we should back off by more.
   listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.currentDaySubmissionFailureCount, 2);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[1]).getTime());
 
   now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 3);
+  do_check_eq(listener.requestDataUploadCount, 3);
 
   now = new Date(now.getTime() + 5000);
   defineNow(policy, now);
 
   // On 3rd failure we should back off by a whole day.
   listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.currentDaySubmissionFailureCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
@@ -447,26 +469,136 @@ add_test(function test_submission_expiri
   let [policy, prefs, listener] = getPolicy("submission_expiring");
 
   policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmission = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  do_check_eq(listener.requestDataUploadCount, 1);
   defineNow(policy, new Date(now.getTime() + 500));
   policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataSubmissionCount, 1);
+  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.requestDataSubmissionCount, 2);
+  do_check_eq(listener.requestDataUploadCount, 2);
+
+  run_next_test();
+});
+
+add_test(function test_delete_remote_data() {
+  let [policy, prefs, listener] = getPolicy("delete_remote_data");
+
+  do_check_false(policy.pendingDeleteRemoteData);
+  let nextSubmissionDate = policy.nextDataSubmissionDate;
+
+  let now = new Date();
+  defineNow(policy, now);
+
+  policy.deleteRemoteData();
+  do_check_true(policy.pendingDeleteRemoteData);
+  do_check_neq(nextSubmissionDate.getTime(),
+               policy.nextDataSubmissionDate.getTime());
+  do_check_eq(now.getTime(), policy.nextDataSubmissionDate.getTime());
+
+  do_check_eq(listener.requestRemoteDeleteCount, 1);
+  do_check_true(listener.lastRemoteDeleteRequest.isDelete);
+  defineNow(policy, policy._futureDate(1000));
+
+  listener.lastRemoteDeleteRequest.onSubmissionSuccess(policy.now());
+  do_check_false(policy.pendingDeleteRemoteData);
+
+  run_next_test();
+});
+
+// Ensure that deletion requests take priority over regular data submission.
+add_test(function test_delete_remote_data_priority() {
+  let [policy, prefs, listener] = getPolicy("delete_remote_data_priority");
+
+  let now = new Date();
+  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
+  policy.recordUserAcceptance();
+  defineNow(policy, new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000));
+
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.requestDataUploadCount, 1);
+  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, prefs, 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();
+  do_check_eq(listener.requestDataUploadCount, 0);
+  do_check_eq(listener.requestRemoteDeleteCount, 1);
+
+  defineNow(policy, policy._futureDate(500));
+  listener.lastRemoteDeleteRequest.onSubmissionFailureSoft();
+  defineNow(policy, policy._futureDate(50));
+
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.requestRemoteDeleteCount, 1);
+
+  defineNow(policy, policy._futureDate(policy.FAILURE_BACKOFF_INTERVALS[0] - 50));
+  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_test(function test_delete_remote_data_in_progress_upload() {
+  let [policy, prefs, listener] = getPolicy("delete_remote_data_in_progress_upload");
+
+  let now = new Date();
+  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
+  policy.recordUserAcceptance();
+  defineNow(policy, policy.nextDataSubmissionDate);
+
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.requestDataUploadCount, 1);
+  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);
+
+  // Now wait a little bit and finish the request.
+  defineNow(policy, policy._futureDate(10 * 1000));
+  listener.lastDataRequest.onSubmissionSuccess(policy._futureDate(1000));
+  defineNow(policy, policy._futureDate(5000));
+
+  policy.checkStateAndTrigger();
+  do_check_eq(listener.requestDataUploadCount, 1);
+  do_check_eq(listener.requestRemoteDeleteCount, 1);
 
   run_next_test();
 });
 
 add_test(function test_polling() {
   let [policy, prefs, listener] = getPolicy("polling");
 
   // Ensure checkStateAndTrigger is called at a regular interval.
@@ -484,17 +616,17 @@ add_test(function test_polling() {
       do_check_true(now2.getTime() - now.getTime() >= 500);
       now = now2;
       HealthReportPolicy.prototype.checkStateAndTrigger.call(policy);
 
       if (count >= 2) {
         policy.stopPolling();
 
         do_check_eq(listener.notifyUserCount, 0);
-        do_check_eq(listener.requestDataSubmissionCount, 0);
+        do_check_eq(listener.requestDataUploadCount, 0);
 
         run_next_test();
       }
     }
   });
   policy.startPolling();
 });
 
@@ -534,26 +666,26 @@ add_test(function test_polling_implicit_
       do_check_eq(listener.notifyUserCount, 1);
 
       if (count == 1) {
         listener.lastNotifyRequest.onUserNotifyComplete();
       }
 
       if (count < 4) {
         do_check_false(policy.dataSubmissionPolicyAccepted);
-        do_check_eq(listener.requestDataSubmissionCount, 0);
+        do_check_eq(listener.requestDataUploadCount, 0);
       } else {
         do_check_true(policy.dataSubmissionPolicyAccepted);
         do_check_eq(policy.dataSubmissionPolicyResponseType,
                     "accepted-implicit-time-elapsed");
-        do_check_eq(listener.requestDataSubmissionCount, 1);
+        do_check_eq(listener.requestDataUploadCount, 1);
       }
 
       if (count > 4) {
-        do_check_eq(listener.requestDataSubmissionCount, 1);
+        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());