summaryrefslogtreecommitdiffstats
path: root/dom/network/NetworkStatsService.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/network/NetworkStatsService.jsm')
-rw-r--r--dom/network/NetworkStatsService.jsm1171
1 files changed, 1171 insertions, 0 deletions
diff --git a/dom/network/NetworkStatsService.jsm b/dom/network/NetworkStatsService.jsm
new file mode 100644
index 000000000..4b6d69498
--- /dev/null
+++ b/dom/network/NetworkStatsService.jsm
@@ -0,0 +1,1171 @@
+/* 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";
+
+const DEBUG = false;
+function debug(s) {
+ if (DEBUG) {
+ dump("-*- NetworkStatsService: " + s + "\n");
+ }
+}
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+this.EXPORTED_SYMBOLS = ["NetworkStatsService"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetworkStatsDB.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+const NET_NETWORKSTATSSERVICE_CONTRACTID = "@mozilla.org/network/netstatsservice;1";
+const NET_NETWORKSTATSSERVICE_CID = Components.ID("{18725604-e9ac-488a-8aa0-2471e7f6c0a4}");
+
+const TOPIC_BANDWIDTH_CONTROL = "netd-bandwidth-control"
+
+const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed";
+const NET_TYPE_WIFI = Ci.nsINetworkInfo.NETWORK_TYPE_WIFI;
+const NET_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE;
+
+// Networks have different status that NetworkStats API needs to be aware of.
+// Network is present and ready, so NetworkManager provides the whole info.
+const NETWORK_STATUS_READY = 0;
+// Network is present but hasn't established a connection yet (e.g. SIM that has not
+// enabled 3G since boot).
+const NETWORK_STATUS_STANDBY = 1;
+// Network is not present, but stored in database by the previous connections.
+const NETWORK_STATUS_AWAY = 2;
+
+// The maximum traffic amount can be saved in the |cachedStats|.
+const MAX_CACHED_TRAFFIC = 500 * 1000 * 1000; // 500 MB
+
+const QUEUE_TYPE_UPDATE_STATS = 0;
+const QUEUE_TYPE_UPDATE_CACHE = 1;
+const QUEUE_TYPE_WRITE_CACHE = 2;
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gRil",
+ "@mozilla.org/ril;1",
+ "nsIRadioInterfaceLayer");
+
+XPCOMUtils.defineLazyServiceGetter(this, "networkService",
+ "@mozilla.org/network/service;1",
+ "nsINetworkService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+ "@mozilla.org/AppsService;1",
+ "nsIAppsService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService",
+ "@mozilla.org/settingsService;1",
+ "nsISettingsService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "messenger",
+ "@mozilla.org/system-message-internal;1",
+ "nsISystemMessagesInternal");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gIccService",
+ "@mozilla.org/icc/iccservice;1",
+ "nsIIccService");
+
+this.NetworkStatsService = {
+ init: function() {
+ debug("Service started");
+
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ Services.obs.addObserver(this, TOPIC_CONNECTION_STATE_CHANGED, false);
+ Services.obs.addObserver(this, TOPIC_BANDWIDTH_CONTROL, false);
+ Services.obs.addObserver(this, "profile-after-change", false);
+
+ this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ // Object to store network interfaces, each network interface is composed
+ // by a network object (network type and network Id) and a interfaceName
+ // that contains the name of the physical interface (wlan0, rmnet0, etc.).
+ // The network type can be 0 for wifi or 1 for mobile. On the other hand,
+ // the network id is '0' for wifi or the iccid for mobile (SIM).
+ // Each networkInterface is placed in the _networks object by the index of
+ // 'networkId + networkType'.
+ //
+ // _networks object allows to map available network interfaces at low level
+ // (wlan0, rmnet0, etc.) to a network. It's not mandatory to have a
+ // networkInterface per network but can't exist a networkInterface not
+ // being mapped to a network.
+
+ this._networks = Object.create(null);
+
+ // There is no way to know a priori if wifi connection is available,
+ // just when the wifi driver is loaded, but it is unloaded when
+ // wifi is switched off. So wifi connection is hardcoded
+ let netId = this.getNetworkId('0', NET_TYPE_WIFI);
+ this._networks[netId] = { network: { id: '0',
+ type: NET_TYPE_WIFI },
+ interfaceName: null,
+ status: NETWORK_STATUS_STANDBY };
+
+ this.messages = ["NetworkStats:Get",
+ "NetworkStats:Clear",
+ "NetworkStats:ClearAll",
+ "NetworkStats:SetAlarm",
+ "NetworkStats:GetAlarms",
+ "NetworkStats:RemoveAlarms",
+ "NetworkStats:GetAvailableNetworks",
+ "NetworkStats:GetAvailableServiceTypes",
+ "NetworkStats:SampleRate",
+ "NetworkStats:MaxStorageAge"];
+
+ this.messages.forEach(function(aMsgName) {
+ ppmm.addMessageListener(aMsgName, this);
+ }, this);
+
+ this._db = new NetworkStatsDB();
+
+ // Stats for all interfaces are updated periodically
+ this.timer.initWithCallback(this, this._db.sampleRate,
+ Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
+
+ // Stats not from netd are firstly stored in the cached.
+ this.cachedStats = Object.create(null);
+ this.cachedStatsDate = new Date();
+
+ this.updateQueue = [];
+ this.isQueueRunning = false;
+
+ this._currentAlarms = {};
+ this.initAlarms();
+ },
+
+ receiveMessage: function(aMessage) {
+ if (!aMessage.target.assertPermission("networkstats-manage")) {
+ return;
+ }
+
+ debug("receiveMessage " + aMessage.name);
+
+ let mm = aMessage.target;
+ let msg = aMessage.json;
+
+ switch (aMessage.name) {
+ case "NetworkStats:Get":
+ this.getSamples(mm, msg);
+ break;
+ case "NetworkStats:Clear":
+ this.clearInterfaceStats(mm, msg);
+ break;
+ case "NetworkStats:ClearAll":
+ this.clearDB(mm, msg);
+ break;
+ case "NetworkStats:SetAlarm":
+ this.setAlarm(mm, msg);
+ break;
+ case "NetworkStats:GetAlarms":
+ this.getAlarms(mm, msg);
+ break;
+ case "NetworkStats:RemoveAlarms":
+ this.removeAlarms(mm, msg);
+ break;
+ case "NetworkStats:GetAvailableNetworks":
+ this.getAvailableNetworks(mm, msg);
+ break;
+ case "NetworkStats:GetAvailableServiceTypes":
+ this.getAvailableServiceTypes(mm, msg);
+ break;
+ case "NetworkStats:SampleRate":
+ // This message is sync.
+ return this._db.sampleRate;
+ case "NetworkStats:MaxStorageAge":
+ // This message is sync.
+ return this._db.maxStorageSamples * this._db.sampleRate;
+ }
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case TOPIC_CONNECTION_STATE_CHANGED:
+
+ // If new interface is registered (notified from NetworkService),
+ // the stats are updated for the new interface without waiting to
+ // complete the updating period.
+
+ let networkInfo = aSubject.QueryInterface(Ci.nsINetworkInfo);
+ debug("Network " + networkInfo.name + " of type " + networkInfo.type + " status change");
+
+ let netId = this.convertNetworkInfo(networkInfo);
+ if (!netId) {
+ break;
+ }
+
+ this._updateCurrentAlarm(netId);
+
+ debug("NetId: " + netId);
+ this.updateStats(netId);
+ break;
+
+ case TOPIC_BANDWIDTH_CONTROL:
+ debug("Bandwidth message from netd: " + JSON.stringify(aData));
+
+ let interfaceName = aData.substring(aData.lastIndexOf(" ") + 1);
+ for (let networkId in this._networks) {
+ if (interfaceName == this._networks[networkId].interfaceName) {
+ let currentAlarm = this._currentAlarms[networkId];
+ if (Object.getOwnPropertyNames(currentAlarm).length !== 0) {
+ this._fireAlarm(currentAlarm.alarm);
+ }
+ break;
+ }
+ }
+ break;
+
+ case "xpcom-shutdown":
+ debug("Service shutdown");
+
+ this.messages.forEach(function(aMsgName) {
+ ppmm.removeMessageListener(aMsgName, this);
+ }, this);
+
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.obs.removeObserver(this, "profile-after-change");
+ Services.obs.removeObserver(this, TOPIC_CONNECTION_STATE_CHANGED);
+ Services.obs.removeObserver(this, TOPIC_BANDWIDTH_CONTROL);
+
+ this.timer.cancel();
+ this.timer = null;
+
+ // Update stats before shutdown
+ this.updateAllStats();
+ break;
+ }
+ },
+
+ /*
+ * nsITimerCallback
+ * Timer triggers the update of all stats
+ */
+ notify: function(aTimer) {
+ this.updateAllStats();
+ },
+
+ /*
+ * nsINetworkStatsService
+ */
+ getRilNetworks: function() {
+ let networks = {};
+ let numRadioInterfaces = gRil.numRadioInterfaces;
+ for (let i = 0; i < numRadioInterfaces; i++) {
+ let icc = gIccService.getIccByServiceId(i);
+ let radioInterface = gRil.getRadioInterface(i);
+ if (icc && icc.iccInfo) {
+ let netId = this.getNetworkId(icc.iccInfo.iccid,
+ NET_TYPE_MOBILE);
+ networks[netId] = { id : icc.iccInfo.iccid,
+ type: NET_TYPE_MOBILE };
+ }
+ }
+ return networks;
+ },
+
+ convertNetworkInfo: function(aNetworkInfo) {
+ if (aNetworkInfo.type != NET_TYPE_MOBILE &&
+ aNetworkInfo.type != NET_TYPE_WIFI) {
+ return null;
+ }
+
+ let id = '0';
+ if (aNetworkInfo.type == NET_TYPE_MOBILE) {
+ if (!(aNetworkInfo instanceof Ci.nsIRilNetworkInfo)) {
+ debug("Error! Mobile network should be an nsIRilNetworkInfo!");
+ return null;
+ }
+
+ let rilNetwork = aNetworkInfo.QueryInterface(Ci.nsIRilNetworkInfo);
+ id = rilNetwork.iccId;
+ }
+
+ let netId = this.getNetworkId(id, aNetworkInfo.type);
+
+ if (!this._networks[netId]) {
+ this._networks[netId] = Object.create(null);
+ this._networks[netId].network = { id: id,
+ type: aNetworkInfo.type };
+ }
+
+ this._networks[netId].status = NETWORK_STATUS_READY;
+ this._networks[netId].interfaceName = aNetworkInfo.name;
+ return netId;
+ },
+
+ getNetworkId: function getNetworkId(aIccId, aNetworkType) {
+ return aIccId + '' + aNetworkType;
+ },
+
+ /* Function to ensure that one network is valid. The network is valid if its status is
+ * NETWORK_STATUS_READY, NETWORK_STATUS_STANDBY or NETWORK_STATUS_AWAY.
+ *
+ * The result is |netId| or null in case of a non-valid network
+ * aCallback is signatured as |function(netId)|.
+ */
+ validateNetwork: function validateNetwork(aNetwork, aCallback) {
+ let netId = this.getNetworkId(aNetwork.id, aNetwork.type);
+
+ if (this._networks[netId]) {
+ aCallback(netId);
+ return;
+ }
+
+ // Check if network is valid (RIL entry) but has not established a connection yet.
+ // If so add to networks list with empty interfaceName.
+ let rilNetworks = this.getRilNetworks();
+ if (rilNetworks[netId]) {
+ this._networks[netId] = Object.create(null);
+ this._networks[netId].network = rilNetworks[netId];
+ this._networks[netId].status = NETWORK_STATUS_STANDBY;
+ this._currentAlarms[netId] = Object.create(null);
+ aCallback(netId);
+ return;
+ }
+
+ // Check if network is available in the DB.
+ this._db.isNetworkAvailable(aNetwork, function(aError, aResult) {
+ if (aResult) {
+ this._networks[netId] = Object.create(null);
+ this._networks[netId].network = aNetwork;
+ this._networks[netId].status = NETWORK_STATUS_AWAY;
+ this._currentAlarms[netId] = Object.create(null);
+ aCallback(netId);
+ return;
+ }
+
+ aCallback(null);
+ }.bind(this));
+ },
+
+ getAvailableNetworks: function getAvailableNetworks(mm, msg) {
+ let self = this;
+ let rilNetworks = this.getRilNetworks();
+ this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) {
+
+ // Also return the networks that are valid but have not
+ // established connections yet.
+ for (let netId in rilNetworks) {
+ let found = false;
+ for (let i = 0; i < aResult.length; i++) {
+ if (netId == self.getNetworkId(aResult[i].id, aResult[i].type)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ aResult.push(rilNetworks[netId]);
+ }
+ }
+
+ mm.sendAsyncMessage("NetworkStats:GetAvailableNetworks:Return",
+ { id: msg.id, error: aError, result: aResult });
+ });
+ },
+
+ getAvailableServiceTypes: function getAvailableServiceTypes(mm, msg) {
+ this._db.getAvailableServiceTypes(function onGetServiceTypes(aError, aResult) {
+ mm.sendAsyncMessage("NetworkStats:GetAvailableServiceTypes:Return",
+ { id: msg.id, error: aError, result: aResult });
+ });
+ },
+
+ initAlarms: function initAlarms() {
+ debug("Init usage alarms");
+ let self = this;
+
+ for (let netId in this._networks) {
+ this._currentAlarms[netId] = Object.create(null);
+
+ this._db.getFirstAlarm(netId, function getResult(error, result) {
+ if (!error && result) {
+ self._setAlarm(result, function onSet(error, success) {
+ if (error == "InvalidStateError") {
+ self._fireAlarm(result);
+ }
+ });
+ }
+ });
+ }
+ },
+
+ /*
+ * Function called from manager to get stats from database.
+ * In order to return updated stats, first is performed a call to
+ * updateAllStats function, which will get last stats from netd
+ * and update the database.
+ * Then, depending on the request (stats per appId or total stats)
+ * it retrieve them from database and return to the manager.
+ */
+ getSamples: function getSamples(mm, msg) {
+ let network = msg.network;
+ let netId = this.getNetworkId(network.id, network.type);
+
+ let appId = 0;
+ let appManifestURL = msg.appManifestURL;
+ if (appManifestURL) {
+ appId = appsService.getAppLocalIdByManifestURL(appManifestURL);
+
+ if (!appId) {
+ mm.sendAsyncMessage("NetworkStats:Get:Return",
+ { id: msg.id,
+ error: "Invalid appManifestURL", result: null });
+ return;
+ }
+ }
+
+ let browsingTrafficOnly = msg.browsingTrafficOnly || false;
+ let serviceType = msg.serviceType || "";
+
+ let start = new Date(msg.start);
+ let end = new Date(msg.end);
+
+ let callback = (function (aError, aResult) {
+ this._db.find(function onStatsFound(aError, aResult) {
+ mm.sendAsyncMessage("NetworkStats:Get:Return",
+ { id: msg.id, error: aError, result: aResult });
+ }, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL);
+ }).bind(this);
+
+ this.validateNetwork(network, function onValidateNetwork(aNetId) {
+ if (!aNetId) {
+ mm.sendAsyncMessage("NetworkStats:Get:Return",
+ { id: msg.id, error: "Invalid connectionType", result: null });
+ return;
+ }
+
+ // If network is currently active we need to update the cached stats first before
+ // retrieving stats from the DB.
+ if (this._networks[aNetId].status == NETWORK_STATUS_READY) {
+ debug("getstats for network " + network.id + " of type " + network.type);
+ debug("appId: " + appId + " from appManifestURL: " + appManifestURL);
+ debug("browsingTrafficOnly: " + browsingTrafficOnly);
+ debug("serviceType: " + serviceType);
+
+ if (appId || serviceType) {
+ this.updateCachedStats(callback);
+ return;
+ }
+
+ this.updateStats(aNetId, function onStatsUpdated(aResult, aMessage) {
+ this.updateCachedStats(callback);
+ }.bind(this));
+ return;
+ }
+
+ // Network not active, so no need to update
+ this._db.find(function onStatsFound(aError, aResult) {
+ mm.sendAsyncMessage("NetworkStats:Get:Return",
+ { id: msg.id, error: aError, result: aResult });
+ }, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL);
+ }.bind(this));
+ },
+
+ clearInterfaceStats: function clearInterfaceStats(mm, msg) {
+ let self = this;
+ let network = msg.network;
+
+ debug("clear stats for network " + network.id + " of type " + network.type);
+
+ this.validateNetwork(network, function onValidateNetwork(aNetId) {
+ if (!aNetId) {
+ mm.sendAsyncMessage("NetworkStats:Clear:Return",
+ { id: msg.id, error: "Invalid connectionType", result: null });
+ return;
+ }
+
+ network = {network: network, networkId: aNetId};
+ self.updateStats(aNetId, function onUpdate(aResult, aMessage) {
+ if (!aResult) {
+ mm.sendAsyncMessage("NetworkStats:Clear:Return",
+ { id: msg.id, error: aMessage, result: null });
+ return;
+ }
+
+ self._db.clearInterfaceStats(network, function onDBCleared(aError, aResult) {
+ self._updateCurrentAlarm(aNetId);
+ mm.sendAsyncMessage("NetworkStats:Clear:Return",
+ { id: msg.id, error: aError, result: aResult });
+ });
+ });
+ });
+ },
+
+ clearDB: function clearDB(mm, msg) {
+ let self = this;
+ this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) {
+ if (aError) {
+ mm.sendAsyncMessage("NetworkStats:ClearAll:Return",
+ { id: msg.id, error: aError, result: aResult });
+ return;
+ }
+
+ let networks = aResult;
+ networks.forEach(function(network, index) {
+ networks[index] = {network: network, networkId: self.getNetworkId(network.id, network.type)};
+ }, self);
+
+ self.updateAllStats(function onUpdate(aResult, aMessage){
+ if (!aResult) {
+ mm.sendAsyncMessage("NetworkStats:ClearAll:Return",
+ { id: msg.id, error: aMessage, result: null });
+ return;
+ }
+
+ self._db.clearStats(networks, function onDBCleared(aError, aResult) {
+ networks.forEach(function(network, index) {
+ self._updateCurrentAlarm(network.networkId);
+ }, self);
+ mm.sendAsyncMessage("NetworkStats:ClearAll:Return",
+ { id: msg.id, error: aError, result: aResult });
+ });
+ });
+ });
+ },
+
+ updateAllStats: function updateAllStats(aCallback) {
+ let elements = [];
+ let lastElement = null;
+ let callback = (function (success, message) {
+ this.updateCachedStats(aCallback);
+ }).bind(this);
+
+ // For each connectionType create an object containning the type
+ // and the 'queueIndex', the 'queueIndex' is an integer representing
+ // the index of a connection type in the global queue array. So, if
+ // the connection type is already in the queue it is not appended again,
+ // else it is pushed in 'elements' array, which later will be pushed to
+ // the queue array.
+ for (let netId in this._networks) {
+ if (this._networks[netId].status != NETWORK_STATUS_READY) {
+ continue;
+ }
+
+ lastElement = { netId: netId,
+ queueIndex: this.updateQueueIndex(netId) };
+
+ if (lastElement.queueIndex == -1) {
+ elements.push({ netId: lastElement.netId,
+ callbacks: [],
+ queueType: QUEUE_TYPE_UPDATE_STATS });
+ }
+ }
+
+ if (!lastElement) {
+ // No elements need to be updated, probably because status is different than
+ // NETWORK_STATUS_READY.
+ if (aCallback) {
+ aCallback(true, "OK");
+ }
+ return;
+ }
+
+ if (elements.length > 0) {
+ // If length of elements is greater than 0, callback is set to
+ // the last element.
+ elements[elements.length - 1].callbacks.push(callback);
+ this.updateQueue = this.updateQueue.concat(elements);
+ } else {
+ // Else, it means that all connection types are already in the queue to
+ // be updated, so callback for this request is added to
+ // the element in the main queue with the index of the last 'lastElement'.
+ // But before is checked that element is still in the queue because it can
+ // be processed while generating 'elements' array.
+ let element = this.updateQueue[lastElement.queueIndex];
+ if (aCallback &&
+ (!element || element.netId != lastElement.netId)) {
+ aCallback();
+ return;
+ }
+
+ this.updateQueue[lastElement.queueIndex].callbacks.push(callback);
+ }
+
+ // Call the function that process the elements of the queue.
+ this.processQueue();
+
+ if (DEBUG) {
+ this.logAllRecords();
+ }
+ },
+
+ updateStats: function updateStats(aNetId, aCallback) {
+ // Check if the connection is in the main queue, push a new element
+ // if it is not being processed or add a callback if it is.
+ let index = this.updateQueueIndex(aNetId);
+ if (index == -1) {
+ this.updateQueue.push({ netId: aNetId,
+ callbacks: [aCallback],
+ queueType: QUEUE_TYPE_UPDATE_STATS });
+ } else {
+ this.updateQueue[index].callbacks.push(aCallback);
+ return;
+ }
+
+ // Call the function that process the elements of the queue.
+ this.processQueue();
+ },
+
+ /*
+ * Find if a connection is in the main queue array and return its
+ * index, if it is not in the array return -1.
+ */
+ updateQueueIndex: function updateQueueIndex(aNetId) {
+ return this.updateQueue.map(function(e) { return e.netId; }).indexOf(aNetId);
+ },
+
+ /*
+ * Function responsible of process all requests in the queue.
+ */
+ processQueue: function processQueue(aResult, aMessage) {
+ // If aResult is not undefined, the caller of the function is the result
+ // of processing an element, so remove that element and call the callbacks
+ // it has.
+ let self = this;
+
+ if (aResult != undefined) {
+ let item = this.updateQueue.shift();
+ for (let callback of item.callbacks) {
+ if (callback) {
+ callback(aResult, aMessage);
+ }
+ }
+ } else {
+ // The caller is a function that has pushed new elements to the queue,
+ // if isQueueRunning is false it means there is no processing currently
+ // being done, so start.
+ if (this.isQueueRunning) {
+ return;
+ } else {
+ this.isQueueRunning = true;
+ }
+ }
+
+ // Check length to determine if queue is empty and stop processing.
+ if (this.updateQueue.length < 1) {
+ this.isQueueRunning = false;
+ return;
+ }
+
+ // Process the next item as soon as possible.
+ setTimeout(function () {
+ self.run(self.updateQueue[0]);
+ }, 0);
+ },
+
+ run: function run(item) {
+ switch (item.queueType) {
+ case QUEUE_TYPE_UPDATE_STATS:
+ this.update(item.netId, this.processQueue.bind(this));
+ break;
+ case QUEUE_TYPE_UPDATE_CACHE:
+ this.updateCache(this.processQueue.bind(this));
+ break;
+ case QUEUE_TYPE_WRITE_CACHE:
+ this.writeCache(item.stats, this.processQueue.bind(this));
+ break;
+ }
+ },
+
+ update: function update(aNetId, aCallback) {
+ // Check if connection type is valid.
+ if (!this._networks[aNetId]) {
+ if (aCallback) {
+ aCallback(false, "Invalid network " + aNetId);
+ }
+ return;
+ }
+
+ let interfaceName = this._networks[aNetId].interfaceName;
+ debug("Update stats for " + interfaceName);
+
+ // Request stats to NetworkService, which will get stats from netd, passing
+ // 'networkStatsAvailable' as a callback.
+ if (interfaceName) {
+ networkService.getNetworkInterfaceStats(interfaceName,
+ this.networkStatsAvailable.bind(this, aCallback, aNetId));
+ return;
+ }
+
+ if (aCallback) {
+ aCallback(true, "ok");
+ }
+ },
+
+ /*
+ * Callback of request stats. Store stats in database.
+ */
+ networkStatsAvailable: function networkStatsAvailable(aCallback, aNetId,
+ aResult, aRxBytes,
+ aTxBytes, aTimestamp) {
+ if (!aResult) {
+ if (aCallback) {
+ aCallback(false, "Netd IPC error");
+ }
+ return;
+ }
+
+ let stats = { appId: 0,
+ isInBrowser: false,
+ serviceType: "",
+ networkId: this._networks[aNetId].network.id,
+ networkType: this._networks[aNetId].network.type,
+ date: new Date(aTimestamp),
+ rxBytes: aTxBytes,
+ txBytes: aRxBytes,
+ isAccumulative: true };
+
+ debug("Update stats for: " + JSON.stringify(stats));
+
+ this._db.saveStats(stats, function onSavedStats(aError, aResult) {
+ if (aCallback) {
+ if (aError) {
+ aCallback(false, aError);
+ return;
+ }
+
+ aCallback(true, "OK");
+ }
+ });
+ },
+
+ /*
+ * Function responsible for receiving stats which are not from netd.
+ */
+ saveStats: function saveStats(aAppId, aIsInIsolatedMozBrowser, aServiceType,
+ aNetworkInfo, aTimeStamp, aRxBytes, aTxBytes,
+ aIsAccumulative, aCallback) {
+ let netId = this.convertNetworkInfo(aNetworkInfo);
+ if (!netId) {
+ if (aCallback) {
+ aCallback(false, "Invalid network type");
+ }
+ return;
+ }
+
+ // Check if |aConnectionType|, |aAppId| and |aServiceType| are valid.
+ // There are two invalid cases for the combination of |aAppId| and
+ // |aServiceType|:
+ // a. Both |aAppId| is non-zero and |aServiceType| is non-empty.
+ // b. Both |aAppId| is zero and |aServiceType| is empty.
+ if (!this._networks[netId] || (aAppId && aServiceType) ||
+ (!aAppId && !aServiceType)) {
+ debug("Invalid network interface, appId or serviceType");
+ return;
+ }
+
+ let stats = { appId: aAppId,
+ isInBrowser: aIsInIsolatedMozBrowser,
+ serviceType: aServiceType,
+ networkId: this._networks[netId].network.id,
+ networkType: this._networks[netId].network.type,
+ date: new Date(aTimeStamp),
+ rxBytes: aRxBytes,
+ txBytes: aTxBytes,
+ isAccumulative: aIsAccumulative };
+
+ this.updateQueue.push({ stats: stats,
+ callbacks: [aCallback],
+ queueType: QUEUE_TYPE_WRITE_CACHE });
+
+ this.processQueue();
+ },
+
+ /*
+ *
+ */
+ writeCache: function writeCache(aStats, aCallback) {
+ debug("saveStats: " + aStats.appId + " " + aStats.isInBrowser + " " +
+ aStats.serviceType + " " + aStats.networkId + " " +
+ aStats.networkType + " " + aStats.date + " " +
+ aStats.rxBytes + " " + aStats.txBytes);
+
+ // Generate an unique key from |appId|, |isInBrowser|, |serviceType| and
+ // |netId|, which is used to retrieve data in |cachedStats|.
+ let netId = this.getNetworkId(aStats.networkId, aStats.networkType);
+ let key = aStats.appId + "" + aStats.isInBrowser + "" +
+ aStats.serviceType + "" + netId;
+
+ // |cachedStats| only keeps the data with the same date.
+ // If the incoming date is different from |cachedStatsDate|,
+ // both |cachedStats| and |cachedStatsDate| will get updated.
+ let diff = (this._db.normalizeDate(aStats.date) -
+ this._db.normalizeDate(this.cachedStatsDate)) /
+ this._db.sampleRate;
+ if (diff != 0) {
+ this.updateCache(function onUpdated(success, message) {
+ this.cachedStatsDate = aStats.date;
+ this.cachedStats[key] = aStats;
+
+ if (aCallback) {
+ aCallback(true, "ok");
+ }
+ }.bind(this));
+ return;
+ }
+
+ // Try to find the matched row in the cached by |appId| and |connectionType|.
+ // If not found, save the incoming data into the cached.
+ let cachedStats = this.cachedStats[key];
+ if (!cachedStats) {
+ this.cachedStats[key] = aStats;
+ if (aCallback) {
+ aCallback(true, "ok");
+ }
+ return;
+ }
+
+ // Find matched row, accumulate the traffic amount.
+ cachedStats.rxBytes += aStats.rxBytes;
+ cachedStats.txBytes += aStats.txBytes;
+
+ // If new rxBytes or txBytes exceeds MAX_CACHED_TRAFFIC
+ // the corresponding row will be saved to indexedDB.
+ // Then, the row will be removed from the cached.
+ if (cachedStats.rxBytes > MAX_CACHED_TRAFFIC ||
+ cachedStats.txBytes > MAX_CACHED_TRAFFIC) {
+ this._db.saveStats(cachedStats, function (error, result) {
+ debug("Application stats inserted in indexedDB");
+ if (aCallback) {
+ aCallback(true, "ok");
+ }
+ });
+ delete this.cachedStats[key];
+ return;
+ }
+
+ if (aCallback) {
+ aCallback(true, "ok");
+ }
+ },
+
+ updateCachedStats: function updateCachedStats(aCallback) {
+ this.updateQueue.push({ callbacks: [aCallback],
+ queueType: QUEUE_TYPE_UPDATE_CACHE });
+
+ this.processQueue();
+ },
+
+ updateCache: function updateCache(aCallback) {
+ debug("updateCache: " + this.cachedStatsDate);
+
+ let stats = Object.keys(this.cachedStats);
+ if (stats.length == 0) {
+ // |cachedStats| is empty, no need to update.
+ if (aCallback) {
+ aCallback(true, "no need to update");
+ }
+ return;
+ }
+
+ let index = 0;
+ this._db.saveStats(this.cachedStats[stats[index]],
+ function onSavedStats(error, result) {
+ debug("Application stats inserted in indexedDB");
+
+ // Clean up the |cachedStats| after updating.
+ if (index == stats.length - 1) {
+ this.cachedStats = Object.create(null);
+
+ if (aCallback) {
+ aCallback(true, "ok");
+ }
+ return;
+ }
+
+ // Update is not finished, keep updating.
+ index += 1;
+ this._db.saveStats(this.cachedStats[stats[index]],
+ onSavedStats.bind(this, error, result));
+ }.bind(this));
+ },
+
+ get maxCachedTraffic () {
+ return MAX_CACHED_TRAFFIC;
+ },
+
+ logAllRecords: function logAllRecords() {
+ this._db.logAllRecords(function onResult(aError, aResult) {
+ if (aError) {
+ debug("Error: " + aError);
+ return;
+ }
+
+ debug("===== LOG =====");
+ debug("There are " + aResult.length + " items");
+ debug(JSON.stringify(aResult));
+ });
+ },
+
+ getAlarms: function getAlarms(mm, msg) {
+ let self = this;
+ let network = msg.data.network;
+ let manifestURL = msg.data.manifestURL;
+
+ if (network) {
+ this.validateNetwork(network, function onValidateNetwork(aNetId) {
+ if (!aNetId) {
+ mm.sendAsyncMessage("NetworkStats:GetAlarms:Return",
+ { id: msg.id, error: "InvalidInterface", result: null });
+ return;
+ }
+
+ self._getAlarms(mm, msg, aNetId, manifestURL);
+ });
+ return;
+ }
+
+ this._getAlarms(mm, msg, null, manifestURL);
+ },
+
+ _getAlarms: function _getAlarms(mm, msg, aNetId, aManifestURL) {
+ let self = this;
+ this._db.getAlarms(aNetId, aManifestURL, function onCompleted(error, result) {
+ if (error) {
+ mm.sendAsyncMessage("NetworkStats:GetAlarms:Return",
+ { id: msg.id, error: error, result: result });
+ return;
+ }
+
+ let alarms = []
+ // NetworkStatsManager must return the network instead of the networkId.
+ for (let i = 0; i < result.length; i++) {
+ let alarm = result[i];
+ alarms.push({ id: alarm.id,
+ network: self._networks[alarm.networkId].network,
+ threshold: alarm.absoluteThreshold,
+ data: alarm.data });
+ }
+
+ mm.sendAsyncMessage("NetworkStats:GetAlarms:Return",
+ { id: msg.id, error: null, result: alarms });
+ });
+ },
+
+ removeAlarms: function removeAlarms(mm, msg) {
+ let alarmId = msg.data.alarmId;
+ let manifestURL = msg.data.manifestURL;
+
+ let self = this;
+ let callback = function onRemove(error, result) {
+ if (error) {
+ mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return",
+ { id: msg.id, error: error, result: result });
+ return;
+ }
+
+ for (let i in self._currentAlarms) {
+ let currentAlarm = self._currentAlarms[i].alarm;
+ if (currentAlarm && ((alarmId == currentAlarm.id) ||
+ (alarmId == -1 && currentAlarm.manifestURL == manifestURL))) {
+
+ self._updateCurrentAlarm(currentAlarm.networkId);
+ }
+ }
+
+ mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return",
+ { id: msg.id, error: error, result: true });
+ };
+
+ if (alarmId == -1) {
+ this._db.removeAlarms(manifestURL, callback);
+ } else {
+ this._db.removeAlarm(alarmId, manifestURL, callback);
+ }
+ },
+
+ /*
+ * Function called from manager to set an alarm.
+ */
+ setAlarm: function setAlarm(mm, msg) {
+ let options = msg.data;
+ let network = options.network;
+ let threshold = options.threshold;
+
+ debug("Set alarm at " + threshold + " for " + JSON.stringify(network));
+
+ if (threshold < 0) {
+ mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
+ { id: msg.id, error: "InvalidThresholdValue", result: null });
+ return;
+ }
+
+ let self = this;
+ this.validateNetwork(network, function onValidateNetwork(aNetId) {
+ if (!aNetId) {
+ mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
+ { id: msg.id, error: "InvalidiConnectionType", result: null });
+ return;
+ }
+
+ let newAlarm = {
+ id: null,
+ networkId: aNetId,
+ absoluteThreshold: threshold,
+ relativeThreshold: null,
+ startTime: options.startTime,
+ data: options.data,
+ pageURL: options.pageURL,
+ manifestURL: options.manifestURL
+ };
+
+ self._getAlarmQuota(newAlarm, function onUpdate(error, quota) {
+ if (error) {
+ mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
+ { id: msg.id, error: error, result: null });
+ return;
+ }
+
+ self._db.addAlarm(newAlarm, function addSuccessCb(error, newId) {
+ if (error) {
+ mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
+ { id: msg.id, error: error, result: null });
+ return;
+ }
+
+ newAlarm.id = newId;
+ self._setAlarm(newAlarm, function onSet(error, success) {
+ mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
+ { id: msg.id, error: error, result: newId });
+
+ if (error == "InvalidStateError") {
+ self._fireAlarm(newAlarm);
+ }
+ });
+ });
+ });
+ });
+ },
+
+ _setAlarm: function _setAlarm(aAlarm, aCallback) {
+ let currentAlarm = this._currentAlarms[aAlarm.networkId];
+ if ((Object.getOwnPropertyNames(currentAlarm).length !== 0 &&
+ aAlarm.relativeThreshold > currentAlarm.alarm.relativeThreshold) ||
+ this._networks[aAlarm.networkId].status != NETWORK_STATUS_READY) {
+ aCallback(null, true);
+ return;
+ }
+
+ let self = this;
+
+ this._getAlarmQuota(aAlarm, function onUpdate(aError, aQuota) {
+ if (aError) {
+ aCallback(aError, null);
+ return;
+ }
+
+ let callback = function onAlarmSet(aError) {
+ if (aError) {
+ debug("Set alarm error: " + aError);
+ aCallback("netdError", null);
+ return;
+ }
+
+ self._currentAlarms[aAlarm.networkId].alarm = aAlarm;
+
+ aCallback(null, true);
+ };
+
+ debug("Set alarm " + JSON.stringify(aAlarm));
+ let interfaceName = self._networks[aAlarm.networkId].interfaceName;
+ if (interfaceName) {
+ networkService.setNetworkInterfaceAlarm(interfaceName,
+ aQuota,
+ callback);
+ return;
+ }
+
+ aCallback(null, true);
+ });
+ },
+
+ _getAlarmQuota: function _getAlarmQuota(aAlarm, aCallback) {
+ let self = this;
+ this.updateStats(aAlarm.networkId, function onStatsUpdated(aResult, aMessage) {
+ self._db.getCurrentStats(self._networks[aAlarm.networkId].network,
+ aAlarm.startTime,
+ function onStatsFound(error, result) {
+ if (error) {
+ debug("Error getting stats for " +
+ JSON.stringify(self._networks[aAlarm.networkId]) + ": " + error);
+ aCallback(error, result);
+ return;
+ }
+
+ let quota = aAlarm.absoluteThreshold - result.rxBytes - result.txBytes;
+
+ // Alarm set to a threshold lower than current rx/tx bytes.
+ if (quota <= 0) {
+ aCallback("InvalidStateError", null);
+ return;
+ }
+
+ aAlarm.relativeThreshold = aAlarm.startTime
+ ? result.rxTotalBytes + result.txTotalBytes + quota
+ : aAlarm.absoluteThreshold;
+
+ aCallback(null, quota);
+ });
+ });
+ },
+
+ _fireAlarm: function _fireAlarm(aAlarm) {
+ debug("Fire alarm");
+
+ let self = this;
+ this._db.removeAlarm(aAlarm.id, null, function onRemove(aError, aResult){
+ if (!aError && !aResult) {
+ return;
+ }
+
+ self._fireSystemMessage(aAlarm);
+ self._updateCurrentAlarm(aAlarm.networkId);
+ });
+ },
+
+ _updateCurrentAlarm: function _updateCurrentAlarm(aNetworkId) {
+ this._currentAlarms[aNetworkId] = Object.create(null);
+
+ let self = this;
+ this._db.getFirstAlarm(aNetworkId, function onGet(error, result){
+ if (error) {
+ debug("Error getting the first alarm");
+ return;
+ }
+
+ if (!result) {
+ let interfaceName = self._networks[aNetworkId].interfaceName;
+ networkService.setNetworkInterfaceAlarm(interfaceName, -1,
+ function onComplete(){});
+ return;
+ }
+
+ self._setAlarm(result, function onSet(error, success){
+ if (error == "InvalidStateError") {
+ self._fireAlarm(result);
+ return;
+ }
+ });
+ });
+ },
+
+ _fireSystemMessage: function _fireSystemMessage(aAlarm) {
+ debug("Fire system message: " + JSON.stringify(aAlarm));
+
+ let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null);
+ let pageURI = Services.io.newURI(aAlarm.pageURL, null, null);
+
+ let alarm = { "id": aAlarm.id,
+ "threshold": aAlarm.absoluteThreshold,
+ "data": aAlarm.data };
+ messenger.sendMessage("networkstats-alarm", alarm, pageURI, manifestURI);
+ }
+};
+
+NetworkStatsService.init();