dom/mobilemessage/src/gonk/MobileMessageDB.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/mobilemessage/src/gonk/MobileMessageDB.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,3500 @@
     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 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    1.11 +
    1.12 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.13 +Cu.import("resource://gre/modules/Services.jsm");
    1.14 +Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
    1.15 +Cu.importGlobalProperties(["indexedDB"]);
    1.16 +
    1.17 +var RIL = {};
    1.18 +Cu.import("resource://gre/modules/ril_consts.js", RIL);
    1.19 +
    1.20 +const RIL_GETMESSAGESCURSOR_CID =
    1.21 +  Components.ID("{484d1ad8-840e-4782-9dc4-9ebc4d914937}");
    1.22 +const RIL_GETTHREADSCURSOR_CID =
    1.23 +  Components.ID("{95ee7c3e-d6f2-4ec4-ade5-0c453c036d35}");
    1.24 +
    1.25 +const DEBUG = false;
    1.26 +const DISABLE_MMS_GROUPING_FOR_RECEIVING = true;
    1.27 +
    1.28 +
    1.29 +const DB_VERSION = 22;
    1.30 +const MESSAGE_STORE_NAME = "sms";
    1.31 +const THREAD_STORE_NAME = "thread";
    1.32 +const PARTICIPANT_STORE_NAME = "participant";
    1.33 +const MOST_RECENT_STORE_NAME = "most-recent";
    1.34 +const SMS_SEGMENT_STORE_NAME = "sms-segment";
    1.35 +
    1.36 +const DELIVERY_SENDING = "sending";
    1.37 +const DELIVERY_SENT = "sent";
    1.38 +const DELIVERY_RECEIVED = "received";
    1.39 +const DELIVERY_NOT_DOWNLOADED = "not-downloaded";
    1.40 +const DELIVERY_ERROR = "error";
    1.41 +
    1.42 +const DELIVERY_STATUS_NOT_APPLICABLE = "not-applicable";
    1.43 +const DELIVERY_STATUS_SUCCESS = "success";
    1.44 +const DELIVERY_STATUS_PENDING = "pending";
    1.45 +const DELIVERY_STATUS_ERROR = "error";
    1.46 +
    1.47 +const MESSAGE_CLASS_NORMAL = "normal";
    1.48 +
    1.49 +const FILTER_TIMESTAMP = "timestamp";
    1.50 +const FILTER_NUMBERS = "numbers";
    1.51 +const FILTER_DELIVERY = "delivery";
    1.52 +const FILTER_READ = "read";
    1.53 +
    1.54 +// We can“t create an IDBKeyCursor with a boolean, so we need to use numbers
    1.55 +// instead.
    1.56 +const FILTER_READ_UNREAD = 0;
    1.57 +const FILTER_READ_READ = 1;
    1.58 +
    1.59 +const READ_ONLY = "readonly";
    1.60 +const READ_WRITE = "readwrite";
    1.61 +const PREV = "prev";
    1.62 +const NEXT = "next";
    1.63 +
    1.64 +const COLLECT_ID_END = 0;
    1.65 +const COLLECT_ID_ERROR = -1;
    1.66 +const COLLECT_TIMESTAMP_UNUSED = 0;
    1.67 +
    1.68 +XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService",
    1.69 +                                   "@mozilla.org/mobilemessage/mobilemessageservice;1",
    1.70 +                                   "nsIMobileMessageService");
    1.71 +
    1.72 +XPCOMUtils.defineLazyServiceGetter(this, "gMMSService",
    1.73 +                                   "@mozilla.org/mms/rilmmsservice;1",
    1.74 +                                   "nsIMmsService");
    1.75 +
    1.76 +XPCOMUtils.defineLazyGetter(this, "MMS", function() {
    1.77 +  let MMS = {};
    1.78 +  Cu.import("resource://gre/modules/MmsPduHelper.jsm", MMS);
    1.79 +  return MMS;
    1.80 +});
    1.81 +
    1.82 +/**
    1.83 + * MobileMessageDB
    1.84 + */
    1.85 +this.MobileMessageDB = function() {};
    1.86 +MobileMessageDB.prototype = {
    1.87 +  dbName: null,
    1.88 +  dbVersion: null,
    1.89 +
    1.90 +  /**
    1.91 +   * Cache the DB here.
    1.92 +   */
    1.93 +  db: null,
    1.94 +
    1.95 +  /**
    1.96 +   * Last sms/mms object store key value in the database.
    1.97 +   */
    1.98 +  lastMessageId: 0,
    1.99 +
   1.100 +  /**
   1.101 +   * An optional hook to check if device storage is full.
   1.102 +   *
   1.103 +   * @return true if full.
   1.104 +   */
   1.105 +  isDiskFull: null,
   1.106 +
   1.107 +  /**
   1.108 +   * Prepare the database. This may include opening the database and upgrading
   1.109 +   * it to the latest schema version.
   1.110 +   *
   1.111 +   * @param callback
   1.112 +   *        Function that takes an error and db argument. It is called when
   1.113 +   *        the database is ready to use or if an error occurs while preparing
   1.114 +   *        the database.
   1.115 +   *
   1.116 +   * @return (via callback) a database ready for use.
   1.117 +   */
   1.118 +  ensureDB: function(callback) {
   1.119 +    if (this.db) {
   1.120 +      if (DEBUG) debug("ensureDB: already have a database, returning early.");
   1.121 +      callback(null, this.db);
   1.122 +      return;
   1.123 +    }
   1.124 +
   1.125 +    let self = this;
   1.126 +    function gotDB(db) {
   1.127 +      self.db = db;
   1.128 +      callback(null, db);
   1.129 +    }
   1.130 +
   1.131 +    let request = indexedDB.open(this.dbName, this.dbVersion);
   1.132 +    request.onsuccess = function(event) {
   1.133 +      if (DEBUG) debug("Opened database:", self.dbName, self.dbVersion);
   1.134 +      gotDB(event.target.result);
   1.135 +    };
   1.136 +    request.onupgradeneeded = function(event) {
   1.137 +      if (DEBUG) {
   1.138 +        debug("Database needs upgrade:", self.dbName,
   1.139 +              event.oldVersion, event.newVersion);
   1.140 +        debug("Correct new database version:", event.newVersion == self.dbVersion);
   1.141 +      }
   1.142 +
   1.143 +      let db = event.target.result;
   1.144 +
   1.145 +      let currentVersion = event.oldVersion;
   1.146 +
   1.147 +      function update(currentVersion) {
   1.148 +        let next = update.bind(self, currentVersion + 1);
   1.149 +
   1.150 +        switch (currentVersion) {
   1.151 +          case 0:
   1.152 +            if (DEBUG) debug("New database");
   1.153 +            self.createSchema(db, next);
   1.154 +            break;
   1.155 +          case 1:
   1.156 +            if (DEBUG) debug("Upgrade to version 2. Including `read` index");
   1.157 +            self.upgradeSchema(event.target.transaction, next);
   1.158 +            break;
   1.159 +          case 2:
   1.160 +            if (DEBUG) debug("Upgrade to version 3. Fix existing entries.");
   1.161 +            self.upgradeSchema2(event.target.transaction, next);
   1.162 +            break;
   1.163 +          case 3:
   1.164 +            if (DEBUG) debug("Upgrade to version 4. Add quick threads view.");
   1.165 +            self.upgradeSchema3(db, event.target.transaction, next);
   1.166 +            break;
   1.167 +          case 4:
   1.168 +            if (DEBUG) debug("Upgrade to version 5. Populate quick threads view.");
   1.169 +            self.upgradeSchema4(event.target.transaction, next);
   1.170 +            break;
   1.171 +          case 5:
   1.172 +            if (DEBUG) debug("Upgrade to version 6. Use PhonenumberJS.");
   1.173 +            self.upgradeSchema5(event.target.transaction, next);
   1.174 +            break;
   1.175 +          case 6:
   1.176 +            if (DEBUG) debug("Upgrade to version 7. Use multiple entry indexes.");
   1.177 +            self.upgradeSchema6(event.target.transaction, next);
   1.178 +            break;
   1.179 +          case 7:
   1.180 +            if (DEBUG) debug("Upgrade to version 8. Add participant/thread stores.");
   1.181 +            self.upgradeSchema7(db, event.target.transaction, next);
   1.182 +            break;
   1.183 +          case 8:
   1.184 +            if (DEBUG) debug("Upgrade to version 9. Add transactionId index for incoming MMS.");
   1.185 +            self.upgradeSchema8(event.target.transaction, next);
   1.186 +            break;
   1.187 +          case 9:
   1.188 +            if (DEBUG) debug("Upgrade to version 10. Upgrade type if it's not existing.");
   1.189 +            self.upgradeSchema9(event.target.transaction, next);
   1.190 +            break;
   1.191 +          case 10:
   1.192 +            if (DEBUG) debug("Upgrade to version 11. Add last message type into threadRecord.");
   1.193 +            self.upgradeSchema10(event.target.transaction, next);
   1.194 +            break;
   1.195 +          case 11:
   1.196 +            if (DEBUG) debug("Upgrade to version 12. Add envelopeId index for outgoing MMS.");
   1.197 +            self.upgradeSchema11(event.target.transaction, next);
   1.198 +            break;
   1.199 +          case 12:
   1.200 +            if (DEBUG) debug("Upgrade to version 13. Replaced deliveryStatus by deliveryInfo.");
   1.201 +            self.upgradeSchema12(event.target.transaction, next);
   1.202 +            break;
   1.203 +          case 13:
   1.204 +            if (DEBUG) debug("Upgrade to version 14. Fix the wrong participants.");
   1.205 +            // A workaround to check if we need to re-upgrade the DB schema 12. We missed this
   1.206 +            // because we didn't properly uplift that logic to b2g_v1.2 and errors could happen
   1.207 +            // when migrating b2g_v1.2 to b2g_v1.3. Please see Bug 960741 for details.
   1.208 +            self.needReUpgradeSchema12(event.target.transaction, function(isNeeded) {
   1.209 +              if (isNeeded) {
   1.210 +                self.upgradeSchema12(event.target.transaction, function() {
   1.211 +                  self.upgradeSchema13(event.target.transaction, next);
   1.212 +                });
   1.213 +              } else {
   1.214 +                self.upgradeSchema13(event.target.transaction, next);
   1.215 +              }
   1.216 +            });
   1.217 +            break;
   1.218 +          case 14:
   1.219 +            if (DEBUG) debug("Upgrade to version 15. Add deliveryTimestamp.");
   1.220 +            self.upgradeSchema14(event.target.transaction, next);
   1.221 +            break;
   1.222 +          case 15:
   1.223 +            if (DEBUG) debug("Upgrade to version 16. Add ICC ID for each message.");
   1.224 +            self.upgradeSchema15(event.target.transaction, next);
   1.225 +            break;
   1.226 +          case 16:
   1.227 +            if (DEBUG) debug("Upgrade to version 17. Add isReadReportSent for incoming MMS.");
   1.228 +            self.upgradeSchema16(event.target.transaction, next);
   1.229 +            break;
   1.230 +          case 17:
   1.231 +            if (DEBUG) debug("Upgrade to version 18. Add last message subject into threadRecord.");
   1.232 +            self.upgradeSchema17(event.target.transaction, next);
   1.233 +            break;
   1.234 +          case 18:
   1.235 +            if (DEBUG) debug("Upgrade to version 19. Add pid for incoming SMS.");
   1.236 +            self.upgradeSchema18(event.target.transaction, next);
   1.237 +            break;
   1.238 +          case 19:
   1.239 +            if (DEBUG) debug("Upgrade to version 20. Add readStatus and readTimestamp.");
   1.240 +            self.upgradeSchema19(event.target.transaction, next);
   1.241 +            break;
   1.242 +          case 20:
   1.243 +            if (DEBUG) debug("Upgrade to version 21. Add sentTimestamp.");
   1.244 +            self.upgradeSchema20(event.target.transaction, next);
   1.245 +            break;
   1.246 +          case 21:
   1.247 +            if (DEBUG) debug("Upgrade to version 22. Add sms-segment store.");
   1.248 +            self.upgradeSchema21(db, event.target.transaction, next);
   1.249 +            break;
   1.250 +          case 22:
   1.251 +            // This will need to be moved for each new version
   1.252 +            if (DEBUG) debug("Upgrade finished.");
   1.253 +            break;
   1.254 +          default:
   1.255 +            event.target.transaction.abort();
   1.256 +            if (DEBUG) debug("unexpected db version: " + event.oldVersion);
   1.257 +            callback(Cr.NS_ERROR_FAILURE, null);
   1.258 +            break;
   1.259 +        }
   1.260 +      }
   1.261 +
   1.262 +      update(currentVersion);
   1.263 +    };
   1.264 +    request.onerror = function(event) {
   1.265 +      //TODO look at event.target.Code and change error constant accordingly
   1.266 +      if (DEBUG) debug("Error opening database!");
   1.267 +      callback(Cr.NS_ERROR_FAILURE, null);
   1.268 +    };
   1.269 +    request.onblocked = function(event) {
   1.270 +      if (DEBUG) debug("Opening database request is blocked.");
   1.271 +      callback(Cr.NS_ERROR_FAILURE, null);
   1.272 +    };
   1.273 +  },
   1.274 +
   1.275 +  /**
   1.276 +   * Start a new transaction.
   1.277 +   *
   1.278 +   * @param txn_type
   1.279 +   *        Type of transaction (e.g. READ_WRITE)
   1.280 +   * @param callback
   1.281 +   *        Function to call when the transaction is available. It will
   1.282 +   *        be invoked with the transaction and opened object stores.
   1.283 +   * @param storeNames
   1.284 +   *        Names of the stores to open.
   1.285 +   */
   1.286 +  newTxn: function(txn_type, callback, storeNames) {
   1.287 +    if (!storeNames) {
   1.288 +      storeNames = [MESSAGE_STORE_NAME];
   1.289 +    }
   1.290 +    if (DEBUG) debug("Opening transaction for object stores: " + storeNames);
   1.291 +    let self = this;
   1.292 +    this.ensureDB(function(error, db) {
   1.293 +      if (!error &&
   1.294 +          txn_type === READ_WRITE &&
   1.295 +          self.isDiskFull && self.isDiskFull()) {
   1.296 +        error = Cr.NS_ERROR_FILE_NO_DEVICE_SPACE;
   1.297 +      }
   1.298 +      if (error) {
   1.299 +        if (DEBUG) debug("Could not open database: " + error);
   1.300 +        callback(error);
   1.301 +        return;
   1.302 +      }
   1.303 +      let txn = db.transaction(storeNames, txn_type);
   1.304 +      if (DEBUG) debug("Started transaction " + txn + " of type " + txn_type);
   1.305 +      if (DEBUG) {
   1.306 +        txn.oncomplete = function oncomplete(event) {
   1.307 +          debug("Transaction " + txn + " completed.");
   1.308 +        };
   1.309 +        txn.onerror = function onerror(event) {
   1.310 +          //TODO check event.target.errorCode and show an appropiate error
   1.311 +          //     message according to it.
   1.312 +          debug("Error occurred during transaction: " + event.target.errorCode);
   1.313 +        };
   1.314 +      }
   1.315 +      let stores;
   1.316 +      if (storeNames.length == 1) {
   1.317 +        if (DEBUG) debug("Retrieving object store " + storeNames[0]);
   1.318 +        stores = txn.objectStore(storeNames[0]);
   1.319 +      } else {
   1.320 +        stores = [];
   1.321 +        for each (let storeName in storeNames) {
   1.322 +          if (DEBUG) debug("Retrieving object store " + storeName);
   1.323 +          stores.push(txn.objectStore(storeName));
   1.324 +        }
   1.325 +      }
   1.326 +      callback(null, txn, stores);
   1.327 +    });
   1.328 +  },
   1.329 +
   1.330 +  /**
   1.331 +   * Initialize this MobileMessageDB.
   1.332 +   *
   1.333 +   * @param aDbName
   1.334 +   *        A string name for that database.
   1.335 +   * @param aDbVersion
   1.336 +   *        The version that mmdb should upgrade to. 0 for the lastest version.
   1.337 +   * @param aCallback
   1.338 +   *        A function when either the initialization transaction is completed
   1.339 +   *        or any error occurs.  Should take only one argument -- null when
   1.340 +   *        initialized with success or the error object otherwise.
   1.341 +   */
   1.342 +  init: function(aDbName, aDbVersion, aCallback) {
   1.343 +    this.dbName = aDbName;
   1.344 +    this.dbVersion = aDbVersion || DB_VERSION;
   1.345 +
   1.346 +    let self = this;
   1.347 +    this.newTxn(READ_ONLY, function(error, txn, messageStore){
   1.348 +      if (error) {
   1.349 +        if (aCallback) {
   1.350 +          aCallback(error);
   1.351 +        }
   1.352 +        return;
   1.353 +      }
   1.354 +
   1.355 +      if (aCallback) {
   1.356 +        txn.oncomplete = function() {
   1.357 +          aCallback(null);
   1.358 +        };
   1.359 +      }
   1.360 +
   1.361 +      // In order to get the highest key value, we open a key cursor in reverse
   1.362 +      // order and get only the first pointed value.
   1.363 +      let request = messageStore.openCursor(null, PREV);
   1.364 +      request.onsuccess = function onsuccess(event) {
   1.365 +        let cursor = event.target.result;
   1.366 +        if (!cursor) {
   1.367 +          if (DEBUG) {
   1.368 +            debug("Could not get the last key from mobile message database. " +
   1.369 +                  "Probably empty database");
   1.370 +          }
   1.371 +          return;
   1.372 +        }
   1.373 +        self.lastMessageId = cursor.key || 0;
   1.374 +        if (DEBUG) debug("Last assigned message ID was " + self.lastMessageId);
   1.375 +      };
   1.376 +      request.onerror = function onerror(event) {
   1.377 +        if (DEBUG) {
   1.378 +          debug("Could not get the last key from mobile message database " +
   1.379 +                event.target.errorCode);
   1.380 +        }
   1.381 +      };
   1.382 +    });
   1.383 +  },
   1.384 +
   1.385 +  close: function() {
   1.386 +    if (!this.db) {
   1.387 +      return;
   1.388 +    }
   1.389 +
   1.390 +    this.db.close();
   1.391 +    this.db = null;
   1.392 +    this.lastMessageId = 0;
   1.393 +  },
   1.394 +
   1.395 +  /**
   1.396 +   * Sometimes user might reboot or remove battery while sending/receiving
   1.397 +   * message. This is function set the status of message records to error.
   1.398 +   */
   1.399 +  updatePendingTransactionToError: function(aError) {
   1.400 +    if (aError) {
   1.401 +      return;
   1.402 +    }
   1.403 +
   1.404 +    this.newTxn(READ_WRITE, function(error, txn, messageStore) {
   1.405 +      if (error) {
   1.406 +        return;
   1.407 +      }
   1.408 +
   1.409 +      let deliveryIndex = messageStore.index("delivery");
   1.410 +
   1.411 +      // Set all 'delivery: sending' records to 'delivery: error' and 'deliveryStatus:
   1.412 +      // error'.
   1.413 +      let keyRange = IDBKeyRange.bound([DELIVERY_SENDING, 0], [DELIVERY_SENDING, ""]);
   1.414 +      let cursorRequestSending = deliveryIndex.openCursor(keyRange);
   1.415 +      cursorRequestSending.onsuccess = function(event) {
   1.416 +        let messageCursor = event.target.result;
   1.417 +        if (!messageCursor) {
   1.418 +          return;
   1.419 +        }
   1.420 +
   1.421 +        let messageRecord = messageCursor.value;
   1.422 +
   1.423 +        // Set delivery to error.
   1.424 +        messageRecord.delivery = DELIVERY_ERROR;
   1.425 +        messageRecord.deliveryIndex = [DELIVERY_ERROR, messageRecord.timestamp];
   1.426 +
   1.427 +        if (messageRecord.type == "sms") {
   1.428 +          messageRecord.deliveryStatus = DELIVERY_STATUS_ERROR;
   1.429 +        } else {
   1.430 +          // Set delivery status to error.
   1.431 +          for (let i = 0; i < messageRecord.deliveryInfo.length; i++) {
   1.432 +            messageRecord.deliveryInfo[i].deliveryStatus = DELIVERY_STATUS_ERROR;
   1.433 +          }
   1.434 +        }
   1.435 +
   1.436 +        messageCursor.update(messageRecord);
   1.437 +        messageCursor.continue();
   1.438 +      };
   1.439 +
   1.440 +      // Set all 'delivery: not-downloaded' and 'deliveryStatus: pending'
   1.441 +      // records to 'delivery: not-downloaded' and 'deliveryStatus: error'.
   1.442 +      keyRange = IDBKeyRange.bound([DELIVERY_NOT_DOWNLOADED, 0], [DELIVERY_NOT_DOWNLOADED, ""]);
   1.443 +      let cursorRequestNotDownloaded = deliveryIndex.openCursor(keyRange);
   1.444 +      cursorRequestNotDownloaded.onsuccess = function(event) {
   1.445 +        let messageCursor = event.target.result;
   1.446 +        if (!messageCursor) {
   1.447 +          return;
   1.448 +        }
   1.449 +
   1.450 +        let messageRecord = messageCursor.value;
   1.451 +
   1.452 +        // We have no "not-downloaded" SMS messages.
   1.453 +        if (messageRecord.type == "sms") {
   1.454 +          messageCursor.continue();
   1.455 +          return;
   1.456 +        }
   1.457 +
   1.458 +        // Set delivery status to error.
   1.459 +        let deliveryInfo = messageRecord.deliveryInfo;
   1.460 +        if (deliveryInfo.length == 1 &&
   1.461 +            deliveryInfo[0].deliveryStatus == DELIVERY_STATUS_PENDING) {
   1.462 +          deliveryInfo[0].deliveryStatus = DELIVERY_STATUS_ERROR;
   1.463 +        }
   1.464 +
   1.465 +        messageCursor.update(messageRecord);
   1.466 +        messageCursor.continue();
   1.467 +      };
   1.468 +    });
   1.469 +  },
   1.470 +
   1.471 +  /**
   1.472 +   * Create the initial database schema.
   1.473 +   *
   1.474 +   * TODO need to worry about number normalization somewhere...
   1.475 +   * TODO full text search on body???
   1.476 +   */
   1.477 +  createSchema: function(db, next) {
   1.478 +    // This messageStore holds the main mobile message data.
   1.479 +    let messageStore = db.createObjectStore(MESSAGE_STORE_NAME, { keyPath: "id" });
   1.480 +    messageStore.createIndex("timestamp", "timestamp", { unique: false });
   1.481 +    if (DEBUG) debug("Created object stores and indexes");
   1.482 +    next();
   1.483 +  },
   1.484 +
   1.485 +  /**
   1.486 +   * Upgrade to the corresponding database schema version.
   1.487 +   */
   1.488 +  upgradeSchema: function(transaction, next) {
   1.489 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.490 +    messageStore.createIndex("read", "read", { unique: false });
   1.491 +    next();
   1.492 +  },
   1.493 +
   1.494 +  upgradeSchema2: function(transaction, next) {
   1.495 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.496 +    messageStore.openCursor().onsuccess = function(event) {
   1.497 +      let cursor = event.target.result;
   1.498 +      if (!cursor) {
   1.499 +        next();
   1.500 +        return;
   1.501 +      }
   1.502 +
   1.503 +      let messageRecord = cursor.value;
   1.504 +      messageRecord.messageClass = MESSAGE_CLASS_NORMAL;
   1.505 +      messageRecord.deliveryStatus = DELIVERY_STATUS_NOT_APPLICABLE;
   1.506 +      cursor.update(messageRecord);
   1.507 +      cursor.continue();
   1.508 +    };
   1.509 +  },
   1.510 +
   1.511 +  upgradeSchema3: function(db, transaction, next) {
   1.512 +    // Delete redundant "id" index.
   1.513 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.514 +    if (messageStore.indexNames.contains("id")) {
   1.515 +      messageStore.deleteIndex("id");
   1.516 +    }
   1.517 +
   1.518 +    /**
   1.519 +     * This mostRecentStore can be used to quickly construct a thread view of
   1.520 +     * the mobile message database. Each entry looks like this:
   1.521 +     *
   1.522 +     * { senderOrReceiver: <String> (primary key),
   1.523 +     *   id: <Number>,
   1.524 +     *   timestamp: <Date>,
   1.525 +     *   body: <String>,
   1.526 +     *   unreadCount: <Number> }
   1.527 +     *
   1.528 +     */
   1.529 +    let mostRecentStore = db.createObjectStore(MOST_RECENT_STORE_NAME,
   1.530 +                                               { keyPath: "senderOrReceiver" });
   1.531 +    mostRecentStore.createIndex("timestamp", "timestamp");
   1.532 +    next();
   1.533 +  },
   1.534 +
   1.535 +  upgradeSchema4: function(transaction, next) {
   1.536 +    let threads = {};
   1.537 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.538 +    let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);
   1.539 +
   1.540 +    messageStore.openCursor().onsuccess = function(event) {
   1.541 +      let cursor = event.target.result;
   1.542 +      if (!cursor) {
   1.543 +        for (let thread in threads) {
   1.544 +          mostRecentStore.put(threads[thread]);
   1.545 +        }
   1.546 +        next();
   1.547 +        return;
   1.548 +      }
   1.549 +
   1.550 +      let messageRecord = cursor.value;
   1.551 +      let contact = messageRecord.sender || messageRecord.receiver;
   1.552 +
   1.553 +      if (contact in threads) {
   1.554 +        let thread = threads[contact];
   1.555 +        if (!messageRecord.read) {
   1.556 +          thread.unreadCount++;
   1.557 +        }
   1.558 +        if (messageRecord.timestamp > thread.timestamp) {
   1.559 +          thread.id = messageRecord.id;
   1.560 +          thread.body = messageRecord.body;
   1.561 +          thread.timestamp = messageRecord.timestamp;
   1.562 +        }
   1.563 +      } else {
   1.564 +        threads[contact] = {
   1.565 +          senderOrReceiver: contact,
   1.566 +          id: messageRecord.id,
   1.567 +          timestamp: messageRecord.timestamp,
   1.568 +          body: messageRecord.body,
   1.569 +          unreadCount: messageRecord.read ? 0 : 1
   1.570 +        };
   1.571 +      }
   1.572 +      cursor.continue();
   1.573 +    };
   1.574 +  },
   1.575 +
   1.576 +  upgradeSchema5: function(transaction, next) {
   1.577 +    // Don't perform any upgrade. See Bug 819560.
   1.578 +    next();
   1.579 +  },
   1.580 +
   1.581 +  upgradeSchema6: function(transaction, next) {
   1.582 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.583 +
   1.584 +    // Delete "delivery" index.
   1.585 +    if (messageStore.indexNames.contains("delivery")) {
   1.586 +      messageStore.deleteIndex("delivery");
   1.587 +    }
   1.588 +    // Delete "sender" index.
   1.589 +    if (messageStore.indexNames.contains("sender")) {
   1.590 +      messageStore.deleteIndex("sender");
   1.591 +    }
   1.592 +    // Delete "receiver" index.
   1.593 +    if (messageStore.indexNames.contains("receiver")) {
   1.594 +      messageStore.deleteIndex("receiver");
   1.595 +    }
   1.596 +    // Delete "read" index.
   1.597 +    if (messageStore.indexNames.contains("read")) {
   1.598 +      messageStore.deleteIndex("read");
   1.599 +    }
   1.600 +
   1.601 +    // Create new "delivery", "number" and "read" indexes.
   1.602 +    messageStore.createIndex("delivery", "deliveryIndex");
   1.603 +    messageStore.createIndex("number", "numberIndex", { multiEntry: true });
   1.604 +    messageStore.createIndex("read", "readIndex");
   1.605 +
   1.606 +    // Populate new "deliverIndex", "numberIndex" and "readIndex" attributes.
   1.607 +    messageStore.openCursor().onsuccess = function(event) {
   1.608 +      let cursor = event.target.result;
   1.609 +      if (!cursor) {
   1.610 +        next();
   1.611 +        return;
   1.612 +      }
   1.613 +
   1.614 +      let messageRecord = cursor.value;
   1.615 +      let timestamp = messageRecord.timestamp;
   1.616 +      messageRecord.deliveryIndex = [messageRecord.delivery, timestamp];
   1.617 +      messageRecord.numberIndex = [
   1.618 +        [messageRecord.sender, timestamp],
   1.619 +        [messageRecord.receiver, timestamp]
   1.620 +      ];
   1.621 +      messageRecord.readIndex = [messageRecord.read, timestamp];
   1.622 +      cursor.update(messageRecord);
   1.623 +      cursor.continue();
   1.624 +    };
   1.625 +  },
   1.626 +
   1.627 +  /**
   1.628 +   * Add participant/thread stores.
   1.629 +   *
   1.630 +   * The message store now saves original phone numbers/addresses input from
   1.631 +   * content to message records. No normalization is made.
   1.632 +   *
   1.633 +   * For filtering messages by phone numbers, it first looks up corresponding
   1.634 +   * participant IDs from participant table and fetch message records with
   1.635 +   * matching keys defined in per record "participantIds" field.
   1.636 +   *
   1.637 +   * For message threading, messages with the same participant ID array are put
   1.638 +   * in the same thread. So updating "unreadCount", "lastMessageId" and
   1.639 +   * "lastTimestamp" are through the "threadId" carried by per message record.
   1.640 +   * Fetching threads list is now simply walking through the thread sotre. The
   1.641 +   * "mostRecentStore" is dropped.
   1.642 +   */
   1.643 +  upgradeSchema7: function(db, transaction, next) {
   1.644 +    /**
   1.645 +     * This "participant" object store keeps mappings of multiple phone numbers
   1.646 +     * of the same recipient to an integer participant id. Each entry looks
   1.647 +     * like:
   1.648 +     *
   1.649 +     * { id: <Number> (primary key),
   1.650 +     *   addresses: <Array of strings> }
   1.651 +     */
   1.652 +    let participantStore = db.createObjectStore(PARTICIPANT_STORE_NAME,
   1.653 +                                                { keyPath: "id",
   1.654 +                                                  autoIncrement: true });
   1.655 +    participantStore.createIndex("addresses", "addresses", { multiEntry: true });
   1.656 +
   1.657 +    /**
   1.658 +     * This "threads" object store keeps mappings from an integer thread id to
   1.659 +     * ids of the participants of that message thread. Each entry looks like:
   1.660 +     *
   1.661 +     * { id: <Number> (primary key),
   1.662 +     *   participantIds: <Array of participant IDs>,
   1.663 +     *   participantAddresses: <Array of the first addresses of the participants>,
   1.664 +     *   lastMessageId: <Number>,
   1.665 +     *   lastTimestamp: <Date>,
   1.666 +     *   subject: <String>,
   1.667 +     *   unreadCount: <Number> }
   1.668 +     *
   1.669 +     */
   1.670 +    let threadStore = db.createObjectStore(THREAD_STORE_NAME,
   1.671 +                                           { keyPath: "id",
   1.672 +                                             autoIncrement: true });
   1.673 +    threadStore.createIndex("participantIds", "participantIds");
   1.674 +    threadStore.createIndex("lastTimestamp", "lastTimestamp");
   1.675 +
   1.676 +    /**
   1.677 +     * Replace "numberIndex" with "participantIdsIndex" and create an additional
   1.678 +     * "threadId". "numberIndex" will be removed later.
   1.679 +     */
   1.680 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.681 +    messageStore.createIndex("threadId", "threadIdIndex");
   1.682 +    messageStore.createIndex("participantIds", "participantIdsIndex",
   1.683 +                             { multiEntry: true });
   1.684 +
   1.685 +    // Now populate participantStore & threadStore.
   1.686 +    let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);
   1.687 +    let self = this;
   1.688 +    let mostRecentRequest = mostRecentStore.openCursor();
   1.689 +    mostRecentRequest.onsuccess = function(event) {
   1.690 +      let mostRecentCursor = event.target.result;
   1.691 +      if (!mostRecentCursor) {
   1.692 +        db.deleteObjectStore(MOST_RECENT_STORE_NAME);
   1.693 +
   1.694 +        // No longer need the "number" index in messageStore, use
   1.695 +        // "participantIds" index instead.
   1.696 +        messageStore.deleteIndex("number");
   1.697 +        next();
   1.698 +        return;
   1.699 +      }
   1.700 +
   1.701 +      let mostRecentRecord = mostRecentCursor.value;
   1.702 +
   1.703 +      // Each entry in mostRecentStore is supposed to be a unique thread, so we
   1.704 +      // retrieve the records out and insert its "senderOrReceiver" column as a
   1.705 +      // new record in participantStore.
   1.706 +      let number = mostRecentRecord.senderOrReceiver;
   1.707 +      self.findParticipantRecordByAddress(participantStore, number, true,
   1.708 +                                          function(participantRecord) {
   1.709 +        // Also create a new record in threadStore.
   1.710 +        let threadRecord = {
   1.711 +          participantIds: [participantRecord.id],
   1.712 +          participantAddresses: [number],
   1.713 +          lastMessageId: mostRecentRecord.id,
   1.714 +          lastTimestamp: mostRecentRecord.timestamp,
   1.715 +          subject: mostRecentRecord.body,
   1.716 +          unreadCount: mostRecentRecord.unreadCount,
   1.717 +        };
   1.718 +        let addThreadRequest = threadStore.add(threadRecord);
   1.719 +        addThreadRequest.onsuccess = function(event) {
   1.720 +          threadRecord.id = event.target.result;
   1.721 +
   1.722 +          let numberRange = IDBKeyRange.bound([number, 0], [number, ""]);
   1.723 +          let messageRequest = messageStore.index("number")
   1.724 +                                           .openCursor(numberRange, NEXT);
   1.725 +          messageRequest.onsuccess = function(event) {
   1.726 +            let messageCursor = event.target.result;
   1.727 +            if (!messageCursor) {
   1.728 +              // No more message records, check next most recent record.
   1.729 +              mostRecentCursor.continue();
   1.730 +              return;
   1.731 +            }
   1.732 +
   1.733 +            let messageRecord = messageCursor.value;
   1.734 +            // Check whether the message really belongs to this thread.
   1.735 +            let matchSenderOrReceiver = false;
   1.736 +            if (messageRecord.delivery == DELIVERY_RECEIVED) {
   1.737 +              if (messageRecord.sender == number) {
   1.738 +                matchSenderOrReceiver = true;
   1.739 +              }
   1.740 +            } else if (messageRecord.receiver == number) {
   1.741 +              matchSenderOrReceiver = true;
   1.742 +            }
   1.743 +            if (!matchSenderOrReceiver) {
   1.744 +              // Check next message record.
   1.745 +              messageCursor.continue();
   1.746 +              return;
   1.747 +            }
   1.748 +
   1.749 +            messageRecord.threadId = threadRecord.id;
   1.750 +            messageRecord.threadIdIndex = [threadRecord.id,
   1.751 +                                           messageRecord.timestamp];
   1.752 +            messageRecord.participantIdsIndex = [
   1.753 +              [participantRecord.id, messageRecord.timestamp]
   1.754 +            ];
   1.755 +            messageCursor.update(messageRecord);
   1.756 +            // Check next message record.
   1.757 +            messageCursor.continue();
   1.758 +          };
   1.759 +          messageRequest.onerror = function() {
   1.760 +            // Error in fetching message records, check next most recent record.
   1.761 +            mostRecentCursor.continue();
   1.762 +          };
   1.763 +        };
   1.764 +        addThreadRequest.onerror = function() {
   1.765 +          // Error in fetching message records, check next most recent record.
   1.766 +          mostRecentCursor.continue();
   1.767 +        };
   1.768 +      });
   1.769 +    };
   1.770 +  },
   1.771 +
   1.772 +  /**
   1.773 +   * Add transactionId index for MMS.
   1.774 +   */
   1.775 +  upgradeSchema8: function(transaction, next) {
   1.776 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.777 +
   1.778 +    // Delete "transactionId" index.
   1.779 +    if (messageStore.indexNames.contains("transactionId")) {
   1.780 +      messageStore.deleteIndex("transactionId");
   1.781 +    }
   1.782 +
   1.783 +    // Create new "transactionId" indexes.
   1.784 +    messageStore.createIndex("transactionId", "transactionIdIndex", { unique: true });
   1.785 +
   1.786 +    // Populate new "transactionIdIndex" attributes.
   1.787 +    messageStore.openCursor().onsuccess = function(event) {
   1.788 +      let cursor = event.target.result;
   1.789 +      if (!cursor) {
   1.790 +        next();
   1.791 +        return;
   1.792 +      }
   1.793 +
   1.794 +      let messageRecord = cursor.value;
   1.795 +      if ("mms" == messageRecord.type &&
   1.796 +          (DELIVERY_NOT_DOWNLOADED == messageRecord.delivery ||
   1.797 +           DELIVERY_RECEIVED == messageRecord.delivery)) {
   1.798 +        messageRecord.transactionIdIndex =
   1.799 +          messageRecord.headers["x-mms-transaction-id"];
   1.800 +        cursor.update(messageRecord);
   1.801 +      }
   1.802 +      cursor.continue();
   1.803 +    };
   1.804 +  },
   1.805 +
   1.806 +  upgradeSchema9: function(transaction, next) {
   1.807 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.808 +
   1.809 +    // Update type attributes.
   1.810 +    messageStore.openCursor().onsuccess = function(event) {
   1.811 +      let cursor = event.target.result;
   1.812 +      if (!cursor) {
   1.813 +        next();
   1.814 +        return;
   1.815 +      }
   1.816 +
   1.817 +      let messageRecord = cursor.value;
   1.818 +      if (messageRecord.type == undefined) {
   1.819 +        messageRecord.type = "sms";
   1.820 +        cursor.update(messageRecord);
   1.821 +      }
   1.822 +      cursor.continue();
   1.823 +    };
   1.824 +  },
   1.825 +
   1.826 +  upgradeSchema10: function(transaction, next) {
   1.827 +    let threadStore = transaction.objectStore(THREAD_STORE_NAME);
   1.828 +
   1.829 +    // Add 'lastMessageType' to each thread record.
   1.830 +    threadStore.openCursor().onsuccess = function(event) {
   1.831 +      let cursor = event.target.result;
   1.832 +      if (!cursor) {
   1.833 +        next();
   1.834 +        return;
   1.835 +      }
   1.836 +
   1.837 +      let threadRecord = cursor.value;
   1.838 +      let lastMessageId = threadRecord.lastMessageId;
   1.839 +      let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.840 +      let request = messageStore.mozGetAll(lastMessageId);
   1.841 +
   1.842 +      request.onsuccess = function onsuccess() {
   1.843 +        let messageRecord = request.result[0];
   1.844 +        if (!messageRecord) {
   1.845 +          if (DEBUG) debug("Message ID " + lastMessageId + " not found");
   1.846 +          return;
   1.847 +        }
   1.848 +        if (messageRecord.id != lastMessageId) {
   1.849 +          if (DEBUG) {
   1.850 +            debug("Requested message ID (" + lastMessageId + ") is different from" +
   1.851 +                  " the one we got");
   1.852 +          }
   1.853 +          return;
   1.854 +        }
   1.855 +        threadRecord.lastMessageType = messageRecord.type;
   1.856 +        cursor.update(threadRecord);
   1.857 +        cursor.continue();
   1.858 +      };
   1.859 +
   1.860 +      request.onerror = function onerror(event) {
   1.861 +        if (DEBUG) {
   1.862 +          if (event.target) {
   1.863 +            debug("Caught error on transaction", event.target.errorCode);
   1.864 +          }
   1.865 +        }
   1.866 +        cursor.continue();
   1.867 +      };
   1.868 +    };
   1.869 +  },
   1.870 +
   1.871 +  /**
   1.872 +   * Add envelopeId index for MMS.
   1.873 +   */
   1.874 +  upgradeSchema11: function(transaction, next) {
   1.875 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.876 +
   1.877 +    // Delete "envelopeId" index.
   1.878 +    if (messageStore.indexNames.contains("envelopeId")) {
   1.879 +      messageStore.deleteIndex("envelopeId");
   1.880 +    }
   1.881 +
   1.882 +    // Create new "envelopeId" indexes.
   1.883 +    messageStore.createIndex("envelopeId", "envelopeIdIndex", { unique: true });
   1.884 +
   1.885 +    // Populate new "envelopeIdIndex" attributes.
   1.886 +    messageStore.openCursor().onsuccess = function(event) {
   1.887 +      let cursor = event.target.result;
   1.888 +      if (!cursor) {
   1.889 +        next();
   1.890 +        return;
   1.891 +      }
   1.892 +
   1.893 +      let messageRecord = cursor.value;
   1.894 +      if (messageRecord.type == "mms" &&
   1.895 +          messageRecord.delivery == DELIVERY_SENT) {
   1.896 +        messageRecord.envelopeIdIndex = messageRecord.headers["message-id"];
   1.897 +        cursor.update(messageRecord);
   1.898 +      }
   1.899 +      cursor.continue();
   1.900 +    };
   1.901 +  },
   1.902 +
   1.903 +  /**
   1.904 +   * Replace deliveryStatus by deliveryInfo.
   1.905 +   */
   1.906 +  upgradeSchema12: function(transaction, next) {
   1.907 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.908 +
   1.909 +    messageStore.openCursor().onsuccess = function(event) {
   1.910 +      let cursor = event.target.result;
   1.911 +      if (!cursor) {
   1.912 +        next();
   1.913 +        return;
   1.914 +      }
   1.915 +
   1.916 +      let messageRecord = cursor.value;
   1.917 +      if (messageRecord.type == "mms") {
   1.918 +        messageRecord.deliveryInfo = [];
   1.919 +
   1.920 +        if (messageRecord.deliveryStatus.length == 1 &&
   1.921 +            (messageRecord.delivery == DELIVERY_NOT_DOWNLOADED ||
   1.922 +             messageRecord.delivery == DELIVERY_RECEIVED)) {
   1.923 +          messageRecord.deliveryInfo.push({
   1.924 +            receiver: null,
   1.925 +            deliveryStatus: messageRecord.deliveryStatus[0] });
   1.926 +        } else {
   1.927 +          for (let i = 0; i < messageRecord.deliveryStatus.length; i++) {
   1.928 +            messageRecord.deliveryInfo.push({
   1.929 +              receiver: messageRecord.receivers[i],
   1.930 +              deliveryStatus: messageRecord.deliveryStatus[i] });
   1.931 +          }
   1.932 +        }
   1.933 +        delete messageRecord.deliveryStatus;
   1.934 +        cursor.update(messageRecord);
   1.935 +      }
   1.936 +      cursor.continue();
   1.937 +    };
   1.938 +  },
   1.939 +
   1.940 +  /**
   1.941 +   * Check if we need to re-upgrade the DB schema 12.
   1.942 +   */
   1.943 +  needReUpgradeSchema12: function(transaction, callback) {
   1.944 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.945 +
   1.946 +    messageStore.openCursor().onsuccess = function(event) {
   1.947 +      let cursor = event.target.result;
   1.948 +      if (!cursor) {
   1.949 +        callback(false);
   1.950 +        return;
   1.951 +      }
   1.952 +
   1.953 +      let messageRecord = cursor.value;
   1.954 +      if (messageRecord.type == "mms" &&
   1.955 +          messageRecord.deliveryInfo === undefined) {
   1.956 +        callback(true);
   1.957 +        return;
   1.958 +      }
   1.959 +      cursor.continue();
   1.960 +    };
   1.961 +  },
   1.962 +
   1.963 +  /**
   1.964 +   * Fix the wrong participants.
   1.965 +   */
   1.966 +  upgradeSchema13: function(transaction, next) {
   1.967 +    let participantStore = transaction.objectStore(PARTICIPANT_STORE_NAME);
   1.968 +    let threadStore = transaction.objectStore(THREAD_STORE_NAME);
   1.969 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
   1.970 +    let self = this;
   1.971 +
   1.972 +    let isInvalid = function(participantRecord) {
   1.973 +      let entries = [];
   1.974 +      for (let addr of participantRecord.addresses) {
   1.975 +        entries.push({
   1.976 +          normalized: addr,
   1.977 +          parsed: PhoneNumberUtils.parseWithMCC(addr, null)
   1.978 +        })
   1.979 +      }
   1.980 +      for (let ix = 0 ; ix < entries.length - 1; ix++) {
   1.981 +        let entry1 = entries[ix];
   1.982 +        for (let iy = ix + 1 ; iy < entries.length; iy ++) {
   1.983 +          let entry2 = entries[iy];
   1.984 +          if (!self.matchPhoneNumbers(entry1.normalized, entry1.parsed,
   1.985 +                                      entry2.normalized, entry2.parsed)) {
   1.986 +            return true;
   1.987 +          }
   1.988 +        }
   1.989 +      }
   1.990 +      return false;
   1.991 +    };
   1.992 +
   1.993 +    let invalidParticipantIds = [];
   1.994 +    participantStore.openCursor().onsuccess = function(event) {
   1.995 +      let cursor = event.target.result;
   1.996 +      if (cursor) {
   1.997 +        let participantRecord = cursor.value;
   1.998 +        // Check if this participant record is valid
   1.999 +        if (isInvalid(participantRecord)) {
  1.1000 +          invalidParticipantIds.push(participantRecord.id);
  1.1001 +          cursor.delete();
  1.1002 +        }
  1.1003 +        cursor.continue();
  1.1004 +        return;
  1.1005 +      }
  1.1006 +
  1.1007 +      // Participant store cursor iteration done.
  1.1008 +      if (!invalidParticipantIds.length) {
  1.1009 +        next();
  1.1010 +        return;
  1.1011 +      }
  1.1012 +
  1.1013 +      // Find affected thread.
  1.1014 +      let wrongThreads = [];
  1.1015 +      threadStore.openCursor().onsuccess = function(event) {
  1.1016 +        let threadCursor = event.target.result;
  1.1017 +        if (threadCursor) {
  1.1018 +          let threadRecord = threadCursor.value;
  1.1019 +          let participantIds = threadRecord.participantIds;
  1.1020 +          let foundInvalid = false;
  1.1021 +          for (let invalidParticipantId of invalidParticipantIds) {
  1.1022 +            if (participantIds.indexOf(invalidParticipantId) != -1) {
  1.1023 +              foundInvalid = true;
  1.1024 +              break;
  1.1025 +            }
  1.1026 +          }
  1.1027 +          if (foundInvalid) {
  1.1028 +            wrongThreads.push(threadRecord.id);
  1.1029 +            threadCursor.delete();
  1.1030 +          }
  1.1031 +          threadCursor.continue();
  1.1032 +          return;
  1.1033 +        }
  1.1034 +
  1.1035 +        if (!wrongThreads.length) {
  1.1036 +          next();
  1.1037 +          return;
  1.1038 +        }
  1.1039 +        // Use recursive function to avoid we add participant twice.
  1.1040 +        (function createUpdateThreadAndParticipant(ix) {
  1.1041 +          let threadId = wrongThreads[ix];
  1.1042 +          let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
  1.1043 +          messageStore.index("threadId").openCursor(range).onsuccess = function(event) {
  1.1044 +            let messageCursor = event.target.result;
  1.1045 +            if (!messageCursor) {
  1.1046 +              ix++;
  1.1047 +              if (ix === wrongThreads.length) {
  1.1048 +                next();
  1.1049 +                return;
  1.1050 +              }
  1.1051 +              createUpdateThreadAndParticipant(ix);
  1.1052 +              return;
  1.1053 +            }
  1.1054 +
  1.1055 +            let messageRecord = messageCursor.value;
  1.1056 +            let timestamp = messageRecord.timestamp;
  1.1057 +            let threadParticipants = [];
  1.1058 +            // Recaculate the thread participants of received message.
  1.1059 +            if (messageRecord.delivery === DELIVERY_RECEIVED ||
  1.1060 +                messageRecord.delivery === DELIVERY_NOT_DOWNLOADED) {
  1.1061 +              threadParticipants.push(messageRecord.sender);
  1.1062 +              if (messageRecord.type == "mms") {
  1.1063 +                this.fillReceivedMmsThreadParticipants(messageRecord, threadParticipants);
  1.1064 +              }
  1.1065 +            }
  1.1066 +            // Recaculate the thread participants of sent messages and error
  1.1067 +            // messages. In error sms messages, we don't have error received sms.
  1.1068 +            // In received MMS, we don't update the error to deliver field but
  1.1069 +            // deliverStatus. So we only consider sent message in DELIVERY_ERROR.
  1.1070 +            else if (messageRecord.delivery === DELIVERY_SENT ||
  1.1071 +                messageRecord.delivery === DELIVERY_ERROR) {
  1.1072 +              if (messageRecord.type == "sms") {
  1.1073 +                threadParticipants = [messageRecord.receiver];
  1.1074 +              } else if (messageRecord.type == "mms") {
  1.1075 +                threadParticipants = messageRecord.receivers;
  1.1076 +              }
  1.1077 +            }
  1.1078 +            self.findThreadRecordByParticipants(threadStore, participantStore,
  1.1079 +                                                threadParticipants, true,
  1.1080 +                                                function(threadRecord,
  1.1081 +                                                          participantIds) {
  1.1082 +              if (!participantIds) {
  1.1083 +                debug("participantIds is empty!");
  1.1084 +                return;
  1.1085 +              }
  1.1086 +
  1.1087 +              let timestamp = messageRecord.timestamp;
  1.1088 +              // Setup participantIdsIndex.
  1.1089 +              messageRecord.participantIdsIndex = [];
  1.1090 +              for each (let id in participantIds) {
  1.1091 +                messageRecord.participantIdsIndex.push([id, timestamp]);
  1.1092 +              }
  1.1093 +              if (threadRecord) {
  1.1094 +                let needsUpdate = false;
  1.1095 +
  1.1096 +                if (threadRecord.lastTimestamp <= timestamp) {
  1.1097 +                  threadRecord.lastTimestamp = timestamp;
  1.1098 +                  threadRecord.subject = messageRecord.body;
  1.1099 +                  threadRecord.lastMessageId = messageRecord.id;
  1.1100 +                  threadRecord.lastMessageType = messageRecord.type;
  1.1101 +                  needsUpdate = true;
  1.1102 +                }
  1.1103 +
  1.1104 +                if (!messageRecord.read) {
  1.1105 +                  threadRecord.unreadCount++;
  1.1106 +                  needsUpdate = true;
  1.1107 +                }
  1.1108 +
  1.1109 +                if (needsUpdate) {
  1.1110 +                  threadStore.put(threadRecord);
  1.1111 +                }
  1.1112 +                messageRecord.threadId = threadRecord.id;
  1.1113 +                messageRecord.threadIdIndex = [threadRecord.id, timestamp];
  1.1114 +                messageCursor.update(messageRecord);
  1.1115 +                messageCursor.continue();
  1.1116 +                return;
  1.1117 +              }
  1.1118 +
  1.1119 +              let threadRecord = {
  1.1120 +                participantIds: participantIds,
  1.1121 +                participantAddresses: threadParticipants,
  1.1122 +                lastMessageId: messageRecord.id,
  1.1123 +                lastTimestamp: timestamp,
  1.1124 +                subject: messageRecord.body,
  1.1125 +                unreadCount: messageRecord.read ? 0 : 1,
  1.1126 +                lastMessageType: messageRecord.type
  1.1127 +              };
  1.1128 +              threadStore.add(threadRecord).onsuccess = function(event) {
  1.1129 +                let threadId = event.target.result;
  1.1130 +                // Setup threadId & threadIdIndex.
  1.1131 +                messageRecord.threadId = threadId;
  1.1132 +                messageRecord.threadIdIndex = [threadId, timestamp];
  1.1133 +                messageCursor.update(messageRecord);
  1.1134 +                messageCursor.continue();
  1.1135 +              };
  1.1136 +            });
  1.1137 +          };
  1.1138 +        })(0);
  1.1139 +      };
  1.1140 +    };
  1.1141 +  },
  1.1142 +
  1.1143 +  /**
  1.1144 +   * Add deliveryTimestamp.
  1.1145 +   */
  1.1146 +  upgradeSchema14: function(transaction, next) {
  1.1147 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
  1.1148 +
  1.1149 +    messageStore.openCursor().onsuccess = function(event) {
  1.1150 +      let cursor = event.target.result;
  1.1151 +      if (!cursor) {
  1.1152 +        next();
  1.1153 +        return;
  1.1154 +      }
  1.1155 +
  1.1156 +      let messageRecord = cursor.value;
  1.1157 +      if (messageRecord.type == "sms") {
  1.1158 +        messageRecord.deliveryTimestamp = 0;
  1.1159 +      } else if (messageRecord.type == "mms") {
  1.1160 +        let deliveryInfo = messageRecord.deliveryInfo;
  1.1161 +        for (let i = 0; i < deliveryInfo.length; i++) {
  1.1162 +          deliveryInfo[i].deliveryTimestamp = 0;
  1.1163 +        }
  1.1164 +      }
  1.1165 +      cursor.update(messageRecord);
  1.1166 +      cursor.continue();
  1.1167 +    };
  1.1168 +  },
  1.1169 +
  1.1170 +  /**
  1.1171 +   * Add ICC ID.
  1.1172 +   */
  1.1173 +  upgradeSchema15: function(transaction, next) {
  1.1174 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
  1.1175 +    messageStore.openCursor().onsuccess = function(event) {
  1.1176 +      let cursor = event.target.result;
  1.1177 +      if (!cursor) {
  1.1178 +        next();
  1.1179 +        return;
  1.1180 +      }
  1.1181 +
  1.1182 +      let messageRecord = cursor.value;
  1.1183 +      messageRecord.iccId = null;
  1.1184 +      cursor.update(messageRecord);
  1.1185 +      cursor.continue();
  1.1186 +    };
  1.1187 +  },
  1.1188 +
  1.1189 +  /**
  1.1190 +   * Add isReadReportSent for incoming MMS.
  1.1191 +   */
  1.1192 +  upgradeSchema16: function(transaction, next) {
  1.1193 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
  1.1194 +
  1.1195 +    // Update type attributes.
  1.1196 +    messageStore.openCursor().onsuccess = function(event) {
  1.1197 +      let cursor = event.target.result;
  1.1198 +      if (!cursor) {
  1.1199 +        next();
  1.1200 +        return;
  1.1201 +      }
  1.1202 +
  1.1203 +      let messageRecord = cursor.value;
  1.1204 +      if (messageRecord.type == "mms") {
  1.1205 +        messageRecord.isReadReportSent = false;
  1.1206 +        cursor.update(messageRecord);
  1.1207 +      }
  1.1208 +      cursor.continue();
  1.1209 +    };
  1.1210 +  },
  1.1211 +
  1.1212 +  upgradeSchema17: function(transaction, next) {
  1.1213 +    let threadStore = transaction.objectStore(THREAD_STORE_NAME);
  1.1214 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
  1.1215 +
  1.1216 +    // Add 'lastMessageSubject' to each thread record.
  1.1217 +    threadStore.openCursor().onsuccess = function(event) {
  1.1218 +      let cursor = event.target.result;
  1.1219 +      if (!cursor) {
  1.1220 +        next();
  1.1221 +        return;
  1.1222 +      }
  1.1223 +
  1.1224 +      let threadRecord = cursor.value;
  1.1225 +      // We have defined 'threadRecord.subject' in upgradeSchema7(), but it
  1.1226 +      // actually means 'threadRecord.body'.  Swap the two values first.
  1.1227 +      threadRecord.body = threadRecord.subject;
  1.1228 +      delete threadRecord.subject;
  1.1229 +
  1.1230 +      // Only MMS supports subject so assign null for non-MMS one.
  1.1231 +      if (threadRecord.lastMessageType != "mms") {
  1.1232 +        threadRecord.lastMessageSubject = null;
  1.1233 +        cursor.update(threadRecord);
  1.1234 +
  1.1235 +        cursor.continue();
  1.1236 +        return;
  1.1237 +      }
  1.1238 +
  1.1239 +      messageStore.get(threadRecord.lastMessageId).onsuccess = function(event) {
  1.1240 +        let messageRecord = event.target.result;
  1.1241 +        let subject = messageRecord.headers.subject;
  1.1242 +        threadRecord.lastMessageSubject = subject || null;
  1.1243 +        cursor.update(threadRecord);
  1.1244 +
  1.1245 +        cursor.continue();
  1.1246 +      };
  1.1247 +    };
  1.1248 +  },
  1.1249 +
  1.1250 +  /**
  1.1251 +   * Add pid for incoming SMS.
  1.1252 +   */
  1.1253 +  upgradeSchema18: function(transaction, next) {
  1.1254 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
  1.1255 +
  1.1256 +    messageStore.openCursor().onsuccess = function(event) {
  1.1257 +      let cursor = event.target.result;
  1.1258 +      if (!cursor) {
  1.1259 +        next();
  1.1260 +        return;
  1.1261 +      }
  1.1262 +
  1.1263 +      let messageRecord = cursor.value;
  1.1264 +      if (messageRecord.type == "sms") {
  1.1265 +        messageRecord.pid = RIL.PDU_PID_DEFAULT;
  1.1266 +        cursor.update(messageRecord);
  1.1267 +      }
  1.1268 +      cursor.continue();
  1.1269 +    };
  1.1270 +  },
  1.1271 +
  1.1272 +  /**
  1.1273 +   * Add readStatus and readTimestamp.
  1.1274 +   */
  1.1275 +  upgradeSchema19: function(transaction, next) {
  1.1276 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
  1.1277 +    messageStore.openCursor().onsuccess = function(event) {
  1.1278 +      let cursor = event.target.result;
  1.1279 +      if (!cursor) {
  1.1280 +        next();
  1.1281 +        return;
  1.1282 +      }
  1.1283 +
  1.1284 +      let messageRecord = cursor.value;
  1.1285 +      if (messageRecord.type == "sms") {
  1.1286 +        cursor.continue();
  1.1287 +        return;
  1.1288 +      }
  1.1289 +
  1.1290 +      // We can always retrieve transaction id from
  1.1291 +      // |messageRecord.headers["x-mms-transaction-id"]|.
  1.1292 +      if (messageRecord.hasOwnProperty("transactionId")) {
  1.1293 +        delete messageRecord.transactionId;
  1.1294 +      }
  1.1295 +
  1.1296 +      // xpconnect gives "undefined" for an unassigned argument of an interface
  1.1297 +      // method.
  1.1298 +      if (messageRecord.envelopeIdIndex === "undefined") {
  1.1299 +        delete messageRecord.envelopeIdIndex;
  1.1300 +      }
  1.1301 +
  1.1302 +      // Convert some header fields that were originally decoded as BooleanValue
  1.1303 +      // to numeric enums.
  1.1304 +      for (let field of ["x-mms-cancel-status",
  1.1305 +                         "x-mms-sender-visibility",
  1.1306 +                         "x-mms-read-status"]) {
  1.1307 +        let value = messageRecord.headers[field];
  1.1308 +        if (value !== undefined) {
  1.1309 +          messageRecord.headers[field] = value ? 128 : 129;
  1.1310 +        }
  1.1311 +      }
  1.1312 +
  1.1313 +      // For all sent and received MMS messages, we have to add their
  1.1314 +      // |readStatus| and |readTimestamp| attributes in |deliveryInfo| array.
  1.1315 +      let readReportRequested =
  1.1316 +        messageRecord.headers["x-mms-read-report"] || false;
  1.1317 +      for (let element of messageRecord.deliveryInfo) {
  1.1318 +        element.readStatus = readReportRequested
  1.1319 +                           ? MMS.DOM_READ_STATUS_PENDING
  1.1320 +                           : MMS.DOM_READ_STATUS_NOT_APPLICABLE;
  1.1321 +        element.readTimestamp = 0;
  1.1322 +      }
  1.1323 +
  1.1324 +      cursor.update(messageRecord);
  1.1325 +      cursor.continue();
  1.1326 +    };
  1.1327 +  },
  1.1328 +
  1.1329 +  /**
  1.1330 +   * Add sentTimestamp.
  1.1331 +   */
  1.1332 +  upgradeSchema20: function(transaction, next) {
  1.1333 +    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
  1.1334 +    messageStore.openCursor().onsuccess = function(event) {
  1.1335 +      let cursor = event.target.result;
  1.1336 +      if (!cursor) {
  1.1337 +        next();
  1.1338 +        return;
  1.1339 +      }
  1.1340 +
  1.1341 +      let messageRecord = cursor.value;
  1.1342 +      messageRecord.sentTimestamp = 0;
  1.1343 +
  1.1344 +      // We can still have changes to assign |sentTimestamp| for the existing
  1.1345 +      // MMS message records.
  1.1346 +      if (messageRecord.type == "mms" && messageRecord.headers["date"]) {
  1.1347 +        messageRecord.sentTimestamp = messageRecord.headers["date"].getTime();
  1.1348 +      }
  1.1349 +
  1.1350 +      cursor.update(messageRecord);
  1.1351 +      cursor.continue();
  1.1352 +    };
  1.1353 +  },
  1.1354 +
  1.1355 +  /**
  1.1356 +   * Add smsSegmentStore to store uncomplete SMS segments.
  1.1357 +   */
  1.1358 +  upgradeSchema21: function(db, transaction, next) {
  1.1359 +    /**
  1.1360 +     * This smsSegmentStore is used to store uncomplete SMS segments.
  1.1361 +     * Each entry looks like this:
  1.1362 +     *
  1.1363 +     * {
  1.1364 +     *   [Common fields in SMS segment]
  1.1365 +     *   messageType: <Number>,
  1.1366 +     *   teleservice: <Number>,
  1.1367 +     *   SMSC: <String>,
  1.1368 +     *   sentTimestamp: <Number>,
  1.1369 +     *   timestamp: <Number>,
  1.1370 +     *   sender: <String>,
  1.1371 +     *   pid: <Number>,
  1.1372 +     *   encoding: <Number>,
  1.1373 +     *   messageClass: <String>,
  1.1374 +     *   iccId: <String>,
  1.1375 +     *
  1.1376 +     *   [Concatenation Info]
  1.1377 +     *   segmentRef: <Number>,
  1.1378 +     *   segmentSeq: <Number>,
  1.1379 +     *   segmentMaxSeq: <Number>,
  1.1380 +     *
  1.1381 +     *   [Application Port Info]
  1.1382 +     *   originatorPort: <Number>,
  1.1383 +     *   destinationPort: <Number>,
  1.1384 +     *
  1.1385 +     *   [MWI status]
  1.1386 +     *   mwiPresent: <Boolean>,
  1.1387 +     *   mwiDiscard: <Boolean>,
  1.1388 +     *   mwiMsgCount: <Number>,
  1.1389 +     *   mwiActive: <Boolean>,
  1.1390 +     *
  1.1391 +     *   [CDMA Cellbroadcast related fields]
  1.1392 +     *   serviceCategory: <Number>,
  1.1393 +     *   language: <String>,
  1.1394 +     *
  1.1395 +     *   [Message Body]
  1.1396 +     *   data: <Uint8Array>, (available if it's 8bit encoding)
  1.1397 +     *   body: <String>, (normal text body)
  1.1398 +     *
  1.1399 +     *   [Handy fields created by DB for concatenation]
  1.1400 +     *   id: <Number>, keypath of this objectStore.
  1.1401 +     *   hash: <String>, Use to identify the segments to the same SMS.
  1.1402 +     *   receivedSegments: <Number>,
  1.1403 +     *   segments: []
  1.1404 +     * }
  1.1405 +     *
  1.1406 +     */
  1.1407 +    let smsSegmentStore = db.createObjectStore(SMS_SEGMENT_STORE_NAME,
  1.1408 +                                               { keyPath: "id",
  1.1409 +                                                 autoIncrement: true });
  1.1410 +    smsSegmentStore.createIndex("hash", "hash", { unique: true });
  1.1411 +    next();
  1.1412 +  },
  1.1413 +
  1.1414 +  matchParsedPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
  1.1415 +    if ((parsedAddr1.internationalNumber &&
  1.1416 +         parsedAddr1.internationalNumber === parsedAddr2.internationalNumber) ||
  1.1417 +        (parsedAddr1.nationalNumber &&
  1.1418 +         parsedAddr1.nationalNumber === parsedAddr2.nationalNumber)) {
  1.1419 +      return true;
  1.1420 +    }
  1.1421 +
  1.1422 +    if (parsedAddr1.countryName != parsedAddr2.countryName) {
  1.1423 +      return false;
  1.1424 +    }
  1.1425 +
  1.1426 +    let ssPref = "dom.phonenumber.substringmatching." + parsedAddr1.countryName;
  1.1427 +    if (Services.prefs.getPrefType(ssPref) != Ci.nsIPrefBranch.PREF_INT) {
  1.1428 +      return false;
  1.1429 +    }
  1.1430 +
  1.1431 +    let val = Services.prefs.getIntPref(ssPref);
  1.1432 +    return addr1.length > val &&
  1.1433 +           addr2.length > val &&
  1.1434 +           addr1.slice(-val) === addr2.slice(-val);
  1.1435 +  },
  1.1436 +
  1.1437 +  matchPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
  1.1438 +    if (parsedAddr1 && parsedAddr2) {
  1.1439 +      return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
  1.1440 +    }
  1.1441 +
  1.1442 +    if (parsedAddr1) {
  1.1443 +      parsedAddr2 = PhoneNumberUtils.parseWithCountryName(addr2, parsedAddr1.countryName);
  1.1444 +      if (parsedAddr2) {
  1.1445 +        return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
  1.1446 +      }
  1.1447 +
  1.1448 +      return false;
  1.1449 +    }
  1.1450 +
  1.1451 +    if (parsedAddr2) {
  1.1452 +      parsedAddr1 = PhoneNumberUtils.parseWithCountryName(addr1, parsedAddr2.countryName);
  1.1453 +      if (parsedAddr1) {
  1.1454 +        return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
  1.1455 +      }
  1.1456 +    }
  1.1457 +
  1.1458 +    return false;
  1.1459 +  },
  1.1460 +
  1.1461 +  createDomMessageFromRecord: function(aMessageRecord) {
  1.1462 +    if (DEBUG) {
  1.1463 +      debug("createDomMessageFromRecord: " + JSON.stringify(aMessageRecord));
  1.1464 +    }
  1.1465 +    if (aMessageRecord.type == "sms") {
  1.1466 +      return gMobileMessageService.createSmsMessage(aMessageRecord.id,
  1.1467 +                                                    aMessageRecord.threadId,
  1.1468 +                                                    aMessageRecord.iccId,
  1.1469 +                                                    aMessageRecord.delivery,
  1.1470 +                                                    aMessageRecord.deliveryStatus,
  1.1471 +                                                    aMessageRecord.sender,
  1.1472 +                                                    aMessageRecord.receiver,
  1.1473 +                                                    aMessageRecord.body,
  1.1474 +                                                    aMessageRecord.messageClass,
  1.1475 +                                                    aMessageRecord.timestamp,
  1.1476 +                                                    aMessageRecord.sentTimestamp,
  1.1477 +                                                    aMessageRecord.deliveryTimestamp,
  1.1478 +                                                    aMessageRecord.read);
  1.1479 +    } else if (aMessageRecord.type == "mms") {
  1.1480 +      let headers = aMessageRecord["headers"];
  1.1481 +      if (DEBUG) {
  1.1482 +        debug("MMS: headers: " + JSON.stringify(headers));
  1.1483 +      }
  1.1484 +
  1.1485 +      let subject = headers["subject"];
  1.1486 +      if (subject == undefined) {
  1.1487 +        subject = "";
  1.1488 +      }
  1.1489 +
  1.1490 +      let smil = "";
  1.1491 +      let attachments = [];
  1.1492 +      let parts = aMessageRecord.parts;
  1.1493 +      if (parts) {
  1.1494 +        for (let i = 0; i < parts.length; i++) {
  1.1495 +          let part = parts[i];
  1.1496 +          if (DEBUG) {
  1.1497 +            debug("MMS: part[" + i + "]: " + JSON.stringify(part));
  1.1498 +          }
  1.1499 +          // Sometimes the part is incomplete because the device reboots when
  1.1500 +          // downloading MMS. Don't need to expose this part to the content.
  1.1501 +          if (!part) {
  1.1502 +            continue;
  1.1503 +          }
  1.1504 +
  1.1505 +          let partHeaders = part["headers"];
  1.1506 +          let partContent = part["content"];
  1.1507 +          // Don't need to make the SMIL part if it's present.
  1.1508 +          if (partHeaders["content-type"]["media"] == "application/smil") {
  1.1509 +            smil = partContent;
  1.1510 +            continue;
  1.1511 +          }
  1.1512 +          attachments.push({
  1.1513 +            "id": partHeaders["content-id"],
  1.1514 +            "location": partHeaders["content-location"],
  1.1515 +            "content": partContent
  1.1516 +          });
  1.1517 +        }
  1.1518 +      }
  1.1519 +      let expiryDate = 0;
  1.1520 +      if (headers["x-mms-expiry"] != undefined) {
  1.1521 +        expiryDate = aMessageRecord.timestamp + headers["x-mms-expiry"] * 1000;
  1.1522 +      }
  1.1523 +      let readReportRequested = headers["x-mms-read-report"] || false;
  1.1524 +      return gMobileMessageService.createMmsMessage(aMessageRecord.id,
  1.1525 +                                                    aMessageRecord.threadId,
  1.1526 +                                                    aMessageRecord.iccId,
  1.1527 +                                                    aMessageRecord.delivery,
  1.1528 +                                                    aMessageRecord.deliveryInfo,
  1.1529 +                                                    aMessageRecord.sender,
  1.1530 +                                                    aMessageRecord.receivers,
  1.1531 +                                                    aMessageRecord.timestamp,
  1.1532 +                                                    aMessageRecord.sentTimestamp,
  1.1533 +                                                    aMessageRecord.read,
  1.1534 +                                                    subject,
  1.1535 +                                                    smil,
  1.1536 +                                                    attachments,
  1.1537 +                                                    expiryDate,
  1.1538 +                                                    readReportRequested);
  1.1539 +    }
  1.1540 +  },
  1.1541 +
  1.1542 +  findParticipantRecordByAddress: function(aParticipantStore, aAddress,
  1.1543 +                                           aCreate, aCallback) {
  1.1544 +    if (DEBUG) {
  1.1545 +      debug("findParticipantRecordByAddress("
  1.1546 +            + JSON.stringify(aAddress) + ", " + aCreate + ")");
  1.1547 +    }
  1.1548 +
  1.1549 +    // Two types of input number to match here, international(+886987654321),
  1.1550 +    // and local(0987654321) types. The "nationalNumber" parsed from
  1.1551 +    // phonenumberutils will be "987654321" in this case.
  1.1552 +
  1.1553 +    // Normalize address before searching for participant record.
  1.1554 +    let normalizedAddress = PhoneNumberUtils.normalize(aAddress, false);
  1.1555 +    let allPossibleAddresses = [normalizedAddress];
  1.1556 +    let parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
  1.1557 +    if (parsedAddress && parsedAddress.internationalNumber &&
  1.1558 +        allPossibleAddresses.indexOf(parsedAddress.internationalNumber) < 0) {
  1.1559 +      // We only stores international numbers into participant store because
  1.1560 +      // the parsed national number doesn't contain country info and may
  1.1561 +      // duplicate in different country.
  1.1562 +      allPossibleAddresses.push(parsedAddress.internationalNumber);
  1.1563 +    }
  1.1564 +    if (DEBUG) {
  1.1565 +      debug("findParticipantRecordByAddress: allPossibleAddresses = " +
  1.1566 +            JSON.stringify(allPossibleAddresses));
  1.1567 +    }
  1.1568 +
  1.1569 +    // Make a copy here because we may need allPossibleAddresses again.
  1.1570 +    let needles = allPossibleAddresses.slice(0);
  1.1571 +    let request = aParticipantStore.index("addresses").get(needles.pop());
  1.1572 +    request.onsuccess = (function onsuccess(event) {
  1.1573 +      let participantRecord = event.target.result;
  1.1574 +      // 1) First try matching through "addresses" index of participant store.
  1.1575 +      //    If we're lucky, return the fetched participant record.
  1.1576 +      if (participantRecord) {
  1.1577 +        if (DEBUG) {
  1.1578 +          debug("findParticipantRecordByAddress: got "
  1.1579 +                + JSON.stringify(participantRecord));
  1.1580 +        }
  1.1581 +        aCallback(participantRecord);
  1.1582 +        return;
  1.1583 +      }
  1.1584 +
  1.1585 +      // Try next possible address again.
  1.1586 +      if (needles.length) {
  1.1587 +        let request = aParticipantStore.index("addresses").get(needles.pop());
  1.1588 +        request.onsuccess = onsuccess.bind(this);
  1.1589 +        return;
  1.1590 +      }
  1.1591 +
  1.1592 +      // 2) Traverse throught all participants and check all alias addresses.
  1.1593 +      aParticipantStore.openCursor().onsuccess = (function(event) {
  1.1594 +        let cursor = event.target.result;
  1.1595 +        if (!cursor) {
  1.1596 +          // Have traversed whole object store but still in vain.
  1.1597 +          if (!aCreate) {
  1.1598 +            aCallback(null);
  1.1599 +            return;
  1.1600 +          }
  1.1601 +
  1.1602 +          let participantRecord = { addresses: [normalizedAddress] };
  1.1603 +          let addRequest = aParticipantStore.add(participantRecord);
  1.1604 +          addRequest.onsuccess = function(event) {
  1.1605 +            participantRecord.id = event.target.result;
  1.1606 +            if (DEBUG) {
  1.1607 +              debug("findParticipantRecordByAddress: created "
  1.1608 +                    + JSON.stringify(participantRecord));
  1.1609 +            }
  1.1610 +            aCallback(participantRecord);
  1.1611 +          };
  1.1612 +          return;
  1.1613 +        }
  1.1614 +
  1.1615 +        let participantRecord = cursor.value;
  1.1616 +        for (let storedAddress of participantRecord.addresses) {
  1.1617 +          let parsedStoredAddress = PhoneNumberUtils.parseWithMCC(storedAddress, null);
  1.1618 +          let match = this.matchPhoneNumbers(normalizedAddress, parsedAddress,
  1.1619 +                                             storedAddress, parsedStoredAddress);
  1.1620 +          if (!match) {
  1.1621 +            // 3) Else we fail to match current stored participant record.
  1.1622 +            continue;
  1.1623 +          }
  1.1624 +          // Match!
  1.1625 +          if (aCreate) {
  1.1626 +            // In a READ-WRITE transaction, append one more possible address for
  1.1627 +            // this participant record.
  1.1628 +            participantRecord.addresses =
  1.1629 +              participantRecord.addresses.concat(allPossibleAddresses);
  1.1630 +            cursor.update(participantRecord);
  1.1631 +          }
  1.1632 +
  1.1633 +          if (DEBUG) {
  1.1634 +            debug("findParticipantRecordByAddress: match "
  1.1635 +                  + JSON.stringify(cursor.value));
  1.1636 +          }
  1.1637 +          aCallback(participantRecord);
  1.1638 +          return;
  1.1639 +        }
  1.1640 +
  1.1641 +        // Check next participant record if available.
  1.1642 +        cursor.continue();
  1.1643 +      }).bind(this);
  1.1644 +    }).bind(this);
  1.1645 +  },
  1.1646 +
  1.1647 +  findParticipantIdsByAddresses: function(aParticipantStore, aAddresses,
  1.1648 +                                          aCreate, aSkipNonexistent, aCallback) {
  1.1649 +    if (DEBUG) {
  1.1650 +      debug("findParticipantIdsByAddresses("
  1.1651 +            + JSON.stringify(aAddresses) + ", "
  1.1652 +            + aCreate + ", " + aSkipNonexistent + ")");
  1.1653 +    }
  1.1654 +
  1.1655 +    if (!aAddresses || !aAddresses.length) {
  1.1656 +      if (DEBUG) debug("findParticipantIdsByAddresses: returning null");
  1.1657 +      aCallback(null);
  1.1658 +      return;
  1.1659 +    }
  1.1660 +
  1.1661 +    let self = this;
  1.1662 +    (function findParticipantId(index, result) {
  1.1663 +      if (index >= aAddresses.length) {
  1.1664 +        // Sort numerically.
  1.1665 +        result.sort(function(a, b) {
  1.1666 +          return a - b;
  1.1667 +        });
  1.1668 +        if (DEBUG) debug("findParticipantIdsByAddresses: returning " + result);
  1.1669 +        aCallback(result);
  1.1670 +        return;
  1.1671 +      }
  1.1672 +
  1.1673 +      self.findParticipantRecordByAddress(aParticipantStore,
  1.1674 +                                          aAddresses[index++], aCreate,
  1.1675 +                                          function(participantRecord) {
  1.1676 +        if (!participantRecord) {
  1.1677 +          if (!aSkipNonexistent) {
  1.1678 +            if (DEBUG) debug("findParticipantIdsByAddresses: returning null");
  1.1679 +            aCallback(null);
  1.1680 +            return;
  1.1681 +          }
  1.1682 +        } else if (result.indexOf(participantRecord.id) < 0) {
  1.1683 +          result.push(participantRecord.id);
  1.1684 +        }
  1.1685 +        findParticipantId(index, result);
  1.1686 +      });
  1.1687 +    }) (0, []);
  1.1688 +  },
  1.1689 +
  1.1690 +  findThreadRecordByParticipants: function(aThreadStore, aParticipantStore,
  1.1691 +                                           aAddresses, aCreateParticipants,
  1.1692 +                                           aCallback) {
  1.1693 +    if (DEBUG) {
  1.1694 +      debug("findThreadRecordByParticipants(" + JSON.stringify(aAddresses)
  1.1695 +            + ", " + aCreateParticipants + ")");
  1.1696 +    }
  1.1697 +    this.findParticipantIdsByAddresses(aParticipantStore, aAddresses,
  1.1698 +                                       aCreateParticipants, false,
  1.1699 +                                       function(participantIds) {
  1.1700 +      if (!participantIds) {
  1.1701 +        if (DEBUG) debug("findThreadRecordByParticipants: returning null");
  1.1702 +        aCallback(null, null);
  1.1703 +        return;
  1.1704 +      }
  1.1705 +      // Find record from thread store.
  1.1706 +      let request = aThreadStore.index("participantIds").get(participantIds);
  1.1707 +      request.onsuccess = function(event) {
  1.1708 +        let threadRecord = event.target.result;
  1.1709 +        if (DEBUG) {
  1.1710 +          debug("findThreadRecordByParticipants: return "
  1.1711 +                + JSON.stringify(threadRecord));
  1.1712 +        }
  1.1713 +        aCallback(threadRecord, participantIds);
  1.1714 +      };
  1.1715 +    });
  1.1716 +  },
  1.1717 +
  1.1718 +  newTxnWithCallback: function(aCallback, aFunc, aStoreNames) {
  1.1719 +    let self = this;
  1.1720 +    this.newTxn(READ_WRITE, function(aError, aTransaction, aStores) {
  1.1721 +      let notifyResult = function(aRv, aMessageRecord) {
  1.1722 +        if (!aCallback) {
  1.1723 +          return;
  1.1724 +        }
  1.1725 +        let domMessage =
  1.1726 +          aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
  1.1727 +        aCallback.notify(aRv, domMessage);
  1.1728 +      };
  1.1729 +
  1.1730 +      if (aError) {
  1.1731 +        notifyResult(aError, null);
  1.1732 +        return;
  1.1733 +      }
  1.1734 +
  1.1735 +      let capture = {};
  1.1736 +      aTransaction.oncomplete = function(event) {
  1.1737 +        notifyResult(Cr.NS_OK, capture.messageRecord);
  1.1738 +      };
  1.1739 +      aTransaction.onabort = function(event) {
  1.1740 +        // TODO bug 832140 check event.target.errorCode
  1.1741 +        notifyResult(Cr.NS_ERROR_FAILURE, null);
  1.1742 +      };
  1.1743 +
  1.1744 +      aFunc(capture, aStores);
  1.1745 +    }, aStoreNames);
  1.1746 +  },
  1.1747 +
  1.1748 +  saveRecord: function(aMessageRecord, aAddresses, aCallback) {
  1.1749 +    if (DEBUG) debug("Going to store " + JSON.stringify(aMessageRecord));
  1.1750 +
  1.1751 +    let self = this;
  1.1752 +    this.newTxn(READ_WRITE, function(error, txn, stores) {
  1.1753 +      let notifyResult = function(aRv, aMessageRecord) {
  1.1754 +        if (!aCallback) {
  1.1755 +          return;
  1.1756 +        }
  1.1757 +        let domMessage =
  1.1758 +          aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
  1.1759 +        aCallback.notify(aRv, domMessage);
  1.1760 +      };
  1.1761 +
  1.1762 +      if (error) {
  1.1763 +        notifyResult(error, null);
  1.1764 +        return;
  1.1765 +      }
  1.1766 +
  1.1767 +      txn.oncomplete = function oncomplete(event) {
  1.1768 +        if (aMessageRecord.id > self.lastMessageId) {
  1.1769 +          self.lastMessageId = aMessageRecord.id;
  1.1770 +        }
  1.1771 +        notifyResult(Cr.NS_OK, aMessageRecord);
  1.1772 +      };
  1.1773 +      txn.onabort = function onabort(event) {
  1.1774 +        // TODO bug 832140 check event.target.errorCode
  1.1775 +        notifyResult(Cr.NS_ERROR_FAILURE, null);
  1.1776 +      };
  1.1777 +
  1.1778 +      let messageStore = stores[0];
  1.1779 +      let participantStore = stores[1];
  1.1780 +      let threadStore = stores[2];
  1.1781 +      self.replaceShortMessageOnSave(txn, messageStore, participantStore,
  1.1782 +                                     threadStore, aMessageRecord, aAddresses);
  1.1783 +    }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME, THREAD_STORE_NAME]);
  1.1784 +  },
  1.1785 +
  1.1786 +  replaceShortMessageOnSave: function(aTransaction, aMessageStore,
  1.1787 +                                      aParticipantStore, aThreadStore,
  1.1788 +                                      aMessageRecord, aAddresses) {
  1.1789 +    let isReplaceTypePid = (aMessageRecord.pid) &&
  1.1790 +                           ((aMessageRecord.pid >= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_1 &&
  1.1791 +                             aMessageRecord.pid <= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_7) ||
  1.1792 +                            aMessageRecord.pid == RIL.PDU_PID_RETURN_CALL_MESSAGE);
  1.1793 +
  1.1794 +    if (aMessageRecord.type != "sms" ||
  1.1795 +        aMessageRecord.delivery != DELIVERY_RECEIVED ||
  1.1796 +        !isReplaceTypePid) {
  1.1797 +      this.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
  1.1798 +                          aThreadStore, aMessageRecord, aAddresses);
  1.1799 +      return;
  1.1800 +    }
  1.1801 +
  1.1802 +    // 3GPP TS 23.040 subclause 9.2.3.9 "TP-Protocol-Identifier (TP-PID)":
  1.1803 +    //
  1.1804 +    //   ... the MS shall check the originating address and replace any
  1.1805 +    //   existing stored message having the same Protocol Identifier code
  1.1806 +    //   and originating address with the new short message and other
  1.1807 +    //   parameter values. If there is no message to be replaced, the MS
  1.1808 +    //   shall store the message in the normal way. ... it is recommended
  1.1809 +    //   that the SC address should not be checked by the MS."
  1.1810 +    let self = this;
  1.1811 +    this.findParticipantRecordByAddress(aParticipantStore,
  1.1812 +                                        aMessageRecord.sender, false,
  1.1813 +                                        function(participantRecord) {
  1.1814 +      if (!participantRecord) {
  1.1815 +        self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
  1.1816 +                            aThreadStore, aMessageRecord, aAddresses);
  1.1817 +        return;
  1.1818 +      }
  1.1819 +
  1.1820 +      let participantId = participantRecord.id;
  1.1821 +      let range = IDBKeyRange.bound([participantId, 0], [participantId, ""]);
  1.1822 +      let request = aMessageStore.index("participantIds").openCursor(range);
  1.1823 +      request.onsuccess = function onsuccess(event) {
  1.1824 +        let cursor = event.target.result;
  1.1825 +        if (!cursor) {
  1.1826 +          self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
  1.1827 +                              aThreadStore, aMessageRecord, aAddresses);
  1.1828 +          return;
  1.1829 +        }
  1.1830 +
  1.1831 +        // A message record with same participantId found.
  1.1832 +        // Verify matching criteria.
  1.1833 +        let foundMessageRecord = cursor.value;
  1.1834 +        if (foundMessageRecord.type != "sms" ||
  1.1835 +            foundMessageRecord.sender != aMessageRecord.sender ||
  1.1836 +            foundMessageRecord.pid != aMessageRecord.pid) {
  1.1837 +          cursor.continue();
  1.1838 +          return;
  1.1839 +        }
  1.1840 +
  1.1841 +        // Match! Now replace that found message record with current one.
  1.1842 +        aMessageRecord.id = foundMessageRecord.id;
  1.1843 +        self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
  1.1844 +                            aThreadStore, aMessageRecord, aAddresses);
  1.1845 +      };
  1.1846 +    });
  1.1847 +  },
  1.1848 +
  1.1849 +  realSaveRecord: function(aTransaction, aMessageStore, aParticipantStore,
  1.1850 +                           aThreadStore, aMessageRecord, aAddresses) {
  1.1851 +    let self = this;
  1.1852 +    this.findThreadRecordByParticipants(aThreadStore, aParticipantStore,
  1.1853 +                                        aAddresses, true,
  1.1854 +                                        function(threadRecord, participantIds) {
  1.1855 +      if (!participantIds) {
  1.1856 +        aTransaction.abort();
  1.1857 +        return;
  1.1858 +      }
  1.1859 +
  1.1860 +      let isOverriding = (aMessageRecord.id !== undefined);
  1.1861 +      if (!isOverriding) {
  1.1862 +        // |self.lastMessageId| is only updated in |txn.oncomplete|.
  1.1863 +        aMessageRecord.id = self.lastMessageId + 1;
  1.1864 +      }
  1.1865 +
  1.1866 +      let timestamp = aMessageRecord.timestamp;
  1.1867 +      let insertMessageRecord = function(threadId) {
  1.1868 +        // Setup threadId & threadIdIndex.
  1.1869 +        aMessageRecord.threadId = threadId;
  1.1870 +        aMessageRecord.threadIdIndex = [threadId, timestamp];
  1.1871 +        // Setup participantIdsIndex.
  1.1872 +        aMessageRecord.participantIdsIndex = [];
  1.1873 +        for each (let id in participantIds) {
  1.1874 +          aMessageRecord.participantIdsIndex.push([id, timestamp]);
  1.1875 +        }
  1.1876 +
  1.1877 +        if (!isOverriding) {
  1.1878 +          // Really add to message store.
  1.1879 +          aMessageStore.put(aMessageRecord);
  1.1880 +          return;
  1.1881 +        }
  1.1882 +
  1.1883 +        // If we're going to override an old message, we need to update the
  1.1884 +        // info of the original thread containing the overridden message.
  1.1885 +        // To get the original thread ID and read status of the overridden
  1.1886 +        // message record, we need to retrieve it before overriding it.
  1.1887 +        aMessageStore.get(aMessageRecord.id).onsuccess = function(event) {
  1.1888 +          let oldMessageRecord = event.target.result;
  1.1889 +          aMessageStore.put(aMessageRecord);
  1.1890 +          if (oldMessageRecord) {
  1.1891 +            self.updateThreadByMessageChange(aMessageStore,
  1.1892 +                                             aThreadStore,
  1.1893 +                                             oldMessageRecord.threadId,
  1.1894 +                                             aMessageRecord.id,
  1.1895 +                                             oldMessageRecord.read);
  1.1896 +          }
  1.1897 +        };
  1.1898 +      };
  1.1899 +
  1.1900 +      if (threadRecord) {
  1.1901 +        let needsUpdate = false;
  1.1902 +
  1.1903 +        if (threadRecord.lastTimestamp <= timestamp) {
  1.1904 +          let lastMessageSubject;
  1.1905 +          if (aMessageRecord.type == "mms") {
  1.1906 +            lastMessageSubject = aMessageRecord.headers.subject;
  1.1907 +          }
  1.1908 +          threadRecord.lastMessageSubject = lastMessageSubject || null;
  1.1909 +          threadRecord.lastTimestamp = timestamp;
  1.1910 +          threadRecord.body = aMessageRecord.body;
  1.1911 +          threadRecord.lastMessageId = aMessageRecord.id;
  1.1912 +          threadRecord.lastMessageType = aMessageRecord.type;
  1.1913 +          needsUpdate = true;
  1.1914 +        }
  1.1915 +
  1.1916 +        if (!aMessageRecord.read) {
  1.1917 +          threadRecord.unreadCount++;
  1.1918 +          needsUpdate = true;
  1.1919 +        }
  1.1920 +
  1.1921 +        if (needsUpdate) {
  1.1922 +          aThreadStore.put(threadRecord);
  1.1923 +        }
  1.1924 +
  1.1925 +        insertMessageRecord(threadRecord.id);
  1.1926 +        return;
  1.1927 +      }
  1.1928 +
  1.1929 +      let lastMessageSubject;
  1.1930 +      if (aMessageRecord.type == "mms") {
  1.1931 +        lastMessageSubject = aMessageRecord.headers.subject;
  1.1932 +      }
  1.1933 +
  1.1934 +      threadRecord = {
  1.1935 +        participantIds: participantIds,
  1.1936 +        participantAddresses: aAddresses,
  1.1937 +        lastMessageId: aMessageRecord.id,
  1.1938 +        lastTimestamp: timestamp,
  1.1939 +        lastMessageSubject: lastMessageSubject || null,
  1.1940 +        body: aMessageRecord.body,
  1.1941 +        unreadCount: aMessageRecord.read ? 0 : 1,
  1.1942 +        lastMessageType: aMessageRecord.type,
  1.1943 +      };
  1.1944 +      aThreadStore.add(threadRecord).onsuccess = function(event) {
  1.1945 +        let threadId = event.target.result;
  1.1946 +        insertMessageRecord(threadId);
  1.1947 +      };
  1.1948 +    });
  1.1949 +  },
  1.1950 +
  1.1951 +  forEachMatchedMmsDeliveryInfo: function(aDeliveryInfo, aNeedle, aCallback) {
  1.1952 +
  1.1953 +    let typedAddress = {
  1.1954 +      type: MMS.Address.resolveType(aNeedle),
  1.1955 +      address: aNeedle
  1.1956 +    };
  1.1957 +    let normalizedAddress, parsedAddress;
  1.1958 +    if (typedAddress.type === "PLMN") {
  1.1959 +      normalizedAddress = PhoneNumberUtils.normalize(aNeedle, false);
  1.1960 +      parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
  1.1961 +    }
  1.1962 +
  1.1963 +    for (let element of aDeliveryInfo) {
  1.1964 +      let typedStoredAddress = {
  1.1965 +        type: MMS.Address.resolveType(element.receiver),
  1.1966 +        address: element.receiver
  1.1967 +      };
  1.1968 +      if (typedAddress.type !== typedStoredAddress.type) {
  1.1969 +        // Not even my type.  Skip.
  1.1970 +        continue;
  1.1971 +      }
  1.1972 +
  1.1973 +      if (typedAddress.address == typedStoredAddress.address) {
  1.1974 +        // Have a direct match.
  1.1975 +        aCallback(element);
  1.1976 +        continue;
  1.1977 +      }
  1.1978 +
  1.1979 +      if (typedAddress.type !== "PLMN") {
  1.1980 +        // Address type other than "PLMN" must have direct match.  Or, skip.
  1.1981 +        continue;
  1.1982 +      }
  1.1983 +
  1.1984 +      // Both are of "PLMN" type.
  1.1985 +      let normalizedStoredAddress =
  1.1986 +        PhoneNumberUtils.normalize(element.receiver, false);
  1.1987 +      let parsedStoredAddress =
  1.1988 +        PhoneNumberUtils.parseWithMCC(normalizedStoredAddress, null);
  1.1989 +      if (this.matchPhoneNumbers(normalizedAddress, parsedAddress,
  1.1990 +                                 normalizedStoredAddress, parsedStoredAddress)) {
  1.1991 +        aCallback(element);
  1.1992 +      }
  1.1993 +    }
  1.1994 +  },
  1.1995 +
  1.1996 +  updateMessageDeliveryById: function(id, type, receiver, delivery,
  1.1997 +                                      deliveryStatus, envelopeId, callback) {
  1.1998 +    if (DEBUG) {
  1.1999 +      debug("Setting message's delivery by " + type + " = "+ id
  1.2000 +            + " receiver: " + receiver
  1.2001 +            + " delivery: " + delivery
  1.2002 +            + " deliveryStatus: " + deliveryStatus
  1.2003 +            + " envelopeId: " + envelopeId);
  1.2004 +    }
  1.2005 +
  1.2006 +    let self = this;
  1.2007 +    this.newTxnWithCallback(callback, function(aCapture, aMessageStore) {
  1.2008 +      let getRequest;
  1.2009 +      if (type === "messageId") {
  1.2010 +        getRequest = aMessageStore.get(id);
  1.2011 +      } else if (type === "envelopeId") {
  1.2012 +        getRequest = aMessageStore.index("envelopeId").get(id);
  1.2013 +      }
  1.2014 +
  1.2015 +      getRequest.onsuccess = function onsuccess(event) {
  1.2016 +        let messageRecord = event.target.result;
  1.2017 +        if (!messageRecord) {
  1.2018 +          if (DEBUG) debug("type = " + id + " is not found");
  1.2019 +          throw Cr.NS_ERROR_FAILURE;
  1.2020 +        }
  1.2021 +
  1.2022 +        let isRecordUpdated = false;
  1.2023 +
  1.2024 +        // Update |messageRecord.delivery| if needed.
  1.2025 +        if (delivery && messageRecord.delivery != delivery) {
  1.2026 +          messageRecord.delivery = delivery;
  1.2027 +          messageRecord.deliveryIndex = [delivery, messageRecord.timestamp];
  1.2028 +          isRecordUpdated = true;
  1.2029 +
  1.2030 +          // When updating an message's delivey state to 'sent', we also update
  1.2031 +          // its |sentTimestamp| by the current device timestamp to represent
  1.2032 +          // when the message is successfully sent.
  1.2033 +          if (delivery == DELIVERY_SENT) {
  1.2034 +            messageRecord.sentTimestamp = Date.now();
  1.2035 +          }
  1.2036 +        }
  1.2037 +
  1.2038 +        // Attempt to update |deliveryStatus| and |deliveryTimestamp| of:
  1.2039 +        // - the |messageRecord| for SMS.
  1.2040 +        // - the element(s) in |messageRecord.deliveryInfo| for MMS.
  1.2041 +        if (deliveryStatus) {
  1.2042 +          // A callback for updating the deliveyStatus/deliveryTimestamp of
  1.2043 +          // each target.
  1.2044 +          let updateFunc = function(aTarget) {
  1.2045 +            if (aTarget.deliveryStatus == deliveryStatus) {
  1.2046 +              return;
  1.2047 +            }
  1.2048 +
  1.2049 +            aTarget.deliveryStatus = deliveryStatus;
  1.2050 +
  1.2051 +            // Update |deliveryTimestamp| if it's successfully delivered.
  1.2052 +            if (deliveryStatus == DELIVERY_STATUS_SUCCESS) {
  1.2053 +              aTarget.deliveryTimestamp = Date.now();
  1.2054 +            }
  1.2055 +
  1.2056 +            isRecordUpdated = true;
  1.2057 +          };
  1.2058 +
  1.2059 +          if (messageRecord.type == "sms") {
  1.2060 +            updateFunc(messageRecord);
  1.2061 +          } else if (messageRecord.type == "mms") {
  1.2062 +            if (!receiver) {
  1.2063 +              // If the receiver is specified, we only need to update the
  1.2064 +              // element(s) in deliveryInfo that match the same receiver.
  1.2065 +              messageRecord.deliveryInfo.forEach(updateFunc);
  1.2066 +            } else {
  1.2067 +              self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
  1.2068 +                                                 receiver, updateFunc);
  1.2069 +            }
  1.2070 +          }
  1.2071 +        }
  1.2072 +
  1.2073 +        // Update |messageRecord.envelopeIdIndex| if needed.
  1.2074 +        if (envelopeId) {
  1.2075 +          if (messageRecord.envelopeIdIndex != envelopeId) {
  1.2076 +            messageRecord.envelopeIdIndex = envelopeId;
  1.2077 +            isRecordUpdated = true;
  1.2078 +          }
  1.2079 +        }
  1.2080 +
  1.2081 +        aCapture.messageRecord = messageRecord;
  1.2082 +        if (!isRecordUpdated) {
  1.2083 +          if (DEBUG) {
  1.2084 +            debug("The values of delivery, deliveryStatus and envelopeId " +
  1.2085 +                  "don't need to be updated.");
  1.2086 +          }
  1.2087 +          return;
  1.2088 +        }
  1.2089 +
  1.2090 +        if (DEBUG) {
  1.2091 +          debug("The delivery, deliveryStatus or envelopeId are updated.");
  1.2092 +        }
  1.2093 +        aMessageStore.put(messageRecord);
  1.2094 +      };
  1.2095 +    });
  1.2096 +  },
  1.2097 +
  1.2098 +  fillReceivedMmsThreadParticipants: function(aMessage, threadParticipants) {
  1.2099 +    let receivers = aMessage.receivers;
  1.2100 +    // If we don't want to disable the MMS grouping for receiving, we need to
  1.2101 +    // add the receivers (excluding the user's own number) to the participants
  1.2102 +    // for creating the thread. Some cases might be investigated as below:
  1.2103 +    //
  1.2104 +    // 1. receivers.length == 0
  1.2105 +    //    This usually happens when receiving an MMS notification indication
  1.2106 +    //    which doesn't carry any receivers.
  1.2107 +    // 2. receivers.length == 1
  1.2108 +    //    If the receivers contain single phone number, we don't need to
  1.2109 +    //    add it into participants because we know that number is our own.
  1.2110 +    // 3. receivers.length >= 2
  1.2111 +    //    If the receivers contain multiple phone numbers, we need to add all
  1.2112 +    //    of them but not the user's own number into participants.
  1.2113 +    if (DISABLE_MMS_GROUPING_FOR_RECEIVING || receivers.length < 2) {
  1.2114 +      return;
  1.2115 +    }
  1.2116 +    let isSuccess = false;
  1.2117 +    let slicedReceivers = receivers.slice();
  1.2118 +    if (aMessage.msisdn) {
  1.2119 +      let found = slicedReceivers.indexOf(aMessage.msisdn);
  1.2120 +      if (found !== -1) {
  1.2121 +        isSuccess = true;
  1.2122 +        slicedReceivers.splice(found, 1);
  1.2123 +      }
  1.2124 +    }
  1.2125 +
  1.2126 +    if (!isSuccess) {
  1.2127 +      // For some SIMs we cannot retrieve the vaild MSISDN (i.e. the user's
  1.2128 +      // own phone number), so we cannot correcly exclude the user's own
  1.2129 +      // number from the receivers, thus wrongly building the thread index.
  1.2130 +      if (DEBUG) debug("Error! Cannot strip out user's own phone number!");
  1.2131 +    }
  1.2132 +
  1.2133 +    threadParticipants = threadParticipants.concat(slicedReceivers);
  1.2134 +  },
  1.2135 +
  1.2136 +  updateThreadByMessageChange: function(messageStore, threadStore, threadId,
  1.2137 +                                        messageId, messageRead) {
  1.2138 +    threadStore.get(threadId).onsuccess = function(event) {
  1.2139 +      // This must exist.
  1.2140 +      let threadRecord = event.target.result;
  1.2141 +      if (DEBUG) debug("Updating thread record " + JSON.stringify(threadRecord));
  1.2142 +
  1.2143 +      if (!messageRead) {
  1.2144 +        threadRecord.unreadCount--;
  1.2145 +      }
  1.2146 +
  1.2147 +      if (threadRecord.lastMessageId == messageId) {
  1.2148 +        // Check most recent sender/receiver.
  1.2149 +        let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
  1.2150 +        let request = messageStore.index("threadId")
  1.2151 +                                  .openCursor(range, PREV);
  1.2152 +        request.onsuccess = function(event) {
  1.2153 +          let cursor = event.target.result;
  1.2154 +          if (!cursor) {
  1.2155 +            if (DEBUG) {
  1.2156 +              debug("Deleting mru entry for thread id " + threadId);
  1.2157 +            }
  1.2158 +            threadStore.delete(threadId);
  1.2159 +            return;
  1.2160 +          }
  1.2161 +
  1.2162 +          let nextMsg = cursor.value;
  1.2163 +          let lastMessageSubject;
  1.2164 +          if (nextMsg.type == "mms") {
  1.2165 +            lastMessageSubject = nextMsg.headers.subject;
  1.2166 +          }
  1.2167 +          threadRecord.lastMessageSubject = lastMessageSubject || null;
  1.2168 +          threadRecord.lastMessageId = nextMsg.id;
  1.2169 +          threadRecord.lastTimestamp = nextMsg.timestamp;
  1.2170 +          threadRecord.body = nextMsg.body;
  1.2171 +          threadRecord.lastMessageType = nextMsg.type;
  1.2172 +          if (DEBUG) {
  1.2173 +            debug("Updating mru entry: " +
  1.2174 +                  JSON.stringify(threadRecord));
  1.2175 +          }
  1.2176 +          threadStore.put(threadRecord);
  1.2177 +        };
  1.2178 +      } else if (!messageRead) {
  1.2179 +        // Shortcut, just update the unread count.
  1.2180 +        if (DEBUG) {
  1.2181 +          debug("Updating unread count for thread id " + threadId + ": " +
  1.2182 +                (threadRecord.unreadCount + 1) + " -> " +
  1.2183 +                threadRecord.unreadCount);
  1.2184 +        }
  1.2185 +        threadStore.put(threadRecord);
  1.2186 +      }
  1.2187 +    };
  1.2188 +  },
  1.2189 +
  1.2190 +  /**
  1.2191 +   * nsIRilMobileMessageDatabaseService API
  1.2192 +   */
  1.2193 +
  1.2194 +  saveReceivedMessage: function(aMessage, aCallback) {
  1.2195 +    if ((aMessage.type != "sms" && aMessage.type != "mms") ||
  1.2196 +        (aMessage.type == "sms" && (aMessage.messageClass == undefined ||
  1.2197 +                                    aMessage.sender == undefined)) ||
  1.2198 +        (aMessage.type == "mms" && (aMessage.delivery == undefined ||
  1.2199 +                                    aMessage.deliveryStatus == undefined ||
  1.2200 +                                    !Array.isArray(aMessage.receivers))) ||
  1.2201 +        aMessage.timestamp == undefined) {
  1.2202 +      if (aCallback) {
  1.2203 +        aCallback.notify(Cr.NS_ERROR_FAILURE, null);
  1.2204 +      }
  1.2205 +      return;
  1.2206 +    }
  1.2207 +
  1.2208 +    let threadParticipants;
  1.2209 +    if (aMessage.type == "mms") {
  1.2210 +      if (aMessage.headers.from) {
  1.2211 +        aMessage.sender = aMessage.headers.from.address;
  1.2212 +      } else {
  1.2213 +        aMessage.sender = "anonymous";
  1.2214 +      }
  1.2215 +
  1.2216 +      threadParticipants = [aMessage.sender];
  1.2217 +      this.fillReceivedMmsThreadParticipants(aMessage, threadParticipants);
  1.2218 +    } else { // SMS
  1.2219 +      threadParticipants = [aMessage.sender];
  1.2220 +    }
  1.2221 +
  1.2222 +    let timestamp = aMessage.timestamp;
  1.2223 +
  1.2224 +    // Adding needed indexes and extra attributes for internal use.
  1.2225 +    // threadIdIndex & participantIdsIndex are filled in saveRecord().
  1.2226 +    aMessage.readIndex = [FILTER_READ_UNREAD, timestamp];
  1.2227 +    aMessage.read = FILTER_READ_UNREAD;
  1.2228 +
  1.2229 +    // If |sentTimestamp| is not specified, use 0 as default.
  1.2230 +    if (aMessage.sentTimestamp == undefined) {
  1.2231 +      aMessage.sentTimestamp = 0;
  1.2232 +    }
  1.2233 +
  1.2234 +    if (aMessage.type == "mms") {
  1.2235 +      aMessage.transactionIdIndex = aMessage.headers["x-mms-transaction-id"];
  1.2236 +      aMessage.isReadReportSent = false;
  1.2237 +
  1.2238 +      // As a receiver, we don't need to care about the delivery status of
  1.2239 +      // others, so we put a single element with self's phone number in the
  1.2240 +      // |deliveryInfo| array.
  1.2241 +      aMessage.deliveryInfo = [{
  1.2242 +        receiver: aMessage.phoneNumber,
  1.2243 +        deliveryStatus: aMessage.deliveryStatus,
  1.2244 +        deliveryTimestamp: 0,
  1.2245 +        readStatus: MMS.DOM_READ_STATUS_NOT_APPLICABLE,
  1.2246 +        readTimestamp: 0,
  1.2247 +      }];
  1.2248 +
  1.2249 +      delete aMessage.deliveryStatus;
  1.2250 +    }
  1.2251 +
  1.2252 +    if (aMessage.type == "sms") {
  1.2253 +      aMessage.delivery = DELIVERY_RECEIVED;
  1.2254 +      aMessage.deliveryStatus = DELIVERY_STATUS_SUCCESS;
  1.2255 +      aMessage.deliveryTimestamp = 0;
  1.2256 +
  1.2257 +      if (aMessage.pid == undefined) {
  1.2258 +        aMessage.pid = RIL.PDU_PID_DEFAULT;
  1.2259 +      }
  1.2260 +    }
  1.2261 +    aMessage.deliveryIndex = [aMessage.delivery, timestamp];
  1.2262 +
  1.2263 +    this.saveRecord(aMessage, threadParticipants, aCallback);
  1.2264 +  },
  1.2265 +
  1.2266 +  saveSendingMessage: function(aMessage, aCallback) {
  1.2267 +    if ((aMessage.type != "sms" && aMessage.type != "mms") ||
  1.2268 +        (aMessage.type == "sms" && aMessage.receiver == undefined) ||
  1.2269 +        (aMessage.type == "mms" && !Array.isArray(aMessage.receivers)) ||
  1.2270 +        aMessage.deliveryStatusRequested == undefined ||
  1.2271 +        aMessage.timestamp == undefined) {
  1.2272 +      if (aCallback) {
  1.2273 +        aCallback.notify(Cr.NS_ERROR_FAILURE, null);
  1.2274 +      }
  1.2275 +      return;
  1.2276 +    }
  1.2277 +
  1.2278 +    // Set |aMessage.deliveryStatus|. Note that for MMS record
  1.2279 +    // it must be an array of strings; For SMS, it's a string.
  1.2280 +    let deliveryStatus = aMessage.deliveryStatusRequested
  1.2281 +                       ? DELIVERY_STATUS_PENDING
  1.2282 +                       : DELIVERY_STATUS_NOT_APPLICABLE;
  1.2283 +    if (aMessage.type == "sms") {
  1.2284 +      aMessage.deliveryStatus = deliveryStatus;
  1.2285 +      // If |deliveryTimestamp| is not specified, use 0 as default.
  1.2286 +      if (aMessage.deliveryTimestamp == undefined) {
  1.2287 +        aMessage.deliveryTimestamp = 0;
  1.2288 +      }
  1.2289 +    } else if (aMessage.type == "mms") {
  1.2290 +      let receivers = aMessage.receivers
  1.2291 +      if (!Array.isArray(receivers)) {
  1.2292 +        if (DEBUG) {
  1.2293 +          debug("Need receivers for MMS. Fail to save the sending message.");
  1.2294 +        }
  1.2295 +        if (aCallback) {
  1.2296 +          aCallback.notify(Cr.NS_ERROR_FAILURE, null);
  1.2297 +        }
  1.2298 +        return;
  1.2299 +      }
  1.2300 +      let readStatus = aMessage.headers["x-mms-read-report"]
  1.2301 +                     ? MMS.DOM_READ_STATUS_PENDING
  1.2302 +                     : MMS.DOM_READ_STATUS_NOT_APPLICABLE;
  1.2303 +      aMessage.deliveryInfo = [];
  1.2304 +      for (let i = 0; i < receivers.length; i++) {
  1.2305 +        aMessage.deliveryInfo.push({
  1.2306 +          receiver: receivers[i],
  1.2307 +          deliveryStatus: deliveryStatus,
  1.2308 +          deliveryTimestamp: 0,
  1.2309 +          readStatus: readStatus,
  1.2310 +          readTimestamp: 0,
  1.2311 +        });
  1.2312 +      }
  1.2313 +    }
  1.2314 +
  1.2315 +    let timestamp = aMessage.timestamp;
  1.2316 +
  1.2317 +    // Adding needed indexes and extra attributes for internal use.
  1.2318 +    // threadIdIndex & participantIdsIndex are filled in saveRecord().
  1.2319 +    aMessage.deliveryIndex = [DELIVERY_SENDING, timestamp];
  1.2320 +    aMessage.readIndex = [FILTER_READ_READ, timestamp];
  1.2321 +    aMessage.delivery = DELIVERY_SENDING;
  1.2322 +    aMessage.messageClass = MESSAGE_CLASS_NORMAL;
  1.2323 +    aMessage.read = FILTER_READ_READ;
  1.2324 +
  1.2325 +    // |sentTimestamp| is not available when the message is still sedning.
  1.2326 +    aMessage.sentTimestamp = 0;
  1.2327 +
  1.2328 +    let addresses;
  1.2329 +    if (aMessage.type == "sms") {
  1.2330 +      addresses = [aMessage.receiver];
  1.2331 +    } else if (aMessage.type == "mms") {
  1.2332 +      addresses = aMessage.receivers;
  1.2333 +    }
  1.2334 +    this.saveRecord(aMessage, addresses, aCallback);
  1.2335 +  },
  1.2336 +
  1.2337 +  setMessageDeliveryByMessageId: function(messageId, receiver, delivery,
  1.2338 +                                          deliveryStatus, envelopeId, callback) {
  1.2339 +    this.updateMessageDeliveryById(messageId, "messageId",
  1.2340 +                                   receiver, delivery, deliveryStatus,
  1.2341 +                                   envelopeId, callback);
  1.2342 +
  1.2343 +  },
  1.2344 +
  1.2345 +  setMessageDeliveryStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
  1.2346 +                                                 aDeliveryStatus, aCallback) {
  1.2347 +    this.updateMessageDeliveryById(aEnvelopeId, "envelopeId", aReceiver, null,
  1.2348 +                                   aDeliveryStatus, null, aCallback);
  1.2349 +  },
  1.2350 +
  1.2351 +  setMessageReadStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
  1.2352 +                                             aReadStatus, aCallback) {
  1.2353 +    if (DEBUG) {
  1.2354 +      debug("Setting message's read status by envelopeId = " + aEnvelopeId +
  1.2355 +            ", receiver: " + aReceiver + ", readStatus: " + aReadStatus);
  1.2356 +    }
  1.2357 +
  1.2358 +    let self = this;
  1.2359 +    this.newTxnWithCallback(aCallback, function(aCapture, aMessageStore) {
  1.2360 +      let getRequest = aMessageStore.index("envelopeId").get(aEnvelopeId);
  1.2361 +      getRequest.onsuccess = function onsuccess(event) {
  1.2362 +        let messageRecord = event.target.result;
  1.2363 +        if (!messageRecord) {
  1.2364 +          if (DEBUG) debug("envelopeId '" + aEnvelopeId + "' not found");
  1.2365 +          throw Cr.NS_ERROR_FAILURE;
  1.2366 +        }
  1.2367 +
  1.2368 +        aCapture.messageRecord = messageRecord;
  1.2369 +
  1.2370 +        let isRecordUpdated = false;
  1.2371 +        self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
  1.2372 +                                           aReceiver, function(aEntry) {
  1.2373 +          if (aEntry.readStatus == aReadStatus) {
  1.2374 +            return;
  1.2375 +          }
  1.2376 +
  1.2377 +          aEntry.readStatus = aReadStatus;
  1.2378 +          if (aReadStatus == MMS.DOM_READ_STATUS_SUCCESS) {
  1.2379 +            aEntry.readTimestamp = Date.now();
  1.2380 +          } else {
  1.2381 +            aEntry.readTimestamp = 0;
  1.2382 +          }
  1.2383 +          isRecordUpdated = true;
  1.2384 +        });
  1.2385 +
  1.2386 +        if (!isRecordUpdated) {
  1.2387 +          if (DEBUG) {
  1.2388 +            debug("The values of readStatus don't need to be updated.");
  1.2389 +          }
  1.2390 +          return;
  1.2391 +        }
  1.2392 +
  1.2393 +        if (DEBUG) {
  1.2394 +          debug("The readStatus is updated.");
  1.2395 +        }
  1.2396 +        aMessageStore.put(messageRecord);
  1.2397 +      };
  1.2398 +    });
  1.2399 +  },
  1.2400 +
  1.2401 +  getMessageRecordByTransactionId: function(aTransactionId, aCallback) {
  1.2402 +    if (DEBUG) debug("Retrieving message with transaction ID " + aTransactionId);
  1.2403 +    let self = this;
  1.2404 +    this.newTxn(READ_ONLY, function(error, txn, messageStore) {
  1.2405 +      if (error) {
  1.2406 +        if (DEBUG) debug(error);
  1.2407 +        aCallback.notify(error, null, null);
  1.2408 +        return;
  1.2409 +      }
  1.2410 +      let request = messageStore.index("transactionId").get(aTransactionId);
  1.2411 +
  1.2412 +      txn.oncomplete = function oncomplete(event) {
  1.2413 +        if (DEBUG) debug("Transaction " + txn + " completed.");
  1.2414 +        let messageRecord = request.result;
  1.2415 +        if (!messageRecord) {
  1.2416 +          if (DEBUG) debug("Transaction ID " + aTransactionId + " not found");
  1.2417 +          aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
  1.2418 +          return;
  1.2419 +        }
  1.2420 +        // In this case, we don't need a dom message. Just pass null to the
  1.2421 +        // third argument.
  1.2422 +        aCallback.notify(Cr.NS_OK, messageRecord, null);
  1.2423 +      };
  1.2424 +
  1.2425 +      txn.onerror = function onerror(event) {
  1.2426 +        if (DEBUG) {
  1.2427 +          if (event.target) {
  1.2428 +            debug("Caught error on transaction", event.target.errorCode);
  1.2429 +          }
  1.2430 +        }
  1.2431 +        aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
  1.2432 +      };
  1.2433 +    });
  1.2434 +  },
  1.2435 +
  1.2436 +  getMessageRecordById: function(aMessageId, aCallback) {
  1.2437 +    if (DEBUG) debug("Retrieving message with ID " + aMessageId);
  1.2438 +    let self = this;
  1.2439 +    this.newTxn(READ_ONLY, function(error, txn, messageStore) {
  1.2440 +      if (error) {
  1.2441 +        if (DEBUG) debug(error);
  1.2442 +        aCallback.notify(error, null, null);
  1.2443 +        return;
  1.2444 +      }
  1.2445 +      let request = messageStore.mozGetAll(aMessageId);
  1.2446 +
  1.2447 +      txn.oncomplete = function oncomplete() {
  1.2448 +        if (DEBUG) debug("Transaction " + txn + " completed.");
  1.2449 +        if (request.result.length > 1) {
  1.2450 +          if (DEBUG) debug("Got too many results for id " + aMessageId);
  1.2451 +          aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
  1.2452 +          return;
  1.2453 +        }
  1.2454 +        let messageRecord = request.result[0];
  1.2455 +        if (!messageRecord) {
  1.2456 +          if (DEBUG) debug("Message ID " + aMessageId + " not found");
  1.2457 +          aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
  1.2458 +          return;
  1.2459 +        }
  1.2460 +        if (messageRecord.id != aMessageId) {
  1.2461 +          if (DEBUG) {
  1.2462 +            debug("Requested message ID (" + aMessageId + ") is " +
  1.2463 +                  "different from the one we got");
  1.2464 +          }
  1.2465 +          aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
  1.2466 +          return;
  1.2467 +        }
  1.2468 +        let domMessage = self.createDomMessageFromRecord(messageRecord);
  1.2469 +        aCallback.notify(Cr.NS_OK, messageRecord, domMessage);
  1.2470 +      };
  1.2471 +
  1.2472 +      txn.onerror = function onerror(event) {
  1.2473 +        if (DEBUG) {
  1.2474 +          if (event.target) {
  1.2475 +            debug("Caught error on transaction", event.target.errorCode);
  1.2476 +          }
  1.2477 +        }
  1.2478 +        aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
  1.2479 +      };
  1.2480 +    });
  1.2481 +  },
  1.2482 +
  1.2483 +  translateCrErrorToMessageCallbackError: function(aCrError) {
  1.2484 +    switch(aCrError) {
  1.2485 +      case Cr.NS_OK:
  1.2486 +        return Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR;
  1.2487 +      case Cr.NS_ERROR_UNEXPECTED:
  1.2488 +        return Ci.nsIMobileMessageCallback.UNKNOWN_ERROR;
  1.2489 +      case Cr.NS_ERROR_FILE_NOT_FOUND:
  1.2490 +        return Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR;
  1.2491 +      case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE:
  1.2492 +        return Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR;
  1.2493 +      default:
  1.2494 +        return Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
  1.2495 +    }
  1.2496 +  },
  1.2497 +
  1.2498 +  saveSmsSegment: function(aSmsSegment, aCallback) {
  1.2499 +    let completeMessage = null;
  1.2500 +    this.newTxn(READ_WRITE, function(error, txn, segmentStore) {
  1.2501 +      if (error) {
  1.2502 +        if (DEBUG) debug(error);
  1.2503 +        aCallback.notify(error, null);
  1.2504 +        return;
  1.2505 +      }
  1.2506 +
  1.2507 +      txn.oncomplete = function oncomplete(event) {
  1.2508 +        if (DEBUG) debug("Transaction " + txn + " completed.");
  1.2509 +        if (completeMessage) {
  1.2510 +          // Rebuild full body
  1.2511 +          if (completeMessage.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
  1.2512 +            // Uint8Array doesn't have `concat`, so
  1.2513 +            // we have to merge all segements by hand.
  1.2514 +            let fullDataLen = 0;
  1.2515 +            for (let i = 1; i <= completeMessage.segmentMaxSeq; i++) {
  1.2516 +              fullDataLen += completeMessage.segments[i].length;
  1.2517 +            }
  1.2518 +
  1.2519 +            completeMessage.fullData = new Uint8Array(fullDataLen);
  1.2520 +            for (let d = 0, i = 1; i <= completeMessage.segmentMaxSeq; i++) {
  1.2521 +              let data = completeMessage.segments[i];
  1.2522 +              for (let j = 0; j < data.length; j++) {
  1.2523 +                completeMessage.fullData[d++] = data[j];
  1.2524 +              }
  1.2525 +            }
  1.2526 +          } else {
  1.2527 +            completeMessage.fullBody = completeMessage.segments.join("");
  1.2528 +          }
  1.2529 +
  1.2530 +          // Remove handy fields after completing the concatenation.
  1.2531 +          delete completeMessage.id;
  1.2532 +          delete completeMessage.hash;
  1.2533 +          delete completeMessage.receivedSegments;
  1.2534 +          delete completeMessage.segments;
  1.2535 +        }
  1.2536 +        aCallback.notify(Cr.NS_OK, completeMessage);
  1.2537 +      };
  1.2538 +
  1.2539 +      txn.onabort = function onerror(event) {
  1.2540 +        if (DEBUG) debug("Caught error on transaction", event.target.errorCode);
  1.2541 +        aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
  1.2542 +      };
  1.2543 +
  1.2544 +      aSmsSegment.hash = aSmsSegment.sender + ":" +
  1.2545 +                         aSmsSegment.segmentRef + ":" +
  1.2546 +                         aSmsSegment.segmentMaxSeq + ":" +
  1.2547 +                         aSmsSegment.iccId;
  1.2548 +      let seq = aSmsSegment.segmentSeq;
  1.2549 +      if (DEBUG) {
  1.2550 +        debug("Saving SMS Segment: " + aSmsSegment.hash + ", seq: " + seq);
  1.2551 +      }
  1.2552 +      let getRequest = segmentStore.index("hash").get(aSmsSegment.hash);
  1.2553 +      getRequest.onsuccess = function(event) {
  1.2554 +        let segmentRecord = event.target.result;
  1.2555 +        if (!segmentRecord) {
  1.2556 +          if (DEBUG) {
  1.2557 +            debug("Not found! Create a new record to store the segments.");
  1.2558 +          }
  1.2559 +          aSmsSegment.receivedSegments = 1;
  1.2560 +          aSmsSegment.segments = [];
  1.2561 +          if (aSmsSegment.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
  1.2562 +            aSmsSegment.segments[seq] = aSmsSegment.data;
  1.2563 +          } else {
  1.2564 +            aSmsSegment.segments[seq] = aSmsSegment.body;
  1.2565 +          }
  1.2566 +
  1.2567 +          segmentStore.add(aSmsSegment);
  1.2568 +
  1.2569 +          return;
  1.2570 +        }
  1.2571 +
  1.2572 +        if (DEBUG) {
  1.2573 +          debug("Append SMS Segment into existed message object: " + segmentRecord.id);
  1.2574 +        }
  1.2575 +
  1.2576 +        if (segmentRecord.segments[seq]) {
  1.2577 +          if (DEBUG) debug("Got duplicated segment no. " + seq);
  1.2578 +          return;
  1.2579 +        }
  1.2580 +
  1.2581 +        segmentRecord.timestamp = aSmsSegment.timestamp;
  1.2582 +
  1.2583 +        if (segmentRecord.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
  1.2584 +          segmentRecord.segments[seq] = aSmsSegment.data;
  1.2585 +        } else {
  1.2586 +          segmentRecord.segments[seq] = aSmsSegment.body;
  1.2587 +        }
  1.2588 +        segmentRecord.receivedSegments++;
  1.2589 +
  1.2590 +        // The port information is only available in 1st segment for CDMA WAP Push.
  1.2591 +        // If the segments of a WAP Push are not received in sequence
  1.2592 +        // (e.g., SMS with seq == 1 is not the 1st segment received by the device),
  1.2593 +        // we have to retrieve the port information from 1st segment and
  1.2594 +        // save it into the segmentRecord.
  1.2595 +        if (aSmsSegment.teleservice === RIL.PDU_CDMA_MSG_TELESERIVCIE_ID_WAP
  1.2596 +            && seq === 1) {
  1.2597 +          if (aSmsSegment.originatorPort) {
  1.2598 +            segmentRecord.originatorPort = aSmsSegment.originatorPort;
  1.2599 +          }
  1.2600 +
  1.2601 +          if (aSmsSegment.destinationPort) {
  1.2602 +            segmentRecord.destinationPort = aSmsSegment.destinationPort;
  1.2603 +          }
  1.2604 +        }
  1.2605 +
  1.2606 +        if (segmentRecord.receivedSegments < segmentRecord.segmentMaxSeq) {
  1.2607 +          if (DEBUG) debug("Message is incomplete.");
  1.2608 +          segmentStore.put(segmentRecord);
  1.2609 +          return;
  1.2610 +        }
  1.2611 +
  1.2612 +        completeMessage = segmentRecord;
  1.2613 +
  1.2614 +        // Delete Record in DB
  1.2615 +        segmentStore.delete(segmentRecord.id);
  1.2616 +      };
  1.2617 +    }, [SMS_SEGMENT_STORE_NAME]);
  1.2618 +  },
  1.2619 +
  1.2620 +  /**
  1.2621 +   * nsIMobileMessageDatabaseService API
  1.2622 +   */
  1.2623 +
  1.2624 +  getMessage: function(aMessageId, aRequest) {
  1.2625 +    if (DEBUG) debug("Retrieving message with ID " + aMessageId);
  1.2626 +    let self = this;
  1.2627 +    let notifyCallback = {
  1.2628 +      notify: function(aRv, aMessageRecord, aDomMessage) {
  1.2629 +        if (Cr.NS_OK == aRv) {
  1.2630 +          aRequest.notifyMessageGot(aDomMessage);
  1.2631 +          return;
  1.2632 +        }
  1.2633 +        aRequest.notifyGetMessageFailed(
  1.2634 +          self.translateCrErrorToMessageCallbackError(aRv), null);
  1.2635 +      }
  1.2636 +    };
  1.2637 +    this.getMessageRecordById(aMessageId, notifyCallback);
  1.2638 +  },
  1.2639 +
  1.2640 +  deleteMessage: function(messageIds, length, aRequest) {
  1.2641 +    if (DEBUG) debug("deleteMessage: message ids " + JSON.stringify(messageIds));
  1.2642 +    let deleted = [];
  1.2643 +    let self = this;
  1.2644 +    this.newTxn(READ_WRITE, function(error, txn, stores) {
  1.2645 +      if (error) {
  1.2646 +        if (DEBUG) debug("deleteMessage: failed to open transaction");
  1.2647 +        aRequest.notifyDeleteMessageFailed(
  1.2648 +          self.translateCrErrorToMessageCallbackError(error));
  1.2649 +        return;
  1.2650 +      }
  1.2651 +      txn.onerror = function onerror(event) {
  1.2652 +        if (DEBUG) debug("Caught error on transaction", event.target.errorCode);
  1.2653 +        //TODO look at event.target.errorCode, pick appropriate error constant
  1.2654 +        aRequest.notifyDeleteMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.2655 +      };
  1.2656 +
  1.2657 +      const messageStore = stores[0];
  1.2658 +      const threadStore = stores[1];
  1.2659 +
  1.2660 +      txn.oncomplete = function oncomplete(event) {
  1.2661 +        if (DEBUG) debug("Transaction " + txn + " completed.");
  1.2662 +        aRequest.notifyMessageDeleted(deleted, length);
  1.2663 +      };
  1.2664 +
  1.2665 +      for (let i = 0; i < length; i++) {
  1.2666 +        let messageId = messageIds[i];
  1.2667 +        deleted[i] = false;
  1.2668 +        messageStore.get(messageId).onsuccess = function(messageIndex, event) {
  1.2669 +          let messageRecord = event.target.result;
  1.2670 +          let messageId = messageIds[messageIndex];
  1.2671 +          if (messageRecord) {
  1.2672 +            if (DEBUG) debug("Deleting message id " + messageId);
  1.2673 +
  1.2674 +            // First actually delete the message.
  1.2675 +            messageStore.delete(messageId).onsuccess = function(event) {
  1.2676 +              if (DEBUG) debug("Message id " + messageId + " deleted");
  1.2677 +              deleted[messageIndex] = true;
  1.2678 +
  1.2679 +              // Then update unread count and most recent message.
  1.2680 +              self.updateThreadByMessageChange(messageStore,
  1.2681 +                                               threadStore,
  1.2682 +                                               messageRecord.threadId,
  1.2683 +                                               messageId,
  1.2684 +                                               messageRecord.read);
  1.2685 +
  1.2686 +              Services.obs.notifyObservers(null,
  1.2687 +                                           "mobile-message-deleted",
  1.2688 +                                           JSON.stringify({ id: messageId }));
  1.2689 +            };
  1.2690 +          } else if (DEBUG) {
  1.2691 +            debug("Message id " + messageId + " does not exist");
  1.2692 +          }
  1.2693 +        }.bind(null, i);
  1.2694 +      }
  1.2695 +    }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
  1.2696 +  },
  1.2697 +
  1.2698 +  createMessageCursor: function(filter, reverse, callback) {
  1.2699 +    if (DEBUG) {
  1.2700 +      debug("Creating a message cursor. Filters:" +
  1.2701 +            " startDate: " + filter.startDate +
  1.2702 +            " endDate: " + filter.endDate +
  1.2703 +            " delivery: " + filter.delivery +
  1.2704 +            " numbers: " + filter.numbers +
  1.2705 +            " read: " + filter.read +
  1.2706 +            " threadId: " + filter.threadId +
  1.2707 +            " reverse: " + reverse);
  1.2708 +    }
  1.2709 +
  1.2710 +    let cursor = new GetMessagesCursor(this, callback);
  1.2711 +
  1.2712 +    let self = this;
  1.2713 +    self.newTxn(READ_ONLY, function(error, txn, stores) {
  1.2714 +      let collector = cursor.collector;
  1.2715 +      let collect = collector.collect.bind(collector);
  1.2716 +      FilterSearcherHelper.transact(self, txn, error, filter, reverse, collect);
  1.2717 +    }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME]);
  1.2718 +
  1.2719 +    return cursor;
  1.2720 +  },
  1.2721 +
  1.2722 +  markMessageRead: function(messageId, value, aSendReadReport, aRequest) {
  1.2723 +    if (DEBUG) debug("Setting message " + messageId + " read to " + value);
  1.2724 +    let self = this;
  1.2725 +    this.newTxn(READ_WRITE, function(error, txn, stores) {
  1.2726 +      if (error) {
  1.2727 +        if (DEBUG) debug(error);
  1.2728 +        aRequest.notifyMarkMessageReadFailed(
  1.2729 +          self.translateCrErrorToMessageCallbackError(error));
  1.2730 +        return;
  1.2731 +      }
  1.2732 +
  1.2733 +      txn.onerror = function onerror(event) {
  1.2734 +        if (DEBUG) debug("Caught error on transaction ", event.target.errorCode);
  1.2735 +        aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.2736 +      };
  1.2737 +
  1.2738 +      let messageStore = stores[0];
  1.2739 +      let threadStore = stores[1];
  1.2740 +      messageStore.get(messageId).onsuccess = function onsuccess(event) {
  1.2741 +        let messageRecord = event.target.result;
  1.2742 +        if (!messageRecord) {
  1.2743 +          if (DEBUG) debug("Message ID " + messageId + " not found");
  1.2744 +          aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR);
  1.2745 +          return;
  1.2746 +        }
  1.2747 +
  1.2748 +        if (messageRecord.id != messageId) {
  1.2749 +          if (DEBUG) {
  1.2750 +            debug("Retrieve message ID (" + messageId + ") is " +
  1.2751 +                  "different from the one we got");
  1.2752 +          }
  1.2753 +          aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.UNKNOWN_ERROR);
  1.2754 +          return;
  1.2755 +        }
  1.2756 +
  1.2757 +        // If the value to be set is the same as the current message `read`
  1.2758 +        // value, we just notify successfully.
  1.2759 +        if (messageRecord.read == value) {
  1.2760 +          if (DEBUG) debug("The value of messageRecord.read is already " + value);
  1.2761 +          aRequest.notifyMessageMarkedRead(messageRecord.read);
  1.2762 +          return;
  1.2763 +        }
  1.2764 +
  1.2765 +        messageRecord.read = value ? FILTER_READ_READ : FILTER_READ_UNREAD;
  1.2766 +        messageRecord.readIndex = [messageRecord.read, messageRecord.timestamp];
  1.2767 +        let readReportMessageId, readReportTo;
  1.2768 +        if (aSendReadReport &&
  1.2769 +            messageRecord.type == "mms" &&
  1.2770 +            messageRecord.delivery == DELIVERY_RECEIVED &&
  1.2771 +            messageRecord.read == FILTER_READ_READ &&
  1.2772 +            !messageRecord.isReadReportSent) {
  1.2773 +          messageRecord.isReadReportSent = true;
  1.2774 +
  1.2775 +          let from = messageRecord.headers["from"];
  1.2776 +          readReportTo = from && from.address;
  1.2777 +          readReportMessageId = messageRecord.headers["message-id"];
  1.2778 +        }
  1.2779 +
  1.2780 +        if (DEBUG) debug("Message.read set to: " + value);
  1.2781 +        messageStore.put(messageRecord).onsuccess = function onsuccess(event) {
  1.2782 +          if (DEBUG) {
  1.2783 +            debug("Update successfully completed. Message: " +
  1.2784 +                  JSON.stringify(event.target.result));
  1.2785 +          }
  1.2786 +
  1.2787 +          // Now update the unread count.
  1.2788 +          let threadId = messageRecord.threadId;
  1.2789 +
  1.2790 +          threadStore.get(threadId).onsuccess = function(event) {
  1.2791 +            let threadRecord = event.target.result;
  1.2792 +            threadRecord.unreadCount += value ? -1 : 1;
  1.2793 +            if (DEBUG) {
  1.2794 +              debug("Updating unreadCount for thread id " + threadId + ": " +
  1.2795 +                    (value ?
  1.2796 +                     threadRecord.unreadCount + 1 :
  1.2797 +                     threadRecord.unreadCount - 1) +
  1.2798 +                     " -> " + threadRecord.unreadCount);
  1.2799 +            }
  1.2800 +            threadStore.put(threadRecord).onsuccess = function(event) {
  1.2801 +              if(readReportMessageId && readReportTo) {
  1.2802 +                gMMSService.sendReadReport(readReportMessageId,
  1.2803 +                                           readReportTo,
  1.2804 +                                           messageRecord.iccId);
  1.2805 +              }
  1.2806 +              aRequest.notifyMessageMarkedRead(messageRecord.read);
  1.2807 +            };
  1.2808 +          };
  1.2809 +        };
  1.2810 +      };
  1.2811 +    }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
  1.2812 +  },
  1.2813 +
  1.2814 +  createThreadCursor: function(callback) {
  1.2815 +    if (DEBUG) debug("Getting thread list");
  1.2816 +
  1.2817 +    let cursor = new GetThreadsCursor(this, callback);
  1.2818 +    this.newTxn(READ_ONLY, function(error, txn, threadStore) {
  1.2819 +      let collector = cursor.collector;
  1.2820 +      if (error) {
  1.2821 +        collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
  1.2822 +        return;
  1.2823 +      }
  1.2824 +      txn.onerror = function onerror(event) {
  1.2825 +        if (DEBUG) debug("Caught error on transaction ", event.target.errorCode);
  1.2826 +        collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
  1.2827 +      };
  1.2828 +      let request = threadStore.index("lastTimestamp").openKeyCursor(null, PREV);
  1.2829 +      request.onsuccess = function(event) {
  1.2830 +        let cursor = event.target.result;
  1.2831 +        if (cursor) {
  1.2832 +          if (collector.collect(txn, cursor.primaryKey, cursor.key)) {
  1.2833 +            cursor.continue();
  1.2834 +          }
  1.2835 +        } else {
  1.2836 +          collector.collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  1.2837 +        }
  1.2838 +      };
  1.2839 +    }, [THREAD_STORE_NAME]);
  1.2840 +
  1.2841 +    return cursor;
  1.2842 +  }
  1.2843 +};
  1.2844 +
  1.2845 +let FilterSearcherHelper = {
  1.2846 +
  1.2847 +  /**
  1.2848 +   * @param index
  1.2849 +   *        The name of a message store index to filter on.
  1.2850 +   * @param range
  1.2851 +   *        A IDBKeyRange.
  1.2852 +   * @param direction
  1.2853 +   *        NEXT or PREV.
  1.2854 +   * @param txn
  1.2855 +   *        Ongoing IDBTransaction context object.
  1.2856 +   * @param collect
  1.2857 +   *        Result colletor function. It takes three parameters -- txn, message
  1.2858 +   *        id, and message timestamp.
  1.2859 +   */
  1.2860 +  filterIndex: function(index, range, direction, txn, collect) {
  1.2861 +    let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
  1.2862 +    let request = messageStore.index(index).openKeyCursor(range, direction);
  1.2863 +    request.onsuccess = function onsuccess(event) {
  1.2864 +      let cursor = event.target.result;
  1.2865 +      // Once the cursor has retrieved all keys that matches its key range,
  1.2866 +      // the filter search is done.
  1.2867 +      if (cursor) {
  1.2868 +        let timestamp = Array.isArray(cursor.key) ? cursor.key[1] : cursor.key;
  1.2869 +        if (collect(txn, cursor.primaryKey, timestamp)) {
  1.2870 +          cursor.continue();
  1.2871 +        }
  1.2872 +      } else {
  1.2873 +        collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  1.2874 +      }
  1.2875 +    };
  1.2876 +    request.onerror = function onerror(event) {
  1.2877 +      if (DEBUG && event) debug("IDBRequest error " + event.target.errorCode);
  1.2878 +      collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
  1.2879 +    };
  1.2880 +  },
  1.2881 +
  1.2882 +  /**
  1.2883 +   * Explicitly fiter message on the timestamp index.
  1.2884 +   *
  1.2885 +   * @param startDate
  1.2886 +   *        Timestamp of the starting date.
  1.2887 +   * @param endDate
  1.2888 +   *        Timestamp of the ending date.
  1.2889 +   * @param direction
  1.2890 +   *        NEXT or PREV.
  1.2891 +   * @param txn
  1.2892 +   *        Ongoing IDBTransaction context object.
  1.2893 +   * @param collect
  1.2894 +   *        Result colletor function. It takes three parameters -- txn, message
  1.2895 +   *        id, and message timestamp.
  1.2896 +   */
  1.2897 +  filterTimestamp: function(startDate, endDate, direction, txn, collect) {
  1.2898 +    let range = null;
  1.2899 +    if (startDate != null && endDate != null) {
  1.2900 +      range = IDBKeyRange.bound(startDate.getTime(), endDate.getTime());
  1.2901 +    } else if (startDate != null) {
  1.2902 +      range = IDBKeyRange.lowerBound(startDate.getTime());
  1.2903 +    } else if (endDate != null) {
  1.2904 +      range = IDBKeyRange.upperBound(endDate.getTime());
  1.2905 +    }
  1.2906 +    this.filterIndex("timestamp", range, direction, txn, collect);
  1.2907 +  },
  1.2908 +
  1.2909 +  /**
  1.2910 +   * Instanciate a filtering transaction.
  1.2911 +   *
  1.2912 +   * @param mmdb
  1.2913 +   *        A MobileMessageDB.
  1.2914 +   * @param txn
  1.2915 +   *        Ongoing IDBTransaction context object.
  1.2916 +   * @param error
  1.2917 +   *        Previous error while creating the transaction.
  1.2918 +   * @param filter
  1.2919 +   *        A SmsFilter object.
  1.2920 +   * @param reverse
  1.2921 +   *        A boolean value indicating whether we should filter message in
  1.2922 +   *        reversed order.
  1.2923 +   * @param collect
  1.2924 +   *        Result colletor function. It takes three parameters -- txn, message
  1.2925 +   *        id, and message timestamp.
  1.2926 +   */
  1.2927 +  transact: function(mmdb, txn, error, filter, reverse, collect) {
  1.2928 +    if (error) {
  1.2929 +      //TODO look at event.target.errorCode, pick appropriate error constant.
  1.2930 +      if (DEBUG) debug("IDBRequest error " + error.target.errorCode);
  1.2931 +      collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
  1.2932 +      return;
  1.2933 +    }
  1.2934 +
  1.2935 +    let direction = reverse ? PREV : NEXT;
  1.2936 +
  1.2937 +    // We support filtering by date range only (see `else` block below) or by
  1.2938 +    // number/delivery status/read status with an optional date range.
  1.2939 +    if (filter.delivery == null &&
  1.2940 +        filter.numbers == null &&
  1.2941 +        filter.read == null &&
  1.2942 +        filter.threadId == null) {
  1.2943 +      // Filtering by date range only.
  1.2944 +      if (DEBUG) {
  1.2945 +        debug("filter.timestamp " + filter.startDate + ", " + filter.endDate);
  1.2946 +      }
  1.2947 +
  1.2948 +      this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
  1.2949 +                           collect);
  1.2950 +      return;
  1.2951 +    }
  1.2952 +
  1.2953 +    // Numeric 0 is smaller than any time stamp, and empty string is larger
  1.2954 +    // than all numeric values.
  1.2955 +    let startDate = 0, endDate = "";
  1.2956 +    if (filter.startDate != null) {
  1.2957 +      startDate = filter.startDate.getTime();
  1.2958 +    }
  1.2959 +    if (filter.endDate != null) {
  1.2960 +      endDate = filter.endDate.getTime();
  1.2961 +    }
  1.2962 +
  1.2963 +    let single, intersectionCollector;
  1.2964 +    {
  1.2965 +      let num = 0;
  1.2966 +      if (filter.delivery) num++;
  1.2967 +      if (filter.numbers) num++;
  1.2968 +      if (filter.read != undefined) num++;
  1.2969 +      if (filter.threadId != undefined) num++;
  1.2970 +      single = (num == 1);
  1.2971 +    }
  1.2972 +
  1.2973 +    if (!single) {
  1.2974 +      intersectionCollector = new IntersectionResultsCollector(collect, reverse);
  1.2975 +    }
  1.2976 +
  1.2977 +    // Retrieve the keys from the 'delivery' index that matches the value of
  1.2978 +    // filter.delivery.
  1.2979 +    if (filter.delivery) {
  1.2980 +      if (DEBUG) debug("filter.delivery " + filter.delivery);
  1.2981 +      let delivery = filter.delivery;
  1.2982 +      let range = IDBKeyRange.bound([delivery, startDate], [delivery, endDate]);
  1.2983 +      this.filterIndex("delivery", range, direction, txn,
  1.2984 +                       single ? collect : intersectionCollector.newContext());
  1.2985 +    }
  1.2986 +
  1.2987 +    // Retrieve the keys from the 'read' index that matches the value of
  1.2988 +    // filter.read.
  1.2989 +    if (filter.read != undefined) {
  1.2990 +      if (DEBUG) debug("filter.read " + filter.read);
  1.2991 +      let read = filter.read ? FILTER_READ_READ : FILTER_READ_UNREAD;
  1.2992 +      let range = IDBKeyRange.bound([read, startDate], [read, endDate]);
  1.2993 +      this.filterIndex("read", range, direction, txn,
  1.2994 +                       single ? collect : intersectionCollector.newContext());
  1.2995 +    }
  1.2996 +
  1.2997 +    // Retrieve the keys from the 'threadId' index that matches the value of
  1.2998 +    // filter.threadId.
  1.2999 +    if (filter.threadId != undefined) {
  1.3000 +      if (DEBUG) debug("filter.threadId " + filter.threadId);
  1.3001 +      let threadId = filter.threadId;
  1.3002 +      let range = IDBKeyRange.bound([threadId, startDate], [threadId, endDate]);
  1.3003 +      this.filterIndex("threadId", range, direction, txn,
  1.3004 +                       single ? collect : intersectionCollector.newContext());
  1.3005 +    }
  1.3006 +
  1.3007 +    // Retrieve the keys from the 'sender' and 'receiver' indexes that
  1.3008 +    // match the values of filter.numbers
  1.3009 +    if (filter.numbers) {
  1.3010 +      if (DEBUG) debug("filter.numbers " + filter.numbers.join(", "));
  1.3011 +
  1.3012 +      if (!single) {
  1.3013 +        collect = intersectionCollector.newContext();
  1.3014 +      }
  1.3015 +
  1.3016 +      let participantStore = txn.objectStore(PARTICIPANT_STORE_NAME);
  1.3017 +      mmdb.findParticipantIdsByAddresses(participantStore, filter.numbers,
  1.3018 +                                         false, true,
  1.3019 +                                         (function(participantIds) {
  1.3020 +        if (!participantIds || !participantIds.length) {
  1.3021 +          // Oops! No such participant at all.
  1.3022 +
  1.3023 +          collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  1.3024 +          return;
  1.3025 +        }
  1.3026 +
  1.3027 +        if (participantIds.length == 1) {
  1.3028 +          let id = participantIds[0];
  1.3029 +          let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
  1.3030 +          this.filterIndex("participantIds", range, direction, txn, collect);
  1.3031 +          return;
  1.3032 +        }
  1.3033 +
  1.3034 +        let unionCollector = new UnionResultsCollector(collect);
  1.3035 +
  1.3036 +        this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
  1.3037 +                             unionCollector.newTimestampContext());
  1.3038 +
  1.3039 +        for (let i = 0; i < participantIds.length; i++) {
  1.3040 +          let id = participantIds[i];
  1.3041 +          let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
  1.3042 +          this.filterIndex("participantIds", range, direction, txn,
  1.3043 +                           unionCollector.newContext());
  1.3044 +        }
  1.3045 +      }).bind(this));
  1.3046 +    }
  1.3047 +  }
  1.3048 +};
  1.3049 +
  1.3050 +function ResultsCollector() {
  1.3051 +  this.results = [];
  1.3052 +  this.done = false;
  1.3053 +}
  1.3054 +ResultsCollector.prototype = {
  1.3055 +  results: null,
  1.3056 +  requestWaiting: null,
  1.3057 +  done: null,
  1.3058 +
  1.3059 +  /**
  1.3060 +   * Queue up passed id, reply if necessary.
  1.3061 +   *
  1.3062 +   * @param txn
  1.3063 +   *        Ongoing IDBTransaction context object.
  1.3064 +   * @param id
  1.3065 +   *        COLLECT_ID_END(0) for no more results, COLLECT_ID_ERROR(-1) for
  1.3066 +   *        errors and valid otherwise.
  1.3067 +   * @param timestamp
  1.3068 +   *        We assume this function is always called in timestamp order. So
  1.3069 +   *        this parameter is actually unused.
  1.3070 +   *
  1.3071 +   * @return true if expects more. false otherwise.
  1.3072 +   */
  1.3073 +  collect: function(txn, id, timestamp) {
  1.3074 +    if (this.done) {
  1.3075 +      return false;
  1.3076 +    }
  1.3077 +
  1.3078 +    if (DEBUG) {
  1.3079 +      debug("collect: message ID = " + id);
  1.3080 +    }
  1.3081 +    if (id) {
  1.3082 +      // Queue up any id but '0' and replies later accordingly.
  1.3083 +      this.results.push(id);
  1.3084 +    }
  1.3085 +    if (id <= 0) {
  1.3086 +      // No more processing on '0' or negative values passed.
  1.3087 +      this.done = true;
  1.3088 +    }
  1.3089 +
  1.3090 +    if (!this.requestWaiting) {
  1.3091 +      if (DEBUG) debug("Cursor.continue() not called yet");
  1.3092 +      return !this.done;
  1.3093 +    }
  1.3094 +
  1.3095 +    // We assume there is only one request waiting throughout the message list
  1.3096 +    // retrieving process. So we don't bother continuing to process further
  1.3097 +    // waiting requests here. This assumption comes from DOMCursor::Continue()
  1.3098 +    // implementation.
  1.3099 +    let callback = this.requestWaiting;
  1.3100 +    this.requestWaiting = null;
  1.3101 +
  1.3102 +    this.drip(txn, callback);
  1.3103 +
  1.3104 +    return !this.done;
  1.3105 +  },
  1.3106 +
  1.3107 +  /**
  1.3108 +   * Callback right away with the first queued result entry if the filtering is
  1.3109 +   * done. Or queue up the request and callback when a new entry is available.
  1.3110 +   *
  1.3111 +   * @param callback
  1.3112 +   *        A callback function that accepts a numeric id.
  1.3113 +   */
  1.3114 +  squeeze: function(callback) {
  1.3115 +    if (this.requestWaiting) {
  1.3116 +      throw new Error("Already waiting for another request!");
  1.3117 +    }
  1.3118 +
  1.3119 +    if (!this.done) {
  1.3120 +      // Database transaction ongoing, let it reply for us so that we won't get
  1.3121 +      // blocked by the existing transaction.
  1.3122 +      this.requestWaiting = callback;
  1.3123 +      return;
  1.3124 +    }
  1.3125 +
  1.3126 +    this.drip(null, callback);
  1.3127 +  },
  1.3128 +
  1.3129 +  /**
  1.3130 +   * @param txn
  1.3131 +   *        Ongoing IDBTransaction context object or null.
  1.3132 +   * @param callback
  1.3133 +   *        A callback function that accepts a numeric id.
  1.3134 +   */
  1.3135 +  drip: function(txn, callback) {
  1.3136 +    if (!this.results.length) {
  1.3137 +      if (DEBUG) debug("No messages matching the filter criteria");
  1.3138 +      callback(txn, COLLECT_ID_END);
  1.3139 +      return;
  1.3140 +    }
  1.3141 +
  1.3142 +    if (this.results[0] < 0) {
  1.3143 +      // An previous error found. Keep the answer in results so that we can
  1.3144 +      // reply INTERNAL_ERROR for further requests.
  1.3145 +      if (DEBUG) debug("An previous error found");
  1.3146 +      callback(txn, COLLECT_ID_ERROR);
  1.3147 +      return;
  1.3148 +    }
  1.3149 +
  1.3150 +    let firstMessageId = this.results.shift();
  1.3151 +    callback(txn, firstMessageId);
  1.3152 +  }
  1.3153 +};
  1.3154 +
  1.3155 +function IntersectionResultsCollector(collect, reverse) {
  1.3156 +  this.cascadedCollect = collect;
  1.3157 +  this.reverse = reverse;
  1.3158 +  this.contexts = [];
  1.3159 +}
  1.3160 +IntersectionResultsCollector.prototype = {
  1.3161 +  cascadedCollect: null,
  1.3162 +  reverse: false,
  1.3163 +  contexts: null,
  1.3164 +
  1.3165 +  /**
  1.3166 +   * Queue up {id, timestamp} pairs, find out intersections and report to
  1.3167 +   * |cascadedCollect|. Return true if it is still possible to have another match.
  1.3168 +   */
  1.3169 +  collect: function(contextIndex, txn, id, timestamp) {
  1.3170 +    if (DEBUG) {
  1.3171 +      debug("IntersectionResultsCollector: "
  1.3172 +            + contextIndex + ", " + id + ", " + timestamp);
  1.3173 +    }
  1.3174 +
  1.3175 +    let contexts = this.contexts;
  1.3176 +    let context = contexts[contextIndex];
  1.3177 +
  1.3178 +    if (id < 0) {
  1.3179 +      // Act as no more matched records.
  1.3180 +      id = 0;
  1.3181 +    }
  1.3182 +    if (!id) {
  1.3183 +      context.done = true;
  1.3184 +
  1.3185 +      if (!context.results.length) {
  1.3186 +        // Already empty, can't have further intersection results.
  1.3187 +        return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  1.3188 +      }
  1.3189 +
  1.3190 +      for (let i = 0; i < contexts.length; i++) {
  1.3191 +        if (!contexts[i].done) {
  1.3192 +          // Don't call |this.cascadedCollect| because |context.results| might not
  1.3193 +          // be empty, so other contexts might still have a chance here.
  1.3194 +          return false;
  1.3195 +        }
  1.3196 +      }
  1.3197 +
  1.3198 +      // It was the last processing context and is no more processing.
  1.3199 +      return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  1.3200 +    }
  1.3201 +
  1.3202 +    // Search id in other existing results. If no other results has it,
  1.3203 +    // and A) the last timestamp is smaller-equal to current timestamp,
  1.3204 +    // we wait for further results; either B) record timestamp is larger
  1.3205 +    // then current timestamp or C) no more processing for a filter, then we
  1.3206 +    // drop this id because there can't be a match anymore.
  1.3207 +    for (let i = 0; i < contexts.length; i++) {
  1.3208 +      if (i == contextIndex) {
  1.3209 +        continue;
  1.3210 +      }
  1.3211 +
  1.3212 +      let ctx = contexts[i];
  1.3213 +      let results = ctx.results;
  1.3214 +      let found = false;
  1.3215 +      for (let j = 0; j < results.length; j++) {
  1.3216 +        let result = results[j];
  1.3217 +        if (result.id == id) {
  1.3218 +          found = true;
  1.3219 +          break;
  1.3220 +        }
  1.3221 +        if ((!this.reverse && (result.timestamp > timestamp)) ||
  1.3222 +            (this.reverse && (result.timestamp < timestamp))) {
  1.3223 +          // B) Cannot find a match anymore. Drop.
  1.3224 +          return true;
  1.3225 +        }
  1.3226 +      }
  1.3227 +
  1.3228 +      if (!found) {
  1.3229 +        if (ctx.done) {
  1.3230 +          // C) Cannot find a match anymore. Drop.
  1.3231 +          if (results.length) {
  1.3232 +            let lastResult = results[results.length - 1];
  1.3233 +            if ((!this.reverse && (lastResult.timestamp >= timestamp)) ||
  1.3234 +                (this.reverse && (lastResult.timestamp <= timestamp))) {
  1.3235 +              // Still have a chance to get another match. Return true.
  1.3236 +              return true;
  1.3237 +            }
  1.3238 +          }
  1.3239 +
  1.3240 +          // Impossible to find another match because all results in ctx have
  1.3241 +          // timestamps smaller than timestamp.
  1.3242 +          context.done = true;
  1.3243 +          return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  1.3244 +        }
  1.3245 +
  1.3246 +        // A) Pending.
  1.3247 +        context.results.push({
  1.3248 +          id: id,
  1.3249 +          timestamp: timestamp
  1.3250 +        });
  1.3251 +        return true;
  1.3252 +      }
  1.3253 +    }
  1.3254 +
  1.3255 +    // Now id is found in all other results. Report it.
  1.3256 +    return this.cascadedCollect(txn, id, timestamp);
  1.3257 +  },
  1.3258 +
  1.3259 +  newContext: function() {
  1.3260 +    let contextIndex = this.contexts.length;
  1.3261 +    this.contexts.push({
  1.3262 +      results: [],
  1.3263 +      done: false
  1.3264 +    });
  1.3265 +    return this.collect.bind(this, contextIndex);
  1.3266 +  }
  1.3267 +};
  1.3268 +
  1.3269 +function UnionResultsCollector(collect) {
  1.3270 +  this.cascadedCollect = collect;
  1.3271 +  this.contexts = [{
  1.3272 +    // Timestamp.
  1.3273 +    processing: 1,
  1.3274 +    results: []
  1.3275 +  }, {
  1.3276 +    processing: 0,
  1.3277 +    results: []
  1.3278 +  }];
  1.3279 +}
  1.3280 +UnionResultsCollector.prototype = {
  1.3281 +  cascadedCollect: null,
  1.3282 +  contexts: null,
  1.3283 +
  1.3284 +  collect: function(contextIndex, txn, id, timestamp) {
  1.3285 +    if (DEBUG) {
  1.3286 +      debug("UnionResultsCollector: "
  1.3287 +            + contextIndex + ", " + id + ", " + timestamp);
  1.3288 +    }
  1.3289 +
  1.3290 +    let contexts = this.contexts;
  1.3291 +    let context = contexts[contextIndex];
  1.3292 +
  1.3293 +    if (id < 0) {
  1.3294 +      // Act as no more matched records.
  1.3295 +      id = 0;
  1.3296 +    }
  1.3297 +    if (id) {
  1.3298 +      if (!contextIndex) {
  1.3299 +        // Timestamp.
  1.3300 +        context.results.push({
  1.3301 +          id: id,
  1.3302 +          timestamp: timestamp
  1.3303 +        });
  1.3304 +      } else {
  1.3305 +        context.results.push(id);
  1.3306 +      }
  1.3307 +      return true;
  1.3308 +    }
  1.3309 +
  1.3310 +    context.processing -= 1;
  1.3311 +    if (contexts[0].processing || contexts[1].processing) {
  1.3312 +      // At least one queue is still processing, but we got here because
  1.3313 +      // current cursor gives 0 as id meaning no more messages are
  1.3314 +      // available. Return false here to stop further cursor.continue() calls.
  1.3315 +      return false;
  1.3316 +    }
  1.3317 +
  1.3318 +    let tres = contexts[0].results;
  1.3319 +    let qres = contexts[1].results;
  1.3320 +    tres = tres.filter(function(element) {
  1.3321 +      return qres.indexOf(element.id) != -1;
  1.3322 +    });
  1.3323 +
  1.3324 +    for (let i = 0; i < tres.length; i++) {
  1.3325 +      this.cascadedCollect(txn, tres[i].id, tres[i].timestamp);
  1.3326 +    }
  1.3327 +    this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  1.3328 +
  1.3329 +    return false;
  1.3330 +  },
  1.3331 +
  1.3332 +  newTimestampContext: function() {
  1.3333 +    return this.collect.bind(this, 0);
  1.3334 +  },
  1.3335 +
  1.3336 +  newContext: function() {
  1.3337 +    this.contexts[1].processing++;
  1.3338 +    return this.collect.bind(this, 1);
  1.3339 +  }
  1.3340 +};
  1.3341 +
  1.3342 +function GetMessagesCursor(mmdb, callback) {
  1.3343 +  this.mmdb = mmdb;
  1.3344 +  this.callback = callback;
  1.3345 +  this.collector = new ResultsCollector();
  1.3346 +
  1.3347 +  this.handleContinue(); // Trigger first run.
  1.3348 +}
  1.3349 +GetMessagesCursor.prototype = {
  1.3350 +  classID: RIL_GETMESSAGESCURSOR_CID,
  1.3351 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),
  1.3352 +
  1.3353 +  mmdb: null,
  1.3354 +  callback: null,
  1.3355 +  collector: null,
  1.3356 +
  1.3357 +  getMessageTxn: function(messageStore, messageId) {
  1.3358 +    if (DEBUG) debug ("Fetching message " + messageId);
  1.3359 +
  1.3360 +    let getRequest = messageStore.get(messageId);
  1.3361 +    let self = this;
  1.3362 +    getRequest.onsuccess = function onsuccess(event) {
  1.3363 +      if (DEBUG) {
  1.3364 +        debug("notifyNextMessageInListGot - messageId: " + messageId);
  1.3365 +      }
  1.3366 +      let domMessage =
  1.3367 +        self.mmdb.createDomMessageFromRecord(event.target.result);
  1.3368 +      self.callback.notifyCursorResult(domMessage);
  1.3369 +    };
  1.3370 +    getRequest.onerror = function onerror(event) {
  1.3371 +      if (DEBUG) {
  1.3372 +        debug("notifyCursorError - messageId: " + messageId);
  1.3373 +      }
  1.3374 +      self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.3375 +    };
  1.3376 +  },
  1.3377 +
  1.3378 +  notify: function(txn, messageId) {
  1.3379 +    if (!messageId) {
  1.3380 +      this.callback.notifyCursorDone();
  1.3381 +      return;
  1.3382 +    }
  1.3383 +
  1.3384 +    if (messageId < 0) {
  1.3385 +      this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.3386 +      return;
  1.3387 +    }
  1.3388 +
  1.3389 +    // When filter transaction is not yet completed, we're called with current
  1.3390 +    // ongoing transaction object.
  1.3391 +    if (txn) {
  1.3392 +      let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
  1.3393 +      this.getMessageTxn(messageStore, messageId);
  1.3394 +      return;
  1.3395 +    }
  1.3396 +
  1.3397 +    // Or, we have to open another transaction ourselves.
  1.3398 +    let self = this;
  1.3399 +    this.mmdb.newTxn(READ_ONLY, function(error, txn, messageStore) {
  1.3400 +      if (error) {
  1.3401 +        self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.3402 +        return;
  1.3403 +      }
  1.3404 +      self.getMessageTxn(messageStore, messageId);
  1.3405 +    }, [MESSAGE_STORE_NAME]);
  1.3406 +  },
  1.3407 +
  1.3408 +  // nsICursorContinueCallback
  1.3409 +
  1.3410 +  handleContinue: function() {
  1.3411 +    if (DEBUG) debug("Getting next message in list");
  1.3412 +    this.collector.squeeze(this.notify.bind(this));
  1.3413 +  }
  1.3414 +};
  1.3415 +
  1.3416 +function GetThreadsCursor(mmdb, callback) {
  1.3417 +  this.mmdb = mmdb;
  1.3418 +  this.callback = callback;
  1.3419 +  this.collector = new ResultsCollector();
  1.3420 +
  1.3421 +  this.handleContinue(); // Trigger first run.
  1.3422 +}
  1.3423 +GetThreadsCursor.prototype = {
  1.3424 +  classID: RIL_GETTHREADSCURSOR_CID,
  1.3425 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),
  1.3426 +
  1.3427 +  mmdb: null,
  1.3428 +  callback: null,
  1.3429 +  collector: null,
  1.3430 +
  1.3431 +  getThreadTxn: function(threadStore, threadId) {
  1.3432 +    if (DEBUG) debug ("Fetching thread " + threadId);
  1.3433 +
  1.3434 +    let getRequest = threadStore.get(threadId);
  1.3435 +    let self = this;
  1.3436 +    getRequest.onsuccess = function onsuccess(event) {
  1.3437 +      let threadRecord = event.target.result;
  1.3438 +      if (DEBUG) {
  1.3439 +        debug("notifyCursorResult: " + JSON.stringify(threadRecord));
  1.3440 +      }
  1.3441 +      let thread =
  1.3442 +        gMobileMessageService.createThread(threadRecord.id,
  1.3443 +                                           threadRecord.participantAddresses,
  1.3444 +                                           threadRecord.lastTimestamp,
  1.3445 +                                           threadRecord.lastMessageSubject || "",
  1.3446 +                                           threadRecord.body,
  1.3447 +                                           threadRecord.unreadCount,
  1.3448 +                                           threadRecord.lastMessageType);
  1.3449 +      self.callback.notifyCursorResult(thread);
  1.3450 +    };
  1.3451 +    getRequest.onerror = function onerror(event) {
  1.3452 +      if (DEBUG) {
  1.3453 +        debug("notifyCursorError - threadId: " + threadId);
  1.3454 +      }
  1.3455 +      self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.3456 +    };
  1.3457 +  },
  1.3458 +
  1.3459 +  notify: function(txn, threadId) {
  1.3460 +    if (!threadId) {
  1.3461 +      this.callback.notifyCursorDone();
  1.3462 +      return;
  1.3463 +    }
  1.3464 +
  1.3465 +    if (threadId < 0) {
  1.3466 +      this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.3467 +      return;
  1.3468 +    }
  1.3469 +
  1.3470 +    // When filter transaction is not yet completed, we're called with current
  1.3471 +    // ongoing transaction object.
  1.3472 +    if (txn) {
  1.3473 +      let threadStore = txn.objectStore(THREAD_STORE_NAME);
  1.3474 +      this.getThreadTxn(threadStore, threadId);
  1.3475 +      return;
  1.3476 +    }
  1.3477 +
  1.3478 +    // Or, we have to open another transaction ourselves.
  1.3479 +    let self = this;
  1.3480 +    this.mmdb.newTxn(READ_ONLY, function(error, txn, threadStore) {
  1.3481 +      if (error) {
  1.3482 +        self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  1.3483 +        return;
  1.3484 +      }
  1.3485 +      self.getThreadTxn(threadStore, threadId);
  1.3486 +    }, [THREAD_STORE_NAME]);
  1.3487 +  },
  1.3488 +
  1.3489 +  // nsICursorContinueCallback
  1.3490 +
  1.3491 +  handleContinue: function() {
  1.3492 +    if (DEBUG) debug("Getting next thread in list");
  1.3493 +    this.collector.squeeze(this.notify.bind(this));
  1.3494 +  }
  1.3495 +}
  1.3496 +
  1.3497 +this.EXPORTED_SYMBOLS = [
  1.3498 +  'MobileMessageDB'
  1.3499 +];
  1.3500 +
  1.3501 +function debug() {
  1.3502 +  dump("MobileMessageDB: " + Array.slice(arguments).join(" ") + "\n");
  1.3503 +}

mercurial