Backed out changeset b1aca8e61c5c (bug 958980) for bustage. a=backout
[gecko.git] / services / datareporting / policy.jsm
blob20f76265619e25e715fa9d84a8555b8f297eeaaa
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * This file is in transition. It was originally conceived to fulfill the
7  * needs of only Firefox Health Report. It is slowly being morphed into
8  * fulfilling the needs of all data reporting facilities in Gecko applications.
9  * As a result, some things feel a bit weird.
10  *
11  * DataReportingPolicy is both a driver for data reporting notification
12  * (a true policy) and the driver for FHR data submission. The latter should
13  * eventually be split into its own type and module.
14  */
16 "use strict";
18 #ifndef MERGED_COMPARTMENT
20 this.EXPORTED_SYMBOLS = [
21   "DataSubmissionRequest", // For test use only.
22   "DataReportingPolicy",
25 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
27 #endif
29 Cu.import("resource://gre/modules/Promise.jsm");
30 Cu.import("resource://gre/modules/Log.jsm");
31 Cu.import("resource://services-common/utils.js");
33 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
35 // Used as a sanity lower bound for dates stored in prefs. This module was
36 // implemented in 2012, so any earlier dates indicate an incorrect clock.
37 const OLDEST_ALLOWED_YEAR = 2012;
39 /**
40  * Represents a request to display data policy.
41  *
42  * Instances of this are created when the policy is requesting the user's
43  * approval to agree to the data submission policy.
44  *
45  * Receivers of these instances are expected to call one or more of the on*
46  * functions when events occur.
47  *
48  * When one of these requests is received, the first thing a callee should do
49  * is present notification to the user of the data policy. When the notice
50  * is displayed to the user, the callee should call `onUserNotifyComplete`.
51  * This begins a countdown timer that upon completion will signal implicit
52  * acceptance of the policy. If for whatever reason the callee could not
53  * display a notice, it should call `onUserNotifyFailed`.
54  *
55  * Once the user is notified of the policy, the callee has the option of
56  * signaling explicit user acceptance or rejection of the policy. They do this
57  * by calling `onUserAccept` or `onUserReject`, respectively. These functions
58  * are essentially proxies to
59  * DataReportingPolicy.{recordUserAcceptance,recordUserRejection}.
60  *
61  * If the user never explicitly accepts or rejects the policy, it will be
62  * implicitly accepted after a specified duration of time. The notice is
63  * expected to remain displayed even after implicit acceptance (in case the
64  * user is away from the device). So, no event signaling implicit acceptance
65  * is exposed.
66  *
67  * Receivers of instances of this type should treat it as a black box with
68  * the exception of the on* functions.
69  *
70  * @param policy
71  *        (DataReportingPolicy) The policy instance this request came from.
72  * @param deferred
73  *        (deferred) The promise that will be fulfilled when display occurs.
74  */
75 function NotifyPolicyRequest(policy, deferred) {
76   this.policy = policy;
77   this.deferred = deferred;
79 NotifyPolicyRequest.prototype = {
80   /**
81    * Called when the user is notified of the policy.
82    *
83    * This starts a countdown timer that will eventually signify implicit
84    * acceptance of the data policy.
85    */
86   onUserNotifyComplete: function onUserNotified() {
87     this.deferred.resolve();
88     return this.deferred.promise;
89   },
91   /**
92    * Called when there was an error notifying the user about the policy.
93    *
94    * @param error
95    *        (Error) Explains what went wrong.
96    */
97   onUserNotifyFailed: function onUserNotifyFailed(error) {
98     this.deferred.reject(error);
99   },
101   /**
102    * Called when the user agreed to the data policy.
103    *
104    * @param reason
105    *        (string) How the user agreed to the policy.
106    */
107   onUserAccept: function onUserAccept(reason) {
108     this.policy.recordUserAcceptance(reason);
109   },
111   /**
112    * Called when the user rejected the data policy.
113    *
114    * @param reason
115    *        (string) How the user rejected the policy.
116    */
117   onUserReject: function onUserReject(reason) {
118     this.policy.recordUserRejection(reason);
119   },
122 Object.freeze(NotifyPolicyRequest.prototype);
125  * Represents a request to submit data.
127  * Instances of this are created when the policy requests data upload or
128  * deletion.
130  * Receivers are expected to call one of the provided on* functions to signal
131  * completion of the request.
133  * Instances of this type should not be instantiated outside of this file.
134  * Receivers of instances of this type should not attempt to do anything with
135  * the instance except call one of the on* methods.
136  */
137 this.DataSubmissionRequest = function (promise, expiresDate, isDelete) {
138   this.promise = promise;
139   this.expiresDate = expiresDate;
140   this.isDelete = isDelete;
142   this.state = null;
143   this.reason = null;
146 this.DataSubmissionRequest.prototype = Object.freeze({
147   NO_DATA_AVAILABLE: "no-data-available",
148   SUBMISSION_SUCCESS: "success",
149   SUBMISSION_FAILURE_SOFT: "failure-soft",
150   SUBMISSION_FAILURE_HARD: "failure-hard",
152   /**
153    * No submission was attempted because no data was available.
154    *
155    * In the case of upload, this means there is no data to upload (perhaps
156    * it isn't available yet). In case of remote deletion, it means that there
157    * is no remote data to delete.
158    */
159   onNoDataAvailable: function onNoDataAvailable() {
160     this.state = this.NO_DATA_AVAILABLE;
161     this.promise.resolve(this);
162     return this.promise.promise;
163   },
165   /**
166    * Data submission has completed successfully.
167    *
168    * In case of upload, this means the upload completed successfully. In case
169    * of deletion, the data was deleted successfully.
170    *
171    * @param date
172    *        (Date) When data submission occurred.
173    */
174   onSubmissionSuccess: function onSubmissionSuccess(date) {
175     this.state = this.SUBMISSION_SUCCESS;
176     this.submissionDate = date;
177     this.promise.resolve(this);
178     return this.promise.promise;
179   },
181   /**
182    * There was a recoverable failure when submitting data.
183    *
184    * Perhaps the server was down. Perhaps the network wasn't available. The
185    * policy may request submission again after a short delay.
186    *
187    * @param reason
188    *        (string) Why the failure occurred. For logging purposes only.
189    */
190   onSubmissionFailureSoft: function onSubmissionFailureSoft(reason=null) {
191     this.state = this.SUBMISSION_FAILURE_SOFT;
192     this.reason = reason;
193     this.promise.resolve(this);
194     return this.promise.promise;
195   },
197   /**
198    * There was an unrecoverable failure when submitting data.
199    *
200    * Perhaps the client is misconfigured. Perhaps the server rejected the data.
201    * Attempts at performing submission again will yield the same result. So,
202    * the policy should not try again (until the next day).
203    *
204    * @param reason
205    *        (string) Why the failure occurred. For logging purposes only.
206    */
207   onSubmissionFailureHard: function onSubmissionFailureHard(reason=null) {
208     this.state = this.SUBMISSION_FAILURE_HARD;
209     this.reason = reason;
210     this.promise.resolve(this);
211     return this.promise.promise;
212   },
216  * Manages scheduling of Firefox Health Report data submission.
218  * The rules of data submission are as follows:
220  *  1. Do not submit data more than once every 24 hours.
221  *  2. Try to submit as close to 24 hours apart as possible.
222  *  3. Do not submit too soon after application startup so as to not negatively
223  *     impact performance at startup.
224  *  4. Before first ever data submission, the user should be notified about
225  *     data collection practices.
226  *  5. User should have opportunity to react to this notification before
227  *     data submission.
228  *  6. Display of notification without any explicit user action constitutes
229  *     implicit consent after a certain duration of time.
230  *  7. If data submission fails, try at most 2 additional times before giving
231  *     up on that day's submission.
233  * The listener passed into the instance must have the following properties
234  * (which are callbacks that will be invoked at certain key events):
236  *   * onRequestDataUpload(request) - Called when the policy is requesting
237  *     data to be submitted. The function is passed a `DataSubmissionRequest`.
238  *     The listener should call one of the special resolving functions on that
239  *     instance (see the documentation for that type).
241  *   * onRequestRemoteDelete(request) - Called when the policy is requesting
242  *     deletion of remotely stored data. The function is passed a
243  *     `DataSubmissionRequest`. The listener should call one of the special
244  *     resolving functions on that instance (just like `onRequestDataUpload`).
246  *   * onNotifyDataPolicy(request) - Called when the policy is requesting the
247  *     user to be notified that data submission will occur. The function
248  *     receives a `NotifyPolicyRequest` instance. The callee should call one or
249  *     more of the functions on that instance when specific events occur. See
250  *     the documentation for that type for more.
252  * Note that the notification method is abstracted. Different applications
253  * can have different mechanisms by which they notify the user of data
254  * submission practices.
256  * @param policyPrefs
257  *        (Preferences) Handle on preferences branch on which state will be
258  *        queried and stored.
259  * @param healthReportPrefs
260  *        (Preferences) Handle on preferences branch holding Health Report state.
261  * @param listener
262  *        (object) Object with callbacks that will be invoked at certain key
263  *        events.
264  */
265 this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
266   this._log = Log.repository.getLogger("Services.DataReporting.Policy");
267   this._log.level = Log.Level["Debug"];
269   for (let handler of this.REQUIRED_LISTENERS) {
270     if (!listener[handler]) {
271       throw new Error("Passed listener does not contain required handler: " +
272                       handler);
273     }
274   }
276   this._prefs = prefs;
277   this._healthReportPrefs = healthReportPrefs;
278   this._listener = listener;
280   // If we've never run before, record the current time.
281   if (!this.firstRunDate.getTime()) {
282     this.firstRunDate = this.now();
283   }
285   // Install an observer so that we can act on changes from external
286   // code (such as Android UI).
287   // Use a function because this is the only place where the Preferences
288   // abstraction is way less usable than nsIPrefBranch.
289   //
290   // Hang on to the observer here so that tests can reach it.
291   this.uploadEnabledObserver = function onUploadEnabledChanged() {
292     if (this.pendingDeleteRemoteData || this.healthReportUploadEnabled) {
293       // Nothing to do: either we're already deleting because the caller
294       // came through the front door (rHRUE), or they set the flag to true.
295       return;
296     }
297     this._log.info("uploadEnabled pref changed. Scheduling deletion.");
298     this.deleteRemoteData();
299   }.bind(this);
301   healthReportPrefs.observe("uploadEnabled", this.uploadEnabledObserver);
303   // Ensure we are scheduled to submit.
304   if (!this.nextDataSubmissionDate.getTime()) {
305     this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
306   }
308   // Date at which we performed user notification of acceptance.
309   // This is an instance variable because implicit acceptance should only
310   // carry forward through a single application instance.
311   this._dataSubmissionPolicyNotifiedDate = null;
313   // Record when we last requested for submitted data to be sent. This is
314   // to avoid having multiple outstanding requests.
315   this._inProgressSubmissionRequest = null;
318 this.DataReportingPolicy.prototype = Object.freeze({
319   /**
320    * How long after first run we should notify about data submission.
321    */
322   SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
324   /**
325    * Time that must elapse with no user action for implicit acceptance.
326    *
327    * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
328    * Privacy and/or Legal before modifying.
329    */
330   IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
332   /**
333    *  How often to poll to see if we need to do something.
334    *
335    * The interval needs to be short enough such that short-lived applications
336    * have an opportunity to submit data. But, it also needs to be long enough
337    * to not negatively impact performance.
338    *
339    * The random bit is to ensure that other systems scheduling around the same
340    * interval don't all get scheduled together.
341    */
342   POLL_INTERVAL_MSEC: (60 * 1000) + Math.floor(2.5 * 1000 * Math.random()),
344   /**
345    * How long individual data submission requests live before expiring.
346    *
347    * Data submission requests have this long to complete before we give up on
348    * them and try again.
349    *
350    * We want this to be short enough that we retry frequently enough but long
351    * enough to give slow networks and systems time to handle it.
352    */
353   SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC: 10 * 60 * 1000,
355   /**
356    * Our backoff schedule in case of submission failure.
357    *
358    * This dictates both the number of times we retry a daily submission and
359    * when to retry after each failure.
360    *
361    * Each element represents how long to wait after each recoverable failure.
362    * After the first failure, we wait the time in element 0 before trying
363    * again. After the second failure, we wait the time in element 1. Once
364    * we run out of values in this array, we give up on that day's submission
365    * and schedule for a day out.
366    */
367   FAILURE_BACKOFF_INTERVALS: [
368     15 * 60 * 1000,
369     60 * 60 * 1000,
370   ],
372   /**
373    * State of user notification of data submission.
374    */
375   STATE_NOTIFY_UNNOTIFIED: "not-notified",
376   STATE_NOTIFY_WAIT: "waiting",
377   STATE_NOTIFY_COMPLETE: "ok",
379   REQUIRED_LISTENERS: [
380     "onRequestDataUpload",
381     "onRequestRemoteDelete",
382     "onNotifyDataPolicy",
383   ],
385   /**
386    * The first time the health report policy came into existence.
387    *
388    * This is used for scheduling of the initial submission.
389    */
390   get firstRunDate() {
391     return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log,
392                                    OLDEST_ALLOWED_YEAR);
393   },
395   set firstRunDate(value) {
396     this._log.debug("Setting first-run date: " + value);
397     CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
398                             OLDEST_ALLOWED_YEAR);
399   },
401   /**
402    * Short circuit policy checking and always assume acceptance.
403    *
404    * This shuld never be set by the user. Instead, it is a per-application or
405    * per-deployment default pref.
406    */
407   get dataSubmissionPolicyBypassAcceptance() {
408     return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
409   },
411   /**
412    * When the user was notified that data submission could occur.
413    *
414    * This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
415    * is what's used internally.
416    */
417   get dataSubmissionPolicyNotifiedDate() {
418     return CommonUtils.getDatePref(this._prefs,
419                                    "dataSubmissionPolicyNotifiedTime", 0,
420                                    this._log, OLDEST_ALLOWED_YEAR);
421   },
423   set dataSubmissionPolicyNotifiedDate(value) {
424     this._log.debug("Setting user notified date: " + value);
425     CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
426                             value, OLDEST_ALLOWED_YEAR);
427   },
429   /**
430    * When the user accepted or rejected the data submission policy.
431    *
432    * If there was implicit acceptance, this will be set to the time of that.
433    */
434   get dataSubmissionPolicyResponseDate() {
435     return CommonUtils.getDatePref(this._prefs,
436                                    "dataSubmissionPolicyResponseTime",
437                                    0, this._log, OLDEST_ALLOWED_YEAR);
438   },
440   set dataSubmissionPolicyResponseDate(value) {
441     this._log.debug("Setting user notified reaction date: " + value);
442     CommonUtils.setDatePref(this._prefs,
443                             "dataSubmissionPolicyResponseTime",
444                             value, OLDEST_ALLOWED_YEAR);
445   },
447   /**
448    * Records the result of user notification of data submission policy.
449    *
450    * This is used for logging and diagnostics purposes. It can answer the
451    * question "how was data submission agreed to on this profile?"
452    *
453    * Not all values are defined by this type and can come from other systems.
454    *
455    * The value must be a string and should be something machine readable. e.g.
456    * "accept-user-clicked-ok-button-in-info-bar"
457    */
458   get dataSubmissionPolicyResponseType() {
459     return this._prefs.get("dataSubmissionPolicyResponseType",
460                            "none-recorded");
461   },
463   set dataSubmissionPolicyResponseType(value) {
464     if (typeof(value) != "string") {
465       throw new Error("Value must be a string. Got " + typeof(value));
466     }
468     this._prefs.set("dataSubmissionPolicyResponseType", value);
469   },
471   /**
472    * Whether submission of data is allowed.
473    *
474    * This is the master switch for remote server communication. If it is
475    * false, we never request upload or deletion.
476    */
477   get dataSubmissionEnabled() {
478     // Default is true because we are opt-out.
479     return this._prefs.get("dataSubmissionEnabled", true);
480   },
482   set dataSubmissionEnabled(value) {
483     this._prefs.set("dataSubmissionEnabled", !!value);
484   },
486   /**
487    * Whether the user has accepted that data submission can occur.
488    *
489    * This overrides dataSubmissionEnabled.
490    */
491   get dataSubmissionPolicyAccepted() {
492     // Be conservative and default to false.
493     return this._prefs.get("dataSubmissionPolicyAccepted", false);
494   },
496   set dataSubmissionPolicyAccepted(value) {
497     this._prefs.set("dataSubmissionPolicyAccepted", !!value);
498   },
500   set dataSubmissionPolicyAcceptedVersion(value) {
501     this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
502   },
504   /**
505    * The state of user notification of the data policy.
506    *
507    * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
508    * submission can occur.
509    *
510    * @return DataReportingPolicy.STATE_NOTIFY_* constant.
511    */
512   get notifyState() {
513     if (this.dataSubmissionPolicyResponseDate.getTime()) {
514       return this.STATE_NOTIFY_COMPLETE;
515     }
517     // We get the local state - not the state from prefs - because we don't want
518     // a value from a previous application run to interfere. This prevents
519     // a scenario where notification occurs just before application shutdown and
520     // notification is displayed for shorter than the policy requires.
521     if (!this._dataSubmissionPolicyNotifiedDate) {
522       return this.STATE_NOTIFY_UNNOTIFIED;
523     }
525     return this.STATE_NOTIFY_WAIT;
526   },
528   /**
529    * When this policy last requested data submission.
530    *
531    * This is used mainly for forensics purposes and should have no bearing
532    * on scheduling or run-time behavior.
533    */
534   get lastDataSubmissionRequestedDate() {
535     return CommonUtils.getDatePref(this._healthReportPrefs,
536                                    "lastDataSubmissionRequestedTime", 0,
537                                    this._log, OLDEST_ALLOWED_YEAR);
538   },
540   set lastDataSubmissionRequestedDate(value) {
541     CommonUtils.setDatePref(this._healthReportPrefs,
542                             "lastDataSubmissionRequestedTime",
543                             value, OLDEST_ALLOWED_YEAR);
544   },
546   /**
547    * When the last data submission actually occurred.
548    *
549    * This is used mainly for forensics purposes and should have no bearing on
550    * actual scheduling.
551    */
552   get lastDataSubmissionSuccessfulDate() {
553     return CommonUtils.getDatePref(this._healthReportPrefs,
554                                    "lastDataSubmissionSuccessfulTime", 0,
555                                    this._log, OLDEST_ALLOWED_YEAR);
556   },
558   set lastDataSubmissionSuccessfulDate(value) {
559     CommonUtils.setDatePref(this._healthReportPrefs,
560                             "lastDataSubmissionSuccessfulTime",
561                             value, OLDEST_ALLOWED_YEAR);
562   },
564   /**
565    * When we last encountered a submission failure.
566    *
567    * This is used for forensics purposes and should have no bearing on
568    * scheduling.
569    */
570   get lastDataSubmissionFailureDate() {
571     return CommonUtils.getDatePref(this._healthReportPrefs,
572                                    "lastDataSubmissionFailureTime",
573                                    0, this._log, OLDEST_ALLOWED_YEAR);
574   },
576   set lastDataSubmissionFailureDate(value) {
577     CommonUtils.setDatePref(this._healthReportPrefs,
578                             "lastDataSubmissionFailureTime",
579                             value, OLDEST_ALLOWED_YEAR);
580   },
582   /**
583    * When the next data submission is scheduled to occur.
584    *
585    * This is maintained internally by this type. External users should not
586    * mutate this value.
587    */
588   get nextDataSubmissionDate() {
589     return CommonUtils.getDatePref(this._healthReportPrefs,
590                                    "nextDataSubmissionTime", 0,
591                                    this._log, OLDEST_ALLOWED_YEAR);
592   },
594   set nextDataSubmissionDate(value) {
595     CommonUtils.setDatePref(this._healthReportPrefs,
596                             "nextDataSubmissionTime", value,
597                             OLDEST_ALLOWED_YEAR);
598   },
600   /**
601    * The number of submission failures for this day's upload.
602    *
603    * This is used to drive backoff and scheduling.
604    */
605   get currentDaySubmissionFailureCount() {
606     let v = this._healthReportPrefs.get("currentDaySubmissionFailureCount", 0);
608     if (!Number.isInteger(v)) {
609       v = 0;
610     }
612     return v;
613   },
615   set currentDaySubmissionFailureCount(value) {
616     if (!Number.isInteger(value)) {
617       throw new Error("Value must be integer: " + value);
618     }
620     this._healthReportPrefs.set("currentDaySubmissionFailureCount", value);
621   },
623   /**
624    * Whether a request to delete remote data is awaiting completion.
625    *
626    * If this is true, the policy will request that remote data be deleted.
627    * Furthermore, no new data will be uploaded (if it's even allowed) until
628    * the remote deletion is fulfilled.
629    */
630   get pendingDeleteRemoteData() {
631     return !!this._healthReportPrefs.get("pendingDeleteRemoteData", false);
632   },
634   set pendingDeleteRemoteData(value) {
635     this._healthReportPrefs.set("pendingDeleteRemoteData", !!value);
636   },
638   /**
639    * Whether upload of Firefox Health Report data is enabled.
640    */
641   get healthReportUploadEnabled() {
642     return !!this._healthReportPrefs.get("uploadEnabled", true);
643   },
645   // External callers should update this via `recordHealthReportUploadEnabled`
646   // to ensure appropriate side-effects are performed.
647   set healthReportUploadEnabled(value) {
648     this._healthReportPrefs.set("uploadEnabled", !!value);
649   },
651   /**
652    * Whether the FHR upload enabled setting is locked and can't be changed.
653    */
654   get healthReportUploadLocked() {
655     return this._healthReportPrefs.locked("uploadEnabled");
656   },
658   /**
659    * Record user acceptance of data submission policy.
660    *
661    * Data submission will not be allowed to occur until this is called.
662    *
663    * This is typically called through the `onUserAccept` property attached to
664    * the promise passed to `onUserNotify` in the policy listener. But, it can
665    * be called through other interfaces at any time and the call will have
666    * an impact on future data submissions.
667    *
668    * @param reason
669    *        (string) How the user accepted the data submission policy.
670    */
671   recordUserAcceptance: function recordUserAcceptance(reason="no-reason") {
672     this._log.info("User accepted data submission policy: " + reason);
673     this.dataSubmissionPolicyResponseDate = this.now();
674     this.dataSubmissionPolicyResponseType = "accepted-" + reason;
675     this.dataSubmissionPolicyAccepted = true;
676     this.dataSubmissionPolicyAcceptedVersion = 1;
677   },
679   /**
680    * Record user rejection of submission policy.
681    *
682    * Data submission will not be allowed to occur if this is called.
683    *
684    * This is typically called through the `onUserReject` property attached to
685    * the promise passed to `onUserNotify` in the policy listener. But, it can
686    * be called through other interfaces at any time and the call will have an
687    * impact on future data submissions.
688    */
689   recordUserRejection: function recordUserRejection(reason="no-reason") {
690     this._log.info("User rejected data submission policy: " + reason);
691     this.dataSubmissionPolicyResponseDate = this.now();
692     this.dataSubmissionPolicyResponseType = "rejected-" + reason;
693     this.dataSubmissionPolicyAccepted = false;
694   },
696   /**
697    * Record the user's intent for whether FHR should upload data.
698    *
699    * This is the preferred way for XUL applications to record a user's
700    * preference on whether Firefox Health Report should upload data to
701    * a server.
702    *
703    * If upload is disabled through this API, a request for remote data
704    * deletion is initiated automatically.
705    *
706    * If upload is being disabled and this operation is scheduled to
707    * occur immediately, a promise will be returned. This promise will be
708    * fulfilled when the deletion attempt finishes. If upload is being
709    * disabled and a promise is not returned, callers must poll
710    * `haveRemoteData` on the HealthReporter instance to see if remote
711    * data has been deleted.
712    *
713    * @param flag
714    *        (bool) Whether data submission is enabled or disabled.
715    * @param reason
716    *        (string) Why this value is being adjusted. For logging
717    *        purposes only.
718    */
719   recordHealthReportUploadEnabled: function (flag, reason="no-reason") {
720     let result = null;
721     if (!flag) {
722       result = this.deleteRemoteData(reason);
723     }
725     this.healthReportUploadEnabled = flag;
726     return result;
727   },
729   /**
730    * Request that remote data be deleted.
731    *
732    * This will record an intent that previously uploaded data is to be deleted.
733    * The policy will eventually issue a request to the listener for data
734    * deletion. It will keep asking for deletion until the listener acknowledges
735    * that data has been deleted.
736    */
737   deleteRemoteData: function deleteRemoteData(reason="no-reason") {
738     this._log.info("Remote data deletion requested: " + reason);
740     this.pendingDeleteRemoteData = true;
742     // We want delete deletion to occur as soon as possible. Move up any
743     // pending scheduled data submission and try to trigger.
744     this.nextDataSubmissionDate = this.now();
745     return this.checkStateAndTrigger();
746   },
748   /**
749    * Start background polling for activity.
750    *
751    * This will set up a recurring timer that will periodically check if
752    * activity is warranted.
753    *
754    * You typically call this function for each constructed instance.
755    */
756   startPolling: function startPolling() {
757     this.stopPolling();
759     this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
760     this._timer.initWithCallback({
761       notify: function notify() {
762         this.checkStateAndTrigger();
763       }.bind(this)
764     }, this.POLL_INTERVAL_MSEC, this._timer.TYPE_REPEATING_SLACK);
765   },
767   /**
768    * Stop background polling for activity.
769    *
770    * This should be called when the instance is no longer needed.
771    */
772   stopPolling: function stopPolling() {
773     if (this._timer) {
774       this._timer.cancel();
775       this._timer = null;
776     }
777   },
779   /**
780    * Abstraction for obtaining current time.
781    *
782    * The purpose of this is to facilitate testing. Testing code can monkeypatch
783    * this on instances instead of modifying the singleton Date object.
784    */
785   now: function now() {
786     return new Date();
787   },
789   /**
790    * Check state and trigger actions, if necessary.
791    *
792    * This is what enforces the submission and notification policy detailed
793    * above. You can think of this as the driver for health report data
794    * submission.
795    *
796    * Typically this function is called automatically by the background polling.
797    * But, it can safely be called manually as needed.
798    */
799   checkStateAndTrigger: function checkStateAndTrigger() {
800     // If the master data submission kill switch is toggled, we have nothing
801     // to do. We don't notify about data policies because this would have
802     // no effect.
803     if (!this.dataSubmissionEnabled) {
804       this._log.debug("Data submission is disabled. Doing nothing.");
805       return;
806     }
808     let now = this.now();
809     let nowT = now.getTime();
810     let nextSubmissionDate = this.nextDataSubmissionDate;
812     // If the system clock were ever set to a time in the distant future,
813     // it's possible our next schedule date is far out as well. We know
814     // we shouldn't schedule for more than a day out, so we reset the next
815     // scheduled date appropriately. 3 days was chosen arbitrarily.
816     if (nextSubmissionDate.getTime() >= nowT + 3 * MILLISECONDS_PER_DAY) {
817       this._log.warn("Next data submission time is far away. Was the system " +
818                      "clock recently readjusted? " + nextSubmissionDate);
820       // It shouldn't really matter what we set this to. 1 day in the future
821       // should be pretty safe.
822       this._moveScheduleForward24h();
824       // Fall through since we may have other actions.
825     }
827     // Tend to any in progress work.
828     if (this._processInProgressSubmission()) {
829       return;
830     }
832     // Requests to delete remote data take priority above everything else.
833     if (this.pendingDeleteRemoteData) {
834       if (nowT < nextSubmissionDate.getTime()) {
835         this._log.debug("Deletion request is scheduled for the future: " +
836                         nextSubmissionDate);
837         return;
838       }
840       return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
841     }
843     if (!this.healthReportUploadEnabled) {
844       this._log.debug("Data upload is disabled. Doing nothing.");
845       return;
846     }
848     // If the user hasn't responded to the data policy, don't do anything.
849     if (!this.ensureNotifyResponse(now)) {
850       return;
851     }
853     // User has opted out of data submission.
854     if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) {
855       this._log.debug("Data submission has been disabled per user request.");
856       return;
857     }
859     // User has responded to data policy and data submission is enabled. Now
860     // comes the scheduling part.
862     if (nowT < nextSubmissionDate.getTime()) {
863       this._log.debug("Next data submission is scheduled in the future: " +
864                      nextSubmissionDate);
865       return;
866     }
868     return this._dispatchSubmissionRequest("onRequestDataUpload", false);
869   },
871   /**
872    * Ensure user has responded to data submission policy.
873    *
874    * This must be called before data submission. If the policy has not been
875    * responded to, data submission must not occur.
876    *
877    * @return bool Whether user has responded to data policy.
878    */
879   ensureNotifyResponse: function ensureNotifyResponse(now) {
880     if (this.dataSubmissionPolicyBypassAcceptance) {
881       return true;
882     }
884     let notifyState = this.notifyState;
886     if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) {
887       let notifyAt = new Date(this.firstRunDate.getTime() +
888                               this.SUBMISSION_NOTIFY_INTERVAL_MSEC);
890       if (now.getTime() < notifyAt.getTime()) {
891         this._log.debug("Don't have to notify about data submission yet.");
892         return false;
893       }
895       let onComplete = function onComplete() {
896         this._log.info("Data submission notification presented.");
897         let now = this.now();
899         this._dataSubmissionPolicyNotifiedDate = now;
900         this.dataSubmissionPolicyNotifiedDate = now;
901       }.bind(this);
903       let deferred = Promise.defer();
905       deferred.promise.then(onComplete, function onError(error) {
906         this._log.warn("Data policy notification presentation failed: " +
907                        CommonUtils.exceptionStr(error));
908       });
910       this._log.info("Requesting display of data policy.");
911       let request = new NotifyPolicyRequest(this, deferred);
913       try {
914         this._listener.onNotifyDataPolicy(request);
915       } catch (ex) {
916         this._log.warn("Exception when calling onNotifyDataPolicy: " +
917                        CommonUtils.exceptionStr(ex));
918       }
919       return false;
920     }
922     // We're waiting for user action or implicit acceptance after display.
923     if (notifyState == this.STATE_NOTIFY_WAIT) {
924       // Check for implicit acceptance.
925       let implicitAcceptance =
926         this._dataSubmissionPolicyNotifiedDate.getTime() +
927         this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC;
929       this._log.debug("Now: " + now.getTime());
930       this._log.debug("Will accept: " + implicitAcceptance);
931       if (now.getTime() < implicitAcceptance) {
932         this._log.debug("Still waiting for reaction or implicit acceptance. " +
933                         "Now: " + now.getTime() + " < " +
934                         "Accept: " + implicitAcceptance);
935         return false;
936       }
938       this.recordUserAcceptance("implicit-time-elapsed");
939       return true;
940     }
942     // If this happens, we have a coding error in this file.
943     if (notifyState != this.STATE_NOTIFY_COMPLETE) {
944       throw new Error("Unknown notification state: " + notifyState);
945     }
947     return true;
948   },
950   _processInProgressSubmission: function _processInProgressSubmission() {
951     if (!this._inProgressSubmissionRequest) {
952       return false;
953     }
955     let now = this.now().getTime();
956     if (this._inProgressSubmissionRequest.expiresDate.getTime() > now) {
957       this._log.info("Waiting on in-progress submission request to finish.");
958       return true;
959     }
961     this._log.warn("Old submission request has expired from no activity.");
962     this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired."));
963     this._inProgressSubmissionRequest = null;
964     this._handleSubmissionFailure();
966     return false;
967   },
969   _dispatchSubmissionRequest: function _dispatchSubmissionRequest(handler, isDelete) {
970     let now = this.now();
972     // We're past our scheduled next data submission date, so let's do it!
973     this.lastDataSubmissionRequestedDate = now;
974     let deferred = Promise.defer();
975     let requestExpiresDate =
976       this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC);
977     this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred,
978                                                                   requestExpiresDate,
979                                                                   isDelete);
981     let onSuccess = function onSuccess(result) {
982       this._inProgressSubmissionRequest = null;
983       this._handleSubmissionResult(result);
984     }.bind(this);
986     let onError = function onError(error) {
987       this._log.error("Error when handling data submission result: " +
988                       CommonUtils.exceptionStr(result));
989       this._inProgressSubmissionRequest = null;
990       this._handleSubmissionFailure();
991     }.bind(this);
993     let chained = deferred.promise.then(onSuccess, onError);
995     this._log.info("Requesting data submission. Will expire at " +
996                    requestExpiresDate);
997     try {
998       this._listener[handler](this._inProgressSubmissionRequest);
999     } catch (ex) {
1000       this._log.warn("Exception when calling " + handler + ": " +
1001                      CommonUtils.exceptionStr(ex));
1002       this._inProgressSubmissionRequest = null;
1003       this._handleSubmissionFailure();
1004       return;
1005     }
1007     return chained;
1008   },
1010   _handleSubmissionResult: function _handleSubmissionResult(request) {
1011     let state = request.state;
1012     let reason = request.reason || "no reason";
1013     this._log.info("Got submission request result: " + state);
1015     if (state == request.SUBMISSION_SUCCESS) {
1016       if (request.isDelete) {
1017         this.pendingDeleteRemoteData = false;
1018         this._log.info("Successful data delete reported.");
1019       } else {
1020         this._log.info("Successful data upload reported.");
1021       }
1023       this.lastDataSubmissionSuccessfulDate = request.submissionDate;
1025       let nextSubmissionDate =
1026         new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY);
1028       // Schedule pending deletes immediately. This has potential to overload
1029       // the server. However, the frequency of delete requests across all
1030       // clients should be low, so this shouldn't pose a problem.
1031       if (this.pendingDeleteRemoteData) {
1032         nextSubmissionDate = this.now();
1033       }
1035       this.nextDataSubmissionDate = nextSubmissionDate;
1036       this.currentDaySubmissionFailureCount = 0;
1037       return;
1038     }
1040     if (state == request.NO_DATA_AVAILABLE) {
1041       if (request.isDelete) {
1042         this._log.info("Remote data delete requested but no remote data was stored.");
1043         this.pendingDeleteRemoteData = false;
1044         return;
1045       }
1047       this._log.info("No data was available to submit. May try later.");
1048       this._handleSubmissionFailure();
1049       return;
1050     }
1052     // We don't special case request.isDelete for these failures because it
1053     // likely means there was a server error.
1055     if (state == request.SUBMISSION_FAILURE_SOFT) {
1056       this._log.warn("Soft error submitting data: " + reason);
1057       this.lastDataSubmissionFailureDate = this.now();
1058       this._handleSubmissionFailure();
1059       return;
1060     }
1062     if (state == request.SUBMISSION_FAILURE_HARD) {
1063       this._log.warn("Hard error submitting data: " + reason);
1064       this.lastDataSubmissionFailureDate = this.now();
1065       this._moveScheduleForward24h();
1066       return;
1067     }
1069     throw new Error("Unknown state on DataSubmissionRequest: " + request.state);
1070   },
1072   _handleSubmissionFailure: function _handleSubmissionFailure() {
1073     if (this.currentDaySubmissionFailureCount >= this.FAILURE_BACKOFF_INTERVALS.length) {
1074       this._log.warn("Reached the limit of daily submission attempts. " +
1075                      "Rescheduling for tomorrow.");
1076       this._moveScheduleForward24h();
1077       return false;
1078     }
1080     let offset = this.FAILURE_BACKOFF_INTERVALS[this.currentDaySubmissionFailureCount];
1081     this.nextDataSubmissionDate = this._futureDate(offset);
1082     this.currentDaySubmissionFailureCount++;
1083     return true;
1084   },
1086   _moveScheduleForward24h: function _moveScheduleForward24h() {
1087     let d = this._futureDate(MILLISECONDS_PER_DAY);
1088     this._log.info("Setting next scheduled data submission for " + d);
1090     this.nextDataSubmissionDate = d;
1091     this.currentDaySubmissionFailureCount = 0;
1092   },
1094   _futureDate: function _futureDate(offset) {
1095     return new Date(this.now().getTime() + offset);
1096   },