dom/network/src/NetworkStatsDB.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/network/src/NetworkStatsDB.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1050 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ['NetworkStatsDB'];
    1.11 +
    1.12 +const DEBUG = false;
    1.13 +function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); }
    1.14 +
    1.15 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    1.16 +
    1.17 +Cu.import("resource://gre/modules/Services.jsm");
    1.18 +Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
    1.19 +Cu.importGlobalProperties(["indexedDB"]);
    1.20 +
    1.21 +const DB_NAME = "net_stats";
    1.22 +const DB_VERSION = 8;
    1.23 +const DEPRECATED_STORE_NAME = "net_stats";
    1.24 +const STATS_STORE_NAME = "net_stats_store";
    1.25 +const ALARMS_STORE_NAME = "net_alarm";
    1.26 +
    1.27 +// Constant defining the maximum values allowed per interface. If more, older
    1.28 +// will be erased.
    1.29 +const VALUES_MAX_LENGTH = 6 * 30;
    1.30 +
    1.31 +// Constant defining the rate of the samples. Daily.
    1.32 +const SAMPLE_RATE = 1000 * 60 * 60 * 24;
    1.33 +
    1.34 +this.NetworkStatsDB = function NetworkStatsDB() {
    1.35 +  if (DEBUG) {
    1.36 +    debug("Constructor");
    1.37 +  }
    1.38 +  this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]);
    1.39 +}
    1.40 +
    1.41 +NetworkStatsDB.prototype = {
    1.42 +  __proto__: IndexedDBHelper.prototype,
    1.43 +
    1.44 +  dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) {
    1.45 +    function successCb(result) {
    1.46 +      txnCb(null, result);
    1.47 +    }
    1.48 +    function errorCb(error) {
    1.49 +      txnCb(error, null);
    1.50 +    }
    1.51 +    return this.newTxn(txn_type, store_name, callback, successCb, errorCb);
    1.52 +  },
    1.53 +
    1.54 +  upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
    1.55 +    if (DEBUG) {
    1.56 +      debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
    1.57 +    }
    1.58 +    let db = aDb;
    1.59 +    let objectStore;
    1.60 +    for (let currVersion = aOldVersion; currVersion < aNewVersion; currVersion++) {
    1.61 +      if (currVersion == 0) {
    1.62 +        /**
    1.63 +         * Create the initial database schema.
    1.64 +         */
    1.65 +
    1.66 +        objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["connectionType", "timestamp"] });
    1.67 +        objectStore.createIndex("connectionType", "connectionType", { unique: false });
    1.68 +        objectStore.createIndex("timestamp", "timestamp", { unique: false });
    1.69 +        objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
    1.70 +        objectStore.createIndex("txBytes", "txBytes", { unique: false });
    1.71 +        objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
    1.72 +        objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
    1.73 +        if (DEBUG) {
    1.74 +          debug("Created object stores and indexes");
    1.75 +        }
    1.76 +      } else if (currVersion == 2) {
    1.77 +        // In order to support per-app traffic data storage, the original
    1.78 +        // objectStore needs to be replaced by a new objectStore with new
    1.79 +        // key path ("appId") and new index ("appId").
    1.80 +        // Also, since now networks are identified by their
    1.81 +        // [networkId, networkType] not just by their connectionType,
    1.82 +        // to modify the keyPath is mandatory to delete the object store
    1.83 +        // and create it again. Old data is going to be deleted because the
    1.84 +        // networkId for each sample can not be set.
    1.85 +
    1.86 +        // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when
    1.87 +        // upgrading from 1.2 to 1.3 objectStore name should be checked.
    1.88 +        let stores = db.objectStoreNames;
    1.89 +        if(stores.contains("net_stats_v2")) {
    1.90 +          db.deleteObjectStore("net_stats_v2");
    1.91 +        } else {
    1.92 +          db.deleteObjectStore(DEPRECATED_STORE_NAME);
    1.93 +        }
    1.94 +
    1.95 +        objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["appId", "network", "timestamp"] });
    1.96 +        objectStore.createIndex("appId", "appId", { unique: false });
    1.97 +        objectStore.createIndex("network", "network", { unique: false });
    1.98 +        objectStore.createIndex("networkType", "networkType", { unique: false });
    1.99 +        objectStore.createIndex("timestamp", "timestamp", { unique: false });
   1.100 +        objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
   1.101 +        objectStore.createIndex("txBytes", "txBytes", { unique: false });
   1.102 +        objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
   1.103 +        objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
   1.104 +
   1.105 +        if (DEBUG) {
   1.106 +          debug("Created object stores and indexes for version 3");
   1.107 +        }
   1.108 +      } else if (currVersion == 3) {
   1.109 +        // Delete redundent indexes (leave "network" only).
   1.110 +        objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME);
   1.111 +        if (objectStore.indexNames.contains("appId")) {
   1.112 +          objectStore.deleteIndex("appId");
   1.113 +        }
   1.114 +        if (objectStore.indexNames.contains("networkType")) {
   1.115 +          objectStore.deleteIndex("networkType");
   1.116 +        }
   1.117 +        if (objectStore.indexNames.contains("timestamp")) {
   1.118 +          objectStore.deleteIndex("timestamp");
   1.119 +        }
   1.120 +        if (objectStore.indexNames.contains("rxBytes")) {
   1.121 +          objectStore.deleteIndex("rxBytes");
   1.122 +        }
   1.123 +        if (objectStore.indexNames.contains("txBytes")) {
   1.124 +          objectStore.deleteIndex("txBytes");
   1.125 +        }
   1.126 +        if (objectStore.indexNames.contains("rxTotalBytes")) {
   1.127 +          objectStore.deleteIndex("rxTotalBytes");
   1.128 +        }
   1.129 +        if (objectStore.indexNames.contains("txTotalBytes")) {
   1.130 +          objectStore.deleteIndex("txTotalBytes");
   1.131 +        }
   1.132 +
   1.133 +        if (DEBUG) {
   1.134 +          debug("Deleted redundent indexes for version 4");
   1.135 +        }
   1.136 +      } else if (currVersion == 4) {
   1.137 +        // In order to manage alarms, it is necessary to use a global counter
   1.138 +        // (totalBytes) that will increase regardless of the system reboot.
   1.139 +        objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME);
   1.140 +
   1.141 +        // Now, systemBytes will hold the old totalBytes and totalBytes will
   1.142 +        // keep the increasing counter. |counters| will keep the track of
   1.143 +        // accumulated values.
   1.144 +        let counters = {};
   1.145 +
   1.146 +        objectStore.openCursor().onsuccess = function(event) {
   1.147 +          let cursor = event.target.result;
   1.148 +          if (!cursor){
   1.149 +            return;
   1.150 +          }
   1.151 +
   1.152 +          cursor.value.rxSystemBytes = cursor.value.rxTotalBytes;
   1.153 +          cursor.value.txSystemBytes = cursor.value.txTotalBytes;
   1.154 +
   1.155 +          if (cursor.value.appId == 0) {
   1.156 +            let netId = cursor.value.network[0] + '' + cursor.value.network[1];
   1.157 +            if (!counters[netId]) {
   1.158 +              counters[netId] = {
   1.159 +                rxCounter: 0,
   1.160 +                txCounter: 0,
   1.161 +                lastRx: 0,
   1.162 +                lastTx: 0
   1.163 +              };
   1.164 +            }
   1.165 +
   1.166 +            let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx;
   1.167 +            let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx;
   1.168 +            if (rxDiff < 0 || txDiff < 0) {
   1.169 +              // System reboot between samples, so take the current one.
   1.170 +              rxDiff = cursor.value.rxSystemBytes;
   1.171 +              txDiff = cursor.value.txSystemBytes;
   1.172 +            }
   1.173 +
   1.174 +            counters[netId].rxCounter += rxDiff;
   1.175 +            counters[netId].txCounter += txDiff;
   1.176 +            cursor.value.rxTotalBytes = counters[netId].rxCounter;
   1.177 +            cursor.value.txTotalBytes = counters[netId].txCounter;
   1.178 +
   1.179 +            counters[netId].lastRx = cursor.value.rxSystemBytes;
   1.180 +            counters[netId].lastTx = cursor.value.txSystemBytes;
   1.181 +          } else {
   1.182 +            cursor.value.rxTotalBytes = cursor.value.rxSystemBytes;
   1.183 +            cursor.value.txTotalBytes = cursor.value.txSystemBytes;
   1.184 +          }
   1.185 +
   1.186 +          cursor.update(cursor.value);
   1.187 +          cursor.continue();
   1.188 +        };
   1.189 +
   1.190 +        // Create object store for alarms.
   1.191 +        objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true });
   1.192 +        objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false });
   1.193 +        objectStore.createIndex("manifestURL", "manifestURL", { unique: false });
   1.194 +
   1.195 +        if (DEBUG) {
   1.196 +          debug("Created alarms store for version 5");
   1.197 +        }
   1.198 +      } else if (currVersion == 5) {
   1.199 +        // In contrast to "per-app" traffic data, "system-only" traffic data
   1.200 +        // refers to data which can not be identified by any applications.
   1.201 +        // To further support "system-only" data storage, the data can be
   1.202 +        // saved by service type (e.g., Tethering, OTA). Thus it's needed to
   1.203 +        // have a new key ("serviceType") for the ojectStore.
   1.204 +        let newObjectStore;
   1.205 +        newObjectStore = db.createObjectStore(STATS_STORE_NAME,
   1.206 +                         { keyPath: ["appId", "serviceType", "network", "timestamp"] });
   1.207 +        newObjectStore.createIndex("network", "network", { unique: false });
   1.208 +
   1.209 +        // Copy the data from the original objectStore to the new objectStore.
   1.210 +        objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME);
   1.211 +        objectStore.openCursor().onsuccess = function(event) {
   1.212 +          let cursor = event.target.result;
   1.213 +          if (!cursor) {
   1.214 +            db.deleteObjectStore(DEPRECATED_STORE_NAME);
   1.215 +            return;
   1.216 +          }
   1.217 +
   1.218 +          let newStats = cursor.value;
   1.219 +          newStats.serviceType = "";
   1.220 +          newObjectStore.put(newStats);
   1.221 +          cursor.continue();
   1.222 +        };
   1.223 +
   1.224 +        if (DEBUG) {
   1.225 +          debug("Added new key 'serviceType' for version 6");
   1.226 +        }
   1.227 +      } else if (currVersion == 6) {
   1.228 +        // Replace threshold attribute of alarm index by relativeThreshold in alarms DB.
   1.229 +        // Now alarms are indexed by relativeThreshold, which is the threshold relative
   1.230 +        // to current system stats.
   1.231 +        let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME);
   1.232 +
   1.233 +        // Delete "alarm" index.
   1.234 +        if (alarmsStore.indexNames.contains("alarm")) {
   1.235 +          alarmsStore.deleteIndex("alarm");
   1.236 +        }
   1.237 +
   1.238 +        // Create new "alarm" index.
   1.239 +        alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false });
   1.240 +
   1.241 +        // Populate new "alarm" index attributes.
   1.242 +        alarmsStore.openCursor().onsuccess = function(event) {
   1.243 +          let cursor = event.target.result;
   1.244 +          if (!cursor) {
   1.245 +            return;
   1.246 +          }
   1.247 +
   1.248 +          cursor.value.relativeThreshold = cursor.value.threshold;
   1.249 +          cursor.value.absoluteThreshold = cursor.value.threshold;
   1.250 +          delete cursor.value.threshold;
   1.251 +
   1.252 +          cursor.update(cursor.value);
   1.253 +          cursor.continue();
   1.254 +        }
   1.255 +
   1.256 +        // Previous versions save accumulative totalBytes, increasing althought the system
   1.257 +        // reboots or resets stats. But is necessary to reset the total counters when reset
   1.258 +        // through 'clearInterfaceStats'.
   1.259 +        let statsStore = aTransaction.objectStore(STATS_STORE_NAME);
   1.260 +        let networks = [];
   1.261 +        // Find networks stored in the database.
   1.262 +        statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) {
   1.263 +          let cursor = event.target.result;
   1.264 +          if (cursor) {
   1.265 +            networks.push(cursor.key);
   1.266 +            cursor.continue();
   1.267 +            return;
   1.268 +          }
   1.269 +
   1.270 +          networks.forEach(function(network) {
   1.271 +            let lowerFilter = [0, "", network, 0];
   1.272 +            let upperFilter = [0, "", network, ""];
   1.273 +            let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
   1.274 +
   1.275 +            // Find number of samples for a given network.
   1.276 +            statsStore.count(range).onsuccess = function(event) {
   1.277 +              // If there are more samples than the max allowed, there is no way to know
   1.278 +              // when does reset take place.
   1.279 +              if (event.target.result >= VALUES_MAX_LENGTH) {
   1.280 +                return;
   1.281 +              }
   1.282 +
   1.283 +              let last = null;
   1.284 +              // Reset detected if the first sample totalCounters are different than bytes
   1.285 +              // counters. If so, the total counters should be recalculated.
   1.286 +              statsStore.openCursor(range).onsuccess = function(event) {
   1.287 +                let cursor = event.target.result;
   1.288 +                if (!cursor) {
   1.289 +                  return;
   1.290 +                }
   1.291 +                if (!last) {
   1.292 +                  if (cursor.value.rxTotalBytes == cursor.value.rxBytes &&
   1.293 +                      cursor.value.txTotalBytes == cursor.value.txBytes) {
   1.294 +                    return;
   1.295 +                  }
   1.296 +
   1.297 +                  cursor.value.rxTotalBytes = cursor.value.rxBytes;
   1.298 +                  cursor.value.txTotalBytes = cursor.value.txBytes;
   1.299 +                  cursor.update(cursor.value);
   1.300 +                  last = cursor.value;
   1.301 +                  cursor.continue();
   1.302 +                  return;
   1.303 +                }
   1.304 +
   1.305 +                // Recalculate the total counter for last / current sample
   1.306 +                cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes;
   1.307 +                cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes;
   1.308 +                cursor.update(cursor.value);
   1.309 +                last = cursor.value;
   1.310 +                cursor.continue();
   1.311 +              }
   1.312 +            }
   1.313 +          }, this);
   1.314 +        };
   1.315 +      } else if (currVersion == 7) {
   1.316 +        // Create index for 'ServiceType' in order to make it retrievable.
   1.317 +        let statsStore = aTransaction.objectStore(STATS_STORE_NAME);
   1.318 +        statsStore.createIndex("serviceType", "serviceType", { unique: false });
   1.319 +
   1.320 +        if (DEBUG) {
   1.321 +          debug("Create index of 'serviceType' for version 8");
   1.322 +        }
   1.323 +      }
   1.324 +    }
   1.325 +  },
   1.326 +
   1.327 +  importData: function importData(aStats) {
   1.328 +    let stats = { appId:         aStats.appId,
   1.329 +                  serviceType:   aStats.serviceType,
   1.330 +                  network:       [aStats.networkId, aStats.networkType],
   1.331 +                  timestamp:     aStats.timestamp,
   1.332 +                  rxBytes:       aStats.rxBytes,
   1.333 +                  txBytes:       aStats.txBytes,
   1.334 +                  rxSystemBytes: aStats.rxSystemBytes,
   1.335 +                  txSystemBytes: aStats.txSystemBytes,
   1.336 +                  rxTotalBytes:  aStats.rxTotalBytes,
   1.337 +                  txTotalBytes:  aStats.txTotalBytes };
   1.338 +
   1.339 +    return stats;
   1.340 +  },
   1.341 +
   1.342 +  exportData: function exportData(aStats) {
   1.343 +    let stats = { appId:        aStats.appId,
   1.344 +                  serviceType:  aStats.serviceType,
   1.345 +                  networkId:    aStats.network[0],
   1.346 +                  networkType:  aStats.network[1],
   1.347 +                  timestamp:    aStats.timestamp,
   1.348 +                  rxBytes:      aStats.rxBytes,
   1.349 +                  txBytes:      aStats.txBytes,
   1.350 +                  rxTotalBytes: aStats.rxTotalBytes,
   1.351 +                  txTotalBytes: aStats.txTotalBytes };
   1.352 +
   1.353 +    return stats;
   1.354 +  },
   1.355 +
   1.356 +  normalizeDate: function normalizeDate(aDate) {
   1.357 +    // Convert to UTC according to timezone and
   1.358 +    // filter timestamp to get SAMPLE_RATE precission
   1.359 +    let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000;
   1.360 +    timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE;
   1.361 +    return timestamp;
   1.362 +  },
   1.363 +
   1.364 +  saveStats: function saveStats(aStats, aResultCb) {
   1.365 +    let isAccumulative = aStats.isAccumulative;
   1.366 +    let timestamp = this.normalizeDate(aStats.date);
   1.367 +
   1.368 +    let stats = { appId:         aStats.appId,
   1.369 +                  serviceType:   aStats.serviceType,
   1.370 +                  networkId:     aStats.networkId,
   1.371 +                  networkType:   aStats.networkType,
   1.372 +                  timestamp:     timestamp,
   1.373 +                  rxBytes:       (isAccumulative) ? 0 : aStats.rxBytes,
   1.374 +                  txBytes:       (isAccumulative) ? 0 : aStats.txBytes,
   1.375 +                  rxSystemBytes: (isAccumulative) ? aStats.rxBytes : 0,
   1.376 +                  txSystemBytes: (isAccumulative) ? aStats.txBytes : 0,
   1.377 +                  rxTotalBytes:  (isAccumulative) ? aStats.rxBytes : 0,
   1.378 +                  txTotalBytes:  (isAccumulative) ? aStats.txBytes : 0 };
   1.379 +
   1.380 +    stats = this.importData(stats);
   1.381 +
   1.382 +    this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) {
   1.383 +      if (DEBUG) {
   1.384 +        debug("Filtered time: " + new Date(timestamp));
   1.385 +        debug("New stats: " + JSON.stringify(stats));
   1.386 +      }
   1.387 +
   1.388 +      let request = aStore.index("network").openCursor(stats.network, "prev");
   1.389 +      request.onsuccess = function onsuccess(event) {
   1.390 +        let cursor = event.target.result;
   1.391 +        if (!cursor) {
   1.392 +          // Empty, so save first element.
   1.393 +
   1.394 +          // There could be a time delay between the point when the network
   1.395 +          // interface comes up and the point when the database is initialized.
   1.396 +          // In this short interval some traffic data are generated but are not
   1.397 +          // registered by the first sample.
   1.398 +          if (isAccumulative) {
   1.399 +            stats.rxBytes = stats.rxTotalBytes;
   1.400 +            stats.txBytes = stats.txTotalBytes;
   1.401 +          }
   1.402 +
   1.403 +          this._saveStats(aTxn, aStore, stats);
   1.404 +          return;
   1.405 +        }
   1.406 +
   1.407 +        let value = cursor.value;
   1.408 +        if (stats.appId != value.appId ||
   1.409 +            (stats.appId == 0 && stats.serviceType != value.serviceType)) {
   1.410 +          cursor.continue();
   1.411 +          return;
   1.412 +        }
   1.413 +
   1.414 +        // There are old samples
   1.415 +        if (DEBUG) {
   1.416 +          debug("Last value " + JSON.stringify(value));
   1.417 +        }
   1.418 +
   1.419 +        // Remove stats previous to now - VALUE_MAX_LENGTH
   1.420 +        this._removeOldStats(aTxn, aStore, stats.appId, stats.serviceType,
   1.421 +                             stats.network, stats.timestamp);
   1.422 +
   1.423 +        // Process stats before save
   1.424 +        this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative);
   1.425 +      }.bind(this);
   1.426 +    }.bind(this), aResultCb);
   1.427 +  },
   1.428 +
   1.429 +  /*
   1.430 +   * This function check that stats are saved in the database following the sample rate.
   1.431 +   * In this way is easier to find elements when stats are requested.
   1.432 +   */
   1.433 +  _processSamplesDiff: function _processSamplesDiff(aTxn,
   1.434 +                                                    aStore,
   1.435 +                                                    aLastSampleCursor,
   1.436 +                                                    aNewSample,
   1.437 +                                                    aIsAccumulative) {
   1.438 +    let lastSample = aLastSampleCursor.value;
   1.439 +
   1.440 +    // Get difference between last and new sample.
   1.441 +    let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE;
   1.442 +    if (diff % 1) {
   1.443 +      // diff is decimal, so some error happened because samples are stored as a multiple
   1.444 +      // of SAMPLE_RATE
   1.445 +      aTxn.abort();
   1.446 +      throw new Error("Error processing samples");
   1.447 +    }
   1.448 +
   1.449 +    if (DEBUG) {
   1.450 +      debug("New: " + aNewSample.timestamp + " - Last: " +
   1.451 +            lastSample.timestamp + " - diff: " + diff);
   1.452 +    }
   1.453 +
   1.454 +    // If the incoming data has a accumulation feature, the new
   1.455 +    // |txBytes|/|rxBytes| is assigend by differnces between the new
   1.456 +    // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|.
   1.457 +    // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes|
   1.458 +    // is the new |txBytes|/|rxBytes|.
   1.459 +    let rxDiff = 0;
   1.460 +    let txDiff = 0;
   1.461 +    if (aIsAccumulative) {
   1.462 +      rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes;
   1.463 +      txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes;
   1.464 +      if (rxDiff < 0 || txDiff < 0) {
   1.465 +        rxDiff = aNewSample.rxSystemBytes;
   1.466 +        txDiff = aNewSample.txSystemBytes;
   1.467 +      }
   1.468 +      aNewSample.rxBytes = rxDiff;
   1.469 +      aNewSample.txBytes = txDiff;
   1.470 +
   1.471 +      aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff;
   1.472 +      aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff;
   1.473 +    } else {
   1.474 +      rxDiff = aNewSample.rxBytes;
   1.475 +      txDiff = aNewSample.txBytes;
   1.476 +    }
   1.477 +
   1.478 +    if (diff == 1) {
   1.479 +      // New element.
   1.480 +
   1.481 +      // If the incoming data is non-accumulative, the new
   1.482 +      // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new
   1.483 +      // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|.
   1.484 +      if (!aIsAccumulative) {
   1.485 +        aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes;
   1.486 +        aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes;
   1.487 +      }
   1.488 +
   1.489 +      this._saveStats(aTxn, aStore, aNewSample);
   1.490 +      return;
   1.491 +    }
   1.492 +    if (diff > 1) {
   1.493 +      // Some samples lost. Device off during one or more samplerate periods.
   1.494 +      // Time or timezone changed
   1.495 +      // Add lost samples with 0 bytes and the actual one.
   1.496 +      if (diff > VALUES_MAX_LENGTH) {
   1.497 +        diff = VALUES_MAX_LENGTH;
   1.498 +      }
   1.499 +
   1.500 +      let data = [];
   1.501 +      for (let i = diff - 2; i >= 0; i--) {
   1.502 +        let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1);
   1.503 +        let sample = { appId:         aNewSample.appId,
   1.504 +                       serviceType:   aNewSample.serviceType,
   1.505 +                       network:       aNewSample.network,
   1.506 +                       timestamp:     time,
   1.507 +                       rxBytes:       0,
   1.508 +                       txBytes:       0,
   1.509 +                       rxSystemBytes: lastSample.rxSystemBytes,
   1.510 +                       txSystemBytes: lastSample.txSystemBytes,
   1.511 +                       rxTotalBytes:  lastSample.rxTotalBytes,
   1.512 +                       txTotalBytes:  lastSample.txTotalBytes };
   1.513 +
   1.514 +        data.push(sample);
   1.515 +      }
   1.516 +
   1.517 +      data.push(aNewSample);
   1.518 +      this._saveStats(aTxn, aStore, data);
   1.519 +      return;
   1.520 +    }
   1.521 +    if (diff == 0 || diff < 0) {
   1.522 +      // New element received before samplerate period. It means that device has
   1.523 +      // been restarted (or clock / timezone change).
   1.524 +      // Update element. If diff < 0, clock or timezone changed back. Place data
   1.525 +      // in the last sample.
   1.526 +
   1.527 +      // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the
   1.528 +      // last |rxTotalBytes|/|txTotalBytes|.
   1.529 +      lastSample.rxBytes += rxDiff;
   1.530 +      lastSample.txBytes += txDiff;
   1.531 +      lastSample.rxSystemBytes = aNewSample.rxSystemBytes;
   1.532 +      lastSample.txSystemBytes = aNewSample.txSystemBytes;
   1.533 +      lastSample.rxTotalBytes += rxDiff;
   1.534 +      lastSample.txTotalBytes += txDiff;
   1.535 +
   1.536 +      if (DEBUG) {
   1.537 +        debug("Update: " + JSON.stringify(lastSample));
   1.538 +      }
   1.539 +      let req = aLastSampleCursor.update(lastSample);
   1.540 +    }
   1.541 +  },
   1.542 +
   1.543 +  _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) {
   1.544 +    if (DEBUG) {
   1.545 +      debug("_saveStats: " + JSON.stringify(aNetworkStats));
   1.546 +    }
   1.547 +
   1.548 +    if (Array.isArray(aNetworkStats)) {
   1.549 +      let len = aNetworkStats.length - 1;
   1.550 +      for (let i = 0; i <= len; i++) {
   1.551 +        aStore.put(aNetworkStats[i]);
   1.552 +      }
   1.553 +    } else {
   1.554 +      aStore.put(aNetworkStats);
   1.555 +    }
   1.556 +  },
   1.557 +
   1.558 +  _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aServiceType,
   1.559 +                                            aNetwork, aDate) {
   1.560 +    // Callback function to remove old items when new ones are added.
   1.561 +    let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1);
   1.562 +    let lowerFilter = [aAppId, aServiceType, aNetwork, 0];
   1.563 +    let upperFilter = [aAppId, aServiceType, aNetwork, filterDate];
   1.564 +    let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
   1.565 +    let lastSample = null;
   1.566 +    let self = this;
   1.567 +
   1.568 +    aStore.openCursor(range).onsuccess = function(event) {
   1.569 +      var cursor = event.target.result;
   1.570 +      if (cursor) {
   1.571 +        lastSample = cursor.value;
   1.572 +        cursor.delete();
   1.573 +        cursor.continue();
   1.574 +        return;
   1.575 +      }
   1.576 +
   1.577 +      // If all samples for a network are removed, an empty sample
   1.578 +      // has to be saved to keep the totalBytes in order to compute
   1.579 +      // future samples because system counters are not set to 0.
   1.580 +      // Thus, if there are no samples left, the last sample removed
   1.581 +      // will be saved again after setting its bytes to 0.
   1.582 +      let request = aStore.index("network").openCursor(aNetwork);
   1.583 +      request.onsuccess = function onsuccess(event) {
   1.584 +        let cursor = event.target.result;
   1.585 +        if (!cursor && lastSample != null) {
   1.586 +          let timestamp = new Date();
   1.587 +          timestamp = self.normalizeDate(timestamp);
   1.588 +          lastSample.timestamp = timestamp;
   1.589 +          lastSample.rxBytes = 0;
   1.590 +          lastSample.txBytes = 0;
   1.591 +          self._saveStats(aTxn, aStore, lastSample);
   1.592 +        }
   1.593 +      };
   1.594 +    };
   1.595 +  },
   1.596 +
   1.597 +  clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) {
   1.598 +    let network = [aNetwork.network.id, aNetwork.network.type];
   1.599 +    let self = this;
   1.600 +
   1.601 +    // Clear and save an empty sample to keep sync with system counters
   1.602 +    this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) {
   1.603 +      let sample = null;
   1.604 +      let request = aStore.index("network").openCursor(network, "prev");
   1.605 +      request.onsuccess = function onsuccess(event) {
   1.606 +        let cursor = event.target.result;
   1.607 +        if (cursor) {
   1.608 +          if (!sample && cursor.value.appId == 0) {
   1.609 +            sample = cursor.value;
   1.610 +          }
   1.611 +
   1.612 +          cursor.delete();
   1.613 +          cursor.continue();
   1.614 +          return;
   1.615 +        }
   1.616 +
   1.617 +        if (sample) {
   1.618 +          let timestamp = new Date();
   1.619 +          timestamp = self.normalizeDate(timestamp);
   1.620 +          sample.timestamp = timestamp;
   1.621 +          sample.appId = 0;
   1.622 +          sample.serviceType = "";
   1.623 +          sample.rxBytes = 0;
   1.624 +          sample.txBytes = 0;
   1.625 +          sample.rxTotalBytes = 0;
   1.626 +          sample.txTotalBytes = 0;
   1.627 +
   1.628 +          self._saveStats(aTxn, aStore, sample);
   1.629 +        }
   1.630 +      };
   1.631 +    }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb));
   1.632 +  },
   1.633 +
   1.634 +  clearStats: function clearStats(aNetworks, aResultCb) {
   1.635 +    let index = 0;
   1.636 +    let stats = [];
   1.637 +    let self = this;
   1.638 +
   1.639 +    let callback = function(aError, aResult) {
   1.640 +      index++;
   1.641 +
   1.642 +      if (!aError && index < aNetworks.length) {
   1.643 +        self.clearInterfaceStats(aNetworks[index], callback);
   1.644 +        return;
   1.645 +      }
   1.646 +
   1.647 +      aResultCb(aError, aResult);
   1.648 +    };
   1.649 +
   1.650 +    if (!aNetworks[index]) {
   1.651 +      aResultCb(null, true);
   1.652 +      return;
   1.653 +    }
   1.654 +    this.clearInterfaceStats(aNetworks[index], callback);
   1.655 +  },
   1.656 +
   1.657 +  getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) {
   1.658 +    if (DEBUG) {
   1.659 +      debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate);
   1.660 +    }
   1.661 +
   1.662 +    let network = [aNetwork.id, aNetwork.type];
   1.663 +    if (aDate) {
   1.664 +      this._getCurrentStatsFromDate(network, aDate, aResultCb);
   1.665 +      return;
   1.666 +    }
   1.667 +
   1.668 +    this._getCurrentStats(network, aResultCb);
   1.669 +  },
   1.670 +
   1.671 +  _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) {
   1.672 +    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
   1.673 +      let request = null;
   1.674 +      let upperFilter = [0, "", aNetwork, Date.now()];
   1.675 +      let range = IDBKeyRange.upperBound(upperFilter, false);
   1.676 +      request = store.openCursor(range, "prev");
   1.677 +
   1.678 +      let result = { rxBytes:      0, txBytes:      0,
   1.679 +                     rxTotalBytes: 0, txTotalBytes: 0 };
   1.680 +
   1.681 +      request.onsuccess = function onsuccess(event) {
   1.682 +        let cursor = event.target.result;
   1.683 +        if (cursor) {
   1.684 +          result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes;
   1.685 +          result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes;
   1.686 +        }
   1.687 +
   1.688 +        txn.result = result;
   1.689 +      };
   1.690 +    }.bind(this), aResultCb);
   1.691 +  },
   1.692 +
   1.693 +  _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) {
   1.694 +    aDate = new Date(aDate);
   1.695 +    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
   1.696 +      let request = null;
   1.697 +      let start = this.normalizeDate(aDate);
   1.698 +      let lowerFilter = [0, "", aNetwork, start];
   1.699 +      let upperFilter = [0, "", aNetwork, Date.now()];
   1.700 +
   1.701 +      let range = IDBKeyRange.upperBound(upperFilter, false);
   1.702 +
   1.703 +      let result = { rxBytes:      0, txBytes:      0,
   1.704 +                     rxTotalBytes: 0, txTotalBytes: 0 };
   1.705 +
   1.706 +      request = store.openCursor(range, "prev");
   1.707 +
   1.708 +      request.onsuccess = function onsuccess(event) {
   1.709 +        let cursor = event.target.result;
   1.710 +        if (cursor) {
   1.711 +          result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes;
   1.712 +          result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes;
   1.713 +        }
   1.714 +
   1.715 +        let timestamp = cursor.value.timestamp;
   1.716 +        let range = IDBKeyRange.lowerBound(lowerFilter, false);
   1.717 +        request = store.openCursor(range);
   1.718 +
   1.719 +        request.onsuccess = function onsuccess(event) {
   1.720 +          let cursor = event.target.result;
   1.721 +          if (cursor) {
   1.722 +            if (cursor.value.timestamp == timestamp) {
   1.723 +              // There is one sample only.
   1.724 +              result.rxBytes = cursor.value.rxBytes;
   1.725 +              result.txBytes = cursor.value.txBytes;
   1.726 +            } else {
   1.727 +              result.rxBytes -= cursor.value.rxTotalBytes;
   1.728 +              result.txBytes -= cursor.value.txTotalBytes;
   1.729 +            }
   1.730 +          }
   1.731 +
   1.732 +          txn.result = result;
   1.733 +        };
   1.734 +      };
   1.735 +    }.bind(this), aResultCb);
   1.736 +  },
   1.737 +
   1.738 +  find: function find(aResultCb, aAppId, aServiceType, aNetwork,
   1.739 +                      aStart, aEnd, aAppManifestURL) {
   1.740 +    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
   1.741 +    let start = this.normalizeDate(aStart);
   1.742 +    let end = this.normalizeDate(aEnd);
   1.743 +
   1.744 +    if (DEBUG) {
   1.745 +      debug("Find samples for appId: " + aAppId + " serviceType: " +
   1.746 +            aServiceType + " network: " + JSON.stringify(aNetwork) + " from " +
   1.747 +            start + " until " + end);
   1.748 +      debug("Start time: " + new Date(start));
   1.749 +      debug("End time: " + new Date(end));
   1.750 +    }
   1.751 +
   1.752 +    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
   1.753 +      let network = [aNetwork.id, aNetwork.type];
   1.754 +      let lowerFilter = [aAppId, aServiceType, network, start];
   1.755 +      let upperFilter = [aAppId, aServiceType, network, end];
   1.756 +      let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
   1.757 +
   1.758 +      let data = [];
   1.759 +
   1.760 +      if (!aTxn.result) {
   1.761 +        aTxn.result = {};
   1.762 +      }
   1.763 +
   1.764 +      let request = aStore.openCursor(range).onsuccess = function(event) {
   1.765 +        var cursor = event.target.result;
   1.766 +        if (cursor){
   1.767 +          data.push({ rxBytes: cursor.value.rxBytes,
   1.768 +                      txBytes: cursor.value.txBytes,
   1.769 +                      date: new Date(cursor.value.timestamp + offset) });
   1.770 +          cursor.continue();
   1.771 +          return;
   1.772 +        }
   1.773 +
   1.774 +        // When requested samples (start / end) are not in the range of now and
   1.775 +        // now - VALUES_MAX_LENGTH, fill with empty samples.
   1.776 +        this.fillResultSamples(start + offset, end + offset, data);
   1.777 +
   1.778 +        aTxn.result.appManifestURL = aAppManifestURL;
   1.779 +        aTxn.result.serviceType = aServiceType;
   1.780 +        aTxn.result.network = aNetwork;
   1.781 +        aTxn.result.start = aStart;
   1.782 +        aTxn.result.end = aEnd;
   1.783 +        aTxn.result.data = data;
   1.784 +      }.bind(this);
   1.785 +    }.bind(this), aResultCb);
   1.786 +  },
   1.787 +
   1.788 +  /*
   1.789 +   * Fill data array (samples from database) with empty samples to match
   1.790 +   * requested start / end dates.
   1.791 +   */
   1.792 +  fillResultSamples: function fillResultSamples(aStart, aEnd, aData) {
   1.793 +    if (aData.length == 0) {
   1.794 +      aData.push({ rxBytes: undefined,
   1.795 +                  txBytes: undefined,
   1.796 +                  date: new Date(aStart) });
   1.797 +    }
   1.798 +
   1.799 +    while (aStart < aData[0].date.getTime()) {
   1.800 +      aData.unshift({ rxBytes: undefined,
   1.801 +                      txBytes: undefined,
   1.802 +                      date: new Date(aData[0].date.getTime() - SAMPLE_RATE) });
   1.803 +    }
   1.804 +
   1.805 +    while (aEnd > aData[aData.length - 1].date.getTime()) {
   1.806 +      aData.push({ rxBytes: undefined,
   1.807 +                   txBytes: undefined,
   1.808 +                   date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) });
   1.809 +    }
   1.810 +  },
   1.811 +
   1.812 +  getAvailableNetworks: function getAvailableNetworks(aResultCb) {
   1.813 +    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
   1.814 +      if (!aTxn.result) {
   1.815 +        aTxn.result = [];
   1.816 +      }
   1.817 +
   1.818 +      let request = aStore.index("network").openKeyCursor(null, "nextunique");
   1.819 +      request.onsuccess = function onsuccess(event) {
   1.820 +        let cursor = event.target.result;
   1.821 +        if (cursor) {
   1.822 +          aTxn.result.push({ id: cursor.key[0],
   1.823 +                             type: cursor.key[1] });
   1.824 +          cursor.continue();
   1.825 +          return;
   1.826 +        }
   1.827 +      };
   1.828 +    }, aResultCb);
   1.829 +  },
   1.830 +
   1.831 +  isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) {
   1.832 +    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
   1.833 +      if (!aTxn.result) {
   1.834 +        aTxn.result = false;
   1.835 +      }
   1.836 +
   1.837 +      let network = [aNetwork.id, aNetwork.type];
   1.838 +      let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network));
   1.839 +      request.onsuccess = function onsuccess(event) {
   1.840 +        if (event.target.result) {
   1.841 +          aTxn.result = true;
   1.842 +        }
   1.843 +      };
   1.844 +    }, aResultCb);
   1.845 +  },
   1.846 +
   1.847 +  getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) {
   1.848 +    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
   1.849 +      if (!aTxn.result) {
   1.850 +        aTxn.result = [];
   1.851 +      }
   1.852 +
   1.853 +      let request = aStore.index("serviceType").openKeyCursor(null, "nextunique");
   1.854 +      request.onsuccess = function onsuccess(event) {
   1.855 +        let cursor = event.target.result;
   1.856 +        if (cursor && cursor.key != "") {
   1.857 +          aTxn.result.push({ serviceType: cursor.key });
   1.858 +          cursor.continue();
   1.859 +          return;
   1.860 +        }
   1.861 +      };
   1.862 +    }, aResultCb);
   1.863 +  },
   1.864 +
   1.865 +  get sampleRate () {
   1.866 +    return SAMPLE_RATE;
   1.867 +  },
   1.868 +
   1.869 +  get maxStorageSamples () {
   1.870 +    return VALUES_MAX_LENGTH;
   1.871 +  },
   1.872 +
   1.873 +  logAllRecords: function logAllRecords(aResultCb) {
   1.874 +    this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
   1.875 +      aStore.mozGetAll().onsuccess = function onsuccess(event) {
   1.876 +        aTxn.result = event.target.result;
   1.877 +      };
   1.878 +    }, aResultCb);
   1.879 +  },
   1.880 +
   1.881 +  alarmToRecord: function alarmToRecord(aAlarm) {
   1.882 +    let record = { networkId: aAlarm.networkId,
   1.883 +                   absoluteThreshold: aAlarm.absoluteThreshold,
   1.884 +                   relativeThreshold: aAlarm.relativeThreshold,
   1.885 +                   startTime: aAlarm.startTime,
   1.886 +                   data: aAlarm.data,
   1.887 +                   manifestURL: aAlarm.manifestURL,
   1.888 +                   pageURL: aAlarm.pageURL };
   1.889 +
   1.890 +    if (aAlarm.id) {
   1.891 +      record.id = aAlarm.id;
   1.892 +    }
   1.893 +
   1.894 +    return record;
   1.895 +  },
   1.896 +
   1.897 +  recordToAlarm: function recordToalarm(aRecord) {
   1.898 +    let alarm = { networkId: aRecord.networkId,
   1.899 +                  absoluteThreshold: aRecord.absoluteThreshold,
   1.900 +                  relativeThreshold: aRecord.relativeThreshold,
   1.901 +                  startTime: aRecord.startTime,
   1.902 +                  data: aRecord.data,
   1.903 +                  manifestURL: aRecord.manifestURL,
   1.904 +                  pageURL: aRecord.pageURL };
   1.905 +
   1.906 +    if (aRecord.id) {
   1.907 +      alarm.id = aRecord.id;
   1.908 +    }
   1.909 +
   1.910 +    return alarm;
   1.911 +  },
   1.912 +
   1.913 +  addAlarm: function addAlarm(aAlarm, aResultCb) {
   1.914 +    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
   1.915 +      if (DEBUG) {
   1.916 +        debug("Going to add " + JSON.stringify(aAlarm));
   1.917 +      }
   1.918 +
   1.919 +      let record = this.alarmToRecord(aAlarm);
   1.920 +      store.put(record).onsuccess = function setResult(aEvent) {
   1.921 +        txn.result = aEvent.target.result;
   1.922 +        if (DEBUG) {
   1.923 +          debug("Request successful. New record ID: " + txn.result);
   1.924 +        }
   1.925 +      };
   1.926 +    }.bind(this), aResultCb);
   1.927 +  },
   1.928 +
   1.929 +  getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) {
   1.930 +    let self = this;
   1.931 +
   1.932 +    this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) {
   1.933 +      if (DEBUG) {
   1.934 +        debug("Get first alarm for network " + aNetworkId);
   1.935 +      }
   1.936 +
   1.937 +      let lowerFilter = [aNetworkId, 0];
   1.938 +      let upperFilter = [aNetworkId, ""];
   1.939 +      let range = IDBKeyRange.bound(lowerFilter, upperFilter);
   1.940 +
   1.941 +      store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) {
   1.942 +        let cursor = event.target.result;
   1.943 +        txn.result = null;
   1.944 +        if (cursor) {
   1.945 +          txn.result = self.recordToAlarm(cursor.value);
   1.946 +        }
   1.947 +      };
   1.948 +    }, aResultCb);
   1.949 +  },
   1.950 +
   1.951 +  removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) {
   1.952 +    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
   1.953 +      if (DEBUG) {
   1.954 +        debug("Remove alarm " + aAlarmId);
   1.955 +      }
   1.956 +
   1.957 +      store.get(aAlarmId).onsuccess = function onsuccess(event) {
   1.958 +        let record = event.target.result;
   1.959 +        txn.result = false;
   1.960 +        if (!record || (aManifestURL && record.manifestURL != aManifestURL)) {
   1.961 +          return;
   1.962 +        }
   1.963 +
   1.964 +        store.delete(aAlarmId);
   1.965 +        txn.result = true;
   1.966 +      }
   1.967 +    }, aResultCb);
   1.968 +  },
   1.969 +
   1.970 +  removeAlarms: function removeAlarms(aManifestURL, aResultCb) {
   1.971 +    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
   1.972 +      if (DEBUG) {
   1.973 +        debug("Remove alarms of " + aManifestURL);
   1.974 +      }
   1.975 +
   1.976 +      store.index("manifestURL").openCursor(aManifestURL)
   1.977 +                                .onsuccess = function onsuccess(event) {
   1.978 +        let cursor = event.target.result;
   1.979 +        if (cursor) {
   1.980 +          cursor.delete();
   1.981 +          cursor.continue();
   1.982 +        }
   1.983 +      }
   1.984 +    }, aResultCb);
   1.985 +  },
   1.986 +
   1.987 +  updateAlarm: function updateAlarm(aAlarm, aResultCb) {
   1.988 +    let self = this;
   1.989 +    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
   1.990 +      if (DEBUG) {
   1.991 +        debug("Update alarm " + aAlarm.id);
   1.992 +      }
   1.993 +
   1.994 +      let record = self.alarmToRecord(aAlarm);
   1.995 +      store.openCursor(record.id).onsuccess = function onsuccess(event) {
   1.996 +        let cursor = event.target.result;
   1.997 +        txn.result = false;
   1.998 +        if (cursor) {
   1.999 +          cursor.update(record);
  1.1000 +          txn.result = true;
  1.1001 +        }
  1.1002 +      }
  1.1003 +    }, aResultCb);
  1.1004 +  },
  1.1005 +
  1.1006 +  getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) {
  1.1007 +    let self = this;
  1.1008 +    this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) {
  1.1009 +      if (DEBUG) {
  1.1010 +        debug("Get alarms for " + aManifestURL);
  1.1011 +      }
  1.1012 +
  1.1013 +      txn.result = [];
  1.1014 +      store.index("manifestURL").openCursor(aManifestURL)
  1.1015 +                                .onsuccess = function onsuccess(event) {
  1.1016 +        let cursor = event.target.result;
  1.1017 +        if (!cursor) {
  1.1018 +          return;
  1.1019 +        }
  1.1020 +
  1.1021 +        if (!aNetworkId || cursor.value.networkId == aNetworkId) {
  1.1022 +          txn.result.push(self.recordToAlarm(cursor.value));
  1.1023 +        }
  1.1024 +
  1.1025 +        cursor.continue();
  1.1026 +      }
  1.1027 +    }, aResultCb);
  1.1028 +  },
  1.1029 +
  1.1030 +  _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) {
  1.1031 +    this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
  1.1032 +      if (DEBUG) {
  1.1033 +        debug("Reset alarms for network " + aNetworkId);
  1.1034 +      }
  1.1035 +
  1.1036 +      let lowerFilter = [aNetworkId, 0];
  1.1037 +      let upperFilter = [aNetworkId, ""];
  1.1038 +      let range = IDBKeyRange.bound(lowerFilter, upperFilter);
  1.1039 +
  1.1040 +      store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) {
  1.1041 +        let cursor = event.target.result;
  1.1042 +        if (cursor) {
  1.1043 +          if (cursor.value.startTime) {
  1.1044 +            cursor.value.relativeThreshold = cursor.value.threshold;
  1.1045 +            cursor.update(cursor.value);
  1.1046 +          }
  1.1047 +          cursor.continue();
  1.1048 +          return;
  1.1049 +        }
  1.1050 +      };
  1.1051 +    }, aResultCb);
  1.1052 +  }
  1.1053 +};

mercurial