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/. */
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.
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.
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;
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;
40 * Represents a request to display data policy.
42 * Instances of this are created when the policy is requesting the user's
43 * approval to agree to the data submission policy.
45 * Receivers of these instances are expected to call one or more of the on*
46 * functions when events occur.
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`.
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}.
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
67 * Receivers of instances of this type should treat it as a black box with
68 * the exception of the on* functions.
71 * (DataReportingPolicy) The policy instance this request came from.
73 * (deferred) The promise that will be fulfilled when display occurs.
75 function NotifyPolicyRequest(policy, deferred) {
77 this.deferred = deferred;
79 NotifyPolicyRequest.prototype = {
81 * Called when the user is notified of the policy.
83 * This starts a countdown timer that will eventually signify implicit
84 * acceptance of the data policy.
86 onUserNotifyComplete: function onUserNotified() {
87 this.deferred.resolve();
88 return this.deferred.promise;
92 * Called when there was an error notifying the user about the policy.
95 * (Error) Explains what went wrong.
97 onUserNotifyFailed: function onUserNotifyFailed(error) {
98 this.deferred.reject(error);
102 * Called when the user agreed to the data policy.
105 * (string) How the user agreed to the policy.
107 onUserAccept: function onUserAccept(reason) {
108 this.policy.recordUserAcceptance(reason);
112 * Called when the user rejected the data policy.
115 * (string) How the user rejected the policy.
117 onUserReject: function onUserReject(reason) {
118 this.policy.recordUserRejection(reason);
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
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.
137 this.DataSubmissionRequest = function (promise, expiresDate, isDelete) {
138 this.promise = promise;
139 this.expiresDate = expiresDate;
140 this.isDelete = isDelete;
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",
153 * No submission was attempted because no data was available.
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.
159 onNoDataAvailable: function onNoDataAvailable() {
160 this.state = this.NO_DATA_AVAILABLE;
161 this.promise.resolve(this);
162 return this.promise.promise;
166 * Data submission has completed successfully.
168 * In case of upload, this means the upload completed successfully. In case
169 * of deletion, the data was deleted successfully.
172 * (Date) When data submission occurred.
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;
182 * There was a recoverable failure when submitting data.
184 * Perhaps the server was down. Perhaps the network wasn't available. The
185 * policy may request submission again after a short delay.
188 * (string) Why the failure occurred. For logging purposes only.
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;
198 * There was an unrecoverable failure when submitting data.
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).
205 * (string) Why the failure occurred. For logging purposes only.
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;
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
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.
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.
262 * (object) Object with callbacks that will be invoked at certain key
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: " +
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();
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.
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.
297 this._log.info("uploadEnabled pref changed. Scheduling deletion.");
298 this.deleteRemoteData();
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);
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({
320 * How long after first run we should notify about data submission.
322 SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
325 * Time that must elapse with no user action for implicit acceptance.
327 * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
328 * Privacy and/or Legal before modifying.
330 IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
333 * How often to poll to see if we need to do something.
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.
339 * The random bit is to ensure that other systems scheduling around the same
340 * interval don't all get scheduled together.
342 POLL_INTERVAL_MSEC: (60 * 1000) + Math.floor(2.5 * 1000 * Math.random()),
345 * How long individual data submission requests live before expiring.
347 * Data submission requests have this long to complete before we give up on
348 * them and try again.
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.
353 SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC: 10 * 60 * 1000,
356 * Our backoff schedule in case of submission failure.
358 * This dictates both the number of times we retry a daily submission and
359 * when to retry after each failure.
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.
367 FAILURE_BACKOFF_INTERVALS: [
373 * State of user notification of data submission.
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",
386 * The first time the health report policy came into existence.
388 * This is used for scheduling of the initial submission.
391 return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log,
392 OLDEST_ALLOWED_YEAR);
395 set firstRunDate(value) {
396 this._log.debug("Setting first-run date: " + value);
397 CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
398 OLDEST_ALLOWED_YEAR);
402 * Short circuit policy checking and always assume acceptance.
404 * This shuld never be set by the user. Instead, it is a per-application or
405 * per-deployment default pref.
407 get dataSubmissionPolicyBypassAcceptance() {
408 return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
412 * When the user was notified that data submission could occur.
414 * This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
415 * is what's used internally.
417 get dataSubmissionPolicyNotifiedDate() {
418 return CommonUtils.getDatePref(this._prefs,
419 "dataSubmissionPolicyNotifiedTime", 0,
420 this._log, OLDEST_ALLOWED_YEAR);
423 set dataSubmissionPolicyNotifiedDate(value) {
424 this._log.debug("Setting user notified date: " + value);
425 CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
426 value, OLDEST_ALLOWED_YEAR);
430 * When the user accepted or rejected the data submission policy.
432 * If there was implicit acceptance, this will be set to the time of that.
434 get dataSubmissionPolicyResponseDate() {
435 return CommonUtils.getDatePref(this._prefs,
436 "dataSubmissionPolicyResponseTime",
437 0, this._log, OLDEST_ALLOWED_YEAR);
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);
448 * Records the result of user notification of data submission policy.
450 * This is used for logging and diagnostics purposes. It can answer the
451 * question "how was data submission agreed to on this profile?"
453 * Not all values are defined by this type and can come from other systems.
455 * The value must be a string and should be something machine readable. e.g.
456 * "accept-user-clicked-ok-button-in-info-bar"
458 get dataSubmissionPolicyResponseType() {
459 return this._prefs.get("dataSubmissionPolicyResponseType",
463 set dataSubmissionPolicyResponseType(value) {
464 if (typeof(value) != "string") {
465 throw new Error("Value must be a string. Got " + typeof(value));
468 this._prefs.set("dataSubmissionPolicyResponseType", value);
472 * Whether submission of data is allowed.
474 * This is the master switch for remote server communication. If it is
475 * false, we never request upload or deletion.
477 get dataSubmissionEnabled() {
478 // Default is true because we are opt-out.
479 return this._prefs.get("dataSubmissionEnabled", true);
482 set dataSubmissionEnabled(value) {
483 this._prefs.set("dataSubmissionEnabled", !!value);
487 * Whether the user has accepted that data submission can occur.
489 * This overrides dataSubmissionEnabled.
491 get dataSubmissionPolicyAccepted() {
492 // Be conservative and default to false.
493 return this._prefs.get("dataSubmissionPolicyAccepted", false);
496 set dataSubmissionPolicyAccepted(value) {
497 this._prefs.set("dataSubmissionPolicyAccepted", !!value);
500 set dataSubmissionPolicyAcceptedVersion(value) {
501 this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
505 * The state of user notification of the data policy.
507 * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
508 * submission can occur.
510 * @return DataReportingPolicy.STATE_NOTIFY_* constant.
513 if (this.dataSubmissionPolicyResponseDate.getTime()) {
514 return this.STATE_NOTIFY_COMPLETE;
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;
525 return this.STATE_NOTIFY_WAIT;
529 * When this policy last requested data submission.
531 * This is used mainly for forensics purposes and should have no bearing
532 * on scheduling or run-time behavior.
534 get lastDataSubmissionRequestedDate() {
535 return CommonUtils.getDatePref(this._healthReportPrefs,
536 "lastDataSubmissionRequestedTime", 0,
537 this._log, OLDEST_ALLOWED_YEAR);
540 set lastDataSubmissionRequestedDate(value) {
541 CommonUtils.setDatePref(this._healthReportPrefs,
542 "lastDataSubmissionRequestedTime",
543 value, OLDEST_ALLOWED_YEAR);
547 * When the last data submission actually occurred.
549 * This is used mainly for forensics purposes and should have no bearing on
552 get lastDataSubmissionSuccessfulDate() {
553 return CommonUtils.getDatePref(this._healthReportPrefs,
554 "lastDataSubmissionSuccessfulTime", 0,
555 this._log, OLDEST_ALLOWED_YEAR);
558 set lastDataSubmissionSuccessfulDate(value) {
559 CommonUtils.setDatePref(this._healthReportPrefs,
560 "lastDataSubmissionSuccessfulTime",
561 value, OLDEST_ALLOWED_YEAR);
565 * When we last encountered a submission failure.
567 * This is used for forensics purposes and should have no bearing on
570 get lastDataSubmissionFailureDate() {
571 return CommonUtils.getDatePref(this._healthReportPrefs,
572 "lastDataSubmissionFailureTime",
573 0, this._log, OLDEST_ALLOWED_YEAR);
576 set lastDataSubmissionFailureDate(value) {
577 CommonUtils.setDatePref(this._healthReportPrefs,
578 "lastDataSubmissionFailureTime",
579 value, OLDEST_ALLOWED_YEAR);
583 * When the next data submission is scheduled to occur.
585 * This is maintained internally by this type. External users should not
588 get nextDataSubmissionDate() {
589 return CommonUtils.getDatePref(this._healthReportPrefs,
590 "nextDataSubmissionTime", 0,
591 this._log, OLDEST_ALLOWED_YEAR);
594 set nextDataSubmissionDate(value) {
595 CommonUtils.setDatePref(this._healthReportPrefs,
596 "nextDataSubmissionTime", value,
597 OLDEST_ALLOWED_YEAR);
601 * The number of submission failures for this day's upload.
603 * This is used to drive backoff and scheduling.
605 get currentDaySubmissionFailureCount() {
606 let v = this._healthReportPrefs.get("currentDaySubmissionFailureCount", 0);
608 if (!Number.isInteger(v)) {
615 set currentDaySubmissionFailureCount(value) {
616 if (!Number.isInteger(value)) {
617 throw new Error("Value must be integer: " + value);
620 this._healthReportPrefs.set("currentDaySubmissionFailureCount", value);
624 * Whether a request to delete remote data is awaiting completion.
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.
630 get pendingDeleteRemoteData() {
631 return !!this._healthReportPrefs.get("pendingDeleteRemoteData", false);
634 set pendingDeleteRemoteData(value) {
635 this._healthReportPrefs.set("pendingDeleteRemoteData", !!value);
639 * Whether upload of Firefox Health Report data is enabled.
641 get healthReportUploadEnabled() {
642 return !!this._healthReportPrefs.get("uploadEnabled", true);
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);
652 * Whether the FHR upload enabled setting is locked and can't be changed.
654 get healthReportUploadLocked() {
655 return this._healthReportPrefs.locked("uploadEnabled");
659 * Record user acceptance of data submission policy.
661 * Data submission will not be allowed to occur until this is called.
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.
669 * (string) How the user accepted the data submission policy.
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;
680 * Record user rejection of submission policy.
682 * Data submission will not be allowed to occur if this is called.
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.
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;
697 * Record the user's intent for whether FHR should upload data.
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
703 * If upload is disabled through this API, a request for remote data
704 * deletion is initiated automatically.
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.
714 * (bool) Whether data submission is enabled or disabled.
716 * (string) Why this value is being adjusted. For logging
719 recordHealthReportUploadEnabled: function (flag, reason="no-reason") {
722 result = this.deleteRemoteData(reason);
725 this.healthReportUploadEnabled = flag;
730 * Request that remote data be deleted.
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.
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();
749 * Start background polling for activity.
751 * This will set up a recurring timer that will periodically check if
752 * activity is warranted.
754 * You typically call this function for each constructed instance.
756 startPolling: function startPolling() {
759 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
760 this._timer.initWithCallback({
761 notify: function notify() {
762 this.checkStateAndTrigger();
764 }, this.POLL_INTERVAL_MSEC, this._timer.TYPE_REPEATING_SLACK);
768 * Stop background polling for activity.
770 * This should be called when the instance is no longer needed.
772 stopPolling: function stopPolling() {
774 this._timer.cancel();
780 * Abstraction for obtaining current time.
782 * The purpose of this is to facilitate testing. Testing code can monkeypatch
783 * this on instances instead of modifying the singleton Date object.
785 now: function now() {
790 * Check state and trigger actions, if necessary.
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
796 * Typically this function is called automatically by the background polling.
797 * But, it can safely be called manually as needed.
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
803 if (!this.dataSubmissionEnabled) {
804 this._log.debug("Data submission is disabled. Doing nothing.");
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.
827 // Tend to any in progress work.
828 if (this._processInProgressSubmission()) {
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: " +
840 return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
843 if (!this.healthReportUploadEnabled) {
844 this._log.debug("Data upload is disabled. Doing nothing.");
848 // If the user hasn't responded to the data policy, don't do anything.
849 if (!this.ensureNotifyResponse(now)) {
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.");
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: " +
868 return this._dispatchSubmissionRequest("onRequestDataUpload", false);
872 * Ensure user has responded to data submission policy.
874 * This must be called before data submission. If the policy has not been
875 * responded to, data submission must not occur.
877 * @return bool Whether user has responded to data policy.
879 ensureNotifyResponse: function ensureNotifyResponse(now) {
880 if (this.dataSubmissionPolicyBypassAcceptance) {
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.");
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;
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));
910 this._log.info("Requesting display of data policy.");
911 let request = new NotifyPolicyRequest(this, deferred);
914 this._listener.onNotifyDataPolicy(request);
916 this._log.warn("Exception when calling onNotifyDataPolicy: " +
917 CommonUtils.exceptionStr(ex));
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);
938 this.recordUserAcceptance("implicit-time-elapsed");
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);
950 _processInProgressSubmission: function _processInProgressSubmission() {
951 if (!this._inProgressSubmissionRequest) {
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.");
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();
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,
981 let onSuccess = function onSuccess(result) {
982 this._inProgressSubmissionRequest = null;
983 this._handleSubmissionResult(result);
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();
993 let chained = deferred.promise.then(onSuccess, onError);
995 this._log.info("Requesting data submission. Will expire at " +
998 this._listener[handler](this._inProgressSubmissionRequest);
1000 this._log.warn("Exception when calling " + handler + ": " +
1001 CommonUtils.exceptionStr(ex));
1002 this._inProgressSubmissionRequest = null;
1003 this._handleSubmissionFailure();
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.");
1020 this._log.info("Successful data upload reported.");
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();
1035 this.nextDataSubmissionDate = nextSubmissionDate;
1036 this.currentDaySubmissionFailureCount = 0;
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;
1047 this._log.info("No data was available to submit. May try later.");
1048 this._handleSubmissionFailure();
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();
1062 if (state == request.SUBMISSION_FAILURE_HARD) {
1063 this._log.warn("Hard error submitting data: " + reason);
1064 this.lastDataSubmissionFailureDate = this.now();
1065 this._moveScheduleForward24h();
1069 throw new Error("Unknown state on DataSubmissionRequest: " + request.state);
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();
1080 let offset = this.FAILURE_BACKOFF_INTERVALS[this.currentDaySubmissionFailureCount];
1081 this.nextDataSubmissionDate = this._futureDate(offset);
1082 this.currentDaySubmissionFailureCount++;
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;
1094 _futureDate: function _futureDate(offset) {
1095 return new Date(this.now().getTime() + offset);