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
7this.EXPORTED_SYMBOLS = ['NetworkStatsDB'];
8
9const DEBUG = false;
10function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); }
11
12const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
13
14Cu.import("resource://gre/modules/XPCOMUtils.jsm");
15Cu.import("resource://gre/modules/Services.jsm");
16Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
17Cu.importGlobalProperties(["indexedDB"]);
18
19XPCOMUtils.defineLazyServiceGetter(this, "appsService",
20                                   "@mozilla.org/AppsService;1",
21                                   "nsIAppsService");
22
23const DB_NAME = "net_stats";
24const DB_VERSION = 9;
25const DEPRECATED_STATS_STORE_NAME =
26  [
27    "net_stats_v2",    // existed only in DB version 2
28    "net_stats",       // existed in DB version 1 and 3 to 5
29    "net_stats_store", // existed in DB version 6 to 8
30  ];
31const STATS_STORE_NAME = "net_stats_store_v3"; // since DB version 9
32const ALARMS_STORE_NAME = "net_alarm";
33
34// Constant defining the maximum values allowed per interface. If more, older
35// will be erased.
36const VALUES_MAX_LENGTH = 6 * 30;
37
38// Constant defining the rate of the samples. Daily.
39const SAMPLE_RATE = 1000 * 60 * 60 * 24;
40
41this.NetworkStatsDB = function NetworkStatsDB() {
42  if (DEBUG) {
43    debug("Constructor");
44  }
45  this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]);
46}
47
48NetworkStatsDB.prototype = {
49  __proto__: IndexedDBHelper.prototype,
50
51  dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) {
52    function successCb(result) {
53      txnCb(null, result);
54    }
55    function errorCb(error) {
56      txnCb(error, null);
57    }
58    return this.newTxn(txn_type, store_name, callback, successCb, errorCb);
59  },
60
61  /**
62   * The onupgradeneeded handler of the IDBOpenDBRequest.
63   * This function is called in IndexedDBHelper open() method.
64   *
65   * @param {IDBTransaction} aTransaction
66   *        {IDBDatabase} aDb
67   *        {64-bit integer} aOldVersion The version number on local storage.
68   *        {64-bit integer} aNewVersion The version number to be upgraded to.
69   *
70   * @note  Be careful with the database upgrade pattern.
71   *        Because IndexedDB operations are performed asynchronously, we must
72   *        apply a recursive approach instead of an iterative approach while
73   *        upgrading versions.
74   */
75  upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
76    if (DEBUG) {
77      debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
78    }
79    let db = aDb;
80    let objectStore;
81
82    // An array of upgrade functions for each version.
83    let upgradeSteps = [
84      function upgrade0to1() {
85        if (DEBUG) debug("Upgrade 0 to 1: Create object stores and indexes.");
86
87        // Create the initial database schema.
88        objectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[1],
89                                           { keyPath: ["connectionType", "timestamp"] });
90        objectStore.createIndex("connectionType", "connectionType", { unique: false });
91        objectStore.createIndex("timestamp", "timestamp", { unique: false });
92        objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
93        objectStore.createIndex("txBytes", "txBytes", { unique: false });
94        objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
95        objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
96
97        upgradeNextVersion();
98      },
99
100      function upgrade1to2() {
101        if (DEBUG) debug("Upgrade 1 to 2: Do nothing.");
102        upgradeNextVersion();
103      },
104
105      function upgrade2to3() {
106        if (DEBUG) debug("Upgrade 2 to 3: Add keyPath appId to object store.");
107
108        // In order to support per-app traffic data storage, the original
109        // objectStore needs to be replaced by a new objectStore with new
110        // key path ("appId") and new index ("appId").
111        // Also, since now networks are identified by their
112        // [networkId, networkType] not just by their connectionType,
113        // to modify the keyPath is mandatory to delete the object store
114        // and create it again. Old data is going to be deleted because the
115        // networkId for each sample can not be set.
116
117        // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when
118        // upgrading from 1.2 to 1.3 objectStore name should be checked.
119        let stores = db.objectStoreNames;
120        let deprecatedName = DEPRECATED_STATS_STORE_NAME[0];
121        let storeName = DEPRECATED_STATS_STORE_NAME[1];
122        if(stores.contains(deprecatedName)) {
123          // Delete the obsolete stats store.
124          db.deleteObjectStore(deprecatedName);
125        } else {
126          // Re-create stats object store without copying records.
127          db.deleteObjectStore(storeName);
128        }
129
130        objectStore = db.createObjectStore(storeName, { keyPath: ["appId", "network", "timestamp"] });
131        objectStore.createIndex("appId", "appId", { unique: false });
132        objectStore.createIndex("network", "network", { unique: false });
133        objectStore.createIndex("networkType", "networkType", { unique: false });
134        objectStore.createIndex("timestamp", "timestamp", { unique: false });
135        objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
136        objectStore.createIndex("txBytes", "txBytes", { unique: false });
137        objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
138        objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
139
140        upgradeNextVersion();
141      },
142
143      function upgrade3to4() {
144        if (DEBUG) debug("Upgrade 3 to 4: Delete redundant indexes.");
145
146        // Delete redundant indexes (leave "network" only).
147        objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]);
148        if (objectStore.indexNames.contains("appId")) {
149          objectStore.deleteIndex("appId");
150        }
151        if (objectStore.indexNames.contains("networkType")) {
152          objectStore.deleteIndex("networkType");
153        }
154        if (objectStore.indexNames.contains("timestamp")) {
155          objectStore.deleteIndex("timestamp");
156        }
157        if (objectStore.indexNames.contains("rxBytes")) {
158          objectStore.deleteIndex("rxBytes");
159        }
160        if (objectStore.indexNames.contains("txBytes")) {
161          objectStore.deleteIndex("txBytes");
162        }
163        if (objectStore.indexNames.contains("rxTotalBytes")) {
164          objectStore.deleteIndex("rxTotalBytes");
165        }
166        if (objectStore.indexNames.contains("txTotalBytes")) {
167          objectStore.deleteIndex("txTotalBytes");
168        }
169
170        upgradeNextVersion();
171      },
172
173      function upgrade4to5() {
174        if (DEBUG) debug("Upgrade 4 to 5: Create object store for alarms.");
175
176        // In order to manage alarms, it is necessary to use a global counter
177        // (totalBytes) that will increase regardless of the system reboot.
178        objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]);
179
180        // Now, systemBytes will hold the old totalBytes and totalBytes will
181        // keep the increasing counter. |counters| will keep the track of
182        // accumulated values.
183        let counters = {};
184
185        objectStore.openCursor().onsuccess = function(event) {
186          let cursor = event.target.result;
187          if (!cursor){
188            // upgrade4to5 completed now.
189            upgradeNextVersion();
190            return;
191          }
192
193          cursor.value.rxSystemBytes = cursor.value.rxTotalBytes;
194          cursor.value.txSystemBytes = cursor.value.txTotalBytes;
195
196          if (cursor.value.appId == 0) {
197            let netId = cursor.value.network[0] + '' + cursor.value.network[1];
198            if (!counters[netId]) {
199              counters[netId] = {
200                rxCounter: 0,
201                txCounter: 0,
202                lastRx: 0,
203                lastTx: 0
204              };
205            }
206
207            let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx;
208            let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx;
209            if (rxDiff < 0 || txDiff < 0) {
210              // System reboot between samples, so take the current one.
211              rxDiff = cursor.value.rxSystemBytes;
212              txDiff = cursor.value.txSystemBytes;
213            }
214
215            counters[netId].rxCounter += rxDiff;
216            counters[netId].txCounter += txDiff;
217            cursor.value.rxTotalBytes = counters[netId].rxCounter;
218            cursor.value.txTotalBytes = counters[netId].txCounter;
219
220            counters[netId].lastRx = cursor.value.rxSystemBytes;
221            counters[netId].lastTx = cursor.value.txSystemBytes;
222          } else {
223            cursor.value.rxTotalBytes = cursor.value.rxSystemBytes;
224            cursor.value.txTotalBytes = cursor.value.txSystemBytes;
225          }
226
227          cursor.update(cursor.value);
228          cursor.continue();
229        };
230
231        // Create object store for alarms.
232        objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true });
233        objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false });
234        objectStore.createIndex("manifestURL", "manifestURL", { unique: false });
235      },
236
237      function upgrade5to6() {
238        if (DEBUG) debug("Upgrade 5 to 6: Add keyPath serviceType to object store.");
239
240        // In contrast to "per-app" traffic data, "system-only" traffic data
241        // refers to data which can not be identified by any applications.
242        // To further support "system-only" data storage, the data can be
243        // saved by service type (e.g., Tethering, OTA). Thus it's needed to
244        // have a new key ("serviceType") for the ojectStore.
245        let newObjectStore;
246        let deprecatedName = DEPRECATED_STATS_STORE_NAME[1];
247        newObjectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[2],
248                         { keyPath: ["appId", "serviceType", "network", "timestamp"] });
249        newObjectStore.createIndex("network", "network", { unique: false });
250
251        // Copy the data from the original objectStore to the new objectStore.
252        objectStore = aTransaction.objectStore(deprecatedName);
253        objectStore.openCursor().onsuccess = function(event) {
254          let cursor = event.target.result;
255          if (!cursor) {
256            db.deleteObjectStore(deprecatedName);
257            // upgrade5to6 completed now.
258            upgradeNextVersion();
259            return;
260          }
261
262          let newStats = cursor.value;
263          newStats.serviceType = "";
264          newObjectStore.put(newStats);
265          cursor.continue();
266        };
267      },
268
269      function upgrade6to7() {
270        if (DEBUG) debug("Upgrade 6 to 7: Replace alarm threshold by relativeThreshold.");
271
272        // Replace threshold attribute of alarm index by relativeThreshold in alarms DB.
273        // Now alarms are indexed by relativeThreshold, which is the threshold relative
274        // to current system stats.
275        let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME);
276
277        // Delete "alarm" index.
278        if (alarmsStore.indexNames.contains("alarm")) {
279          alarmsStore.deleteIndex("alarm");
280        }
281
282        // Create new "alarm" index.
283        alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false });
284
285        // Populate new "alarm" index attributes.
286        alarmsStore.openCursor().onsuccess = function(event) {
287          let cursor = event.target.result;
288          if (!cursor) {
289            upgrade6to7_updateTotalBytes();
290            return;
291          }
292
293          cursor.value.relativeThreshold = cursor.value.threshold;
294          cursor.value.absoluteThreshold = cursor.value.threshold;
295          delete cursor.value.threshold;
296
297          cursor.update(cursor.value);
298          cursor.continue();
299        }
300
301        function upgrade6to7_updateTotalBytes() {
302          if (DEBUG) debug("Upgrade 6 to 7: Update TotalBytes.");
303          // Previous versions save accumulative totalBytes, increasing although the system
304          // reboots or resets stats. But is necessary to reset the total counters when reset
305          // through 'clearInterfaceStats'.
306          let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]);
307          let networks = [];
308
309          // Find networks stored in the database.
310          statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) {
311            let cursor = event.target.result;
312
313            // Store each network into an array.
314            if (cursor) {
315              networks.push(cursor.key);
316              cursor.continue();
317              return;
318            }
319
320            // Start to deal with each network.
321            let pending = networks.length;
322
323            if (pending === 0) {
324              // Found no records of network. upgrade6to7 completed now.
325              upgradeNextVersion();
326              return;
327            }
328
329            networks.forEach(function(network) {
330              let lowerFilter = [0, "", network, 0];
331              let upperFilter = [0, "", network, ""];
332              let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
333
334              // Find number of samples for a given network.
335              statsStore.count(range).onsuccess = function(event) {
336                let recordCount = event.target.result;
337
338                // If there are more samples than the max allowed, there is no way to know
339                // when does reset take place.
340                if (recordCount === 0 || recordCount >= VALUES_MAX_LENGTH) {
341                  pending--;
342                  if (pending === 0) {
343                    upgradeNextVersion();
344                  }
345                  return;
346                }
347
348                let last = null;
349                // Reset detected if the first sample totalCounters are different than bytes
350                // counters. If so, the total counters should be recalculated.
351                statsStore.openCursor(range).onsuccess = function(event) {
352                  let cursor = event.target.result;
353                  if (!cursor) {
354                    pending--;
355                    if (pending === 0) {
356                      upgradeNextVersion();
357                    }
358                    return;
359                  }
360                  if (!last) {
361                    if (cursor.value.rxTotalBytes == cursor.value.rxBytes &&
362                        cursor.value.txTotalBytes == cursor.value.txBytes) {
363                      pending--;
364                      if (pending === 0) {
365                        upgradeNextVersion();
366                      }
367                      return;
368                    }
369
370                    cursor.value.rxTotalBytes = cursor.value.rxBytes;
371                    cursor.value.txTotalBytes = cursor.value.txBytes;
372                    cursor.update(cursor.value);
373                    last = cursor.value;
374                    cursor.continue();
375                    return;
376                  }
377
378                  // Recalculate the total counter for last / current sample
379                  cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes;
380                  cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes;
381                  cursor.update(cursor.value);
382                  last = cursor.value;
383                  cursor.continue();
384                }
385              }
386            }, this); // end of networks.forEach()
387          }; // end of statsStore.index("network").openKeyCursor().onsuccess callback
388        } // end of function upgrade6to7_updateTotalBytes
389      },
390
391      function upgrade7to8() {
392        if (DEBUG) debug("Upgrade 7 to 8: Create index serviceType.");
393
394        // Create index for 'ServiceType' in order to make it retrievable.
395        let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]);
396        statsStore.createIndex("serviceType", "serviceType", { unique: false });
397
398        upgradeNextVersion();
399      },
400
401      function upgrade8to9() {
402        if (DEBUG) debug("Upgrade 8 to 9: Add keyPath isInBrowser to " +
403                         "network stats object store");
404
405        // Since B2G v2.0, there is no stand-alone browser app anymore.
406        // The browser app is a mozbrowser iframe element owned by system app.
407        // In order to separate traffic generated from system and browser, we
408        // have to add a new attribute |isInBrowser| as keyPath.
409        // Refer to bug 1070944 for more detail.
410        let newObjectStore;
411        let deprecatedName = DEPRECATED_STATS_STORE_NAME[2];
412        newObjectStore = db.createObjectStore(STATS_STORE_NAME,
413                         { keyPath: ["appId", "isInBrowser", "serviceType",
414                                     "network", "timestamp"] });
415        newObjectStore.createIndex("network", "network", { unique: false });
416        newObjectStore.createIndex("serviceType", "serviceType", { unique: false });
417
418        // Copy records from the current object store to the new one.
419        objectStore = aTransaction.objectStore(deprecatedName);
420        objectStore.openCursor().onsuccess = function (event) {
421          let cursor = event.target.result;
422          if (!cursor) {
423            db.deleteObjectStore(deprecatedName);
424            // upgrade8to9 completed now.
425            return;
426          }
427          let newStats = cursor.value;
428          // Augment records by adding the new isInBrowser attribute.
429          // Notes:
430          // 1. Key value cannot be boolean type. Use 1/0 instead of true/false.
431          // 2. Most traffic of system app should come from its browser iframe,
432          //    thus assign isInBrowser as 1 for system app.
433          let manifestURL = appsService.getManifestURLByLocalId(newStats.appId);
434          if (manifestURL && manifestURL.search(/app:\/\/system\./) === 0) {
435            newStats.isInBrowser = 1;
436          } else {
437            newStats.isInBrowser = 0;
438          }
439          newObjectStore.put(newStats);
440          cursor.continue();
441        };
442      }
443    ];
444
445    let index = aOldVersion;
446    let outer = this;
447
448    function upgradeNextVersion() {
449      if (index == aNewVersion) {
450        debug("Upgrade finished.");
451        return;
452      }
453
454      try {
455        var i = index++;
456        if (DEBUG) debug("Upgrade step: " + i + "\n");
457        upgradeSteps[i].call(outer);
458      } catch (ex) {
459        dump("Caught exception " + ex);
460        throw ex;
461        return;
462      }
463    }
464
465    if (aNewVersion > upgradeSteps.length) {
466      debug("No migration steps for the new version!");
467      aTransaction.abort();
468      return;
469    }
470
471    upgradeNextVersion();
472  },
473
474  importData: function importData(aStats) {
475    let stats = { appId:         aStats.appId,
476                  isInBrowser:   aStats.isInBrowser ? 1 : 0,
477                  serviceType:   aStats.serviceType,
478                  network:       [aStats.networkId, aStats.networkType],
479                  timestamp:     aStats.timestamp,
480                  rxBytes:       aStats.rxBytes,
481                  txBytes:       aStats.txBytes,
482                  rxSystemBytes: aStats.rxSystemBytes,
483                  txSystemBytes: aStats.txSystemBytes,
484                  rxTotalBytes:  aStats.rxTotalBytes,
485                  txTotalBytes:  aStats.txTotalBytes };
486
487    return stats;
488  },
489
490  exportData: function exportData(aStats) {
491    let stats = { appId:        aStats.appId,
492                  isInBrowser:  aStats.isInBrowser ? true : false,
493                  serviceType:  aStats.serviceType,
494                  networkId:    aStats.network[0],
495                  networkType:  aStats.network[1],
496                  timestamp:    aStats.timestamp,
497                  rxBytes:      aStats.rxBytes,
498                  txBytes:      aStats.txBytes,
499                  rxTotalBytes: aStats.rxTotalBytes,
500                  txTotalBytes: aStats.txTotalBytes };
501
502    return stats;
503  },
504
505  normalizeDate: function normalizeDate(aDate) {
506    // Convert to UTC according to timezone and
507    // filter timestamp to get SAMPLE_RATE precission
508    let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000;
509    timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE;
510    return timestamp;
511  },
512
513  saveStats: function saveStats(aStats, aResultCb) {
514    let isAccumulative = aStats.isAccumulative;
515    let timestamp = this.normalizeDate(aStats.date);
516
517    let stats = { appId:         aStats.appId,
518                  isInBrowser:   aStats.isInBrowser,
519                  serviceType:   aStats.serviceType,
520                  networkId:     aStats.networkId,
521                  networkType:   aStats.networkType,
522                  timestamp:     timestamp,
523                  rxBytes:       isAccumulative ? 0 : aStats.rxBytes,
524                  txBytes:       isAccumulative ? 0 : aStats.txBytes,
525                  rxSystemBytes: isAccumulative ? aStats.rxBytes : 0,
526                  txSystemBytes: isAccumulative ? aStats.txBytes : 0,
527                  rxTotalBytes:  isAccumulative ? aStats.rxBytes : 0,
528                  txTotalBytes:  isAccumulative ? aStats.txBytes : 0 };
529
530    stats = this.importData(stats);
531
532    this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) {
533      if (DEBUG) {
534        debug("Filtered time: " + new Date(timestamp));
535        debug("New stats: " + JSON.stringify(stats));
536      }
537
538      let lowerFilter = [stats.appId, stats.isInBrowser, stats.serviceType,
539                         stats.network, 0];
540      let upperFilter = [stats.appId, stats.isInBrowser, stats.serviceType,
541                         stats.network, ""];
542      let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
543
544      let request = aStore.openCursor(range, 'prev');
545      request.onsuccess = function onsuccess(event) {
546        let cursor = event.target.result;
547        if (!cursor) {
548          // Empty, so save first element.
549
550          if (!isAccumulative) {
551            this._saveStats(aTxn, aStore, stats);
552            return;
553          }
554
555          // There could be a time delay between the point when the network
556          // interface comes up and the point when the database is initialized.
557          // In this short interval some traffic data are generated but are not
558          // registered by the first sample.
559          stats.rxBytes = stats.rxTotalBytes;
560          stats.txBytes = stats.txTotalBytes;
561
562          // However, if the interface is not switched on after the database is
563          // initialized (dual sim use case) stats should be set to 0.
564          let req = aStore.index("network").openKeyCursor(null, "nextunique");
565          req.onsuccess = function onsuccess(event) {
566            let cursor = event.target.result;
567            if (cursor) {
568              if (cursor.key[1] == stats.network[1]) {
569                stats.rxBytes = 0;
570                stats.txBytes = 0;
571                this._saveStats(aTxn, aStore, stats);
572                return;
573              }
574
575              cursor.continue();
576              return;
577            }
578
579            this._saveStats(aTxn, aStore, stats);
580          }.bind(this);
581
582          return;
583        }
584
585        // There are old samples
586        if (DEBUG) {
587          debug("Last value " + JSON.stringify(cursor.value));
588        }
589
590        // Remove stats previous to now - VALUE_MAX_LENGTH
591        this._removeOldStats(aTxn, aStore, stats.appId, stats.isInBrowser,
592                             stats.serviceType, stats.network, stats.timestamp);
593
594        // Process stats before save
595        this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative);
596      }.bind(this);
597    }.bind(this), aResultCb);
598  },
599
600  /*
601   * This function check that stats are saved in the database following the sample rate.
602   * In this way is easier to find elements when stats are requested.
603   */
604  _processSamplesDiff: function _processSamplesDiff(aTxn,
605                                                    aStore,
606                                                    aLastSampleCursor,
607                                                    aNewSample,
608                                                    aIsAccumulative) {
609    let lastSample = aLastSampleCursor.value;
610
611    // Get difference between last and new sample.
612    let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE;
613    if (diff % 1) {
614      // diff is decimal, so some error happened because samples are stored as a multiple
615      // of SAMPLE_RATE
616      aTxn.abort();
617      throw new Error("Error processing samples");
618    }
619
620    if (DEBUG) {
621      debug("New: " + aNewSample.timestamp + " - Last: " +
622            lastSample.timestamp + " - diff: " + diff);
623    }
624
625    // If the incoming data has a accumulation feature, the new
626    // |txBytes|/|rxBytes| is assigend by differnces between the new
627    // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|.
628    // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes|
629    // is the new |txBytes|/|rxBytes|.
630    let rxDiff = 0;
631    let txDiff = 0;
632    if (aIsAccumulative) {
633      rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes;
634      txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes;
635      if (rxDiff < 0 || txDiff < 0) {
636        rxDiff = aNewSample.rxSystemBytes;
637        txDiff = aNewSample.txSystemBytes;
638      }
639      aNewSample.rxBytes = rxDiff;
640      aNewSample.txBytes = txDiff;
641
642      aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff;
643      aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff;
644    } else {
645      rxDiff = aNewSample.rxBytes;
646      txDiff = aNewSample.txBytes;
647    }
648
649    if (diff == 1) {
650      // New element.
651
652      // If the incoming data is non-accumulative, the new
653      // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new
654      // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|.
655      if (!aIsAccumulative) {
656        aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes;
657        aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes;
658      }
659
660      this._saveStats(aTxn, aStore, aNewSample);
661      return;
662    }
663    if (diff > 1) {
664      // Some samples lost. Device off during one or more samplerate periods.
665      // Time or timezone changed
666      // Add lost samples with 0 bytes and the actual one.
667      if (diff > VALUES_MAX_LENGTH) {
668        diff = VALUES_MAX_LENGTH;
669      }
670
671      let data = [];
672      for (let i = diff - 2; i >= 0; i--) {
673        let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1);
674        let sample = { appId:         aNewSample.appId,
675                       isInBrowser:   aNewSample.isInBrowser,
676                       serviceType:   aNewSample.serviceType,
677                       network:       aNewSample.network,
678                       timestamp:     time,
679                       rxBytes:       0,
680                       txBytes:       0,
681                       rxSystemBytes: lastSample.rxSystemBytes,
682                       txSystemBytes: lastSample.txSystemBytes,
683                       rxTotalBytes:  lastSample.rxTotalBytes,
684                       txTotalBytes:  lastSample.txTotalBytes };
685
686        data.push(sample);
687      }
688
689      data.push(aNewSample);
690      this._saveStats(aTxn, aStore, data);
691      return;
692    }
693    if (diff == 0 || diff < 0) {
694      // New element received before samplerate period. It means that device has
695      // been restarted (or clock / timezone change).
696      // Update element. If diff < 0, clock or timezone changed back. Place data
697      // in the last sample.
698
699      // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the
700      // last |rxTotalBytes|/|txTotalBytes|.
701      lastSample.rxBytes += rxDiff;
702      lastSample.txBytes += txDiff;
703      lastSample.rxSystemBytes = aNewSample.rxSystemBytes;
704      lastSample.txSystemBytes = aNewSample.txSystemBytes;
705      lastSample.rxTotalBytes += rxDiff;
706      lastSample.txTotalBytes += txDiff;
707
708      if (DEBUG) {
709        debug("Update: " + JSON.stringify(lastSample));
710      }
711      let req = aLastSampleCursor.update(lastSample);
712    }
713  },
714
715  _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) {
716    if (DEBUG) {
717      debug("_saveStats: " + JSON.stringify(aNetworkStats));
718    }
719
720    if (Array.isArray(aNetworkStats)) {
721      let len = aNetworkStats.length - 1;
722      for (let i = 0; i <= len; i++) {
723        aStore.put(aNetworkStats[i]);
724      }
725    } else {
726      aStore.put(aNetworkStats);
727    }
728  },
729
730  _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aIsInBrowser,
731                                            aServiceType, aNetwork, aDate) {
732    // Callback function to remove old items when new ones are added.
733    let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1);
734    let lowerFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, 0];
735    let upperFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, filterDate];
736    let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
737    let lastSample = null;
738    let self = this;
739
740    aStore.openCursor(range).onsuccess = function(event) {
741      var cursor = event.target.result;
742      if (cursor) {
743        lastSample = cursor.value;
744        cursor.delete();
745        cursor.continue();
746        return;
747      }
748
749      // If all samples for a network are removed, an empty sample
750      // has to be saved to keep the totalBytes in order to compute
751      // future samples because system counters are not set to 0.
752      // Thus, if there are no samples left, the last sample removed
753      // will be saved again after setting its bytes to 0.
754      let request = aStore.index("network").openCursor(aNetwork);
755      request.onsuccess = function onsuccess(event) {
756        let cursor = event.target.result;
757        if (!cursor && lastSample != null) {
758          let timestamp = new Date();
759          timestamp = self.normalizeDate(timestamp);
760          lastSample.timestamp = timestamp;
761          lastSample.rxBytes = 0;
762          lastSample.txBytes = 0;
763          self._saveStats(aTxn, aStore, lastSample);
764        }
765      };
766    };
767  },
768
769  clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) {
770    let network = [aNetwork.network.id, aNetwork.network.type];
771    let self = this;
772
773    // Clear and save an empty sample to keep sync with system counters
774    this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) {
775      let sample = null;
776      let request = aStore.index("network").openCursor(network, "prev");
777      request.onsuccess = function onsuccess(event) {
778        let cursor = event.target.result;
779        if (cursor) {
780          if (!sample && cursor.value.appId == 0) {
781            sample = cursor.value;
782          }
783
784          cursor.delete();
785          cursor.continue();
786          return;
787        }
788
789        if (sample) {
790          let timestamp = new Date();
791          timestamp = self.normalizeDate(timestamp);
792          sample.timestamp = timestamp;
793          sample.appId = 0;
794          sample.isInBrowser = 0;
795          sample.serviceType = "";
796          sample.rxBytes = 0;
797          sample.txBytes = 0;
798          sample.rxTotalBytes = 0;
799          sample.txTotalBytes = 0;
800
801          self._saveStats(aTxn, aStore, sample);
802        }
803      };
804    }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb));
805  },
806
807  clearStats: function clearStats(aNetworks, aResultCb) {
808    let index = 0;
809    let stats = [];
810    let self = this;
811
812    let callback = function(aError, aResult) {
813      index++;
814
815      if (!aError && index < aNetworks.length) {
816        self.clearInterfaceStats(aNetworks[index], callback);
817        return;
818      }
819
820      aResultCb(aError, aResult);
821    };
822
823    if (!aNetworks[index]) {
824      aResultCb(null, true);
825      return;
826    }
827    this.clearInterfaceStats(aNetworks[index], callback);
828  },
829
830  getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) {
831    if (DEBUG) {
832      debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate);
833    }
834
835    let network = [aNetwork.id, aNetwork.type];
836    if (aDate) {
837      this._getCurrentStatsFromDate(network, aDate, aResultCb);
838      return;
839    }
840
841    this._getCurrentStats(network, aResultCb);
842  },
843
844  _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) {
845    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
846      let request = null;
847      let upperFilter = [0, 1, "", aNetwork, Date.now()];
848      let range = IDBKeyRange.upperBound(upperFilter, false);
849      let result = { rxBytes:      0, txBytes:      0,
850                     rxTotalBytes: 0, txTotalBytes: 0 };
851
852      request = store.openCursor(range, "prev");
853
854      request.onsuccess = function onsuccess(event) {
855        let cursor = event.target.result;
856        if (cursor) {
857          result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes;
858          result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes;
859        }
860
861        txn.result = result;
862      };
863    }.bind(this), aResultCb);
864  },
865
866  _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) {
867    aDate = new Date(aDate);
868    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
869      let request = null;
870      let start = this.normalizeDate(aDate);
871      let upperFilter = [0, 1, "", aNetwork, Date.now()];
872      let range = IDBKeyRange.upperBound(upperFilter, false);
873      let result = { rxBytes:      0, txBytes:      0,
874                     rxTotalBytes: 0, txTotalBytes: 0 };
875
876      request = store.openCursor(range, "prev");
877
878      request.onsuccess = function onsuccess(event) {
879        let cursor = event.target.result;
880        if (cursor) {
881          result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes;
882          result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes;
883        }
884
885        let timestamp = cursor.value.timestamp;
886        let range = IDBKeyRange.lowerBound(lowerFilter, false);
887        request = store.openCursor(range);
888
889        request.onsuccess = function onsuccess(event) {
890          let cursor = event.target.result;
891          if (cursor) {
892            if (cursor.value.timestamp == timestamp) {
893              // There is one sample only.
894              result.rxBytes = cursor.value.rxBytes;
895              result.txBytes = cursor.value.txBytes;
896            } else {
897              result.rxBytes -= cursor.value.rxTotalBytes;
898              result.txBytes -= cursor.value.txTotalBytes;
899            }
900          }
901
902          txn.result = result;
903        };
904      };
905    }.bind(this), aResultCb);
906  },
907
908  find: function find(aResultCb, aAppId, aBrowsingTrafficOnly, aServiceType,
909                      aNetwork, aStart, aEnd, aAppManifestURL) {
910    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
911    let start = this.normalizeDate(aStart);
912    let end = this.normalizeDate(aEnd);
913
914    if (DEBUG) {
915      debug("Find samples for appId: " + aAppId +
916            " browsingTrafficOnly: " + aBrowsingTrafficOnly +
917            " serviceType: " + aServiceType +
918            " network: " + JSON.stringify(aNetwork) + " from " + start +
919            " until " + end);
920      debug("Start time: " + new Date(start));
921      debug("End time: " + new Date(end));
922    }
923
924    // Find samples of browsing traffic (isInBrowser = 1) first since they are
925    // needed no matter browsingTrafficOnly is true or false.
926    // We have to make two queries to database because we cannot filter correct
927    // records by a single query that sets ranges for two keys (isInBrowser and
928    // timestamp). We think it is because the keyPath contains an array
929    // (network) so such query does not work.
930    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
931      let network = [aNetwork.id, aNetwork.type];
932      let lowerFilter = [aAppId, 1, aServiceType, network, start];
933      let upperFilter = [aAppId, 1, aServiceType, network, end];
934      let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
935
936      let data = [];
937
938      if (!aTxn.result) {
939        aTxn.result = {};
940      }
941      aTxn.result.appManifestURL = aAppManifestURL;
942      aTxn.result.browsingTrafficOnly = aBrowsingTrafficOnly;
943      aTxn.result.serviceType = aServiceType;
944      aTxn.result.network = aNetwork;
945      aTxn.result.start = aStart;
946      aTxn.result.end = aEnd;
947
948      let request = aStore.openCursor(range).onsuccess = function(event) {
949        var cursor = event.target.result;
950        if (cursor){
951          // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes for
952          // the first (oldest) sample. The rx/txTotalBytes fields record
953          // accumulative usage amount, which means even if old samples were
954          // expired and removed from the Database, we can still obtain the
955          // correct network usage.
956          if (data.length == 0) {
957            data.push({ rxBytes: cursor.value.rxTotalBytes,
958                        txBytes: cursor.value.txTotalBytes,
959                        date: new Date(cursor.value.timestamp + offset) });
960          } else {
961            data.push({ rxBytes: cursor.value.rxBytes,
962                        txBytes: cursor.value.txBytes,
963                        date: new Date(cursor.value.timestamp + offset) });
964          }
965          cursor.continue();
966          return;
967        }
968
969        if (aBrowsingTrafficOnly) {
970          this.fillResultSamples(start + offset, end + offset, data);
971          aTxn.result.data = data;
972          return;
973        }
974
975        // Find samples of app traffic (isInBrowser = 0) as well if
976        // browsingTrafficOnly is false.
977        lowerFilter = [aAppId, 0, aServiceType, network, start];
978        upperFilter = [aAppId, 0, aServiceType, network, end];
979        range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
980        request = aStore.openCursor(range).onsuccess = function(event) {
981          cursor = event.target.result;
982          if (cursor) {
983            var date = new Date(cursor.value.timestamp + offset);
984            var foundData = data.find(function (element, index, array) {
985              if (element.date.getTime() !== date.getTime()) {
986                return false;
987              }
988              return element;
989            }, date);
990
991            if (foundData) {
992              foundData.rxBytes += cursor.value.rxBytes;
993              foundData.txBytes += cursor.value.txBytes;
994            } else {
995              // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes
996              // for the first (oldest) sample. The rx/txTotalBytes fields
997              // record accumulative usage amount, which means even if old
998              // samples were expired and removed from the Database, we can
999              // still obtain the correct network usage.
1000              if (data.length == 0) {
1001                data.push({ rxBytes: cursor.value.rxTotalBytes,
1002                            txBytes: cursor.value.txTotalBytes,
1003                            date: new Date(cursor.value.timestamp + offset) });
1004              } else {
1005                data.push({ rxBytes: cursor.value.rxBytes,
1006                            txBytes: cursor.value.txBytes,
1007                            date: new Date(cursor.value.timestamp + offset) });
1008              }
1009            }
1010            cursor.continue();
1011            return;
1012          }
1013          this.fillResultSamples(start + offset, end + offset, data);
1014          aTxn.result.data = data;
1015        }.bind(this);  // openCursor(range).onsuccess() callback
1016      }.bind(this);  // openCursor(range).onsuccess() callback
1017    }.bind(this), aResultCb);
1018  },
1019
1020  /*
1021   * Fill data array (samples from database) with empty samples to match
1022   * requested start / end dates.
1023   */
1024  fillResultSamples: function fillResultSamples(aStart, aEnd, aData) {
1025    if (aData.length == 0) {
1026      aData.push({ rxBytes: undefined,
1027                  txBytes: undefined,
1028                  date: new Date(aStart) });
1029    }
1030
1031    while (aStart < aData[0].date.getTime()) {
1032      aData.unshift({ rxBytes: undefined,
1033                      txBytes: undefined,
1034                      date: new Date(aData[0].date.getTime() - SAMPLE_RATE) });
1035    }
1036
1037    while (aEnd > aData[aData.length - 1].date.getTime()) {
1038      aData.push({ rxBytes: undefined,
1039                   txBytes: undefined,
1040                   date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) });
1041    }
1042  },
1043
1044  getAvailableNetworks: function getAvailableNetworks(aResultCb) {
1045    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
1046      if (!aTxn.result) {
1047        aTxn.result = [];
1048      }
1049
1050      let request = aStore.index("network").openKeyCursor(null, "nextunique");
1051      request.onsuccess = function onsuccess(event) {
1052        let cursor = event.target.result;
1053        if (cursor) {
1054          aTxn.result.push({ id: cursor.key[0],
1055                             type: cursor.key[1] });
1056          cursor.continue();
1057          return;
1058        }
1059      };
1060    }, aResultCb);
1061  },
1062
1063  isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) {
1064    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
1065      if (!aTxn.result) {
1066        aTxn.result = false;
1067      }
1068
1069      let network = [aNetwork.id, aNetwork.type];
1070      let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network));
1071      request.onsuccess = function onsuccess(event) {
1072        if (event.target.result) {
1073          aTxn.result = true;
1074        }
1075      };
1076    }, aResultCb);
1077  },
1078
1079  getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) {
1080    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
1081      if (!aTxn.result) {
1082        aTxn.result = [];
1083      }
1084
1085      let request = aStore.index("serviceType").openKeyCursor(null, "nextunique");
1086      request.onsuccess = function onsuccess(event) {
1087        let cursor = event.target.result;
1088        if (cursor && cursor.key != "") {
1089          aTxn.result.push({ serviceType: cursor.key });
1090          cursor.continue();
1091          return;
1092        }
1093      };
1094    }, aResultCb);
1095  },
1096
1097  get sampleRate () {
1098    return SAMPLE_RATE;
1099  },
1100
1101  get maxStorageSamples () {
1102    return VALUES_MAX_LENGTH;
1103  },
1104
1105  logAllRecords: function logAllRecords(aResultCb) {
1106    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
1107      aStore.mozGetAll().onsuccess = function onsuccess(event) {
1108        aTxn.result = event.target.result;
1109      };
1110    }, aResultCb);
1111  },
1112
1113  alarmToRecord: function alarmToRecord(aAlarm) {
1114    let record = { networkId: aAlarm.networkId,
1115                   absoluteThreshold: aAlarm.absoluteThreshold,
1116                   relativeThreshold: aAlarm.relativeThreshold,
1117                   startTime: aAlarm.startTime,
1118                   data: aAlarm.data,
1119                   manifestURL: aAlarm.manifestURL,
1120                   pageURL: aAlarm.pageURL };
1121
1122    if (aAlarm.id) {
1123      record.id = aAlarm.id;
1124    }
1125
1126    return record;
1127  },
1128
1129  recordToAlarm: function recordToalarm(aRecord) {
1130    let alarm = { networkId: aRecord.networkId,
1131                  absoluteThreshold: aRecord.absoluteThreshold,
1132                  relativeThreshold: aRecord.relativeThreshold,
1133                  startTime: aRecord.startTime,
1134                  data: aRecord.data,
1135                  manifestURL: aRecord.manifestURL,
1136                  pageURL: aRecord.pageURL };
1137
1138    if (aRecord.id) {
1139      alarm.id = aRecord.id;
1140    }
1141
1142    return alarm;
1143  },
1144
1145  addAlarm: function addAlarm(aAlarm, aResultCb) {
1146    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
1147      if (DEBUG) {
1148        debug("Going to add " + JSON.stringify(aAlarm));
1149      }
1150
1151      let record = this.alarmToRecord(aAlarm);
1152      store.put(record).onsuccess = function setResult(aEvent) {
1153        txn.result = aEvent.target.result;
1154        if (DEBUG) {
1155          debug("Request successful. New record ID: " + txn.result);
1156        }
1157      };
1158    }.bind(this), aResultCb);
1159  },
1160
1161  getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) {
1162    let self = this;
1163
1164    this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) {
1165      if (DEBUG) {
1166        debug("Get first alarm for network " + aNetworkId);
1167      }
1168
1169      let lowerFilter = [aNetworkId, 0];
1170      let upperFilter = [aNetworkId, ""];
1171      let range = IDBKeyRange.bound(lowerFilter, upperFilter);
1172
1173      store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) {
1174        let cursor = event.target.result;
1175        txn.result = null;
1176        if (cursor) {
1177          txn.result = self.recordToAlarm(cursor.value);
1178        }
1179      };
1180    }, aResultCb);
1181  },
1182
1183  removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) {
1184    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
1185      if (DEBUG) {
1186        debug("Remove alarm " + aAlarmId);
1187      }
1188
1189      store.get(aAlarmId).onsuccess = function onsuccess(event) {
1190        let record = event.target.result;
1191        txn.result = false;
1192        if (!record || (aManifestURL && record.manifestURL != aManifestURL)) {
1193          return;
1194        }
1195
1196        store.delete(aAlarmId);
1197        txn.result = true;
1198      }
1199    }, aResultCb);
1200  },
1201
1202  removeAlarms: function removeAlarms(aManifestURL, aResultCb) {
1203    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
1204      if (DEBUG) {
1205        debug("Remove alarms of " + aManifestURL);
1206      }
1207
1208      store.index("manifestURL").openCursor(aManifestURL)
1209                                .onsuccess = function onsuccess(event) {
1210        let cursor = event.target.result;
1211        if (cursor) {
1212          cursor.delete();
1213          cursor.continue();
1214        }
1215      }
1216    }, aResultCb);
1217  },
1218
1219  updateAlarm: function updateAlarm(aAlarm, aResultCb) {
1220    let self = this;
1221    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
1222      if (DEBUG) {
1223        debug("Update alarm " + aAlarm.id);
1224      }
1225
1226      let record = self.alarmToRecord(aAlarm);
1227      store.openCursor(record.id).onsuccess = function onsuccess(event) {
1228        let cursor = event.target.result;
1229        txn.result = false;
1230        if (cursor) {
1231          cursor.update(record);
1232          txn.result = true;
1233        }
1234      }
1235    }, aResultCb);
1236  },
1237
1238  getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) {
1239    let self = this;
1240    this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) {
1241      if (DEBUG) {
1242        debug("Get alarms for " + aManifestURL);
1243      }
1244
1245      txn.result = [];
1246      store.index("manifestURL").openCursor(aManifestURL)
1247                                .onsuccess = function onsuccess(event) {
1248        let cursor = event.target.result;
1249        if (!cursor) {
1250          return;
1251        }
1252
1253        if (!aNetworkId || cursor.value.networkId == aNetworkId) {
1254          txn.result.push(self.recordToAlarm(cursor.value));
1255        }
1256
1257        cursor.continue();
1258      }
1259    }, aResultCb);
1260  },
1261
1262  _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) {
1263    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
1264      if (DEBUG) {
1265        debug("Reset alarms for network " + aNetworkId);
1266      }
1267
1268      let lowerFilter = [aNetworkId, 0];
1269      let upperFilter = [aNetworkId, ""];
1270      let range = IDBKeyRange.bound(lowerFilter, upperFilter);
1271
1272      store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) {
1273        let cursor = event.target.result;
1274        if (cursor) {
1275          if (cursor.value.startTime) {
1276            cursor.value.relativeThreshold = cursor.value.threshold;
1277            cursor.update(cursor.value);
1278          }
1279          cursor.continue();
1280          return;
1281        }
1282      };
1283    }, aResultCb);
1284  }
1285};
1286