|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const DEBUG = false; |
|
8 function debug(s) { |
|
9 if (DEBUG) { |
|
10 dump("-*- NetworkStatsService: " + s + "\n"); |
|
11 } |
|
12 } |
|
13 |
|
14 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
15 |
|
16 this.EXPORTED_SYMBOLS = ["NetworkStatsService"]; |
|
17 |
|
18 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
19 Cu.import("resource://gre/modules/Services.jsm"); |
|
20 Cu.import("resource://gre/modules/NetworkStatsDB.jsm"); |
|
21 |
|
22 const NET_NETWORKSTATSSERVICE_CONTRACTID = "@mozilla.org/network/netstatsservice;1"; |
|
23 const NET_NETWORKSTATSSERVICE_CID = Components.ID("{18725604-e9ac-488a-8aa0-2471e7f6c0a4}"); |
|
24 |
|
25 const TOPIC_BANDWIDTH_CONTROL = "netd-bandwidth-control" |
|
26 |
|
27 const TOPIC_INTERFACE_REGISTERED = "network-interface-registered"; |
|
28 const TOPIC_INTERFACE_UNREGISTERED = "network-interface-unregistered"; |
|
29 const NET_TYPE_WIFI = Ci.nsINetworkInterface.NETWORK_TYPE_WIFI; |
|
30 const NET_TYPE_MOBILE = Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE; |
|
31 |
|
32 // Networks have different status that NetworkStats API needs to be aware of. |
|
33 // Network is present and ready, so NetworkManager provides the whole info. |
|
34 const NETWORK_STATUS_READY = 0; |
|
35 // Network is present but hasn't established a connection yet (e.g. SIM that has not |
|
36 // enabled 3G since boot). |
|
37 const NETWORK_STATUS_STANDBY = 1; |
|
38 // Network is not present, but stored in database by the previous connections. |
|
39 const NETWORK_STATUS_AWAY = 2; |
|
40 |
|
41 // The maximum traffic amount can be saved in the |cachedStats|. |
|
42 const MAX_CACHED_TRAFFIC = 500 * 1000 * 1000; // 500 MB |
|
43 |
|
44 const QUEUE_TYPE_UPDATE_STATS = 0; |
|
45 const QUEUE_TYPE_UPDATE_CACHE = 1; |
|
46 const QUEUE_TYPE_WRITE_CACHE = 2; |
|
47 |
|
48 XPCOMUtils.defineLazyServiceGetter(this, "ppmm", |
|
49 "@mozilla.org/parentprocessmessagemanager;1", |
|
50 "nsIMessageListenerManager"); |
|
51 |
|
52 XPCOMUtils.defineLazyServiceGetter(this, "gRil", |
|
53 "@mozilla.org/ril;1", |
|
54 "nsIRadioInterfaceLayer"); |
|
55 |
|
56 XPCOMUtils.defineLazyServiceGetter(this, "networkService", |
|
57 "@mozilla.org/network/service;1", |
|
58 "nsINetworkService"); |
|
59 |
|
60 XPCOMUtils.defineLazyServiceGetter(this, "appsService", |
|
61 "@mozilla.org/AppsService;1", |
|
62 "nsIAppsService"); |
|
63 |
|
64 XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", |
|
65 "@mozilla.org/settingsService;1", |
|
66 "nsISettingsService"); |
|
67 |
|
68 XPCOMUtils.defineLazyServiceGetter(this, "messenger", |
|
69 "@mozilla.org/system-message-internal;1", |
|
70 "nsISystemMessagesInternal"); |
|
71 |
|
72 this.NetworkStatsService = { |
|
73 init: function() { |
|
74 debug("Service started"); |
|
75 |
|
76 Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
77 Services.obs.addObserver(this, TOPIC_INTERFACE_REGISTERED, false); |
|
78 Services.obs.addObserver(this, TOPIC_INTERFACE_UNREGISTERED, false); |
|
79 Services.obs.addObserver(this, TOPIC_BANDWIDTH_CONTROL, false); |
|
80 Services.obs.addObserver(this, "profile-after-change", false); |
|
81 |
|
82 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
83 |
|
84 // Object to store network interfaces, each network interface is composed |
|
85 // by a network object (network type and network Id) and a interfaceName |
|
86 // that contains the name of the physical interface (wlan0, rmnet0, etc.). |
|
87 // The network type can be 0 for wifi or 1 for mobile. On the other hand, |
|
88 // the network id is '0' for wifi or the iccid for mobile (SIM). |
|
89 // Each networkInterface is placed in the _networks object by the index of |
|
90 // 'networkId + networkType'. |
|
91 // |
|
92 // _networks object allows to map available network interfaces at low level |
|
93 // (wlan0, rmnet0, etc.) to a network. It's not mandatory to have a |
|
94 // networkInterface per network but can't exist a networkInterface not |
|
95 // being mapped to a network. |
|
96 |
|
97 this._networks = Object.create(null); |
|
98 |
|
99 // There is no way to know a priori if wifi connection is available, |
|
100 // just when the wifi driver is loaded, but it is unloaded when |
|
101 // wifi is switched off. So wifi connection is hardcoded |
|
102 let netId = this.getNetworkId('0', NET_TYPE_WIFI); |
|
103 this._networks[netId] = { network: { id: '0', |
|
104 type: NET_TYPE_WIFI }, |
|
105 interfaceName: null, |
|
106 status: NETWORK_STATUS_STANDBY }; |
|
107 |
|
108 this.messages = ["NetworkStats:Get", |
|
109 "NetworkStats:Clear", |
|
110 "NetworkStats:ClearAll", |
|
111 "NetworkStats:SetAlarm", |
|
112 "NetworkStats:GetAlarms", |
|
113 "NetworkStats:RemoveAlarms", |
|
114 "NetworkStats:GetAvailableNetworks", |
|
115 "NetworkStats:GetAvailableServiceTypes", |
|
116 "NetworkStats:SampleRate", |
|
117 "NetworkStats:MaxStorageAge"]; |
|
118 |
|
119 this.messages.forEach(function(aMsgName) { |
|
120 ppmm.addMessageListener(aMsgName, this); |
|
121 }, this); |
|
122 |
|
123 this._db = new NetworkStatsDB(); |
|
124 |
|
125 // Stats for all interfaces are updated periodically |
|
126 this.timer.initWithCallback(this, this._db.sampleRate, |
|
127 Ci.nsITimer.TYPE_REPEATING_PRECISE); |
|
128 |
|
129 // Stats not from netd are firstly stored in the cached. |
|
130 this.cachedStats = Object.create(null); |
|
131 this.cachedStatsDate = new Date(); |
|
132 |
|
133 this.updateQueue = []; |
|
134 this.isQueueRunning = false; |
|
135 |
|
136 this._currentAlarms = {}; |
|
137 this.initAlarms(); |
|
138 }, |
|
139 |
|
140 receiveMessage: function(aMessage) { |
|
141 if (!aMessage.target.assertPermission("networkstats-manage")) { |
|
142 return; |
|
143 } |
|
144 |
|
145 debug("receiveMessage " + aMessage.name); |
|
146 |
|
147 let mm = aMessage.target; |
|
148 let msg = aMessage.json; |
|
149 |
|
150 switch (aMessage.name) { |
|
151 case "NetworkStats:Get": |
|
152 this.getSamples(mm, msg); |
|
153 break; |
|
154 case "NetworkStats:Clear": |
|
155 this.clearInterfaceStats(mm, msg); |
|
156 break; |
|
157 case "NetworkStats:ClearAll": |
|
158 this.clearDB(mm, msg); |
|
159 break; |
|
160 case "NetworkStats:SetAlarm": |
|
161 this.setAlarm(mm, msg); |
|
162 break; |
|
163 case "NetworkStats:GetAlarms": |
|
164 this.getAlarms(mm, msg); |
|
165 break; |
|
166 case "NetworkStats:RemoveAlarms": |
|
167 this.removeAlarms(mm, msg); |
|
168 break; |
|
169 case "NetworkStats:GetAvailableNetworks": |
|
170 this.getAvailableNetworks(mm, msg); |
|
171 break; |
|
172 case "NetworkStats:GetAvailableServiceTypes": |
|
173 this.getAvailableServiceTypes(mm, msg); |
|
174 break; |
|
175 case "NetworkStats:SampleRate": |
|
176 // This message is sync. |
|
177 return this._db.sampleRate; |
|
178 case "NetworkStats:MaxStorageAge": |
|
179 // This message is sync. |
|
180 return this._db.maxStorageSamples * this._db.sampleRate; |
|
181 } |
|
182 }, |
|
183 |
|
184 observe: function observe(aSubject, aTopic, aData) { |
|
185 switch (aTopic) { |
|
186 case TOPIC_INTERFACE_REGISTERED: |
|
187 case TOPIC_INTERFACE_UNREGISTERED: |
|
188 |
|
189 // If new interface is registered (notified from NetworkService), |
|
190 // the stats are updated for the new interface without waiting to |
|
191 // complete the updating period. |
|
192 |
|
193 let network = aSubject.QueryInterface(Ci.nsINetworkInterface); |
|
194 debug("Network " + network.name + " of type " + network.type + " status change"); |
|
195 |
|
196 let netId = this.convertNetworkInterface(network); |
|
197 if (!netId) { |
|
198 break; |
|
199 } |
|
200 |
|
201 this._updateCurrentAlarm(netId); |
|
202 |
|
203 debug("NetId: " + netId); |
|
204 this.updateStats(netId); |
|
205 break; |
|
206 |
|
207 case TOPIC_BANDWIDTH_CONTROL: |
|
208 debug("Bandwidth message from netd: " + JSON.stringify(aData)); |
|
209 |
|
210 let interfaceName = aData.substring(aData.lastIndexOf(" ") + 1); |
|
211 for (let networkId in this._networks) { |
|
212 if (interfaceName == this._networks[networkId].interfaceName) { |
|
213 let currentAlarm = this._currentAlarms[networkId]; |
|
214 if (Object.getOwnPropertyNames(currentAlarm).length !== 0) { |
|
215 this._fireAlarm(currentAlarm.alarm); |
|
216 } |
|
217 break; |
|
218 } |
|
219 } |
|
220 break; |
|
221 |
|
222 case "xpcom-shutdown": |
|
223 debug("Service shutdown"); |
|
224 |
|
225 this.messages.forEach(function(aMsgName) { |
|
226 ppmm.removeMessageListener(aMsgName, this); |
|
227 }, this); |
|
228 |
|
229 Services.obs.removeObserver(this, "xpcom-shutdown"); |
|
230 Services.obs.removeObserver(this, "profile-after-change"); |
|
231 Services.obs.removeObserver(this, TOPIC_INTERFACE_REGISTERED); |
|
232 Services.obs.removeObserver(this, TOPIC_INTERFACE_UNREGISTERED); |
|
233 Services.obs.removeObserver(this, TOPIC_BANDWIDTH_CONTROL); |
|
234 |
|
235 this.timer.cancel(); |
|
236 this.timer = null; |
|
237 |
|
238 // Update stats before shutdown |
|
239 this.updateAllStats(); |
|
240 break; |
|
241 } |
|
242 }, |
|
243 |
|
244 /* |
|
245 * nsITimerCallback |
|
246 * Timer triggers the update of all stats |
|
247 */ |
|
248 notify: function(aTimer) { |
|
249 this.updateAllStats(); |
|
250 }, |
|
251 |
|
252 /* |
|
253 * nsINetworkStatsService |
|
254 */ |
|
255 getRilNetworks: function() { |
|
256 let networks = {}; |
|
257 let numRadioInterfaces = gRil.numRadioInterfaces; |
|
258 for (let i = 0; i < numRadioInterfaces; i++) { |
|
259 let radioInterface = gRil.getRadioInterface(i); |
|
260 if (radioInterface.rilContext.iccInfo) { |
|
261 let netId = this.getNetworkId(radioInterface.rilContext.iccInfo.iccid, |
|
262 NET_TYPE_MOBILE); |
|
263 networks[netId] = { id : radioInterface.rilContext.iccInfo.iccid, |
|
264 type: NET_TYPE_MOBILE }; |
|
265 } |
|
266 } |
|
267 return networks; |
|
268 }, |
|
269 |
|
270 convertNetworkInterface: function(aNetwork) { |
|
271 if (aNetwork.type != NET_TYPE_MOBILE && |
|
272 aNetwork.type != NET_TYPE_WIFI) { |
|
273 return null; |
|
274 } |
|
275 |
|
276 let id = '0'; |
|
277 if (aNetwork.type == NET_TYPE_MOBILE) { |
|
278 if (!(aNetwork instanceof Ci.nsIRilNetworkInterface)) { |
|
279 debug("Error! Mobile network should be an nsIRilNetworkInterface!"); |
|
280 return null; |
|
281 } |
|
282 |
|
283 let rilNetwork = aNetwork.QueryInterface(Ci.nsIRilNetworkInterface); |
|
284 id = rilNetwork.iccId; |
|
285 } |
|
286 |
|
287 let netId = this.getNetworkId(id, aNetwork.type); |
|
288 |
|
289 if (!this._networks[netId]) { |
|
290 this._networks[netId] = Object.create(null); |
|
291 this._networks[netId].network = { id: id, |
|
292 type: aNetwork.type }; |
|
293 } |
|
294 |
|
295 this._networks[netId].status = NETWORK_STATUS_READY; |
|
296 this._networks[netId].interfaceName = aNetwork.name; |
|
297 return netId; |
|
298 }, |
|
299 |
|
300 getNetworkId: function getNetworkId(aIccId, aNetworkType) { |
|
301 return aIccId + '' + aNetworkType; |
|
302 }, |
|
303 |
|
304 /* Function to ensure that one network is valid. The network is valid if its status is |
|
305 * NETWORK_STATUS_READY, NETWORK_STATUS_STANDBY or NETWORK_STATUS_AWAY. |
|
306 * |
|
307 * The result is |netId| or null in case of a non-valid network |
|
308 * aCallback is signatured as |function(netId)|. |
|
309 */ |
|
310 validateNetwork: function validateNetwork(aNetwork, aCallback) { |
|
311 let netId = this.getNetworkId(aNetwork.id, aNetwork.type); |
|
312 |
|
313 if (this._networks[netId]) { |
|
314 aCallback(netId); |
|
315 return; |
|
316 } |
|
317 |
|
318 // Check if network is valid (RIL entry) but has not established a connection yet. |
|
319 // If so add to networks list with empty interfaceName. |
|
320 let rilNetworks = this.getRilNetworks(); |
|
321 if (rilNetworks[netId]) { |
|
322 this._networks[netId] = Object.create(null); |
|
323 this._networks[netId].network = rilNetworks[netId]; |
|
324 this._networks[netId].status = NETWORK_STATUS_STANDBY; |
|
325 this._currentAlarms[netId] = Object.create(null); |
|
326 aCallback(netId); |
|
327 return; |
|
328 } |
|
329 |
|
330 // Check if network is available in the DB. |
|
331 this._db.isNetworkAvailable(aNetwork, function(aError, aResult) { |
|
332 if (aResult) { |
|
333 this._networks[netId] = Object.create(null); |
|
334 this._networks[netId].network = aNetwork; |
|
335 this._networks[netId].status = NETWORK_STATUS_AWAY; |
|
336 this._currentAlarms[netId] = Object.create(null); |
|
337 aCallback(netId); |
|
338 return; |
|
339 } |
|
340 |
|
341 aCallback(null); |
|
342 }.bind(this)); |
|
343 }, |
|
344 |
|
345 getAvailableNetworks: function getAvailableNetworks(mm, msg) { |
|
346 let self = this; |
|
347 let rilNetworks = this.getRilNetworks(); |
|
348 this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) { |
|
349 |
|
350 // Also return the networks that are valid but have not |
|
351 // established connections yet. |
|
352 for (let netId in rilNetworks) { |
|
353 let found = false; |
|
354 for (let i = 0; i < aResult.length; i++) { |
|
355 if (netId == self.getNetworkId(aResult[i].id, aResult[i].type)) { |
|
356 found = true; |
|
357 break; |
|
358 } |
|
359 } |
|
360 if (!found) { |
|
361 aResult.push(rilNetworks[netId]); |
|
362 } |
|
363 } |
|
364 |
|
365 mm.sendAsyncMessage("NetworkStats:GetAvailableNetworks:Return", |
|
366 { id: msg.id, error: aError, result: aResult }); |
|
367 }); |
|
368 }, |
|
369 |
|
370 getAvailableServiceTypes: function getAvailableServiceTypes(mm, msg) { |
|
371 this._db.getAvailableServiceTypes(function onGetServiceTypes(aError, aResult) { |
|
372 mm.sendAsyncMessage("NetworkStats:GetAvailableServiceTypes:Return", |
|
373 { id: msg.id, error: aError, result: aResult }); |
|
374 }); |
|
375 }, |
|
376 |
|
377 initAlarms: function initAlarms() { |
|
378 debug("Init usage alarms"); |
|
379 let self = this; |
|
380 |
|
381 for (let netId in this._networks) { |
|
382 this._currentAlarms[netId] = Object.create(null); |
|
383 |
|
384 this._db.getFirstAlarm(netId, function getResult(error, result) { |
|
385 if (!error && result) { |
|
386 self._setAlarm(result, function onSet(error, success) { |
|
387 if (error == "InvalidStateError") { |
|
388 self._fireAlarm(result); |
|
389 } |
|
390 }); |
|
391 } |
|
392 }); |
|
393 } |
|
394 }, |
|
395 |
|
396 /* |
|
397 * Function called from manager to get stats from database. |
|
398 * In order to return updated stats, first is performed a call to |
|
399 * updateAllStats function, which will get last stats from netd |
|
400 * and update the database. |
|
401 * Then, depending on the request (stats per appId or total stats) |
|
402 * it retrieve them from database and return to the manager. |
|
403 */ |
|
404 getSamples: function getSamples(mm, msg) { |
|
405 let network = msg.network; |
|
406 let netId = this.getNetworkId(network.id, network.type); |
|
407 |
|
408 let appId = 0; |
|
409 let appManifestURL = msg.appManifestURL; |
|
410 if (appManifestURL) { |
|
411 appId = appsService.getAppLocalIdByManifestURL(appManifestURL); |
|
412 |
|
413 if (!appId) { |
|
414 mm.sendAsyncMessage("NetworkStats:Get:Return", |
|
415 { id: msg.id, |
|
416 error: "Invalid appManifestURL", result: null }); |
|
417 return; |
|
418 } |
|
419 } |
|
420 |
|
421 let serviceType = msg.serviceType || ""; |
|
422 |
|
423 let start = new Date(msg.start); |
|
424 let end = new Date(msg.end); |
|
425 |
|
426 let callback = (function (aError, aResult) { |
|
427 this._db.find(function onStatsFound(aError, aResult) { |
|
428 mm.sendAsyncMessage("NetworkStats:Get:Return", |
|
429 { id: msg.id, error: aError, result: aResult }); |
|
430 }, appId, serviceType, network, start, end, appManifestURL); |
|
431 }).bind(this); |
|
432 |
|
433 this.validateNetwork(network, function onValidateNetwork(aNetId) { |
|
434 if (!aNetId) { |
|
435 mm.sendAsyncMessage("NetworkStats:Get:Return", |
|
436 { id: msg.id, error: "Invalid connectionType", result: null }); |
|
437 return; |
|
438 } |
|
439 |
|
440 // If network is currently active we need to update the cached stats first before |
|
441 // retrieving stats from the DB. |
|
442 if (this._networks[aNetId].status == NETWORK_STATUS_READY) { |
|
443 debug("getstats for network " + network.id + " of type " + network.type); |
|
444 debug("appId: " + appId + " from appManifestURL: " + appManifestURL); |
|
445 debug("serviceType: " + serviceType); |
|
446 |
|
447 if (appId || serviceType) { |
|
448 this.updateCachedStats(callback); |
|
449 return; |
|
450 } |
|
451 |
|
452 this.updateStats(aNetId, function onStatsUpdated(aResult, aMessage) { |
|
453 this.updateCachedStats(callback); |
|
454 }.bind(this)); |
|
455 return; |
|
456 } |
|
457 |
|
458 // Network not active, so no need to update |
|
459 this._db.find(function onStatsFound(aError, aResult) { |
|
460 mm.sendAsyncMessage("NetworkStats:Get:Return", |
|
461 { id: msg.id, error: aError, result: aResult }); |
|
462 }, appId, serviceType, network, start, end, appManifestURL); |
|
463 }.bind(this)); |
|
464 }, |
|
465 |
|
466 clearInterfaceStats: function clearInterfaceStats(mm, msg) { |
|
467 let self = this; |
|
468 let network = msg.network; |
|
469 |
|
470 debug("clear stats for network " + network.id + " of type " + network.type); |
|
471 |
|
472 this.validateNetwork(network, function onValidateNetwork(aNetId) { |
|
473 if (!aNetId) { |
|
474 mm.sendAsyncMessage("NetworkStats:Clear:Return", |
|
475 { id: msg.id, error: "Invalid connectionType", result: null }); |
|
476 return; |
|
477 } |
|
478 |
|
479 network = {network: network, networkId: aNetId}; |
|
480 self.updateStats(aNetId, function onUpdate(aResult, aMessage) { |
|
481 if (!aResult) { |
|
482 mm.sendAsyncMessage("NetworkStats:Clear:Return", |
|
483 { id: msg.id, error: aMessage, result: null }); |
|
484 return; |
|
485 } |
|
486 |
|
487 self._db.clearInterfaceStats(network, function onDBCleared(aError, aResult) { |
|
488 self._updateCurrentAlarm(aNetId); |
|
489 mm.sendAsyncMessage("NetworkStats:Clear:Return", |
|
490 { id: msg.id, error: aError, result: aResult }); |
|
491 }); |
|
492 }); |
|
493 }); |
|
494 }, |
|
495 |
|
496 clearDB: function clearDB(mm, msg) { |
|
497 let self = this; |
|
498 this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) { |
|
499 if (aError) { |
|
500 mm.sendAsyncMessage("NetworkStats:ClearAll:Return", |
|
501 { id: msg.id, error: aError, result: aResult }); |
|
502 return; |
|
503 } |
|
504 |
|
505 let networks = aResult; |
|
506 networks.forEach(function(network, index) { |
|
507 networks[index] = {network: network, networkId: self.getNetworkId(network.id, network.type)}; |
|
508 }, self); |
|
509 |
|
510 self.updateAllStats(function onUpdate(aResult, aMessage){ |
|
511 if (!aResult) { |
|
512 mm.sendAsyncMessage("NetworkStats:ClearAll:Return", |
|
513 { id: msg.id, error: aMessage, result: null }); |
|
514 return; |
|
515 } |
|
516 |
|
517 self._db.clearStats(networks, function onDBCleared(aError, aResult) { |
|
518 networks.forEach(function(network, index) { |
|
519 self._updateCurrentAlarm(network.networkId); |
|
520 }, self); |
|
521 mm.sendAsyncMessage("NetworkStats:ClearAll:Return", |
|
522 { id: msg.id, error: aError, result: aResult }); |
|
523 }); |
|
524 }); |
|
525 }); |
|
526 }, |
|
527 |
|
528 updateAllStats: function updateAllStats(aCallback) { |
|
529 let elements = []; |
|
530 let lastElement = null; |
|
531 let callback = (function (success, message) { |
|
532 this.updateCachedStats(aCallback); |
|
533 }).bind(this); |
|
534 |
|
535 // For each connectionType create an object containning the type |
|
536 // and the 'queueIndex', the 'queueIndex' is an integer representing |
|
537 // the index of a connection type in the global queue array. So, if |
|
538 // the connection type is already in the queue it is not appended again, |
|
539 // else it is pushed in 'elements' array, which later will be pushed to |
|
540 // the queue array. |
|
541 for (let netId in this._networks) { |
|
542 if (this._networks[netId].status != NETWORK_STATUS_READY) { |
|
543 continue; |
|
544 } |
|
545 |
|
546 lastElement = { netId: netId, |
|
547 queueIndex: this.updateQueueIndex(netId) }; |
|
548 |
|
549 if (lastElement.queueIndex == -1) { |
|
550 elements.push({ netId: lastElement.netId, |
|
551 callbacks: [], |
|
552 queueType: QUEUE_TYPE_UPDATE_STATS }); |
|
553 } |
|
554 } |
|
555 |
|
556 if (!lastElement) { |
|
557 // No elements need to be updated, probably because status is different than |
|
558 // NETWORK_STATUS_READY. |
|
559 if (aCallback) { |
|
560 aCallback(true, "OK"); |
|
561 } |
|
562 return; |
|
563 } |
|
564 |
|
565 if (elements.length > 0) { |
|
566 // If length of elements is greater than 0, callback is set to |
|
567 // the last element. |
|
568 elements[elements.length - 1].callbacks.push(callback); |
|
569 this.updateQueue = this.updateQueue.concat(elements); |
|
570 } else { |
|
571 // Else, it means that all connection types are already in the queue to |
|
572 // be updated, so callback for this request is added to |
|
573 // the element in the main queue with the index of the last 'lastElement'. |
|
574 // But before is checked that element is still in the queue because it can |
|
575 // be processed while generating 'elements' array. |
|
576 let element = this.updateQueue[lastElement.queueIndex]; |
|
577 if (aCallback && |
|
578 (!element || element.netId != lastElement.netId)) { |
|
579 aCallback(); |
|
580 return; |
|
581 } |
|
582 |
|
583 this.updateQueue[lastElement.queueIndex].callbacks.push(callback); |
|
584 } |
|
585 |
|
586 // Call the function that process the elements of the queue. |
|
587 this.processQueue(); |
|
588 |
|
589 if (DEBUG) { |
|
590 this.logAllRecords(); |
|
591 } |
|
592 }, |
|
593 |
|
594 updateStats: function updateStats(aNetId, aCallback) { |
|
595 // Check if the connection is in the main queue, push a new element |
|
596 // if it is not being processed or add a callback if it is. |
|
597 let index = this.updateQueueIndex(aNetId); |
|
598 if (index == -1) { |
|
599 this.updateQueue.push({ netId: aNetId, |
|
600 callbacks: [aCallback], |
|
601 queueType: QUEUE_TYPE_UPDATE_STATS }); |
|
602 } else { |
|
603 this.updateQueue[index].callbacks.push(aCallback); |
|
604 return; |
|
605 } |
|
606 |
|
607 // Call the function that process the elements of the queue. |
|
608 this.processQueue(); |
|
609 }, |
|
610 |
|
611 /* |
|
612 * Find if a connection is in the main queue array and return its |
|
613 * index, if it is not in the array return -1. |
|
614 */ |
|
615 updateQueueIndex: function updateQueueIndex(aNetId) { |
|
616 return this.updateQueue.map(function(e) { return e.netId; }).indexOf(aNetId); |
|
617 }, |
|
618 |
|
619 /* |
|
620 * Function responsible of process all requests in the queue. |
|
621 */ |
|
622 processQueue: function processQueue(aResult, aMessage) { |
|
623 // If aResult is not undefined, the caller of the function is the result |
|
624 // of processing an element, so remove that element and call the callbacks |
|
625 // it has. |
|
626 if (aResult != undefined) { |
|
627 let item = this.updateQueue.shift(); |
|
628 for (let callback of item.callbacks) { |
|
629 if (callback) { |
|
630 callback(aResult, aMessage); |
|
631 } |
|
632 } |
|
633 } else { |
|
634 // The caller is a function that has pushed new elements to the queue, |
|
635 // if isQueueRunning is false it means there is no processing currently |
|
636 // being done, so start. |
|
637 if (this.isQueueRunning) { |
|
638 return; |
|
639 } else { |
|
640 this.isQueueRunning = true; |
|
641 } |
|
642 } |
|
643 |
|
644 // Check length to determine if queue is empty and stop processing. |
|
645 if (this.updateQueue.length < 1) { |
|
646 this.isQueueRunning = false; |
|
647 return; |
|
648 } |
|
649 |
|
650 // Call the update function for the next element. |
|
651 switch (this.updateQueue[0].queueType) { |
|
652 case QUEUE_TYPE_UPDATE_STATS: |
|
653 this.update(this.updateQueue[0].netId, this.processQueue.bind(this)); |
|
654 break; |
|
655 case QUEUE_TYPE_UPDATE_CACHE: |
|
656 this.updateCache(this.processQueue.bind(this)); |
|
657 break; |
|
658 case QUEUE_TYPE_WRITE_CACHE: |
|
659 this.writeCache(this.updateQueue[0].stats, this.processQueue.bind(this)); |
|
660 break; |
|
661 } |
|
662 }, |
|
663 |
|
664 update: function update(aNetId, aCallback) { |
|
665 // Check if connection type is valid. |
|
666 if (!this._networks[aNetId]) { |
|
667 if (aCallback) { |
|
668 aCallback(false, "Invalid network " + aNetId); |
|
669 } |
|
670 return; |
|
671 } |
|
672 |
|
673 let interfaceName = this._networks[aNetId].interfaceName; |
|
674 debug("Update stats for " + interfaceName); |
|
675 |
|
676 // Request stats to NetworkService, which will get stats from netd, passing |
|
677 // 'networkStatsAvailable' as a callback. |
|
678 if (interfaceName) { |
|
679 networkService.getNetworkInterfaceStats(interfaceName, |
|
680 this.networkStatsAvailable.bind(this, aCallback, aNetId)); |
|
681 return; |
|
682 } |
|
683 |
|
684 if (aCallback) { |
|
685 aCallback(true, "ok"); |
|
686 } |
|
687 }, |
|
688 |
|
689 /* |
|
690 * Callback of request stats. Store stats in database. |
|
691 */ |
|
692 networkStatsAvailable: function networkStatsAvailable(aCallback, aNetId, |
|
693 aResult, aRxBytes, |
|
694 aTxBytes, aDate) { |
|
695 if (!aResult) { |
|
696 if (aCallback) { |
|
697 aCallback(false, "Netd IPC error"); |
|
698 } |
|
699 return; |
|
700 } |
|
701 |
|
702 let stats = { appId: 0, |
|
703 serviceType: "", |
|
704 networkId: this._networks[aNetId].network.id, |
|
705 networkType: this._networks[aNetId].network.type, |
|
706 date: aDate, |
|
707 rxBytes: aTxBytes, |
|
708 txBytes: aRxBytes, |
|
709 isAccumulative: true }; |
|
710 |
|
711 debug("Update stats for: " + JSON.stringify(stats)); |
|
712 |
|
713 this._db.saveStats(stats, function onSavedStats(aError, aResult) { |
|
714 if (aCallback) { |
|
715 if (aError) { |
|
716 aCallback(false, aError); |
|
717 return; |
|
718 } |
|
719 |
|
720 aCallback(true, "OK"); |
|
721 } |
|
722 }); |
|
723 }, |
|
724 |
|
725 /* |
|
726 * Function responsible for receiving stats which are not from netd. |
|
727 */ |
|
728 saveStats: function saveStats(aAppId, aServiceType, aNetwork, aTimeStamp, |
|
729 aRxBytes, aTxBytes, aIsAccumulative, |
|
730 aCallback) { |
|
731 let netId = this.convertNetworkInterface(aNetwork); |
|
732 if (!netId) { |
|
733 if (aCallback) { |
|
734 aCallback(false, "Invalid network type"); |
|
735 } |
|
736 return; |
|
737 } |
|
738 |
|
739 // Check if |aConnectionType|, |aAppId| and |aServiceType| are valid. |
|
740 // There are two invalid cases for the combination of |aAppId| and |
|
741 // |aServiceType|: |
|
742 // a. Both |aAppId| is non-zero and |aServiceType| is non-empty. |
|
743 // b. Both |aAppId| is zero and |aServiceType| is empty. |
|
744 if (!this._networks[netId] || (aAppId && aServiceType) || |
|
745 (!aAppId && !aServiceType)) { |
|
746 debug("Invalid network interface, appId or serviceType"); |
|
747 return; |
|
748 } |
|
749 |
|
750 let stats = { appId: aAppId, |
|
751 serviceType: aServiceType, |
|
752 networkId: this._networks[netId].network.id, |
|
753 networkType: this._networks[netId].network.type, |
|
754 date: new Date(aTimeStamp), |
|
755 rxBytes: aRxBytes, |
|
756 txBytes: aTxBytes, |
|
757 isAccumulative: aIsAccumulative }; |
|
758 |
|
759 this.updateQueue.push({ stats: stats, |
|
760 callbacks: [aCallback], |
|
761 queueType: QUEUE_TYPE_WRITE_CACHE }); |
|
762 |
|
763 this.processQueue(); |
|
764 }, |
|
765 |
|
766 /* |
|
767 * |
|
768 */ |
|
769 writeCache: function writeCache(aStats, aCallback) { |
|
770 debug("saveStats: " + aStats.appId + " " + aStats.serviceType + " " + |
|
771 aStats.networkId + " " + aStats.networkType + " " + aStats.date + " " |
|
772 + aStats.date + " " + aStats.rxBytes + " " + aStats.txBytes); |
|
773 |
|
774 // Generate an unique key from |appId|, |serviceType| and |netId|, |
|
775 // which is used to retrieve data in |cachedStats|. |
|
776 let netId = this.getNetworkId(aStats.networkId, aStats.networkType); |
|
777 let key = aStats.appId + "" + aStats.serviceType + "" + netId; |
|
778 |
|
779 // |cachedStats| only keeps the data with the same date. |
|
780 // If the incoming date is different from |cachedStatsDate|, |
|
781 // both |cachedStats| and |cachedStatsDate| will get updated. |
|
782 let diff = (this._db.normalizeDate(aStats.date) - |
|
783 this._db.normalizeDate(this.cachedStatsDate)) / |
|
784 this._db.sampleRate; |
|
785 if (diff != 0) { |
|
786 this.updateCache(function onUpdated(success, message) { |
|
787 this.cachedStatsDate = aStats.date; |
|
788 this.cachedStats[key] = aStats; |
|
789 |
|
790 if (aCallback) { |
|
791 aCallback(true, "ok"); |
|
792 } |
|
793 }.bind(this)); |
|
794 return; |
|
795 } |
|
796 |
|
797 // Try to find the matched row in the cached by |appId| and |connectionType|. |
|
798 // If not found, save the incoming data into the cached. |
|
799 let cachedStats = this.cachedStats[key]; |
|
800 if (!cachedStats) { |
|
801 this.cachedStats[key] = aStats; |
|
802 if (aCallback) { |
|
803 aCallback(true, "ok"); |
|
804 } |
|
805 return; |
|
806 } |
|
807 |
|
808 // Find matched row, accumulate the traffic amount. |
|
809 cachedStats.rxBytes += aStats.rxBytes; |
|
810 cachedStats.txBytes += aStats.txBytes; |
|
811 |
|
812 // If new rxBytes or txBytes exceeds MAX_CACHED_TRAFFIC |
|
813 // the corresponding row will be saved to indexedDB. |
|
814 // Then, the row will be removed from the cached. |
|
815 if (cachedStats.rxBytes > MAX_CACHED_TRAFFIC || |
|
816 cachedStats.txBytes > MAX_CACHED_TRAFFIC) { |
|
817 this._db.saveStats(cachedStats, function (error, result) { |
|
818 debug("Application stats inserted in indexedDB"); |
|
819 if (aCallback) { |
|
820 aCallback(true, "ok"); |
|
821 } |
|
822 }); |
|
823 delete this.cachedStats[key]; |
|
824 return; |
|
825 } |
|
826 |
|
827 if (aCallback) { |
|
828 aCallback(true, "ok"); |
|
829 } |
|
830 }, |
|
831 |
|
832 updateCachedStats: function updateCachedStats(aCallback) { |
|
833 this.updateQueue.push({ callbacks: [aCallback], |
|
834 queueType: QUEUE_TYPE_UPDATE_CACHE }); |
|
835 |
|
836 this.processQueue(); |
|
837 }, |
|
838 |
|
839 updateCache: function updateCache(aCallback) { |
|
840 debug("updateCache: " + this.cachedStatsDate); |
|
841 |
|
842 let stats = Object.keys(this.cachedStats); |
|
843 if (stats.length == 0) { |
|
844 // |cachedStats| is empty, no need to update. |
|
845 if (aCallback) { |
|
846 aCallback(true, "no need to update"); |
|
847 } |
|
848 return; |
|
849 } |
|
850 |
|
851 let index = 0; |
|
852 this._db.saveStats(this.cachedStats[stats[index]], |
|
853 function onSavedStats(error, result) { |
|
854 debug("Application stats inserted in indexedDB"); |
|
855 |
|
856 // Clean up the |cachedStats| after updating. |
|
857 if (index == stats.length - 1) { |
|
858 this.cachedStats = Object.create(null); |
|
859 |
|
860 if (aCallback) { |
|
861 aCallback(true, "ok"); |
|
862 } |
|
863 return; |
|
864 } |
|
865 |
|
866 // Update is not finished, keep updating. |
|
867 index += 1; |
|
868 this._db.saveStats(this.cachedStats[stats[index]], |
|
869 onSavedStats.bind(this, error, result)); |
|
870 }.bind(this)); |
|
871 }, |
|
872 |
|
873 get maxCachedTraffic () { |
|
874 return MAX_CACHED_TRAFFIC; |
|
875 }, |
|
876 |
|
877 logAllRecords: function logAllRecords() { |
|
878 this._db.logAllRecords(function onResult(aError, aResult) { |
|
879 if (aError) { |
|
880 debug("Error: " + aError); |
|
881 return; |
|
882 } |
|
883 |
|
884 debug("===== LOG ====="); |
|
885 debug("There are " + aResult.length + " items"); |
|
886 debug(JSON.stringify(aResult)); |
|
887 }); |
|
888 }, |
|
889 |
|
890 getAlarms: function getAlarms(mm, msg) { |
|
891 let self = this; |
|
892 let network = msg.data.network; |
|
893 let manifestURL = msg.data.manifestURL; |
|
894 |
|
895 if (network) { |
|
896 this.validateNetwork(network, function onValidateNetwork(aNetId) { |
|
897 if (!aNetId) { |
|
898 mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", |
|
899 { id: msg.id, error: "InvalidInterface", result: null }); |
|
900 return; |
|
901 } |
|
902 |
|
903 self._getAlarms(mm, msg, aNetId, manifestURL); |
|
904 }); |
|
905 return; |
|
906 } |
|
907 |
|
908 this._getAlarms(mm, msg, null, manifestURL); |
|
909 }, |
|
910 |
|
911 _getAlarms: function _getAlarms(mm, msg, aNetId, aManifestURL) { |
|
912 let self = this; |
|
913 this._db.getAlarms(aNetId, aManifestURL, function onCompleted(error, result) { |
|
914 if (error) { |
|
915 mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", |
|
916 { id: msg.id, error: error, result: result }); |
|
917 return; |
|
918 } |
|
919 |
|
920 let alarms = [] |
|
921 // NetworkStatsManager must return the network instead of the networkId. |
|
922 for (let i = 0; i < result.length; i++) { |
|
923 let alarm = result[i]; |
|
924 alarms.push({ id: alarm.id, |
|
925 network: self._networks[alarm.networkId].network, |
|
926 threshold: alarm.absoluteThreshold, |
|
927 data: alarm.data }); |
|
928 } |
|
929 |
|
930 mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", |
|
931 { id: msg.id, error: null, result: alarms }); |
|
932 }); |
|
933 }, |
|
934 |
|
935 removeAlarms: function removeAlarms(mm, msg) { |
|
936 let alarmId = msg.data.alarmId; |
|
937 let manifestURL = msg.data.manifestURL; |
|
938 |
|
939 let self = this; |
|
940 let callback = function onRemove(error, result) { |
|
941 if (error) { |
|
942 mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return", |
|
943 { id: msg.id, error: error, result: result }); |
|
944 return; |
|
945 } |
|
946 |
|
947 for (let i in self._currentAlarms) { |
|
948 let currentAlarm = self._currentAlarms[i].alarm; |
|
949 if (currentAlarm && ((alarmId == currentAlarm.id) || |
|
950 (alarmId == -1 && currentAlarm.manifestURL == manifestURL))) { |
|
951 |
|
952 self._updateCurrentAlarm(currentAlarm.networkId); |
|
953 } |
|
954 } |
|
955 |
|
956 mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return", |
|
957 { id: msg.id, error: error, result: true }); |
|
958 }; |
|
959 |
|
960 if (alarmId == -1) { |
|
961 this._db.removeAlarms(manifestURL, callback); |
|
962 } else { |
|
963 this._db.removeAlarm(alarmId, manifestURL, callback); |
|
964 } |
|
965 }, |
|
966 |
|
967 /* |
|
968 * Function called from manager to set an alarm. |
|
969 */ |
|
970 setAlarm: function setAlarm(mm, msg) { |
|
971 let options = msg.data; |
|
972 let network = options.network; |
|
973 let threshold = options.threshold; |
|
974 |
|
975 debug("Set alarm at " + threshold + " for " + JSON.stringify(network)); |
|
976 |
|
977 if (threshold < 0) { |
|
978 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", |
|
979 { id: msg.id, error: "InvalidThresholdValue", result: null }); |
|
980 return; |
|
981 } |
|
982 |
|
983 let self = this; |
|
984 this.validateNetwork(network, function onValidateNetwork(aNetId) { |
|
985 if (!aNetId) { |
|
986 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", |
|
987 { id: msg.id, error: "InvalidiConnectionType", result: null }); |
|
988 return; |
|
989 } |
|
990 |
|
991 let newAlarm = { |
|
992 id: null, |
|
993 networkId: aNetId, |
|
994 absoluteThreshold: threshold, |
|
995 relativeThreshold: null, |
|
996 startTime: options.startTime, |
|
997 data: options.data, |
|
998 pageURL: options.pageURL, |
|
999 manifestURL: options.manifestURL |
|
1000 }; |
|
1001 |
|
1002 self._getAlarmQuota(newAlarm, function onUpdate(error, quota) { |
|
1003 if (error) { |
|
1004 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", |
|
1005 { id: msg.id, error: error, result: null }); |
|
1006 return; |
|
1007 } |
|
1008 |
|
1009 self._db.addAlarm(newAlarm, function addSuccessCb(error, newId) { |
|
1010 if (error) { |
|
1011 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", |
|
1012 { id: msg.id, error: error, result: null }); |
|
1013 return; |
|
1014 } |
|
1015 |
|
1016 newAlarm.id = newId; |
|
1017 self._setAlarm(newAlarm, function onSet(error, success) { |
|
1018 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", |
|
1019 { id: msg.id, error: error, result: newId }); |
|
1020 |
|
1021 if (error == "InvalidStateError") { |
|
1022 self._fireAlarm(newAlarm); |
|
1023 } |
|
1024 }); |
|
1025 }); |
|
1026 }); |
|
1027 }); |
|
1028 }, |
|
1029 |
|
1030 _setAlarm: function _setAlarm(aAlarm, aCallback) { |
|
1031 let currentAlarm = this._currentAlarms[aAlarm.networkId]; |
|
1032 if ((Object.getOwnPropertyNames(currentAlarm).length !== 0 && |
|
1033 aAlarm.relativeThreshold > currentAlarm.alarm.relativeThreshold) || |
|
1034 this._networks[aAlarm.networkId].status != NETWORK_STATUS_READY) { |
|
1035 aCallback(null, true); |
|
1036 return; |
|
1037 } |
|
1038 |
|
1039 let self = this; |
|
1040 |
|
1041 this._getAlarmQuota(aAlarm, function onUpdate(aError, aQuota) { |
|
1042 if (aError) { |
|
1043 aCallback(aError, null); |
|
1044 return; |
|
1045 } |
|
1046 |
|
1047 let callback = function onAlarmSet(aError) { |
|
1048 if (aError) { |
|
1049 debug("Set alarm error: " + aError); |
|
1050 aCallback("netdError", null); |
|
1051 return; |
|
1052 } |
|
1053 |
|
1054 self._currentAlarms[aAlarm.networkId].alarm = aAlarm; |
|
1055 |
|
1056 aCallback(null, true); |
|
1057 }; |
|
1058 |
|
1059 debug("Set alarm " + JSON.stringify(aAlarm)); |
|
1060 let interfaceName = self._networks[aAlarm.networkId].interfaceName; |
|
1061 if (interfaceName) { |
|
1062 networkService.setNetworkInterfaceAlarm(interfaceName, |
|
1063 aQuota, |
|
1064 callback); |
|
1065 return; |
|
1066 } |
|
1067 |
|
1068 aCallback(null, true); |
|
1069 }); |
|
1070 }, |
|
1071 |
|
1072 _getAlarmQuota: function _getAlarmQuota(aAlarm, aCallback) { |
|
1073 let self = this; |
|
1074 this.updateStats(aAlarm.networkId, function onStatsUpdated(aResult, aMessage) { |
|
1075 self._db.getCurrentStats(self._networks[aAlarm.networkId].network, |
|
1076 aAlarm.startTime, |
|
1077 function onStatsFound(error, result) { |
|
1078 if (error) { |
|
1079 debug("Error getting stats for " + |
|
1080 JSON.stringify(self._networks[aAlarm.networkId]) + ": " + error); |
|
1081 aCallback(error, result); |
|
1082 return; |
|
1083 } |
|
1084 |
|
1085 let quota = aAlarm.absoluteThreshold - result.rxBytes - result.txBytes; |
|
1086 |
|
1087 // Alarm set to a threshold lower than current rx/tx bytes. |
|
1088 if (quota <= 0) { |
|
1089 aCallback("InvalidStateError", null); |
|
1090 return; |
|
1091 } |
|
1092 |
|
1093 aAlarm.relativeThreshold = aAlarm.startTime |
|
1094 ? result.rxTotalBytes + result.txTotalBytes + quota |
|
1095 : aAlarm.absoluteThreshold; |
|
1096 |
|
1097 aCallback(null, quota); |
|
1098 }); |
|
1099 }); |
|
1100 }, |
|
1101 |
|
1102 _fireAlarm: function _fireAlarm(aAlarm) { |
|
1103 debug("Fire alarm"); |
|
1104 |
|
1105 let self = this; |
|
1106 this._db.removeAlarm(aAlarm.id, null, function onRemove(aError, aResult){ |
|
1107 if (!aError && !aResult) { |
|
1108 return; |
|
1109 } |
|
1110 |
|
1111 self._fireSystemMessage(aAlarm); |
|
1112 self._updateCurrentAlarm(aAlarm.networkId); |
|
1113 }); |
|
1114 }, |
|
1115 |
|
1116 _updateCurrentAlarm: function _updateCurrentAlarm(aNetworkId) { |
|
1117 this._currentAlarms[aNetworkId] = Object.create(null); |
|
1118 |
|
1119 let self = this; |
|
1120 this._db.getFirstAlarm(aNetworkId, function onGet(error, result){ |
|
1121 if (error) { |
|
1122 debug("Error getting the first alarm"); |
|
1123 return; |
|
1124 } |
|
1125 |
|
1126 if (!result) { |
|
1127 let interfaceName = self._networks[aNetworkId].interfaceName; |
|
1128 networkService.setNetworkInterfaceAlarm(interfaceName, -1, |
|
1129 function onComplete(){}); |
|
1130 return; |
|
1131 } |
|
1132 |
|
1133 self._setAlarm(result, function onSet(error, success){ |
|
1134 if (error == "InvalidStateError") { |
|
1135 self._fireAlarm(result); |
|
1136 return; |
|
1137 } |
|
1138 }); |
|
1139 }); |
|
1140 }, |
|
1141 |
|
1142 _fireSystemMessage: function _fireSystemMessage(aAlarm) { |
|
1143 debug("Fire system message: " + JSON.stringify(aAlarm)); |
|
1144 |
|
1145 let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null); |
|
1146 let pageURI = Services.io.newURI(aAlarm.pageURL, null, null); |
|
1147 |
|
1148 let alarm = { "id": aAlarm.id, |
|
1149 "threshold": aAlarm.absoluteThreshold, |
|
1150 "data": aAlarm.data }; |
|
1151 messenger.sendMessage("networkstats-alarm", alarm, pageURI, manifestURI); |
|
1152 } |
|
1153 }; |
|
1154 |
|
1155 NetworkStatsService.init(); |