diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index 946ffc61fa7..bb5fedc3b1b 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -747,6 +747,14 @@ pref("ping.manifestURL", "https://marketplace.firefox.com/packaged.webapp"); // Enable the disk space watcher pref("disk_space_watcher.enabled", true); +// SNTP preferences. +pref("network.sntp.maxRetryCount", 10); +pref("network.sntp.refreshPeriod", 86400); // In seconds. +pref("network.sntp.pools", // Servers separated by ';'. + "0.pool.ntp.org;1.pool.ntp.org;2.pool.ntp.org;3.pool.ntp.org"); +pref("network.sntp.port", 123); +pref("network.sntp.timeout", 30); // In seconds. + // Enable promise pref("dom.promise.enabled", false); diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js index 34ce2807249..1e405a741e1 100644 --- a/dom/system/gonk/RadioInterfaceLayer.js +++ b/dom/system/gonk/RadioInterfaceLayer.js @@ -19,6 +19,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Sntp.jsm"); var RIL = {}; Cu.import("resource://gre/modules/ril_consts.js", RIL); @@ -58,8 +59,10 @@ const kMozSettingsChangedObserverTopic = "mozsettings-changed"; const kSysMsgListenerReadyObserverTopic = "system-message-listener-ready"; const kSysClockChangeObserverTopic = "system-clock-change"; const kScreenStateChangedTopic = "screen-state-changed"; -const kTimeNitzAutomaticUpdateEnabled = "time.nitz.automatic-update.enabled"; -const kTimeNitzAvailable = "time.nitz.available"; +const kClockAutoUpdateEnabled = "time.clock.automatic-update.enabled"; +const kClockAutoUpdateAvailable = "time.clock.automatic-update.available"; +const kTimezoneAutoUpdateEnabled = "time.timezone.automatic-update.enabled"; +const kTimezoneAutoUpdateAvailable = "time.timezone.automatic-update.available"; const kCellBroadcastSearchList = "ril.cellbroadcast.searchlist"; const kCellBroadcastDisabled = "ril.cellbroadcast.disabled"; const kPrefenceChangedObserverTopic = "nsPref:changed"; @@ -709,12 +712,19 @@ function RadioInterface(options) { lock.get("ril.data.enabled", this); lock.get("ril.data.apnSettings", this); - // Read the 'time.nitz.automatic-update.enabled' setting to see if - // we need to adjust the system clock time and time zone by NITZ. - lock.get(kTimeNitzAutomaticUpdateEnabled, this); + // Read the 'time.clock.automatic-update.enabled' setting to see if + // we need to adjust the system clock time by NITZ or SNTP. + lock.get(kClockAutoUpdateEnabled, this); - // Set "time.nitz.available" to false when starting up. - this.setNitzAvailable(false); + // Read the 'time.timezone.automatic-update.enabled' setting to see if + // we need to adjust the system timezone by NITZ. + lock.get(kTimezoneAutoUpdateEnabled, this); + + // Set "time.clock.automatic-update.available" to false when starting up. + this.setClockAutoUpdateAvailable(false); + + // Set "time.timezone.automatic-update.available" to false when starting up. + this.setTimezoneAutoUpdateAvailable(false); // Read the Cell Broadcast Search List setting, string of integers or integer // ranges separated by comma, to set listening channels. @@ -726,11 +736,20 @@ function RadioInterface(options) { Services.obs.addObserver(this, kSysClockChangeObserverTopic, false); Services.obs.addObserver(this, kScreenStateChangedTopic, false); + Services.obs.addObserver(this, kNetworkInterfaceStateChangedTopic, false); Services.prefs.addObserver(kCellBroadcastDisabled, this, false); this.portAddressedSmsApps = {}; this.portAddressedSmsApps[WAP.WDP_PORT_PUSH] = this.handleSmsWdpPortPush.bind(this); + + this._sntp = new Sntp(this.setClockBySntp.bind(this), + Services.prefs.getIntPref('network.sntp.maxRetryCount'), + Services.prefs.getIntPref('network.sntp.refreshPeriod'), + Services.prefs.getIntPref('network.sntp.timeout'), + Services.prefs.getCharPref('network.sntp.pools').split(';'), + Services.prefs.getIntPref('network.sntp.port')); } + RadioInterface.prototype = { classID: RADIOINTERFACE_CID, @@ -1860,22 +1879,35 @@ RadioInterface.prototype = { }, /** - * Set the setting value of "time.nitz.available". + * Set the setting value of "time.clock.automatic-update.available". */ - setNitzAvailable: function setNitzAvailable(value) { - gSettingsService.createLock().set(kTimeNitzAvailable, value, null, + setClockAutoUpdateAvailable: function setClockAutoUpdateAvailable(value) { + gSettingsService.createLock().set(kClockAutoUpdateAvailable, value, null, "fromInternalSetting"); }, /** - * Set the NITZ message in our system time. + * Set the setting value of "time.timezone.automatic-update.available". */ - setNitzTime: function setNitzTime(message) { + setTimezoneAutoUpdateAvailable: function setTimezoneAutoUpdateAvailable(value) { + gSettingsService.createLock().set(kTimezoneAutoUpdateAvailable, value, null, + "fromInternalSetting"); + }, + + /** + * Set the system clock by NITZ. + */ + setClockByNitz: function setClockByNitz(message) { // To set the system clock time. Note that there could be a time diff // between when the NITZ was received and when the time is actually set. gTimeService.set( message.networkTimeInMS + (Date.now() - message.receiveTimeInMS)); + }, + /** + * Set the system time zone by NITZ. + */ + setTimezoneByNitz: function setTimezoneByNitz(message) { // To set the sytem timezone. Note that we need to convert the time zone // value to a UTC repesentation string in the format of "UTC(+/-)hh:mm". // Ex, time zone -480 is "UTC-08:00"; time zone 630 is "UTC+10:30". @@ -1898,15 +1930,36 @@ RadioInterface.prototype = { */ handleNitzTime: function handleNitzTime(message) { // Got the NITZ info received from the ril_worker. - this.setNitzAvailable(true); + this.setClockAutoUpdateAvailable(true); + this.setTimezoneAutoUpdateAvailable(true); // Cache the latest NITZ message whenever receiving it. this._lastNitzMessage = message; - // Set the received NITZ time if the setting is enabled. - if (this._nitzAutomaticUpdateEnabled) { - this.setNitzTime(message); + // Set the received NITZ clock if the setting is enabled. + if (this._clockAutoUpdateEnabled) { + this.setClockByNitz(message); } + // Set the received NITZ timezone if the setting is enabled. + if (this._timezoneAutoUpdateEnabled) { + this.setTimezoneByNitz(message); + } + }, + + /** + * Set the system clock by SNTP. + */ + setClockBySntp: function setClockBySntp(offset) { + // Got the SNTP info. + this.setClockAutoUpdateAvailable(true); + if (!this._clockAutoUpdateEnabled) { + return; + } + if (this._lastNitzMessage) { + debug("SNTP: NITZ available, discard SNTP"); + return; + } + gTimeService.set(Date.now() + offset); }, handleIccMbdn: function handleIccMbdn(message) { @@ -2023,11 +2076,24 @@ RadioInterface.prototype = { Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); Services.obs.removeObserver(this, kSysClockChangeObserverTopic); Services.obs.removeObserver(this, kScreenStateChangedTopic); + Services.obs.removeObserver(this, kNetworkInterfaceStateChangedTopic); Services.prefs.removeObserver(kCellBroadcastDisabled, this); break; case kSysClockChangeObserverTopic: + let offset = parseInt(data, 10); if (this._lastNitzMessage) { - this._lastNitzMessage.receiveTimeInMS += parseInt(data, 10); + this._lastNitzMessage.receiveTimeInMS += offset; + } + this._sntp.updateOffset(offset); + break; + case kNetworkInterfaceStateChangedTopic: + let network = subject.QueryInterface(Ci.nsINetworkInterface); + if (network.state == Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED) { + // Check SNTP when we have data connection, this may not take + // effect immediately before the setting get enabled. + if (this._sntp.isExpired()) { + this._sntp.request(); + } } break; case kScreenStateChangedTopic: @@ -2053,28 +2119,51 @@ RadioInterface.prototype = { apnSettings: null, - // Flag to determine whether to use NITZ. It corresponds to the - // 'time.nitz.automatic-update.enabled' setting from the UI. - _nitzAutomaticUpdateEnabled: null, + // Flag to determine whether to update system clock automatically. It + // corresponds to the 'time.clock.automatic-update.enabled' setting. + _clockAutoUpdateEnabled: null, + + // Flag to determine whether to update system timezone automatically. It + // corresponds to the 'time.clock.automatic-update.enabled' setting. + _timezoneAutoUpdateEnabled: null, // Remember the last NITZ message so that we can set the time based on // the network immediately when users enable network-based time. _lastNitzMessage: null, + // Object that handles SNTP. + _sntp: null, + // Cell Broadcast settings values. _cellBroadcastSearchListStr: null, handleSettingsChange: function handleSettingsChange(aName, aResult, aMessage) { // Don't allow any content processes to modify the setting - // "time.nitz.available" except for the chrome process. - let isNitzAvailable = (this._lastNitzMessage !== null); - if (aName === kTimeNitzAvailable && aMessage !== "fromInternalSetting" && - aResult !== isNitzAvailable) { - if (DEBUG) { - this.debug("Content processes cannot modify 'time.nitz.available'. Restore!"); + // "time.clock.automatic-update.available" except for the chrome process. + if (aName === kClockAutoUpdateAvailable && + aMessage !== "fromInternalSetting") { + let isClockAutoUpdateAvailable = this._lastNitzMessage !== null || + this._sntp.isAvailable(); + if (aResult !== isClockAutoUpdateAvailable) { + debug("Content processes cannot modify 'time.clock.automatic-update.available'. Restore!"); + // Restore the setting to the current value. + this.setClockAutoUpdateAvailable(isClockAutoUpdateAvailable); + } + } + + // Don't allow any content processes to modify the setting + // "time.timezone.automatic-update.available" except for the chrome + // process. + if (aName === kTimezoneAutoUpdateAvailable && + aMessage !== "fromInternalSetting") { + let isTimezoneAutoUpdateAvailable = this._lastNitzMessage !== null; + if (aResult !== isTimezoneAutoUpdateAvailable) { + if (DEBUG) { + this.debug("Content processes cannot modify 'time.timezone.automatic-update.available'. Restore!"); + } + // Restore the setting to the current value. + this.setTimezoneAutoUpdateAvailable(isTimezoneAutoUpdateAvailable); } - // Restore the setting to the current value. - this.setNitzAvailable(isNitzAvailable); } this.handle(aName, aResult); @@ -2117,12 +2206,34 @@ RadioInterface.prototype = { this.updateRILNetworkInterface(); } break; - case kTimeNitzAutomaticUpdateEnabled: - this._nitzAutomaticUpdateEnabled = aResult; + case kClockAutoUpdateEnabled: + this._clockAutoUpdateEnabled = aResult; + if (!this._clockAutoUpdateEnabled) { + break; + } - // Set the latest cached NITZ time if the setting is enabled. - if (this._nitzAutomaticUpdateEnabled && this._lastNitzMessage) { - this.setNitzTime(this._lastNitzMessage); + // Set the latest cached NITZ time if it's available. + if (this._lastNitzMessage) { + this.setClockByNitz(this._lastNitzMessage); + } else if (gNetworkManager.active && gNetworkManager.active.state == + Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED) { + // Set the latest cached SNTP time if it's available. + if (!this._sntp.isExpired()) { + this.setClockBySntp(this._sntp.getOffset()); + } else { + // Or refresh the SNTP. + this._sntp.request(); + } + } + break; + case kTimezoneAutoUpdateEnabled: + this._timezoneAutoUpdateEnabled = aResult; + + if (this._timezoneAutoUpdateEnabled) { + // Apply the latest cached NITZ for timezone if it's available. + if (this._timezoneAutoUpdateEnabled && this._lastNitzMessage) { + this.setTimezoneByNitz(this._lastNitzMessage); + } } break; case kCellBroadcastSearchList: diff --git a/toolkit/modules/Sntp.jsm b/toolkit/modules/Sntp.jsm new file mode 100644 index 00000000000..672354b16db --- /dev/null +++ b/toolkit/modules/Sntp.jsm @@ -0,0 +1,334 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "Sntp", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +// Set to true to see debug messages. +let DEBUG = false; + +/** + * Constructor of Sntp. + * + * @param dataAvailableCb + * Callback function gets called when SNTP offset available. Signature + * is function dataAvailableCb(offsetInMS). + * @param maxRetryCount + * Maximum retry count when SNTP failed to connect to server; set to + * zero to disable the retry. + * @param refreshPeriodInSecs + * Refresh period; set to zero to disable refresh. + * @param timeoutInSecs + * Timeout value used for connection. + * @param pools + * SNTP server lists separated by ';'. + * @param port + * SNTP port. + */ +this.Sntp = function Sntp(dataAvailableCb, maxRetryCount, refreshPeriodInSecs, + timeoutInSecs, pools, port) { + if (dataAvailableCb != null) { + this._dataAvailableCb = dataAvailableCb; + } + if (maxRetryCount != null) { + this._maxRetryCount = maxRetryCount; + } + if (refreshPeriodInSecs != null) { + this._refreshPeriodInMS = refreshPeriodInSecs * 1000; + } + if (timeoutInSecs != null) { + this._timeoutInMS = timeoutInSecs * 1000; + } + if (pools != null && Array.isArray(pools) && pools.length > 0) { + this._pools = pools; + } + if (port != null) { + this._port = port; + } +} + +Sntp.prototype = { + isAvailable: function isAvailable() { + return this._cachedOffset != null; + }, + + isExpired: function isExpired() { + let valid = this._cachedOffset != null && this._cachedTimeInMS != null; + if (this._refreshPeriodInMS > 0) { + valid = valid && Date.now() < this._cachedTimeInMS + + this._refreshPeriodInMS; + } + return !valid; + }, + + request: function request() { + this._request(); + }, + + getOffset: function getOffset() { + return this._cachedOffset; + }, + + /** + * Indicates the system clock has been changed by [offset]ms so we need to + * adjust the stored value. + */ + updateOffset: function updateOffset(offset) { + if (this._cachedOffset != null) { + this._cachedOffset -= offset; + } + }, + + /** + * Used to schedule a retry or periodic updates. + */ + _schedule: function _schedule(timeInMS) { + if (this._updateTimer == null) { + this._updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + + this._updateTimer.initWithCallback(this._request.bind(this), + timeInMS, + Ci.nsITimer.TYPE_ONE_SHOT); + debug("Scheduled SNTP request in " + timeInMS + "ms"); + }, + + /** + * Handle the SNTP response. + */ + _handleSntp: function _handleSntp(originateTimeInMS, receiveTimeInMS, + transmitTimeInMS, respondTimeInMS) { + let clockOffset = Math.floor(((receiveTimeInMS - originateTimeInMS) + + (transmitTimeInMS - respondTimeInMS)) / 2); + debug("Clock offset: " + clockOffset); + + // We've succeeded so clear the retry status. + this._retryCount = 0; + this._retryPeriodInMS = 0; + + // Cache the latest SNTP offset whenever receiving it. + this._cachedOffset = clockOffset; + this._cachedTimeInMS = respondTimeInMS; + + if (this._dataAvailableCb != null) { + this._dataAvailableCb(clockOffset); + } + + this._schedule(this._refreshPeriodInMS); + }, + + /** + * Used for retry SNTP requests. + */ + _retry: function _retry() { + this._retryCount++; + if (this._retryCount > this._maxRetryCount) { + debug ("stop retrying SNTP"); + // Clear so we can start with clean status next time we have network. + this._retryCount = 0; + this._retryPeriodInMS = 0; + return; + } + this._retryPeriodInMS = Math.max(1000, this._retryPeriodInMS * 2); + + this._schedule(this._retryPeriodInMS); + }, + + /** + * Request SNTP. + */ + _request: function _request() { + function GetRequest() { + let NTP_PACKET_SIZE = 48; + let NTP_MODE_CLIENT = 3; + let NTP_VERSION = 3; + let TRANSMIT_TIME_OFFSET = 40; + + // Send the NTP request. + let requestTimeInMS = Date.now(); + let s = requestTimeInMS / 1000; + let ms = requestTimeInMS % 1000; + // NTP time is relative to 1900. + s += OFFSET_1900_TO_1970; + let f = ms * 0x100000000 / 1000; + s = Math.floor(s); + f = Math.floor(f); + + let buffer = new ArrayBuffer(NTP_PACKET_SIZE); + let data = new DataView(buffer); + data.setUint8(0, NTP_MODE_CLIENT | (NTP_VERSION << 3)); + data.setUint32(TRANSMIT_TIME_OFFSET, s, false); + data.setUint32(TRANSMIT_TIME_OFFSET + 4, f, false); + + return String.fromCharCode.apply(null, new Uint8Array(buffer)); + } + + function SNTPListener() {}; + SNTPListener.prototype = { + onStartRequest: function onStartRequest(request, context) { + }, + + onStopRequest: function onStopRequest(request, context, status) { + if (!Components.isSuccessCode(status)) { + debug ("Connection failed"); + this._requesting = false; + this._retry(); + } + }.bind(this), + + onDataAvailable: function onDataAvailable(request, context, inputStream, + offset, count) { + function GetTimeStamp(binaryInputStream) { + let s = binaryInputStream.read32(); + let f = binaryInputStream.read32(); + return Math.floor( + ((s - OFFSET_1900_TO_1970) * 1000) + ((f * 1000) / 0x100000000) + ); + } + debug ("Data available: " + count + " bytes"); + + try { + let binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"] + .createInstance(Ci.nsIBinaryInputStream); + binaryInputStream.setInputStream(inputStream); + // We don't need first 24 bytes. + for (let i = 0; i < 6; i++) { + binaryInputStream.read32(); + } + // Offset 24: originate time. + let originateTimeInMS = GetTimeStamp(binaryInputStream); + // Offset 32: receive time. + let receiveTimeInMS = GetTimeStamp(binaryInputStream); + // Offset 40: transmit time. + let transmitTimeInMS = GetTimeStamp(binaryInputStream); + let respondTimeInMS = Date.now(); + + this._handleSntp(originateTimeInMS, receiveTimeInMS, + transmitTimeInMS, respondTimeInMS); + this._requesting = false; + } catch (e) { + debug ("SNTPListener Error: " + e.message); + this._requesting = false; + this._retry(); + } + inputStream.close(); + }.bind(this) + }; + + function SNTPRequester() {} + SNTPRequester.prototype = { + onOutputStreamReady: function(stream) { + try { + let data = GetRequest(); + let bytes_write = stream.write(data, data.length); + debug ("SNTP: sent " + bytes_write + " bytes"); + stream.close(); + } catch (e) { + debug ("SNTPRequester Error: " + e.message); + this._requesting = false; + this._retry(); + } + }.bind(this) + }; + + // Number of seconds between Jan 1, 1900 and Jan 1, 1970. + // 70 years plus 17 leap days. + let OFFSET_1900_TO_1970 = ((365 * 70) + 17) * 24 * 60 * 60; + + if (this._requesting) { + return; + } + if (this._pools.length < 1) { + debug("No server defined"); + return; + } + if (this._updateTimer) { + this._updateTimer.cancel(); + } + + debug ("Making request"); + this._requesting = true; + + let currentThread = Cc["@mozilla.org/thread-manager;1"] + .getService().currentThread; + let socketTransportService = + Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + let pump = Cc["@mozilla.org/network/input-stream-pump;1"] + .createInstance(Ci.nsIInputStreamPump); + let transport = socketTransportService + .createTransport(["udp"], + 1, + this._pools[Math.floor(this._pools.length * Math.random())], + this._port, + null); + + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, this._timeoutInMS); + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, this._timeoutInMS); + + let outStream = transport.openOutputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncOutputStream); + let inStream = transport.openInputStream(0, 0, 0); + + pump.init(inStream, -1, -1, 0, 0, false); + pump.asyncRead(new SNTPListener(), null); + + outStream.asyncWait(new SNTPRequester(), 0, 0, currentThread); + }, + + // Callback function. + _dataAvailableCb: null, + + // Sntp servers. + _pools: [ + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org" + ], + + // The SNTP port. + _port: 123, + + // Maximum retry count allowed when request failed. + _maxRetryCount: 0, + + // Refresh period. + _refreshPeriodInMS: 0, + + // Timeout value used for connecting. + _timeoutInMS: 30 * 1000, + + // Cached SNTP offset. + _cachedOffset: null, + + // The time point when we cache the offset. + _cachedTimeInMS: null, + + // Flag to avoid redundant requests. + _requesting: false, + + // Retry counter. + _retryCount: 0, + + // Retry time offset (in seconds). + _retryPeriodInMS: 0, + + // Timer used for retries & daily updates. + _updateTimer: null +}; + +let debug; +if (DEBUG) { + debug = function (s) { + dump("-*- Sntp: " + s + "\n"); + }; +} else { + debug = function (s) {}; +} diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index 62237c0bf09..d2d6fd3d44e 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -29,6 +29,7 @@ EXTRA_JS_MODULES += [ 'RemoteWebProgress.jsm', 'SelectContentHelper.jsm', 'SelectParentHelper.jsm', + 'Sntp.jsm', 'Sqlite.jsm', 'Task.jsm', 'TelemetryTimestamps.jsm',