services/datareporting/policy.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/datareporting/policy.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1135 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/**
     1.9 + * This file is in transition. It was originally conceived to fulfill the
    1.10 + * needs of only Firefox Health Report. It is slowly being morphed into
    1.11 + * fulfilling the needs of all data reporting facilities in Gecko applications.
    1.12 + * As a result, some things feel a bit weird.
    1.13 + *
    1.14 + * DataReportingPolicy is both a driver for data reporting notification
    1.15 + * (a true policy) and the driver for FHR data submission. The latter should
    1.16 + * eventually be split into its own type and module.
    1.17 + */
    1.18 +
    1.19 +"use strict";
    1.20 +
    1.21 +#ifndef MERGED_COMPARTMENT
    1.22 +
    1.23 +this.EXPORTED_SYMBOLS = [
    1.24 +  "DataSubmissionRequest", // For test use only.
    1.25 +  "DataReportingPolicy",
    1.26 +];
    1.27 +
    1.28 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.29 +
    1.30 +#endif
    1.31 +
    1.32 +Cu.import("resource://gre/modules/Services.jsm");
    1.33 +Cu.import("resource://gre/modules/Promise.jsm");
    1.34 +Cu.import("resource://gre/modules/Log.jsm");
    1.35 +Cu.import("resource://services-common/utils.js");
    1.36 +Cu.import("resource://gre/modules/UpdateChannel.jsm");
    1.37 +
    1.38 +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
    1.39 +
    1.40 +// Used as a sanity lower bound for dates stored in prefs. This module was
    1.41 +// implemented in 2012, so any earlier dates indicate an incorrect clock.
    1.42 +const OLDEST_ALLOWED_YEAR = 2012;
    1.43 +
    1.44 +/**
    1.45 + * Represents a request to display data policy.
    1.46 + *
    1.47 + * Instances of this are created when the policy is requesting the user's
    1.48 + * approval to agree to the data submission policy.
    1.49 + *
    1.50 + * Receivers of these instances are expected to call one or more of the on*
    1.51 + * functions when events occur.
    1.52 + *
    1.53 + * When one of these requests is received, the first thing a callee should do
    1.54 + * is present notification to the user of the data policy. When the notice
    1.55 + * is displayed to the user, the callee should call `onUserNotifyComplete`.
    1.56 + * This begins a countdown timer that upon completion will signal implicit
    1.57 + * acceptance of the policy. If for whatever reason the callee could not
    1.58 + * display a notice, it should call `onUserNotifyFailed`.
    1.59 + *
    1.60 + * Once the user is notified of the policy, the callee has the option of
    1.61 + * signaling explicit user acceptance or rejection of the policy. They do this
    1.62 + * by calling `onUserAccept` or `onUserReject`, respectively. These functions
    1.63 + * are essentially proxies to
    1.64 + * DataReportingPolicy.{recordUserAcceptance,recordUserRejection}.
    1.65 + *
    1.66 + * If the user never explicitly accepts or rejects the policy, it will be
    1.67 + * implicitly accepted after a specified duration of time. The notice is
    1.68 + * expected to remain displayed even after implicit acceptance (in case the
    1.69 + * user is away from the device). So, no event signaling implicit acceptance
    1.70 + * is exposed.
    1.71 + *
    1.72 + * Receivers of instances of this type should treat it as a black box with
    1.73 + * the exception of the on* functions.
    1.74 + *
    1.75 + * @param policy
    1.76 + *        (DataReportingPolicy) The policy instance this request came from.
    1.77 + * @param deferred
    1.78 + *        (deferred) The promise that will be fulfilled when display occurs.
    1.79 + */
    1.80 +function NotifyPolicyRequest(policy, deferred) {
    1.81 +  this.policy = policy;
    1.82 +  this.deferred = deferred;
    1.83 +}
    1.84 +NotifyPolicyRequest.prototype = {
    1.85 +  /**
    1.86 +   * Called when the user is notified of the policy.
    1.87 +   *
    1.88 +   * This starts a countdown timer that will eventually signify implicit
    1.89 +   * acceptance of the data policy.
    1.90 +   */
    1.91 +  onUserNotifyComplete: function onUserNotified() {
    1.92 +    this.deferred.resolve();
    1.93 +    return this.deferred.promise;
    1.94 +  },
    1.95 +
    1.96 +  /**
    1.97 +   * Called when there was an error notifying the user about the policy.
    1.98 +   *
    1.99 +   * @param error
   1.100 +   *        (Error) Explains what went wrong.
   1.101 +   */
   1.102 +  onUserNotifyFailed: function onUserNotifyFailed(error) {
   1.103 +    this.deferred.reject(error);
   1.104 +  },
   1.105 +
   1.106 +  /**
   1.107 +   * Called when the user agreed to the data policy.
   1.108 +   *
   1.109 +   * @param reason
   1.110 +   *        (string) How the user agreed to the policy.
   1.111 +   */
   1.112 +  onUserAccept: function onUserAccept(reason) {
   1.113 +    this.policy.recordUserAcceptance(reason);
   1.114 +  },
   1.115 +
   1.116 +  /**
   1.117 +   * Called when the user rejected the data policy.
   1.118 +   *
   1.119 +   * @param reason
   1.120 +   *        (string) How the user rejected the policy.
   1.121 +   */
   1.122 +  onUserReject: function onUserReject(reason) {
   1.123 +    this.policy.recordUserRejection(reason);
   1.124 +  },
   1.125 +};
   1.126 +
   1.127 +Object.freeze(NotifyPolicyRequest.prototype);
   1.128 +
   1.129 +/**
   1.130 + * Represents a request to submit data.
   1.131 + *
   1.132 + * Instances of this are created when the policy requests data upload or
   1.133 + * deletion.
   1.134 + *
   1.135 + * Receivers are expected to call one of the provided on* functions to signal
   1.136 + * completion of the request.
   1.137 + *
   1.138 + * Instances of this type should not be instantiated outside of this file.
   1.139 + * Receivers of instances of this type should not attempt to do anything with
   1.140 + * the instance except call one of the on* methods.
   1.141 + */
   1.142 +this.DataSubmissionRequest = function (promise, expiresDate, isDelete) {
   1.143 +  this.promise = promise;
   1.144 +  this.expiresDate = expiresDate;
   1.145 +  this.isDelete = isDelete;
   1.146 +
   1.147 +  this.state = null;
   1.148 +  this.reason = null;
   1.149 +}
   1.150 +
   1.151 +this.DataSubmissionRequest.prototype = Object.freeze({
   1.152 +  NO_DATA_AVAILABLE: "no-data-available",
   1.153 +  SUBMISSION_SUCCESS: "success",
   1.154 +  SUBMISSION_FAILURE_SOFT: "failure-soft",
   1.155 +  SUBMISSION_FAILURE_HARD: "failure-hard",
   1.156 +  UPLOAD_IN_PROGRESS: "upload-in-progress",
   1.157 +
   1.158 +  /**
   1.159 +   * No submission was attempted because no data was available.
   1.160 +   *
   1.161 +   * In the case of upload, this means there is no data to upload (perhaps
   1.162 +   * it isn't available yet). In case of remote deletion, it means that there
   1.163 +   * is no remote data to delete.
   1.164 +   */
   1.165 +  onNoDataAvailable: function onNoDataAvailable() {
   1.166 +    this.state = this.NO_DATA_AVAILABLE;
   1.167 +    this.promise.resolve(this);
   1.168 +    return this.promise.promise;
   1.169 +  },
   1.170 +
   1.171 +  /**
   1.172 +   * Data submission has completed successfully.
   1.173 +   *
   1.174 +   * In case of upload, this means the upload completed successfully. In case
   1.175 +   * of deletion, the data was deleted successfully.
   1.176 +   *
   1.177 +   * @param date
   1.178 +   *        (Date) When data submission occurred.
   1.179 +   */
   1.180 +  onSubmissionSuccess: function onSubmissionSuccess(date) {
   1.181 +    this.state = this.SUBMISSION_SUCCESS;
   1.182 +    this.submissionDate = date;
   1.183 +    this.promise.resolve(this);
   1.184 +    return this.promise.promise;
   1.185 +  },
   1.186 +
   1.187 +  /**
   1.188 +   * There was a recoverable failure when submitting data.
   1.189 +   *
   1.190 +   * Perhaps the server was down. Perhaps the network wasn't available. The
   1.191 +   * policy may request submission again after a short delay.
   1.192 +   *
   1.193 +   * @param reason
   1.194 +   *        (string) Why the failure occurred. For logging purposes only.
   1.195 +   */
   1.196 +  onSubmissionFailureSoft: function onSubmissionFailureSoft(reason=null) {
   1.197 +    this.state = this.SUBMISSION_FAILURE_SOFT;
   1.198 +    this.reason = reason;
   1.199 +    this.promise.resolve(this);
   1.200 +    return this.promise.promise;
   1.201 +  },
   1.202 +
   1.203 +  /**
   1.204 +   * There was an unrecoverable failure when submitting data.
   1.205 +   *
   1.206 +   * Perhaps the client is misconfigured. Perhaps the server rejected the data.
   1.207 +   * Attempts at performing submission again will yield the same result. So,
   1.208 +   * the policy should not try again (until the next day).
   1.209 +   *
   1.210 +   * @param reason
   1.211 +   *        (string) Why the failure occurred. For logging purposes only.
   1.212 +   */
   1.213 +  onSubmissionFailureHard: function onSubmissionFailureHard(reason=null) {
   1.214 +    this.state = this.SUBMISSION_FAILURE_HARD;
   1.215 +    this.reason = reason;
   1.216 +    this.promise.resolve(this);
   1.217 +    return this.promise.promise;
   1.218 +  },
   1.219 +
   1.220 +  /**
   1.221 +   * The request was aborted because an upload was already in progress.
   1.222 +   */
   1.223 +  onUploadInProgress: function (reason=null) {
   1.224 +    this.state = this.UPLOAD_IN_PROGRESS;
   1.225 +    this.reason = reason;
   1.226 +    this.promise.resolve(this);
   1.227 +    return this.promise.promise;
   1.228 +  },
   1.229 +});
   1.230 +
   1.231 +/**
   1.232 + * Manages scheduling of Firefox Health Report data submission.
   1.233 + *
   1.234 + * The rules of data submission are as follows:
   1.235 + *
   1.236 + *  1. Do not submit data more than once every 24 hours.
   1.237 + *  2. Try to submit as close to 24 hours apart as possible.
   1.238 + *  3. Do not submit too soon after application startup so as to not negatively
   1.239 + *     impact performance at startup.
   1.240 + *  4. Before first ever data submission, the user should be notified about
   1.241 + *     data collection practices.
   1.242 + *  5. User should have opportunity to react to this notification before
   1.243 + *     data submission.
   1.244 + *  6. Display of notification without any explicit user action constitutes
   1.245 + *     implicit consent after a certain duration of time.
   1.246 + *  7. If data submission fails, try at most 2 additional times before giving
   1.247 + *     up on that day's submission.
   1.248 + *
   1.249 + * The listener passed into the instance must have the following properties
   1.250 + * (which are callbacks that will be invoked at certain key events):
   1.251 + *
   1.252 + *   * onRequestDataUpload(request) - Called when the policy is requesting
   1.253 + *     data to be submitted. The function is passed a `DataSubmissionRequest`.
   1.254 + *     The listener should call one of the special resolving functions on that
   1.255 + *     instance (see the documentation for that type).
   1.256 + *
   1.257 + *   * onRequestRemoteDelete(request) - Called when the policy is requesting
   1.258 + *     deletion of remotely stored data. The function is passed a
   1.259 + *     `DataSubmissionRequest`. The listener should call one of the special
   1.260 + *     resolving functions on that instance (just like `onRequestDataUpload`).
   1.261 + *
   1.262 + *   * onNotifyDataPolicy(request) - Called when the policy is requesting the
   1.263 + *     user to be notified that data submission will occur. The function
   1.264 + *     receives a `NotifyPolicyRequest` instance. The callee should call one or
   1.265 + *     more of the functions on that instance when specific events occur. See
   1.266 + *     the documentation for that type for more.
   1.267 + *
   1.268 + * Note that the notification method is abstracted. Different applications
   1.269 + * can have different mechanisms by which they notify the user of data
   1.270 + * submission practices.
   1.271 + *
   1.272 + * @param policyPrefs
   1.273 + *        (Preferences) Handle on preferences branch on which state will be
   1.274 + *        queried and stored.
   1.275 + * @param healthReportPrefs
   1.276 + *        (Preferences) Handle on preferences branch holding Health Report state.
   1.277 + * @param listener
   1.278 + *        (object) Object with callbacks that will be invoked at certain key
   1.279 + *        events.
   1.280 + */
   1.281 +this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
   1.282 +  this._log = Log.repository.getLogger("Services.DataReporting.Policy");
   1.283 +  this._log.level = Log.Level["Debug"];
   1.284 +
   1.285 +  for (let handler of this.REQUIRED_LISTENERS) {
   1.286 +    if (!listener[handler]) {
   1.287 +      throw new Error("Passed listener does not contain required handler: " +
   1.288 +                      handler);
   1.289 +    }
   1.290 +  }
   1.291 +
   1.292 +  this._prefs = prefs;
   1.293 +  this._healthReportPrefs = healthReportPrefs;
   1.294 +  this._listener = listener;
   1.295 +
   1.296 +  // If the policy version has changed, reset all preferences, so that
   1.297 +  // the notification reappears.
   1.298 +  let acceptedVersion = this._prefs.get("dataSubmissionPolicyAcceptedVersion");
   1.299 +  if (typeof(acceptedVersion) == "number" &&
   1.300 +      acceptedVersion < this.minimumPolicyVersion) {
   1.301 +    this._log.info("policy version has changed - resetting all prefs");
   1.302 +    // We don't want to delay the notification in this case.
   1.303 +    let firstRunToRestore = this.firstRunDate;
   1.304 +    this._prefs.resetBranch();
   1.305 +    this.firstRunDate = firstRunToRestore.getTime() ?
   1.306 +                        firstRunToRestore : this.now();
   1.307 +  } else if (!this.firstRunDate.getTime()) {
   1.308 +    // If we've never run before, record the current time.
   1.309 +    this.firstRunDate = this.now();
   1.310 +  }
   1.311 +
   1.312 +  // Install an observer so that we can act on changes from external
   1.313 +  // code (such as Android UI).
   1.314 +  // Use a function because this is the only place where the Preferences
   1.315 +  // abstraction is way less usable than nsIPrefBranch.
   1.316 +  //
   1.317 +  // Hang on to the observer here so that tests can reach it.
   1.318 +  this.uploadEnabledObserver = function onUploadEnabledChanged() {
   1.319 +    if (this.pendingDeleteRemoteData || this.healthReportUploadEnabled) {
   1.320 +      // Nothing to do: either we're already deleting because the caller
   1.321 +      // came through the front door (rHRUE), or they set the flag to true.
   1.322 +      return;
   1.323 +    }
   1.324 +    this._log.info("uploadEnabled pref changed. Scheduling deletion.");
   1.325 +    this.deleteRemoteData();
   1.326 +  }.bind(this);
   1.327 +
   1.328 +  healthReportPrefs.observe("uploadEnabled", this.uploadEnabledObserver);
   1.329 +
   1.330 +  // Ensure we are scheduled to submit.
   1.331 +  if (!this.nextDataSubmissionDate.getTime()) {
   1.332 +    this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
   1.333 +  }
   1.334 +
   1.335 +  // Date at which we performed user notification of acceptance.
   1.336 +  // This is an instance variable because implicit acceptance should only
   1.337 +  // carry forward through a single application instance.
   1.338 +  this._dataSubmissionPolicyNotifiedDate = null;
   1.339 +
   1.340 +  // Record when we last requested for submitted data to be sent. This is
   1.341 +  // to avoid having multiple outstanding requests.
   1.342 +  this._inProgressSubmissionRequest = null;
   1.343 +};
   1.344 +
   1.345 +this.DataReportingPolicy.prototype = Object.freeze({
   1.346 +  /**
   1.347 +   * How long after first run we should notify about data submission.
   1.348 +   */
   1.349 +  SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
   1.350 +
   1.351 +  /**
   1.352 +   * Time that must elapse with no user action for implicit acceptance.
   1.353 +   *
   1.354 +   * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
   1.355 +   * Privacy and/or Legal before modifying.
   1.356 +   */
   1.357 +  IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
   1.358 +
   1.359 +  /**
   1.360 +   *  How often to poll to see if we need to do something.
   1.361 +   *
   1.362 +   * The interval needs to be short enough such that short-lived applications
   1.363 +   * have an opportunity to submit data. But, it also needs to be long enough
   1.364 +   * to not negatively impact performance.
   1.365 +   *
   1.366 +   * The random bit is to ensure that other systems scheduling around the same
   1.367 +   * interval don't all get scheduled together.
   1.368 +   */
   1.369 +  POLL_INTERVAL_MSEC: (60 * 1000) + Math.floor(2.5 * 1000 * Math.random()),
   1.370 +
   1.371 +  /**
   1.372 +   * How long individual data submission requests live before expiring.
   1.373 +   *
   1.374 +   * Data submission requests have this long to complete before we give up on
   1.375 +   * them and try again.
   1.376 +   *
   1.377 +   * We want this to be short enough that we retry frequently enough but long
   1.378 +   * enough to give slow networks and systems time to handle it.
   1.379 +   */
   1.380 +  SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC: 10 * 60 * 1000,
   1.381 +
   1.382 +  /**
   1.383 +   * Our backoff schedule in case of submission failure.
   1.384 +   *
   1.385 +   * This dictates both the number of times we retry a daily submission and
   1.386 +   * when to retry after each failure.
   1.387 +   *
   1.388 +   * Each element represents how long to wait after each recoverable failure.
   1.389 +   * After the first failure, we wait the time in element 0 before trying
   1.390 +   * again. After the second failure, we wait the time in element 1. Once
   1.391 +   * we run out of values in this array, we give up on that day's submission
   1.392 +   * and schedule for a day out.
   1.393 +   */
   1.394 +  FAILURE_BACKOFF_INTERVALS: [
   1.395 +    15 * 60 * 1000,
   1.396 +    60 * 60 * 1000,
   1.397 +  ],
   1.398 +
   1.399 +  /**
   1.400 +   * State of user notification of data submission.
   1.401 +   */
   1.402 +  STATE_NOTIFY_UNNOTIFIED: "not-notified",
   1.403 +  STATE_NOTIFY_WAIT: "waiting",
   1.404 +  STATE_NOTIFY_COMPLETE: "ok",
   1.405 +
   1.406 +  REQUIRED_LISTENERS: [
   1.407 +    "onRequestDataUpload",
   1.408 +    "onRequestRemoteDelete",
   1.409 +    "onNotifyDataPolicy",
   1.410 +  ],
   1.411 +
   1.412 +  /**
   1.413 +   * The first time the health report policy came into existence.
   1.414 +   *
   1.415 +   * This is used for scheduling of the initial submission.
   1.416 +   */
   1.417 +  get firstRunDate() {
   1.418 +    return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log,
   1.419 +                                   OLDEST_ALLOWED_YEAR);
   1.420 +  },
   1.421 +
   1.422 +  set firstRunDate(value) {
   1.423 +    this._log.debug("Setting first-run date: " + value);
   1.424 +    CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
   1.425 +                            OLDEST_ALLOWED_YEAR);
   1.426 +  },
   1.427 +
   1.428 +  /**
   1.429 +   * Short circuit policy checking and always assume acceptance.
   1.430 +   *
   1.431 +   * This shuld never be set by the user. Instead, it is a per-application or
   1.432 +   * per-deployment default pref.
   1.433 +   */
   1.434 +  get dataSubmissionPolicyBypassAcceptance() {
   1.435 +    return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
   1.436 +  },
   1.437 +
   1.438 +  /**
   1.439 +   * When the user was notified that data submission could occur.
   1.440 +   *
   1.441 +   * This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
   1.442 +   * is what's used internally.
   1.443 +   */
   1.444 +  get dataSubmissionPolicyNotifiedDate() {
   1.445 +    return CommonUtils.getDatePref(this._prefs,
   1.446 +                                   "dataSubmissionPolicyNotifiedTime", 0,
   1.447 +                                   this._log, OLDEST_ALLOWED_YEAR);
   1.448 +  },
   1.449 +
   1.450 +  set dataSubmissionPolicyNotifiedDate(value) {
   1.451 +    this._log.debug("Setting user notified date: " + value);
   1.452 +    CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
   1.453 +                            value, OLDEST_ALLOWED_YEAR);
   1.454 +  },
   1.455 +
   1.456 +  /**
   1.457 +   * When the user accepted or rejected the data submission policy.
   1.458 +   *
   1.459 +   * If there was implicit acceptance, this will be set to the time of that.
   1.460 +   */
   1.461 +  get dataSubmissionPolicyResponseDate() {
   1.462 +    return CommonUtils.getDatePref(this._prefs,
   1.463 +                                   "dataSubmissionPolicyResponseTime",
   1.464 +                                   0, this._log, OLDEST_ALLOWED_YEAR);
   1.465 +  },
   1.466 +
   1.467 +  set dataSubmissionPolicyResponseDate(value) {
   1.468 +    this._log.debug("Setting user notified reaction date: " + value);
   1.469 +    CommonUtils.setDatePref(this._prefs,
   1.470 +                            "dataSubmissionPolicyResponseTime",
   1.471 +                            value, OLDEST_ALLOWED_YEAR);
   1.472 +  },
   1.473 +
   1.474 +  /**
   1.475 +   * Records the result of user notification of data submission policy.
   1.476 +   *
   1.477 +   * This is used for logging and diagnostics purposes. It can answer the
   1.478 +   * question "how was data submission agreed to on this profile?"
   1.479 +   *
   1.480 +   * Not all values are defined by this type and can come from other systems.
   1.481 +   *
   1.482 +   * The value must be a string and should be something machine readable. e.g.
   1.483 +   * "accept-user-clicked-ok-button-in-info-bar"
   1.484 +   */
   1.485 +  get dataSubmissionPolicyResponseType() {
   1.486 +    return this._prefs.get("dataSubmissionPolicyResponseType",
   1.487 +                           "none-recorded");
   1.488 +  },
   1.489 +
   1.490 +  set dataSubmissionPolicyResponseType(value) {
   1.491 +    if (typeof(value) != "string") {
   1.492 +      throw new Error("Value must be a string. Got " + typeof(value));
   1.493 +    }
   1.494 +
   1.495 +    this._prefs.set("dataSubmissionPolicyResponseType", value);
   1.496 +  },
   1.497 +
   1.498 +  /**
   1.499 +   * Whether submission of data is allowed.
   1.500 +   *
   1.501 +   * This is the master switch for remote server communication. If it is
   1.502 +   * false, we never request upload or deletion.
   1.503 +   */
   1.504 +  get dataSubmissionEnabled() {
   1.505 +    // Default is true because we are opt-out.
   1.506 +    return this._prefs.get("dataSubmissionEnabled", true);
   1.507 +  },
   1.508 +
   1.509 +  set dataSubmissionEnabled(value) {
   1.510 +    this._prefs.set("dataSubmissionEnabled", !!value);
   1.511 +  },
   1.512 +
   1.513 +  /**
   1.514 +   * The minimum policy version which for dataSubmissionPolicyAccepted to
   1.515 +   * to be valid.
   1.516 +   */
   1.517 +  get minimumPolicyVersion() {
   1.518 +    // First check if the current channel has an ove
   1.519 +    let channel = UpdateChannel.get(false);
   1.520 +    let channelPref = this._prefs.get("minimumPolicyVersion.channel-" + channel);
   1.521 +    return channelPref !== undefined ?
   1.522 +           channelPref : this._prefs.get("minimumPolicyVersion", 1);
   1.523 +  },
   1.524 +
   1.525 +  /**
   1.526 +   * Whether the user has accepted that data submission can occur.
   1.527 +   *
   1.528 +   * This overrides dataSubmissionEnabled.
   1.529 +   */
   1.530 +  get dataSubmissionPolicyAccepted() {
   1.531 +    // Be conservative and default to false.
   1.532 +    return this._prefs.get("dataSubmissionPolicyAccepted", false);
   1.533 +  },
   1.534 +
   1.535 +  set dataSubmissionPolicyAccepted(value) {
   1.536 +    this._prefs.set("dataSubmissionPolicyAccepted", !!value);
   1.537 +    if (!!value) {
   1.538 +      let currentPolicyVersion = this._prefs.get("currentPolicyVersion", 1);
   1.539 +      this._prefs.set("dataSubmissionPolicyAcceptedVersion", currentPolicyVersion);
   1.540 +    } else {
   1.541 +      this._prefs.reset("dataSubmissionPolicyAcceptedVersion");
   1.542 +    }
   1.543 +  },
   1.544 +
   1.545 +  /**
   1.546 +   * The state of user notification of the data policy.
   1.547 +   *
   1.548 +   * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
   1.549 +   * submission can occur.
   1.550 +   *
   1.551 +   * @return DataReportingPolicy.STATE_NOTIFY_* constant.
   1.552 +   */
   1.553 +  get notifyState() {
   1.554 +    if (this.dataSubmissionPolicyResponseDate.getTime()) {
   1.555 +      return this.STATE_NOTIFY_COMPLETE;
   1.556 +    }
   1.557 +
   1.558 +    // We get the local state - not the state from prefs - because we don't want
   1.559 +    // a value from a previous application run to interfere. This prevents
   1.560 +    // a scenario where notification occurs just before application shutdown and
   1.561 +    // notification is displayed for shorter than the policy requires.
   1.562 +    if (!this._dataSubmissionPolicyNotifiedDate) {
   1.563 +      return this.STATE_NOTIFY_UNNOTIFIED;
   1.564 +    }
   1.565 +
   1.566 +    return this.STATE_NOTIFY_WAIT;
   1.567 +  },
   1.568 +
   1.569 +  /**
   1.570 +   * When this policy last requested data submission.
   1.571 +   *
   1.572 +   * This is used mainly for forensics purposes and should have no bearing
   1.573 +   * on scheduling or run-time behavior.
   1.574 +   */
   1.575 +  get lastDataSubmissionRequestedDate() {
   1.576 +    return CommonUtils.getDatePref(this._healthReportPrefs,
   1.577 +                                   "lastDataSubmissionRequestedTime", 0,
   1.578 +                                   this._log, OLDEST_ALLOWED_YEAR);
   1.579 +  },
   1.580 +
   1.581 +  set lastDataSubmissionRequestedDate(value) {
   1.582 +    CommonUtils.setDatePref(this._healthReportPrefs,
   1.583 +                            "lastDataSubmissionRequestedTime",
   1.584 +                            value, OLDEST_ALLOWED_YEAR);
   1.585 +  },
   1.586 +
   1.587 +  /**
   1.588 +   * When the last data submission actually occurred.
   1.589 +   *
   1.590 +   * This is used mainly for forensics purposes and should have no bearing on
   1.591 +   * actual scheduling.
   1.592 +   */
   1.593 +  get lastDataSubmissionSuccessfulDate() {
   1.594 +    return CommonUtils.getDatePref(this._healthReportPrefs,
   1.595 +                                   "lastDataSubmissionSuccessfulTime", 0,
   1.596 +                                   this._log, OLDEST_ALLOWED_YEAR);
   1.597 +  },
   1.598 +
   1.599 +  set lastDataSubmissionSuccessfulDate(value) {
   1.600 +    CommonUtils.setDatePref(this._healthReportPrefs,
   1.601 +                            "lastDataSubmissionSuccessfulTime",
   1.602 +                            value, OLDEST_ALLOWED_YEAR);
   1.603 +  },
   1.604 +
   1.605 +  /**
   1.606 +   * When we last encountered a submission failure.
   1.607 +   *
   1.608 +   * This is used for forensics purposes and should have no bearing on
   1.609 +   * scheduling.
   1.610 +   */
   1.611 +  get lastDataSubmissionFailureDate() {
   1.612 +    return CommonUtils.getDatePref(this._healthReportPrefs,
   1.613 +                                   "lastDataSubmissionFailureTime",
   1.614 +                                   0, this._log, OLDEST_ALLOWED_YEAR);
   1.615 +  },
   1.616 +
   1.617 +  set lastDataSubmissionFailureDate(value) {
   1.618 +    CommonUtils.setDatePref(this._healthReportPrefs,
   1.619 +                            "lastDataSubmissionFailureTime",
   1.620 +                            value, OLDEST_ALLOWED_YEAR);
   1.621 +  },
   1.622 +
   1.623 +  /**
   1.624 +   * When the next data submission is scheduled to occur.
   1.625 +   *
   1.626 +   * This is maintained internally by this type. External users should not
   1.627 +   * mutate this value.
   1.628 +   */
   1.629 +  get nextDataSubmissionDate() {
   1.630 +    return CommonUtils.getDatePref(this._healthReportPrefs,
   1.631 +                                   "nextDataSubmissionTime", 0,
   1.632 +                                   this._log, OLDEST_ALLOWED_YEAR);
   1.633 +  },
   1.634 +
   1.635 +  set nextDataSubmissionDate(value) {
   1.636 +    CommonUtils.setDatePref(this._healthReportPrefs,
   1.637 +                            "nextDataSubmissionTime", value,
   1.638 +                            OLDEST_ALLOWED_YEAR);
   1.639 +  },
   1.640 +
   1.641 +  /**
   1.642 +   * The number of submission failures for this day's upload.
   1.643 +   *
   1.644 +   * This is used to drive backoff and scheduling.
   1.645 +   */
   1.646 +  get currentDaySubmissionFailureCount() {
   1.647 +    let v = this._healthReportPrefs.get("currentDaySubmissionFailureCount", 0);
   1.648 +
   1.649 +    if (!Number.isInteger(v)) {
   1.650 +      v = 0;
   1.651 +    }
   1.652 +
   1.653 +    return v;
   1.654 +  },
   1.655 +
   1.656 +  set currentDaySubmissionFailureCount(value) {
   1.657 +    if (!Number.isInteger(value)) {
   1.658 +      throw new Error("Value must be integer: " + value);
   1.659 +    }
   1.660 +
   1.661 +    this._healthReportPrefs.set("currentDaySubmissionFailureCount", value);
   1.662 +  },
   1.663 +
   1.664 +  /**
   1.665 +   * Whether a request to delete remote data is awaiting completion.
   1.666 +   *
   1.667 +   * If this is true, the policy will request that remote data be deleted.
   1.668 +   * Furthermore, no new data will be uploaded (if it's even allowed) until
   1.669 +   * the remote deletion is fulfilled.
   1.670 +   */
   1.671 +  get pendingDeleteRemoteData() {
   1.672 +    return !!this._healthReportPrefs.get("pendingDeleteRemoteData", false);
   1.673 +  },
   1.674 +
   1.675 +  set pendingDeleteRemoteData(value) {
   1.676 +    this._healthReportPrefs.set("pendingDeleteRemoteData", !!value);
   1.677 +  },
   1.678 +
   1.679 +  /**
   1.680 +   * Whether upload of Firefox Health Report data is enabled.
   1.681 +   */
   1.682 +  get healthReportUploadEnabled() {
   1.683 +    return !!this._healthReportPrefs.get("uploadEnabled", true);
   1.684 +  },
   1.685 +
   1.686 +  // External callers should update this via `recordHealthReportUploadEnabled`
   1.687 +  // to ensure appropriate side-effects are performed.
   1.688 +  set healthReportUploadEnabled(value) {
   1.689 +    this._healthReportPrefs.set("uploadEnabled", !!value);
   1.690 +  },
   1.691 +
   1.692 +  /**
   1.693 +   * Whether the FHR upload enabled setting is locked and can't be changed.
   1.694 +   */
   1.695 +  get healthReportUploadLocked() {
   1.696 +    return this._healthReportPrefs.locked("uploadEnabled");
   1.697 +  },
   1.698 +
   1.699 +  /**
   1.700 +   * Record user acceptance of data submission policy.
   1.701 +   *
   1.702 +   * Data submission will not be allowed to occur until this is called.
   1.703 +   *
   1.704 +   * This is typically called through the `onUserAccept` property attached to
   1.705 +   * the promise passed to `onUserNotify` in the policy listener. But, it can
   1.706 +   * be called through other interfaces at any time and the call will have
   1.707 +   * an impact on future data submissions.
   1.708 +   *
   1.709 +   * @param reason
   1.710 +   *        (string) How the user accepted the data submission policy.
   1.711 +   */
   1.712 +  recordUserAcceptance: function recordUserAcceptance(reason="no-reason") {
   1.713 +    this._log.info("User accepted data submission policy: " + reason);
   1.714 +    this.dataSubmissionPolicyResponseDate = this.now();
   1.715 +    this.dataSubmissionPolicyResponseType = "accepted-" + reason;
   1.716 +    this.dataSubmissionPolicyAccepted = true;
   1.717 +  },
   1.718 +
   1.719 +  /**
   1.720 +   * Record user rejection of submission policy.
   1.721 +   *
   1.722 +   * Data submission will not be allowed to occur if this is called.
   1.723 +   *
   1.724 +   * This is typically called through the `onUserReject` property attached to
   1.725 +   * the promise passed to `onUserNotify` in the policy listener. But, it can
   1.726 +   * be called through other interfaces at any time and the call will have an
   1.727 +   * impact on future data submissions.
   1.728 +   */
   1.729 +  recordUserRejection: function recordUserRejection(reason="no-reason") {
   1.730 +    this._log.info("User rejected data submission policy: " + reason);
   1.731 +    this.dataSubmissionPolicyResponseDate = this.now();
   1.732 +    this.dataSubmissionPolicyResponseType = "rejected-" + reason;
   1.733 +    this.dataSubmissionPolicyAccepted = false;
   1.734 +  },
   1.735 +
   1.736 +  /**
   1.737 +   * Record the user's intent for whether FHR should upload data.
   1.738 +   *
   1.739 +   * This is the preferred way for XUL applications to record a user's
   1.740 +   * preference on whether Firefox Health Report should upload data to
   1.741 +   * a server.
   1.742 +   *
   1.743 +   * If upload is disabled through this API, a request for remote data
   1.744 +   * deletion is initiated automatically.
   1.745 +   *
   1.746 +   * If upload is being disabled and this operation is scheduled to
   1.747 +   * occur immediately, a promise will be returned. This promise will be
   1.748 +   * fulfilled when the deletion attempt finishes. If upload is being
   1.749 +   * disabled and a promise is not returned, callers must poll
   1.750 +   * `haveRemoteData` on the HealthReporter instance to see if remote
   1.751 +   * data has been deleted.
   1.752 +   *
   1.753 +   * @param flag
   1.754 +   *        (bool) Whether data submission is enabled or disabled.
   1.755 +   * @param reason
   1.756 +   *        (string) Why this value is being adjusted. For logging
   1.757 +   *        purposes only.
   1.758 +   */
   1.759 +  recordHealthReportUploadEnabled: function (flag, reason="no-reason") {
   1.760 +    let result = null;
   1.761 +    if (!flag) {
   1.762 +      result = this.deleteRemoteData(reason);
   1.763 +    }
   1.764 +
   1.765 +    this.healthReportUploadEnabled = flag;
   1.766 +    return result;
   1.767 +  },
   1.768 +
   1.769 +  /**
   1.770 +   * Request that remote data be deleted.
   1.771 +   *
   1.772 +   * This will record an intent that previously uploaded data is to be deleted.
   1.773 +   * The policy will eventually issue a request to the listener for data
   1.774 +   * deletion. It will keep asking for deletion until the listener acknowledges
   1.775 +   * that data has been deleted.
   1.776 +   */
   1.777 +  deleteRemoteData: function deleteRemoteData(reason="no-reason") {
   1.778 +    this._log.info("Remote data deletion requested: " + reason);
   1.779 +
   1.780 +    this.pendingDeleteRemoteData = true;
   1.781 +
   1.782 +    // We want delete deletion to occur as soon as possible. Move up any
   1.783 +    // pending scheduled data submission and try to trigger.
   1.784 +    this.nextDataSubmissionDate = this.now();
   1.785 +    return this.checkStateAndTrigger();
   1.786 +  },
   1.787 +
   1.788 +  /**
   1.789 +   * Start background polling for activity.
   1.790 +   *
   1.791 +   * This will set up a recurring timer that will periodically check if
   1.792 +   * activity is warranted.
   1.793 +   *
   1.794 +   * You typically call this function for each constructed instance.
   1.795 +   */
   1.796 +  startPolling: function startPolling() {
   1.797 +    this.stopPolling();
   1.798 +
   1.799 +    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   1.800 +    this._timer.initWithCallback({
   1.801 +      notify: function notify() {
   1.802 +        this.checkStateAndTrigger();
   1.803 +      }.bind(this)
   1.804 +    }, this.POLL_INTERVAL_MSEC, this._timer.TYPE_REPEATING_SLACK);
   1.805 +  },
   1.806 +
   1.807 +  /**
   1.808 +   * Stop background polling for activity.
   1.809 +   *
   1.810 +   * This should be called when the instance is no longer needed.
   1.811 +   */
   1.812 +  stopPolling: function stopPolling() {
   1.813 +    if (this._timer) {
   1.814 +      this._timer.cancel();
   1.815 +      this._timer = null;
   1.816 +    }
   1.817 +  },
   1.818 +
   1.819 +  /**
   1.820 +   * Abstraction for obtaining current time.
   1.821 +   *
   1.822 +   * The purpose of this is to facilitate testing. Testing code can monkeypatch
   1.823 +   * this on instances instead of modifying the singleton Date object.
   1.824 +   */
   1.825 +  now: function now() {
   1.826 +    return new Date();
   1.827 +  },
   1.828 +
   1.829 +  /**
   1.830 +   * Check state and trigger actions, if necessary.
   1.831 +   *
   1.832 +   * This is what enforces the submission and notification policy detailed
   1.833 +   * above. You can think of this as the driver for health report data
   1.834 +   * submission.
   1.835 +   *
   1.836 +   * Typically this function is called automatically by the background polling.
   1.837 +   * But, it can safely be called manually as needed.
   1.838 +   */
   1.839 +  checkStateAndTrigger: function checkStateAndTrigger() {
   1.840 +    // If the master data submission kill switch is toggled, we have nothing
   1.841 +    // to do. We don't notify about data policies because this would have
   1.842 +    // no effect.
   1.843 +    if (!this.dataSubmissionEnabled) {
   1.844 +      this._log.debug("Data submission is disabled. Doing nothing.");
   1.845 +      return;
   1.846 +    }
   1.847 +
   1.848 +    let now = this.now();
   1.849 +    let nowT = now.getTime();
   1.850 +    let nextSubmissionDate = this.nextDataSubmissionDate;
   1.851 +
   1.852 +    // If the system clock were ever set to a time in the distant future,
   1.853 +    // it's possible our next schedule date is far out as well. We know
   1.854 +    // we shouldn't schedule for more than a day out, so we reset the next
   1.855 +    // scheduled date appropriately. 3 days was chosen arbitrarily.
   1.856 +    if (nextSubmissionDate.getTime() >= nowT + 3 * MILLISECONDS_PER_DAY) {
   1.857 +      this._log.warn("Next data submission time is far away. Was the system " +
   1.858 +                     "clock recently readjusted? " + nextSubmissionDate);
   1.859 +
   1.860 +      // It shouldn't really matter what we set this to. 1 day in the future
   1.861 +      // should be pretty safe.
   1.862 +      this._moveScheduleForward24h();
   1.863 +
   1.864 +      // Fall through since we may have other actions.
   1.865 +    }
   1.866 +
   1.867 +    // Tend to any in progress work.
   1.868 +    if (this._processInProgressSubmission()) {
   1.869 +      return;
   1.870 +    }
   1.871 +
   1.872 +    // Requests to delete remote data take priority above everything else.
   1.873 +    if (this.pendingDeleteRemoteData) {
   1.874 +      if (nowT < nextSubmissionDate.getTime()) {
   1.875 +        this._log.debug("Deletion request is scheduled for the future: " +
   1.876 +                        nextSubmissionDate);
   1.877 +        return;
   1.878 +      }
   1.879 +
   1.880 +      return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
   1.881 +    }
   1.882 +
   1.883 +    if (!this.healthReportUploadEnabled) {
   1.884 +      this._log.debug("Data upload is disabled. Doing nothing.");
   1.885 +      return;
   1.886 +    }
   1.887 +
   1.888 +    // If the user hasn't responded to the data policy, don't do anything.
   1.889 +    if (!this.ensureNotifyResponse(now)) {
   1.890 +      return;
   1.891 +    }
   1.892 +
   1.893 +    // User has opted out of data submission.
   1.894 +    if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) {
   1.895 +      this._log.debug("Data submission has been disabled per user request.");
   1.896 +      return;
   1.897 +    }
   1.898 +
   1.899 +    // User has responded to data policy and data submission is enabled. Now
   1.900 +    // comes the scheduling part.
   1.901 +
   1.902 +    if (nowT < nextSubmissionDate.getTime()) {
   1.903 +      this._log.debug("Next data submission is scheduled in the future: " +
   1.904 +                     nextSubmissionDate);
   1.905 +      return;
   1.906 +    }
   1.907 +
   1.908 +    return this._dispatchSubmissionRequest("onRequestDataUpload", false);
   1.909 +  },
   1.910 +
   1.911 +  /**
   1.912 +   * Ensure user has responded to data submission policy.
   1.913 +   *
   1.914 +   * This must be called before data submission. If the policy has not been
   1.915 +   * responded to, data submission must not occur.
   1.916 +   *
   1.917 +   * @return bool Whether user has responded to data policy.
   1.918 +   */
   1.919 +  ensureNotifyResponse: function ensureNotifyResponse(now) {
   1.920 +    if (this.dataSubmissionPolicyBypassAcceptance) {
   1.921 +      return true;
   1.922 +    }
   1.923 +
   1.924 +    let notifyState = this.notifyState;
   1.925 +
   1.926 +    if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) {
   1.927 +      let notifyAt = new Date(this.firstRunDate.getTime() +
   1.928 +                              this.SUBMISSION_NOTIFY_INTERVAL_MSEC);
   1.929 +
   1.930 +      if (now.getTime() < notifyAt.getTime()) {
   1.931 +        this._log.debug("Don't have to notify about data submission yet.");
   1.932 +        return false;
   1.933 +      }
   1.934 +
   1.935 +      let onComplete = function onComplete() {
   1.936 +        this._log.info("Data submission notification presented.");
   1.937 +        let now = this.now();
   1.938 +
   1.939 +        this._dataSubmissionPolicyNotifiedDate = now;
   1.940 +        this.dataSubmissionPolicyNotifiedDate = now;
   1.941 +      }.bind(this);
   1.942 +
   1.943 +      let deferred = Promise.defer();
   1.944 +
   1.945 +      deferred.promise.then(onComplete, (error) => {
   1.946 +        this._log.warn("Data policy notification presentation failed: " +
   1.947 +                       CommonUtils.exceptionStr(error));
   1.948 +      });
   1.949 +
   1.950 +      this._log.info("Requesting display of data policy.");
   1.951 +      let request = new NotifyPolicyRequest(this, deferred);
   1.952 +
   1.953 +      try {
   1.954 +        this._listener.onNotifyDataPolicy(request);
   1.955 +      } catch (ex) {
   1.956 +        this._log.warn("Exception when calling onNotifyDataPolicy: " +
   1.957 +                       CommonUtils.exceptionStr(ex));
   1.958 +      }
   1.959 +      return false;
   1.960 +    }
   1.961 +
   1.962 +    // We're waiting for user action or implicit acceptance after display.
   1.963 +    if (notifyState == this.STATE_NOTIFY_WAIT) {
   1.964 +      // Check for implicit acceptance.
   1.965 +      let implicitAcceptance =
   1.966 +        this._dataSubmissionPolicyNotifiedDate.getTime() +
   1.967 +        this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC;
   1.968 +
   1.969 +      this._log.debug("Now: " + now.getTime());
   1.970 +      this._log.debug("Will accept: " + implicitAcceptance);
   1.971 +      if (now.getTime() < implicitAcceptance) {
   1.972 +        this._log.debug("Still waiting for reaction or implicit acceptance. " +
   1.973 +                        "Now: " + now.getTime() + " < " +
   1.974 +                        "Accept: " + implicitAcceptance);
   1.975 +        return false;
   1.976 +      }
   1.977 +
   1.978 +      this.recordUserAcceptance("implicit-time-elapsed");
   1.979 +      return true;
   1.980 +    }
   1.981 +
   1.982 +    // If this happens, we have a coding error in this file.
   1.983 +    if (notifyState != this.STATE_NOTIFY_COMPLETE) {
   1.984 +      throw new Error("Unknown notification state: " + notifyState);
   1.985 +    }
   1.986 +
   1.987 +    return true;
   1.988 +  },
   1.989 +
   1.990 +  _processInProgressSubmission: function _processInProgressSubmission() {
   1.991 +    if (!this._inProgressSubmissionRequest) {
   1.992 +      return false;
   1.993 +    }
   1.994 +
   1.995 +    let now = this.now().getTime();
   1.996 +    if (this._inProgressSubmissionRequest.expiresDate.getTime() > now) {
   1.997 +      this._log.info("Waiting on in-progress submission request to finish.");
   1.998 +      return true;
   1.999 +    }
  1.1000 +
  1.1001 +    this._log.warn("Old submission request has expired from no activity.");
  1.1002 +    this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired."));
  1.1003 +    this._inProgressSubmissionRequest = null;
  1.1004 +    this._handleSubmissionFailure();
  1.1005 +
  1.1006 +    return false;
  1.1007 +  },
  1.1008 +
  1.1009 +  _dispatchSubmissionRequest: function _dispatchSubmissionRequest(handler, isDelete) {
  1.1010 +    let now = this.now();
  1.1011 +
  1.1012 +    // We're past our scheduled next data submission date, so let's do it!
  1.1013 +    this.lastDataSubmissionRequestedDate = now;
  1.1014 +    let deferred = Promise.defer();
  1.1015 +    let requestExpiresDate =
  1.1016 +      this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC);
  1.1017 +    this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred,
  1.1018 +                                                                  requestExpiresDate,
  1.1019 +                                                                  isDelete);
  1.1020 +
  1.1021 +    let onSuccess = function onSuccess(result) {
  1.1022 +      this._inProgressSubmissionRequest = null;
  1.1023 +      this._handleSubmissionResult(result);
  1.1024 +    }.bind(this);
  1.1025 +
  1.1026 +    let onError = function onError(error) {
  1.1027 +      this._log.error("Error when handling data submission result: " +
  1.1028 +                      CommonUtils.exceptionStr(error));
  1.1029 +      this._inProgressSubmissionRequest = null;
  1.1030 +      this._handleSubmissionFailure();
  1.1031 +    }.bind(this);
  1.1032 +
  1.1033 +    let chained = deferred.promise.then(onSuccess, onError);
  1.1034 +
  1.1035 +    this._log.info("Requesting data submission. Will expire at " +
  1.1036 +                   requestExpiresDate);
  1.1037 +    try {
  1.1038 +      this._listener[handler](this._inProgressSubmissionRequest);
  1.1039 +    } catch (ex) {
  1.1040 +      this._log.warn("Exception when calling " + handler + ": " +
  1.1041 +                     CommonUtils.exceptionStr(ex));
  1.1042 +      this._inProgressSubmissionRequest = null;
  1.1043 +      this._handleSubmissionFailure();
  1.1044 +      return;
  1.1045 +    }
  1.1046 +
  1.1047 +    return chained;
  1.1048 +  },
  1.1049 +
  1.1050 +  _handleSubmissionResult: function _handleSubmissionResult(request) {
  1.1051 +    let state = request.state;
  1.1052 +    let reason = request.reason || "no reason";
  1.1053 +    this._log.info("Got submission request result: " + state);
  1.1054 +
  1.1055 +    if (state == request.SUBMISSION_SUCCESS) {
  1.1056 +      if (request.isDelete) {
  1.1057 +        this.pendingDeleteRemoteData = false;
  1.1058 +        this._log.info("Successful data delete reported.");
  1.1059 +      } else {
  1.1060 +        this._log.info("Successful data upload reported.");
  1.1061 +      }
  1.1062 +
  1.1063 +      this.lastDataSubmissionSuccessfulDate = request.submissionDate;
  1.1064 +
  1.1065 +      let nextSubmissionDate =
  1.1066 +        new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY);
  1.1067 +
  1.1068 +      // Schedule pending deletes immediately. This has potential to overload
  1.1069 +      // the server. However, the frequency of delete requests across all
  1.1070 +      // clients should be low, so this shouldn't pose a problem.
  1.1071 +      if (this.pendingDeleteRemoteData) {
  1.1072 +        nextSubmissionDate = this.now();
  1.1073 +      }
  1.1074 +
  1.1075 +      this.nextDataSubmissionDate = nextSubmissionDate;
  1.1076 +      this.currentDaySubmissionFailureCount = 0;
  1.1077 +      return;
  1.1078 +    }
  1.1079 +
  1.1080 +    if (state == request.NO_DATA_AVAILABLE) {
  1.1081 +      if (request.isDelete) {
  1.1082 +        this._log.info("Remote data delete requested but no remote data was stored.");
  1.1083 +        this.pendingDeleteRemoteData = false;
  1.1084 +        return;
  1.1085 +      }
  1.1086 +
  1.1087 +      this._log.info("No data was available to submit. May try later.");
  1.1088 +      this._handleSubmissionFailure();
  1.1089 +      return;
  1.1090 +    }
  1.1091 +
  1.1092 +    // We don't special case request.isDelete for these failures because it
  1.1093 +    // likely means there was a server error.
  1.1094 +
  1.1095 +    if (state == request.SUBMISSION_FAILURE_SOFT) {
  1.1096 +      this._log.warn("Soft error submitting data: " + reason);
  1.1097 +      this.lastDataSubmissionFailureDate = this.now();
  1.1098 +      this._handleSubmissionFailure();
  1.1099 +      return;
  1.1100 +    }
  1.1101 +
  1.1102 +    if (state == request.SUBMISSION_FAILURE_HARD) {
  1.1103 +      this._log.warn("Hard error submitting data: " + reason);
  1.1104 +      this.lastDataSubmissionFailureDate = this.now();
  1.1105 +      this._moveScheduleForward24h();
  1.1106 +      return;
  1.1107 +    }
  1.1108 +
  1.1109 +    throw new Error("Unknown state on DataSubmissionRequest: " + request.state);
  1.1110 +  },
  1.1111 +
  1.1112 +  _handleSubmissionFailure: function _handleSubmissionFailure() {
  1.1113 +    if (this.currentDaySubmissionFailureCount >= this.FAILURE_BACKOFF_INTERVALS.length) {
  1.1114 +      this._log.warn("Reached the limit of daily submission attempts. " +
  1.1115 +                     "Rescheduling for tomorrow.");
  1.1116 +      this._moveScheduleForward24h();
  1.1117 +      return false;
  1.1118 +    }
  1.1119 +
  1.1120 +    let offset = this.FAILURE_BACKOFF_INTERVALS[this.currentDaySubmissionFailureCount];
  1.1121 +    this.nextDataSubmissionDate = this._futureDate(offset);
  1.1122 +    this.currentDaySubmissionFailureCount++;
  1.1123 +    return true;
  1.1124 +  },
  1.1125 +
  1.1126 +  _moveScheduleForward24h: function _moveScheduleForward24h() {
  1.1127 +    let d = this._futureDate(MILLISECONDS_PER_DAY);
  1.1128 +    this._log.info("Setting next scheduled data submission for " + d);
  1.1129 +
  1.1130 +    this.nextDataSubmissionDate = d;
  1.1131 +    this.currentDaySubmissionFailureCount = 0;
  1.1132 +  },
  1.1133 +
  1.1134 +  _futureDate: function _futureDate(offset) {
  1.1135 +    return new Date(this.now().getTime() + offset);
  1.1136 +  },
  1.1137 +});
  1.1138 +

mercurial