michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%"; michael@0: const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; michael@0: const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval"; michael@0: const PREF_APP_UPDATE_LOG = "app.update.log"; michael@0: michael@0: const CATEGORY_UPDATE_TIMER = "update-timer"; michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function tm_gLogEnabled() { michael@0: return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); michael@0: }); michael@0: michael@0: /** michael@0: * Gets a preference value, handling the case where there is no default. michael@0: * @param func michael@0: * The name of the preference function to call, on nsIPrefBranch michael@0: * @param preference michael@0: * The name of the preference michael@0: * @param defaultValue michael@0: * The default value to return in the event the preference has michael@0: * no setting michael@0: * @returns The value of the preference, or undefined if there was no michael@0: * user or default value. michael@0: */ michael@0: function getPref(func, preference, defaultValue) { michael@0: try { michael@0: return Services.prefs[func](preference); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return defaultValue; michael@0: } michael@0: michael@0: /** michael@0: * Logs a string to the error console. michael@0: * @param string michael@0: * The string to write to the error console. michael@0: */ michael@0: function LOG(string) { michael@0: if (gLogEnabled) { michael@0: dump("*** UTM:SVC " + string + "\n"); michael@0: Services.console.logStringMessage("UTM:SVC " + string); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A manager for timers. Manages timers that fire over long periods of time michael@0: * (e.g. days, weeks, months). michael@0: * @constructor michael@0: */ michael@0: function TimerManager() { michael@0: Services.obs.addObserver(this, "xpcom-shutdown", false); michael@0: } michael@0: TimerManager.prototype = { michael@0: /** michael@0: * The Checker Timer michael@0: */ michael@0: _timer: null, michael@0: michael@0: /** michael@0: * The Checker Timer minimum delay interval as specified by the michael@0: * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay michael@0: * pref doesn't exist this will default to 120000. michael@0: */ michael@0: _timerMinimumDelay: null, michael@0: michael@0: /** michael@0: * The set of registered timers. michael@0: */ michael@0: _timers: { }, michael@0: michael@0: /** michael@0: * See nsIObserver.idl michael@0: */ michael@0: observe: function TM_observe(aSubject, aTopic, aData) { michael@0: // Prevent setting the timer interval to a value of less than 30 seconds. michael@0: var minInterval = 30000; michael@0: // Prevent setting the first timer interval to a value of less than 10 michael@0: // seconds. michael@0: var minFirstInterval = 10000; michael@0: switch (aTopic) { michael@0: case "utm-test-init": michael@0: // Enforce a minimum timer interval of 500 ms for tests and fall through michael@0: // to profile-after-change to initialize the timer. michael@0: minInterval = 500; michael@0: minFirstInterval = 500; michael@0: case "profile-after-change": michael@0: // Cancel the timer if it has already been initialized. This is primarily michael@0: // for tests. michael@0: this._timerMinimumDelay = Math.max(1000 * getPref("getIntPref", PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120), michael@0: minInterval); michael@0: let firstInterval = Math.max(getPref("getIntPref", PREF_APP_UPDATE_TIMERFIRSTINTERVAL, michael@0: this._timerMinimumDelay), minFirstInterval); michael@0: this._canEnsureTimer = true; michael@0: this._ensureTimer(firstInterval); michael@0: break; michael@0: case "xpcom-shutdown": michael@0: Services.obs.removeObserver(this, "xpcom-shutdown"); michael@0: michael@0: // Release everything we hold onto. michael@0: this._cancelTimer(); michael@0: for (var timerID in this._timers) michael@0: delete this._timers[timerID]; michael@0: this._timers = null; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when the checking timer fires. michael@0: * michael@0: * We only fire one notification each time, so that the operations are michael@0: * staggered. We don't want too many to happen at once, which could michael@0: * negatively impact responsiveness. michael@0: * michael@0: * @param timer michael@0: * The checking timer that fired. michael@0: */ michael@0: notify: function TM_notify(timer) { michael@0: var nextDelay = null; michael@0: function updateNextDelay(delay) { michael@0: if (nextDelay === null || delay < nextDelay) michael@0: nextDelay = delay; michael@0: } michael@0: michael@0: // Each timer calls tryFire(), which figures out which is the one that michael@0: // wanted to be called earliest. That one will be fired; the others are michael@0: // skipped and will be done later. michael@0: var now = Math.round(Date.now() / 1000); michael@0: michael@0: var callbackToFire = null; michael@0: var earliestIntendedTime = null; michael@0: var skippedFirings = false; michael@0: function tryFire(callback, intendedTime) { michael@0: var selected = false; michael@0: if (intendedTime <= now) { michael@0: if (intendedTime < earliestIntendedTime || michael@0: earliestIntendedTime === null) { michael@0: callbackToFire = callback; michael@0: earliestIntendedTime = intendedTime; michael@0: selected = true; michael@0: } michael@0: else if (earliestIntendedTime !== null) michael@0: skippedFirings = true; michael@0: } michael@0: // We do not need to updateNextDelay for the timer that actually fires; michael@0: // we'll update right after it fires, with the proper intended time. michael@0: // Note that we might select one, then select another later (with an michael@0: // earlier intended time); it is still ok that we did not update for michael@0: // the first one, since if we have skipped firings, the next delay michael@0: // will be the minimum delay anyhow. michael@0: if (!selected) michael@0: updateNextDelay(intendedTime - now); michael@0: } michael@0: michael@0: var catMan = Cc["@mozilla.org/categorymanager;1"]. michael@0: getService(Ci.nsICategoryManager); michael@0: var entries = catMan.enumerateCategory(CATEGORY_UPDATE_TIMER); michael@0: while (entries.hasMoreElements()) { michael@0: let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; michael@0: let value = catMan.getCategoryEntry(CATEGORY_UPDATE_TIMER, entry); michael@0: let [cid, method, timerID, prefInterval, defaultInterval] = value.split(","); michael@0: michael@0: defaultInterval = parseInt(defaultInterval); michael@0: // cid and method are validated below when calling notify. michael@0: if (!timerID || !defaultInterval || isNaN(defaultInterval)) { michael@0: LOG("TimerManager:notify - update-timer category registered" + michael@0: (cid ? " for " + cid : "") + " without required parameters - " + michael@0: "skipping"); michael@0: continue; michael@0: } michael@0: michael@0: let interval = getPref("getIntPref", prefInterval, defaultInterval); michael@0: let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, michael@0: timerID); michael@0: // Initialize the last update time to 0 when the preference isn't set so michael@0: // the timer will be notified soon after a new profile's first use. michael@0: let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0); michael@0: michael@0: // If the last update time is greater than the current time then reset michael@0: // it to 0 and the timer manager will correct the value when it fires michael@0: // next for this consumer. michael@0: if (lastUpdateTime > now) michael@0: lastUpdateTime = 0; michael@0: michael@0: if (lastUpdateTime == 0) michael@0: Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); michael@0: michael@0: tryFire(function() { michael@0: try { michael@0: Components.classes[cid][method](Ci.nsITimerCallback).notify(timer); michael@0: LOG("TimerManager:notify - notified " + cid); michael@0: } michael@0: catch (e) { michael@0: LOG("TimerManager:notify - error notifying component id: " + michael@0: cid + " ,error: " + e); michael@0: } michael@0: lastUpdateTime = now; michael@0: Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); michael@0: updateNextDelay(lastUpdateTime + interval - now); michael@0: }, lastUpdateTime + interval); michael@0: } michael@0: michael@0: for (let _timerID in this._timers) { michael@0: let timerID = _timerID; // necessary for the closure to work properly michael@0: let timerData = this._timers[timerID]; michael@0: // If the last update time is greater than the current time then reset michael@0: // it to 0 and the timer manager will correct the value when it fires michael@0: // next for this consumer. michael@0: if (timerData.lastUpdateTime > now) { michael@0: let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); michael@0: timerData.lastUpdateTime = 0; michael@0: Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime); michael@0: } michael@0: tryFire(function() { michael@0: if (timerData.callback && timerData.callback.notify) { michael@0: try { michael@0: timerData.callback.notify(timer); michael@0: LOG("TimerManager:notify - notified timerID: " + timerID); michael@0: } michael@0: catch (e) { michael@0: LOG("TimerManager:notify - error notifying timerID: " + timerID + michael@0: ", error: " + e); michael@0: } michael@0: } michael@0: else { michael@0: LOG("TimerManager:notify - timerID: " + timerID + " doesn't " + michael@0: "implement nsITimerCallback - skipping"); michael@0: } michael@0: lastUpdateTime = now; michael@0: timerData.lastUpdateTime = lastUpdateTime; michael@0: let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); michael@0: Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); michael@0: updateNextDelay(timerData.lastUpdateTime + timerData.interval - now); michael@0: }, timerData.lastUpdateTime + timerData.interval); michael@0: } michael@0: michael@0: if (callbackToFire) michael@0: callbackToFire(); michael@0: michael@0: if (nextDelay !== null) { michael@0: if (skippedFirings) michael@0: timer.delay = this._timerMinimumDelay; michael@0: else michael@0: timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay); michael@0: this.lastTimerReset = Date.now(); michael@0: } else { michael@0: this._cancelTimer(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Starts the timer, if necessary, and ensures that it will fire soon enough michael@0: * to happen after time |interval| (in milliseconds). michael@0: */ michael@0: _ensureTimer: function(interval) { michael@0: if (!this._canEnsureTimer) michael@0: return; michael@0: if (!this._timer) { michael@0: this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this._timer.initWithCallback(this, interval, michael@0: Ci.nsITimer.TYPE_REPEATING_SLACK); michael@0: this.lastTimerReset = Date.now(); michael@0: } else { michael@0: if (Date.now() + interval < this.lastTimerReset + this._timer.delay) michael@0: this._timer.delay = Math.max(this.lastTimerReset + interval - Date.now(), 0); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Stops the timer, if it is running. michael@0: */ michael@0: _cancelTimer: function() { michael@0: if (this._timer) { michael@0: this._timer.cancel(); michael@0: this._timer = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIUpdateTimerManager.idl michael@0: */ michael@0: registerTimer: function TM_registerTimer(id, callback, interval) { michael@0: LOG("TimerManager:registerTimer - id: " + id); michael@0: let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id); michael@0: // Initialize the last update time to 0 when the preference isn't set so michael@0: // the timer will be notified soon after a new profile's first use. michael@0: let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0); michael@0: let now = Math.round(Date.now() / 1000); michael@0: if (lastUpdateTime > now) michael@0: lastUpdateTime = 0; michael@0: if (lastUpdateTime == 0) michael@0: Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); michael@0: this._timers[id] = { callback : callback, michael@0: interval : interval, michael@0: lastUpdateTime : lastUpdateTime }; michael@0: michael@0: this._ensureTimer(interval * 1000); michael@0: }, michael@0: michael@0: classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateTimerManager, michael@0: Ci.nsITimerCallback, michael@0: Ci.nsIObserver]) michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TimerManager]);