dom/mobilemessage/gonk/MobileMessageDB.jsm
author Bevis Tseng <btseng@mozilla.com>
Tue, 03 Mar 2015 15:41:15 +0800
changeset 237442 d1c2dd2f5d4f6ed7f89edaad493e9d27eeb39ef5
parent 220453 15894c19ddd65dcf87fd9927b8cb3d75015da19c
permissions -rw-r--r--
Bug 1138757 - Part 1: Fix the logic of checking invalid port in CDMA WAP Push. r=echen, a=bajaj

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
Cu.importGlobalProperties(["indexedDB"]);

XPCOMUtils.defineLazyGetter(this, "RIL", function () {
  let obj = {};
  Cu.import("resource://gre/modules/ril_consts.js", obj);
  return obj;
});

const RIL_GETMESSAGESCURSOR_CID =
  Components.ID("{484d1ad8-840e-4782-9dc4-9ebc4d914937}");
const RIL_GETTHREADSCURSOR_CID =
  Components.ID("{95ee7c3e-d6f2-4ec4-ade5-0c453c036d35}");

const DEBUG = false;
const DISABLE_MMS_GROUPING_FOR_RECEIVING = true;

const DB_VERSION = 23;

const MESSAGE_STORE_NAME = "sms";
const THREAD_STORE_NAME = "thread";
const PARTICIPANT_STORE_NAME = "participant";
const MOST_RECENT_STORE_NAME = "most-recent";
const SMS_SEGMENT_STORE_NAME = "sms-segment";

const DELIVERY_SENDING = "sending";
const DELIVERY_SENT = "sent";
const DELIVERY_RECEIVED = "received";
const DELIVERY_NOT_DOWNLOADED = "not-downloaded";
const DELIVERY_ERROR = "error";

const DELIVERY_STATUS_NOT_APPLICABLE = "not-applicable";
const DELIVERY_STATUS_SUCCESS = "success";
const DELIVERY_STATUS_PENDING = "pending";
const DELIVERY_STATUS_ERROR = "error";

const MESSAGE_CLASS_NORMAL = "normal";

const FILTER_TIMESTAMP = "timestamp";
const FILTER_NUMBERS = "numbers";
const FILTER_DELIVERY = "delivery";
const FILTER_READ = "read";

// We canĀ“t create an IDBKeyCursor with a boolean, so we need to use numbers
// instead.
const FILTER_READ_UNREAD = 0;
const FILTER_READ_READ = 1;

const READ_ONLY = "readonly";
const READ_WRITE = "readwrite";
const PREV = "prev";
const NEXT = "next";

const COLLECT_ID_END = 0;
const COLLECT_ID_ERROR = -1;
const COLLECT_TIMESTAMP_UNUSED = 0;

// Default value for integer preference "dom.sms.maxReadAheadEntries".
const DEFAULT_READ_AHEAD_ENTRIES = 7;

XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService",
                                   "@mozilla.org/mobilemessage/mobilemessageservice;1",
                                   "nsIMobileMessageService");

XPCOMUtils.defineLazyServiceGetter(this, "gMMSService",
                                   "@mozilla.org/mms/gonkmmsservice;1",
                                   "nsIMmsService");

XPCOMUtils.defineLazyGetter(this, "MMS", function() {
  let MMS = {};
  Cu.import("resource://gre/modules/MmsPduHelper.jsm", MMS);
  return MMS;
});

/**
 * MobileMessageDB
 */
this.MobileMessageDB = function() {};
MobileMessageDB.prototype = {
  dbName: null,
  dbVersion: null,

  /**
   * Cache the DB here.
   */
  db: null,

  /**
   * Last sms/mms object store key value in the database.
   */
  lastMessageId: 0,

  /**
   * Prepare the database. This may include opening the database and upgrading
   * it to the latest schema version.
   *
   * @param callback
   *        Function that takes an error and db argument. It is called when
   *        the database is ready to use or if an error occurs while preparing
   *        the database.
   *
   * @return (via callback) a database ready for use.
   */
  ensureDB: function(callback) {
    if (this.db) {
      if (DEBUG) debug("ensureDB: already have a database, returning early.");
      callback(null, this.db);
      return;
    }

    let self = this;
    function gotDB(db) {
      self.db = db;
      callback(null, db);
    }

    let request = indexedDB.open(this.dbName, this.dbVersion);
    request.onsuccess = function(event) {
      if (DEBUG) debug("Opened database:", self.dbName, self.dbVersion);
      gotDB(event.target.result);
    };
    request.onupgradeneeded = function(event) {
      if (DEBUG) {
        debug("Database needs upgrade:", self.dbName,
              event.oldVersion, event.newVersion);
        debug("Correct new database version:", event.newVersion == self.dbVersion);
      }

      let db = event.target.result;

      let currentVersion = event.oldVersion;

      function update(currentVersion) {
        if (currentVersion >= self.dbVersion) {
          if (DEBUG) debug("Upgrade finished.");
          return;
        }

        let next = update.bind(self, currentVersion + 1);
        switch (currentVersion) {
          case 0:
            if (DEBUG) debug("New database");
            self.createSchema(db, next);
            break;
          case 1:
            if (DEBUG) debug("Upgrade to version 2. Including `read` index");
            self.upgradeSchema(event.target.transaction, next);
            break;
          case 2:
            if (DEBUG) debug("Upgrade to version 3. Fix existing entries.");
            self.upgradeSchema2(event.target.transaction, next);
            break;
          case 3:
            if (DEBUG) debug("Upgrade to version 4. Add quick threads view.");
            self.upgradeSchema3(db, event.target.transaction, next);
            break;
          case 4:
            if (DEBUG) debug("Upgrade to version 5. Populate quick threads view.");
            self.upgradeSchema4(event.target.transaction, next);
            break;
          case 5:
            if (DEBUG) debug("Upgrade to version 6. Use PhonenumberJS.");
            self.upgradeSchema5(event.target.transaction, next);
            break;
          case 6:
            if (DEBUG) debug("Upgrade to version 7. Use multiple entry indexes.");
            self.upgradeSchema6(event.target.transaction, next);
            break;
          case 7:
            if (DEBUG) debug("Upgrade to version 8. Add participant/thread stores.");
            self.upgradeSchema7(db, event.target.transaction, next);
            break;
          case 8:
            if (DEBUG) debug("Upgrade to version 9. Add transactionId index for incoming MMS.");
            self.upgradeSchema8(event.target.transaction, next);
            break;
          case 9:
            if (DEBUG) debug("Upgrade to version 10. Upgrade type if it's not existing.");
            self.upgradeSchema9(event.target.transaction, next);
            break;
          case 10:
            if (DEBUG) debug("Upgrade to version 11. Add last message type into threadRecord.");
            self.upgradeSchema10(event.target.transaction, next);
            break;
          case 11:
            if (DEBUG) debug("Upgrade to version 12. Add envelopeId index for outgoing MMS.");
            self.upgradeSchema11(event.target.transaction, next);
            break;
          case 12:
            if (DEBUG) debug("Upgrade to version 13. Replaced deliveryStatus by deliveryInfo.");
            self.upgradeSchema12(event.target.transaction, next);
            break;
          case 13:
            if (DEBUG) debug("Upgrade to version 14. Fix the wrong participants.");
            // A workaround to check if we need to re-upgrade the DB schema 12. We missed this
            // because we didn't properly uplift that logic to b2g_v1.2 and errors could happen
            // when migrating b2g_v1.2 to b2g_v1.3. Please see Bug 960741 for details.
            self.needReUpgradeSchema12(event.target.transaction, function(isNeeded) {
              if (isNeeded) {
                self.upgradeSchema12(event.target.transaction, function() {
                  self.upgradeSchema13(event.target.transaction, next);
                });
              } else {
                self.upgradeSchema13(event.target.transaction, next);
              }
            });
            break;
          case 14:
            if (DEBUG) debug("Upgrade to version 15. Add deliveryTimestamp.");
            self.upgradeSchema14(event.target.transaction, next);
            break;
          case 15:
            if (DEBUG) debug("Upgrade to version 16. Add ICC ID for each message.");
            self.upgradeSchema15(event.target.transaction, next);
            break;
          case 16:
            if (DEBUG) debug("Upgrade to version 17. Add isReadReportSent for incoming MMS.");
            self.upgradeSchema16(event.target.transaction, next);
            break;
          case 17:
            if (DEBUG) debug("Upgrade to version 18. Add last message subject into threadRecord.");
            self.upgradeSchema17(event.target.transaction, next);
            break;
          case 18:
            if (DEBUG) debug("Upgrade to version 19. Add pid for incoming SMS.");
            self.upgradeSchema18(event.target.transaction, next);
            break;
          case 19:
            if (DEBUG) debug("Upgrade to version 20. Add readStatus and readTimestamp.");
            self.upgradeSchema19(event.target.transaction, next);
            break;
          case 20:
            if (DEBUG) debug("Upgrade to version 21. Add sentTimestamp.");
            self.upgradeSchema20(event.target.transaction, next);
            break;
          case 21:
            if (DEBUG) debug("Upgrade to version 22. Add sms-segment store.");
            self.upgradeSchema21(db, event.target.transaction, next);
            break;
          case 22:
            if (DEBUG) debug("Upgrade to version 23. Add type information to receivers and to");
            self.upgradeSchema22(event.target.transaction, next);
            break;
          default:
            event.target.transaction.abort();
            if (DEBUG) debug("unexpected db version: " + event.oldVersion);
            callback(Cr.NS_ERROR_FAILURE, null);
            break;
        }
      }

      update(currentVersion);
    };
    request.onerror = function(event) {
      // TODO look at event.target.Code and change error constant accordingly.
      if (DEBUG) debug("Error opening database!");
      callback(Cr.NS_ERROR_FAILURE, null);
    };
    request.onblocked = function(event) {
      if (DEBUG) debug("Opening database request is blocked.");
      callback(Cr.NS_ERROR_FAILURE, null);
    };
  },

  /**
   * Start a new transaction.
   *
   * @param txn_type
   *        Type of transaction (e.g. READ_WRITE)
   * @param callback
   *        Function to call when the transaction is available. It will
   *        be invoked with the transaction and opened object stores.
   * @param storeNames
   *        Names of the stores to open.
   */
  newTxn: function(txn_type, callback, storeNames) {
    if (!storeNames) {
      storeNames = [MESSAGE_STORE_NAME];
    }
    if (DEBUG) debug("Opening transaction for object stores: " + storeNames);
    let self = this;
    this.ensureDB(function(error, db) {
      if (error) {
        if (DEBUG) debug("Could not open database: " + error);
        callback(error);
        return;
      }
      let txn = db.transaction(storeNames, txn_type);
      if (DEBUG) debug("Started transaction " + txn + " of type " + txn_type);
      if (DEBUG) {
        txn.oncomplete = function(event) {
          debug("Transaction " + txn + " completed.");
        };
        txn.onerror = function(event) {
          // TODO check event.target.error.name and show an appropiate error
          // message according to it.
          debug("Error occurred during transaction: " + event.target.error.name);
        };
      }
      let stores;
      if (storeNames.length == 1) {
        if (DEBUG) debug("Retrieving object store " + storeNames[0]);
        stores = txn.objectStore(storeNames[0]);
      } else {
        stores = [];
        for each (let storeName in storeNames) {
          if (DEBUG) debug("Retrieving object store " + storeName);
          stores.push(txn.objectStore(storeName));
        }
      }
      callback(null, txn, stores);
    });
  },

  /**
   * Initialize this MobileMessageDB.
   *
   * @param aDbName
   *        A string name for that database.
   * @param aDbVersion
   *        The version that mmdb should upgrade to. 0 for the lastest version.
   * @param aCallback
   *        A function when either the initialization transaction is completed
   *        or any error occurs.  Should take only one argument -- null when
   *        initialized with success or the error object otherwise.
   */
  init: function(aDbName, aDbVersion, aCallback) {
    this.dbName = aDbName;
    this.dbVersion = aDbVersion || DB_VERSION;

    let self = this;
    this.newTxn(READ_ONLY, function(error, txn, messageStore){
      if (error) {
        if (aCallback) {
          aCallback(error);
        }
        return;
      }

      if (aCallback) {
        txn.oncomplete = function() {
          aCallback(null);
        };
      }

      // In order to get the highest key value, we open a key cursor in reverse
      // order and get only the first pointed value.
      let request = messageStore.openCursor(null, PREV);
      request.onsuccess = function(event) {
        let cursor = event.target.result;
        if (!cursor) {
          if (DEBUG) {
            debug("Could not get the last key from mobile message database. " +
                  "Probably empty database");
          }
          return;
        }
        self.lastMessageId = cursor.key || 0;
        if (DEBUG) debug("Last assigned message ID was " + self.lastMessageId);
      };
      request.onerror = function(event) {
        if (DEBUG) {
          debug("Could not get the last key from mobile message database " +
                event.target.error.name);
        }
      };
    });
  },

  close: function() {
    if (!this.db) {
      return;
    }

    this.db.close();
    this.db = null;
    this.lastMessageId = 0;
  },

  /**
   * Sometimes user might reboot or remove battery while sending/receiving
   * message. This is function set the status of message records to error.
   */
  updatePendingTransactionToError: function(aError) {
    if (aError) {
      return;
    }

    this.newTxn(READ_WRITE, function(error, txn, messageStore) {
      if (error) {
        return;
      }

      let deliveryIndex = messageStore.index("delivery");

      // Set all 'delivery: sending' records to 'delivery: error' and 'deliveryStatus:
      // error'.
      let keyRange = IDBKeyRange.bound([DELIVERY_SENDING, 0], [DELIVERY_SENDING, ""]);
      let cursorRequestSending = deliveryIndex.openCursor(keyRange);
      cursorRequestSending.onsuccess = function(event) {
        let messageCursor = event.target.result;
        if (!messageCursor) {
          return;
        }

        let messageRecord = messageCursor.value;

        // Set delivery to error.
        messageRecord.delivery = DELIVERY_ERROR;
        messageRecord.deliveryIndex = [DELIVERY_ERROR, messageRecord.timestamp];

        if (messageRecord.type == "sms") {
          messageRecord.deliveryStatus = DELIVERY_STATUS_ERROR;
        } else {
          // Set delivery status to error.
          for (let i = 0; i < messageRecord.deliveryInfo.length; i++) {
            messageRecord.deliveryInfo[i].deliveryStatus = DELIVERY_STATUS_ERROR;
          }
        }

        messageCursor.update(messageRecord);
        messageCursor.continue();
      };

      // Set all 'delivery: not-downloaded' and 'deliveryStatus: pending'
      // records to 'delivery: not-downloaded' and 'deliveryStatus: error'.
      keyRange = IDBKeyRange.bound([DELIVERY_NOT_DOWNLOADED, 0], [DELIVERY_NOT_DOWNLOADED, ""]);
      let cursorRequestNotDownloaded = deliveryIndex.openCursor(keyRange);
      cursorRequestNotDownloaded.onsuccess = function(event) {
        let messageCursor = event.target.result;
        if (!messageCursor) {
          return;
        }

        let messageRecord = messageCursor.value;

        // We have no "not-downloaded" SMS messages.
        if (messageRecord.type == "sms") {
          messageCursor.continue();
          return;
        }

        // Set delivery status to error.
        let deliveryInfo = messageRecord.deliveryInfo;
        if (deliveryInfo.length == 1 &&
            deliveryInfo[0].deliveryStatus == DELIVERY_STATUS_PENDING) {
          deliveryInfo[0].deliveryStatus = DELIVERY_STATUS_ERROR;
        }

        messageCursor.update(messageRecord);
        messageCursor.continue();
      };
    });
  },

  /**
   * Create the initial database schema.
   *
   * TODO need to worry about number normalization somewhere...
   * TODO full text search on body???
   */
  createSchema: function(db, next) {
    // This messageStore holds the main mobile message data.
    let messageStore = db.createObjectStore(MESSAGE_STORE_NAME, { keyPath: "id" });
    messageStore.createIndex("timestamp", "timestamp", { unique: false });
    if (DEBUG) debug("Created object stores and indexes");
    next();
  },

  /**
   * Upgrade to the corresponding database schema version.
   */
  upgradeSchema: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    messageStore.createIndex("read", "read", { unique: false });
    next();
  },

  upgradeSchema2: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      messageRecord.messageClass = MESSAGE_CLASS_NORMAL;
      messageRecord.deliveryStatus = DELIVERY_STATUS_NOT_APPLICABLE;
      cursor.update(messageRecord);
      cursor.continue();
    };
  },

  upgradeSchema3: function(db, transaction, next) {
    // Delete redundant "id" index.
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    if (messageStore.indexNames.contains("id")) {
      messageStore.deleteIndex("id");
    }

    /**
     * This mostRecentStore can be used to quickly construct a thread view of
     * the mobile message database. Each entry looks like this:
     *
     * { senderOrReceiver: <String> (primary key),
     *   id: <Number>,
     *   timestamp: <Date>,
     *   body: <String>,
     *   unreadCount: <Number> }
     *
     */
    let mostRecentStore = db.createObjectStore(MOST_RECENT_STORE_NAME,
                                               { keyPath: "senderOrReceiver" });
    mostRecentStore.createIndex("timestamp", "timestamp");
    next();
  },

  upgradeSchema4: function(transaction, next) {
    let threads = {};
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);

    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        for (let thread in threads) {
          mostRecentStore.put(threads[thread]);
        }
        next();
        return;
      }

      let messageRecord = cursor.value;
      let contact = messageRecord.sender || messageRecord.receiver;

      if (contact in threads) {
        let thread = threads[contact];
        if (!messageRecord.read) {
          thread.unreadCount++;
        }
        if (messageRecord.timestamp > thread.timestamp) {
          thread.id = messageRecord.id;
          thread.body = messageRecord.body;
          thread.timestamp = messageRecord.timestamp;
        }
      } else {
        threads[contact] = {
          senderOrReceiver: contact,
          id: messageRecord.id,
          timestamp: messageRecord.timestamp,
          body: messageRecord.body,
          unreadCount: messageRecord.read ? 0 : 1
        };
      }
      cursor.continue();
    };
  },

  upgradeSchema5: function(transaction, next) {
    // Don't perform any upgrade. See Bug 819560.
    next();
  },

  upgradeSchema6: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    // Delete "delivery" index.
    if (messageStore.indexNames.contains("delivery")) {
      messageStore.deleteIndex("delivery");
    }
    // Delete "sender" index.
    if (messageStore.indexNames.contains("sender")) {
      messageStore.deleteIndex("sender");
    }
    // Delete "receiver" index.
    if (messageStore.indexNames.contains("receiver")) {
      messageStore.deleteIndex("receiver");
    }
    // Delete "read" index.
    if (messageStore.indexNames.contains("read")) {
      messageStore.deleteIndex("read");
    }

    // Create new "delivery", "number" and "read" indexes.
    messageStore.createIndex("delivery", "deliveryIndex");
    messageStore.createIndex("number", "numberIndex", { multiEntry: true });
    messageStore.createIndex("read", "readIndex");

    // Populate new "deliverIndex", "numberIndex" and "readIndex" attributes.
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      let timestamp = messageRecord.timestamp;
      messageRecord.deliveryIndex = [messageRecord.delivery, timestamp];
      messageRecord.numberIndex = [
        [messageRecord.sender, timestamp],
        [messageRecord.receiver, timestamp]
      ];
      messageRecord.readIndex = [messageRecord.read, timestamp];
      cursor.update(messageRecord);
      cursor.continue();
    };
  },

  /**
   * Add participant/thread stores.
   *
   * The message store now saves original phone numbers/addresses input from
   * content to message records. No normalization is made.
   *
   * For filtering messages by phone numbers, it first looks up corresponding
   * participant IDs from participant table and fetch message records with
   * matching keys defined in per record "participantIds" field.
   *
   * For message threading, messages with the same participant ID array are put
   * in the same thread. So updating "unreadCount", "lastMessageId" and
   * "lastTimestamp" are through the "threadId" carried by per message record.
   * Fetching threads list is now simply walking through the thread sotre. The
   * "mostRecentStore" is dropped.
   */
  upgradeSchema7: function(db, transaction, next) {
    /**
     * This "participant" object store keeps mappings of multiple phone numbers
     * of the same recipient to an integer participant id. Each entry looks
     * like:
     *
     * { id: <Number> (primary key),
     *   addresses: <Array of strings> }
     */
    let participantStore = db.createObjectStore(PARTICIPANT_STORE_NAME,
                                                { keyPath: "id",
                                                  autoIncrement: true });
    participantStore.createIndex("addresses", "addresses", { multiEntry: true });

    /**
     * This "threads" object store keeps mappings from an integer thread id to
     * ids of the participants of that message thread. Each entry looks like:
     *
     * { id: <Number> (primary key),
     *   participantIds: <Array of participant IDs>,
     *   participantAddresses: <Array of the first addresses of the participants>,
     *   lastMessageId: <Number>,
     *   lastTimestamp: <Date>,
     *   subject: <String>,
     *   unreadCount: <Number> }
     *
     */
    let threadStore = db.createObjectStore(THREAD_STORE_NAME,
                                           { keyPath: "id",
                                             autoIncrement: true });
    threadStore.createIndex("participantIds", "participantIds");
    threadStore.createIndex("lastTimestamp", "lastTimestamp");

    /**
     * Replace "numberIndex" with "participantIdsIndex" and create an additional
     * "threadId". "numberIndex" will be removed later.
     */
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    messageStore.createIndex("threadId", "threadIdIndex");
    messageStore.createIndex("participantIds", "participantIdsIndex",
                             { multiEntry: true });

    // Now populate participantStore & threadStore.
    let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);
    let self = this;
    let mostRecentRequest = mostRecentStore.openCursor();
    mostRecentRequest.onsuccess = function(event) {
      let mostRecentCursor = event.target.result;
      if (!mostRecentCursor) {
        db.deleteObjectStore(MOST_RECENT_STORE_NAME);

        // No longer need the "number" index in messageStore, use
        // "participantIds" index instead.
        messageStore.deleteIndex("number");
        next();
        return;
      }

      let mostRecentRecord = mostRecentCursor.value;

      // Each entry in mostRecentStore is supposed to be a unique thread, so we
      // retrieve the records out and insert its "senderOrReceiver" column as a
      // new record in participantStore.
      let number = mostRecentRecord.senderOrReceiver;
      self.findParticipantRecordByPlmnAddress(participantStore, number, true,
                                              function(participantRecord) {
        // Also create a new record in threadStore.
        let threadRecord = {
          participantIds: [participantRecord.id],
          participantAddresses: [number],
          lastMessageId: mostRecentRecord.id,
          lastTimestamp: mostRecentRecord.timestamp,
          subject: mostRecentRecord.body,
          unreadCount: mostRecentRecord.unreadCount,
        };
        let addThreadRequest = threadStore.add(threadRecord);
        addThreadRequest.onsuccess = function(event) {
          threadRecord.id = event.target.result;

          let numberRange = IDBKeyRange.bound([number, 0], [number, ""]);
          let messageRequest = messageStore.index("number")
                                           .openCursor(numberRange, NEXT);
          messageRequest.onsuccess = function(event) {
            let messageCursor = event.target.result;
            if (!messageCursor) {
              // No more message records, check next most recent record.
              mostRecentCursor.continue();
              return;
            }

            let messageRecord = messageCursor.value;
            // Check whether the message really belongs to this thread.
            let matchSenderOrReceiver = false;
            if (messageRecord.delivery == DELIVERY_RECEIVED) {
              if (messageRecord.sender == number) {
                matchSenderOrReceiver = true;
              }
            } else if (messageRecord.receiver == number) {
              matchSenderOrReceiver = true;
            }
            if (!matchSenderOrReceiver) {
              // Check next message record.
              messageCursor.continue();
              return;
            }

            messageRecord.threadId = threadRecord.id;
            messageRecord.threadIdIndex = [threadRecord.id,
                                           messageRecord.timestamp];
            messageRecord.participantIdsIndex = [
              [participantRecord.id, messageRecord.timestamp]
            ];
            messageCursor.update(messageRecord);
            // Check next message record.
            messageCursor.continue();
          };
          messageRequest.onerror = function() {
            // Error in fetching message records, check next most recent record.
            mostRecentCursor.continue();
          };
        };
        addThreadRequest.onerror = function() {
          // Error in fetching message records, check next most recent record.
          mostRecentCursor.continue();
        };
      });
    };
  },

  /**
   * Add transactionId index for MMS.
   */
  upgradeSchema8: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    // Delete "transactionId" index.
    if (messageStore.indexNames.contains("transactionId")) {
      messageStore.deleteIndex("transactionId");
    }

    // Create new "transactionId" indexes.
    messageStore.createIndex("transactionId", "transactionIdIndex", { unique: true });

    // Populate new "transactionIdIndex" attributes.
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if ("mms" == messageRecord.type &&
          (DELIVERY_NOT_DOWNLOADED == messageRecord.delivery ||
           DELIVERY_RECEIVED == messageRecord.delivery)) {
        messageRecord.transactionIdIndex =
          messageRecord.headers["x-mms-transaction-id"];
        cursor.update(messageRecord);
      }
      cursor.continue();
    };
  },

  upgradeSchema9: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    // Update type attributes.
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == undefined) {
        messageRecord.type = "sms";
        cursor.update(messageRecord);
      }
      cursor.continue();
    };
  },

  upgradeSchema10: function(transaction, next) {
    let threadStore = transaction.objectStore(THREAD_STORE_NAME);

    // Add 'lastMessageType' to each thread record.
    threadStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let threadRecord = cursor.value;
      let lastMessageId = threadRecord.lastMessageId;
      let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
      let request = messageStore.mozGetAll(lastMessageId);

      request.onsuccess = function() {
        let messageRecord = request.result[0];
        if (!messageRecord) {
          if (DEBUG) debug("Message ID " + lastMessageId + " not found");
          return;
        }
        if (messageRecord.id != lastMessageId) {
          if (DEBUG) {
            debug("Requested message ID (" + lastMessageId + ") is different from" +
                  " the one we got");
          }
          return;
        }
        threadRecord.lastMessageType = messageRecord.type;
        cursor.update(threadRecord);
        cursor.continue();
      };

      request.onerror = function(event) {
        if (DEBUG) {
          if (event.target) {
            debug("Caught error on transaction", event.target.error.name);
          }
        }
        cursor.continue();
      };
    };
  },

  /**
   * Add envelopeId index for MMS.
   */
  upgradeSchema11: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    // Delete "envelopeId" index.
    if (messageStore.indexNames.contains("envelopeId")) {
      messageStore.deleteIndex("envelopeId");
    }

    // Create new "envelopeId" indexes.
    messageStore.createIndex("envelopeId", "envelopeIdIndex", { unique: true });

    // Populate new "envelopeIdIndex" attributes.
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == "mms" &&
          messageRecord.delivery == DELIVERY_SENT) {
        messageRecord.envelopeIdIndex = messageRecord.headers["message-id"];
        cursor.update(messageRecord);
      }
      cursor.continue();
    };
  },

  /**
   * Replace deliveryStatus by deliveryInfo.
   */
  upgradeSchema12: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == "mms") {
        messageRecord.deliveryInfo = [];

        if (messageRecord.deliveryStatus.length == 1 &&
            (messageRecord.delivery == DELIVERY_NOT_DOWNLOADED ||
             messageRecord.delivery == DELIVERY_RECEIVED)) {
          messageRecord.deliveryInfo.push({
            receiver: null,
            deliveryStatus: messageRecord.deliveryStatus[0] });
        } else {
          for (let i = 0; i < messageRecord.deliveryStatus.length; i++) {
            messageRecord.deliveryInfo.push({
              receiver: messageRecord.receivers[i],
              deliveryStatus: messageRecord.deliveryStatus[i] });
          }
        }
        delete messageRecord.deliveryStatus;
        cursor.update(messageRecord);
      }
      cursor.continue();
    };
  },

  /**
   * Check if we need to re-upgrade the DB schema 12.
   */
  needReUpgradeSchema12: function(transaction, callback) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        callback(false);
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == "mms" &&
          messageRecord.deliveryInfo === undefined) {
        callback(true);
        return;
      }
      cursor.continue();
    };
  },

  /**
   * Fix the wrong participants.
   */
  upgradeSchema13: function(transaction, next) {
    let participantStore = transaction.objectStore(PARTICIPANT_STORE_NAME);
    let threadStore = transaction.objectStore(THREAD_STORE_NAME);
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    let self = this;

    let isInvalid = function(participantRecord) {
      let entries = [];
      for (let addr of participantRecord.addresses) {
        entries.push({
          normalized: addr,
          parsed: PhoneNumberUtils.parseWithMCC(addr, null)
        })
      }
      for (let ix = 0 ; ix < entries.length - 1; ix++) {
        let entry1 = entries[ix];
        for (let iy = ix + 1 ; iy < entries.length; iy ++) {
          let entry2 = entries[iy];
          if (!self.matchPhoneNumbers(entry1.normalized, entry1.parsed,
                                      entry2.normalized, entry2.parsed)) {
            return true;
          }
        }
      }
      return false;
    };

    let invalidParticipantIds = [];
    participantStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        let participantRecord = cursor.value;
        // Check if this participant record is valid
        if (isInvalid(participantRecord)) {
          invalidParticipantIds.push(participantRecord.id);
          cursor.delete();
        }
        cursor.continue();
        return;
      }

      // Participant store cursor iteration done.
      if (!invalidParticipantIds.length) {
        next();
        return;
      }

      // Find affected thread.
      let wrongThreads = [];
      threadStore.openCursor().onsuccess = function(event) {
        let threadCursor = event.target.result;
        if (threadCursor) {
          let threadRecord = threadCursor.value;
          let participantIds = threadRecord.participantIds;
          let foundInvalid = false;
          for (let invalidParticipantId of invalidParticipantIds) {
            if (participantIds.indexOf(invalidParticipantId) != -1) {
              foundInvalid = true;
              break;
            }
          }
          if (foundInvalid) {
            wrongThreads.push(threadRecord.id);
            threadCursor.delete();
          }
          threadCursor.continue();
          return;
        }

        if (!wrongThreads.length) {
          next();
          return;
        }
        // Use recursive function to avoid we add participant twice.
        (function createUpdateThreadAndParticipant(ix) {
          let threadId = wrongThreads[ix];
          let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
          messageStore.index("threadId").openCursor(range).onsuccess = function(event) {
            let messageCursor = event.target.result;
            if (!messageCursor) {
              ix++;
              if (ix === wrongThreads.length) {
                next();
                return;
              }
              createUpdateThreadAndParticipant(ix);
              return;
            }

            let messageRecord = messageCursor.value;
            let timestamp = messageRecord.timestamp;
            let threadParticipants = [];
            // Recaculate the thread participants of received message.
            if (messageRecord.delivery === DELIVERY_RECEIVED ||
                messageRecord.delivery === DELIVERY_NOT_DOWNLOADED) {
              threadParticipants.push(messageRecord.sender);
              if (messageRecord.type == "mms") {
                this.fillReceivedMmsThreadParticipants(messageRecord, threadParticipants);
              }
            }
            // Recaculate the thread participants of sent messages and error
            // messages. In error sms messages, we don't have error received sms.
            // In received MMS, we don't update the error to deliver field but
            // deliverStatus. So we only consider sent message in DELIVERY_ERROR.
            else if (messageRecord.delivery === DELIVERY_SENT ||
                messageRecord.delivery === DELIVERY_ERROR) {
              if (messageRecord.type == "sms") {
                threadParticipants = [messageRecord.receiver];
              } else if (messageRecord.type == "mms") {
                threadParticipants = messageRecord.receivers;
              }
            }
            self.findThreadRecordByPlmnAddresses(threadStore, participantStore,
                                                 threadParticipants, true,
                                                 function(threadRecord,
                                                          participantIds) {
              if (!participantIds) {
                debug("participantIds is empty!");
                return;
              }

              let timestamp = messageRecord.timestamp;
              // Setup participantIdsIndex.
              messageRecord.participantIdsIndex = [];
              for each (let id in participantIds) {
                messageRecord.participantIdsIndex.push([id, timestamp]);
              }
              if (threadRecord) {
                let needsUpdate = false;

                if (threadRecord.lastTimestamp <= timestamp) {
                  threadRecord.lastTimestamp = timestamp;
                  threadRecord.subject = messageRecord.body;
                  threadRecord.lastMessageId = messageRecord.id;
                  threadRecord.lastMessageType = messageRecord.type;
                  needsUpdate = true;
                }

                if (!messageRecord.read) {
                  threadRecord.unreadCount++;
                  needsUpdate = true;
                }

                if (needsUpdate) {
                  threadStore.put(threadRecord);
                }
                messageRecord.threadId = threadRecord.id;
                messageRecord.threadIdIndex = [threadRecord.id, timestamp];
                messageCursor.update(messageRecord);
                messageCursor.continue();
                return;
              }

              threadRecord = {
                participantIds: participantIds,
                participantAddresses: threadParticipants,
                lastMessageId: messageRecord.id,
                lastTimestamp: timestamp,
                subject: messageRecord.body,
                unreadCount: messageRecord.read ? 0 : 1,
                lastMessageType: messageRecord.type
              };
              threadStore.add(threadRecord).onsuccess = function(event) {
                let threadId = event.target.result;
                // Setup threadId & threadIdIndex.
                messageRecord.threadId = threadId;
                messageRecord.threadIdIndex = [threadId, timestamp];
                messageCursor.update(messageRecord);
                messageCursor.continue();
              };
            });
          };
        })(0);
      };
    };
  },

  /**
   * Add deliveryTimestamp.
   */
  upgradeSchema14: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == "sms") {
        messageRecord.deliveryTimestamp = 0;
      } else if (messageRecord.type == "mms") {
        let deliveryInfo = messageRecord.deliveryInfo;
        for (let i = 0; i < deliveryInfo.length; i++) {
          deliveryInfo[i].deliveryTimestamp = 0;
        }
      }
      cursor.update(messageRecord);
      cursor.continue();
    };
  },

  /**
   * Add ICC ID.
   */
  upgradeSchema15: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      messageRecord.iccId = null;
      cursor.update(messageRecord);
      cursor.continue();
    };
  },

  /**
   * Add isReadReportSent for incoming MMS.
   */
  upgradeSchema16: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    // Update type attributes.
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == "mms") {
        messageRecord.isReadReportSent = false;
        cursor.update(messageRecord);
      }
      cursor.continue();
    };
  },

  upgradeSchema17: function(transaction, next) {
    let threadStore = transaction.objectStore(THREAD_STORE_NAME);
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    // Add 'lastMessageSubject' to each thread record.
    threadStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let threadRecord = cursor.value;
      // We have defined 'threadRecord.subject' in upgradeSchema7(), but it
      // actually means 'threadRecord.body'.  Swap the two values first.
      threadRecord.body = threadRecord.subject;
      delete threadRecord.subject;

      // Only MMS supports subject so assign null for non-MMS one.
      if (threadRecord.lastMessageType != "mms") {
        threadRecord.lastMessageSubject = null;
        cursor.update(threadRecord);

        cursor.continue();
        return;
      }

      messageStore.get(threadRecord.lastMessageId).onsuccess = function(event) {
        let messageRecord = event.target.result;
        let subject = messageRecord.headers.subject;
        threadRecord.lastMessageSubject = subject || null;
        cursor.update(threadRecord);

        cursor.continue();
      };
    };
  },

  /**
   * Add pid for incoming SMS.
   */
  upgradeSchema18: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == "sms") {
        messageRecord.pid = RIL.PDU_PID_DEFAULT;
        cursor.update(messageRecord);
      }
      cursor.continue();
    };
  },

  /**
   * Add readStatus and readTimestamp.
   */
  upgradeSchema19: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      if (messageRecord.type == "sms") {
        cursor.continue();
        return;
      }

      // We can always retrieve transaction id from
      // |messageRecord.headers["x-mms-transaction-id"]|.
      if (messageRecord.hasOwnProperty("transactionId")) {
        delete messageRecord.transactionId;
      }

      // xpconnect gives "undefined" for an unassigned argument of an interface
      // method.
      if (messageRecord.envelopeIdIndex === "undefined") {
        delete messageRecord.envelopeIdIndex;
      }

      // Convert some header fields that were originally decoded as BooleanValue
      // to numeric enums.
      for (let field of ["x-mms-cancel-status",
                         "x-mms-sender-visibility",
                         "x-mms-read-status"]) {
        let value = messageRecord.headers[field];
        if (value !== undefined) {
          messageRecord.headers[field] = value ? 128 : 129;
        }
      }

      // For all sent and received MMS messages, we have to add their
      // |readStatus| and |readTimestamp| attributes in |deliveryInfo| array.
      let readReportRequested =
        messageRecord.headers["x-mms-read-report"] || false;
      for (let element of messageRecord.deliveryInfo) {
        element.readStatus = readReportRequested
                           ? MMS.DOM_READ_STATUS_PENDING
                           : MMS.DOM_READ_STATUS_NOT_APPLICABLE;
        element.readTimestamp = 0;
      }

      cursor.update(messageRecord);
      cursor.continue();
    };
  },

  /**
   * Add sentTimestamp.
   */
  upgradeSchema20: function(transaction, next) {
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
    messageStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (!cursor) {
        next();
        return;
      }

      let messageRecord = cursor.value;
      messageRecord.sentTimestamp = 0;

      // We can still have changes to assign |sentTimestamp| for the existing
      // MMS message records.
      if (messageRecord.type == "mms" && messageRecord.headers["date"]) {
        messageRecord.sentTimestamp = messageRecord.headers["date"].getTime();
      }

      cursor.update(messageRecord);
      cursor.continue();
    };
  },

  /**
   * Add smsSegmentStore to store uncomplete SMS segments.
   */
  upgradeSchema21: function(db, transaction, next) {
    /**
     * This smsSegmentStore is used to store uncomplete SMS segments.
     * Each entry looks like this:
     *
     * {
     *   [Common fields in SMS segment]
     *   messageType: <Number>,
     *   teleservice: <Number>,
     *   SMSC: <String>,
     *   sentTimestamp: <Number>,
     *   timestamp: <Number>,
     *   sender: <String>,
     *   pid: <Number>,
     *   encoding: <Number>,
     *   messageClass: <String>,
     *   iccId: <String>,
     *
     *   [Concatenation Info]
     *   segmentRef: <Number>,
     *   segmentSeq: <Number>,
     *   segmentMaxSeq: <Number>,
     *
     *   [Application Port Info]
     *   originatorPort: <Number>,
     *   destinationPort: <Number>,
     *
     *   [MWI status]
     *   mwiPresent: <Boolean>,
     *   mwiDiscard: <Boolean>,
     *   mwiMsgCount: <Number>,
     *   mwiActive: <Boolean>,
     *
     *   [CDMA Cellbroadcast related fields]
     *   serviceCategory: <Number>,
     *   language: <String>,
     *
     *   [Message Body]
     *   data: <Uint8Array>, (available if it's 8bit encoding)
     *   body: <String>, (normal text body)
     *
     *   [Handy fields created by DB for concatenation]
     *   id: <Number>, keypath of this objectStore.
     *   hash: <String>, Use to identify the segments to the same SMS.
     *   receivedSegments: <Number>,
     *   segments: []
     * }
     *
     */
    let smsSegmentStore = db.createObjectStore(SMS_SEGMENT_STORE_NAME,
                                               { keyPath: "id",
                                                 autoIncrement: true });
    smsSegmentStore.createIndex("hash", "hash", { unique: true });
    next();
  },

  /**
   * Change receivers format to address and type.
   */
  upgradeSchema22: function(transaction, next) {
    // Since bug 871433 (DB_VERSION 11), we normalize addresses before really
    // diving into participant store in findParticipantRecordByPlmnAddress.
    // This also follows that all addresses stored in participant store are
    // normalized phone numbers, although they might not be phone numbers at the
    // first place.  So addresses in participant store are not reliable.
    //
    // |participantAddresses| in a thread record are reliable, but several
    // distinct threads can be wrongly mapped into one.  For example, an IPv4
    // address "55.252.255.54" was normalized as US phone number "5525225554".
    // So beginning with thread store is not really a good idea.
    //
    // The only correct way is to begin with all messages records and check if
    // the findThreadRecordByTypedAddresses() call using a message record's
    // thread participants returns the same thread record with the one it
    // currently belong to.

    function getThreadParticipantsFromMessageRecord(aMessageRecord) {
      let threadParticipants;

      if (aMessageRecord.type == "sms") {
        let address;
        if (aMessageRecord.delivery == DELIVERY_RECEIVED) {
          address = aMessageRecord.sender;
        } else {
          address = aMessageRecord.receiver;
        }
        threadParticipants = [{
          address: address,
          type: MMS.Address.resolveType(address)
        }];
      } else { // MMS
        if ((aMessageRecord.delivery == DELIVERY_RECEIVED) ||
            (aMessageRecord.delivery == DELIVERY_NOT_DOWNLOADED)) {
          // DISABLE_MMS_GROUPING_FOR_RECEIVING is set to true at the time, so
          // we consider only |aMessageRecord.sender|.
          threadParticipants = [{
            address: aMessageRecord.sender,
            type: MMS.Address.resolveType(aMessageRecord.sender)
          }];
        } else {
          threadParticipants = aMessageRecord.headers.to;
        }
      }

      return threadParticipants;
    }

    let participantStore = transaction.objectStore(PARTICIPANT_STORE_NAME);
    let threadStore = transaction.objectStore(THREAD_STORE_NAME);
    let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);

    let invalidThreadIds = [];

    let self = this;
    let messageCursorReq = messageStore.openCursor();
    messageCursorReq.onsuccess = function(aEvent) {
      let messageCursor = aEvent.target.result;
      if (messageCursor) {
        let messageRecord = messageCursor.value;
        let threadParticipants =
          getThreadParticipantsFromMessageRecord(messageRecord);

        // 1. If thread ID of this message record has been marked as invalid,
        //    skip further checks and go ahead for the next one.
        if (invalidThreadIds.indexOf(messageRecord.threadId) >= 0) {
          messageCursor.continue();
          return;
        }

        // 2. Check if the thread record found with the new algorithm matches
        //    the original one.
        self.findThreadRecordByTypedAddresses(threadStore, participantStore,
                                              threadParticipants, true,
                                              function(aThreadRecord,
                                                       aParticipantIds) {
          if (!aThreadRecord || aThreadRecord.id !== messageRecord.threadId) {
            invalidThreadIds.push(messageRecord.threadId);
          }

          messageCursor.continue();
        });

        // Only calls |messageCursor.continue()| inside the callback of
        // findThreadRecordByTypedAddresses() because that may inserts new
        // participant records and hurt concurrency.
        return;
      } // End of |if (messageCursor)|.

      // 3. If there is no any mis-grouped message found, go on to next upgrade.
      if (!invalidThreadIds.length) {
        next();
        return;
      }

      // 4. Remove invalid thread records first, so that we don't have
      //    unexpected match in findThreadRecordByTypedAddresses().
      invalidThreadIds.forEach(function(aInvalidThreadId) {
        threadStore.delete(aInvalidThreadId);
      });

      // 5. For each affected thread, re-create a valid thread record for it.
      (function redoThreading(aInvalidThreadId) {
        // 5-1. For each message record originally belongs to this thread, find
        //      a new home for it.
        let range = IDBKeyRange.bound([aInvalidThreadId, 0],
                                      [aInvalidThreadId, ""]);
        let threadMessageCursorReq = messageStore.index("threadId")
                                                 .openCursor(range, NEXT);
        threadMessageCursorReq.onsuccess = function(aEvent) {
          let messageCursor = aEvent.target.result;

          // 5-2. If no more message records to process in this invalid thread,
          //      go on to next invalid thread if available, or pass to next
          //      upgradeSchema function.
          if (!messageCursor) {
            if (invalidThreadIds.length) {
              redoThreading(invalidThreadIds.shift());
            } else {
              next();
            }
            return;
          }

          let messageRecord = messageCursor.value;
          let threadParticipants =
            getThreadParticipantsFromMessageRecord(messageRecord);

          // 5-3. Assign a thread record for this message record. Basically
          //      copied from |realSaveRecord|, but we don't have to worry
          //      about |updateThreadByMessageChange| because we've removed
          //      affected threads.
          self.findThreadRecordByTypedAddresses(threadStore, participantStore,
                                                threadParticipants, true,
                                                function(aThreadRecord,
                                                         aParticipantIds) {
            // Setup participantIdsIndex.
            messageRecord.participantIdsIndex =
              aParticipantIds.map(function(aParticipantId) {
                return [aParticipantId, messageRecord.timestamp];
              });

            let threadExists = aThreadRecord ? true : false;
            if (!threadExists) {
              aThreadRecord = {
                participantIds: aParticipantIds,
                participantAddresses:
                  threadParticipants.map(function(aTypedAddress) {
                    return aTypedAddress.address;
                  }),
                unreadCount: 0,
                lastTimestamp: -1
              };
            }

            let needsUpdate = false;
            if (aThreadRecord.lastTimestamp <= messageRecord.timestamp) {
              let lastMessageSubject;
              if (messageRecord.type == "mms") {
                lastMessageSubject = messageRecord.headers.subject;
              }
              aThreadRecord.lastMessageSubject = lastMessageSubject || null;
              aThreadRecord.lastTimestamp = messageRecord.timestamp;
              aThreadRecord.body = messageRecord.body;
              aThreadRecord.lastMessageId = messageRecord.id;
              aThreadRecord.lastMessageType = messageRecord.type;
              needsUpdate = true;
            }

            if (!messageRecord.read) {
              aThreadRecord.unreadCount++;
              needsUpdate = true;
            }

            let updateMessageRecordThreadId = function(aThreadId) {
              // Setup threadId & threadIdIndex.
              messageRecord.threadId = aThreadId;
              messageRecord.threadIdIndex = [aThreadId, messageRecord.timestamp];

              messageCursor.update(messageRecord);
              messageCursor.continue();
            };

            if (threadExists) {
              if (needsUpdate) {
                threadStore.put(aThreadRecord);
              }
              updateMessageRecordThreadId(aThreadRecord.id);
            } else {
              threadStore.add(aThreadRecord).onsuccess = function(aEvent) {
                let threadId = aEvent.target.result;
                updateMessageRecordThreadId(threadId);
              };
            }
          }); // End of findThreadRecordByTypedAddresses().
        }; // End of threadMessageCursorReq.onsuccess.
      })(invalidThreadIds.shift()); // End of function redoThreading.
    }; // End of messageStore.openCursor().onsuccess
  },

  matchParsedPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
    if ((parsedAddr1.internationalNumber &&
         parsedAddr1.internationalNumber === parsedAddr2.internationalNumber) ||
        (parsedAddr1.nationalNumber &&
         parsedAddr1.nationalNumber === parsedAddr2.nationalNumber)) {
      return true;
    }

    if (parsedAddr1.countryName != parsedAddr2.countryName) {
      return false;
    }

    let ssPref = "dom.phonenumber.substringmatching." + parsedAddr1.countryName;
    if (Services.prefs.getPrefType(ssPref) != Ci.nsIPrefBranch.PREF_INT) {
      return false;
    }

    let val = Services.prefs.getIntPref(ssPref);
    return addr1.length > val &&
           addr2.length > val &&
           addr1.slice(-val) === addr2.slice(-val);
  },

  matchPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
    if (parsedAddr1 && parsedAddr2) {
      return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
    }

    if (parsedAddr1) {
      parsedAddr2 = PhoneNumberUtils.parseWithCountryName(addr2, parsedAddr1.countryName);
      if (parsedAddr2) {
        return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
      }

      return false;
    }

    if (parsedAddr2) {
      parsedAddr1 = PhoneNumberUtils.parseWithCountryName(addr1, parsedAddr2.countryName);
      if (parsedAddr1) {
        return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
      }
    }

    return false;
  },

  createDomMessageFromRecord: function(aMessageRecord) {
    if (DEBUG) {
      debug("createDomMessageFromRecord: " + JSON.stringify(aMessageRecord));
    }
    if (aMessageRecord.type == "sms") {
      return gMobileMessageService.createSmsMessage(aMessageRecord.id,
                                                    aMessageRecord.threadId,
                                                    aMessageRecord.iccId,
                                                    aMessageRecord.delivery,
                                                    aMessageRecord.deliveryStatus,
                                                    aMessageRecord.sender,
                                                    aMessageRecord.receiver,
                                                    aMessageRecord.body,
                                                    aMessageRecord.messageClass,
                                                    aMessageRecord.timestamp,
                                                    aMessageRecord.sentTimestamp,
                                                    aMessageRecord.deliveryTimestamp,
                                                    aMessageRecord.read);
    } else if (aMessageRecord.type == "mms") {
      let headers = aMessageRecord["headers"];
      if (DEBUG) {
        debug("MMS: headers: " + JSON.stringify(headers));
      }

      let subject = headers["subject"];
      if (subject == undefined) {
        subject = "";
      }

      let smil = "";
      let attachments = [];
      let parts = aMessageRecord.parts;
      if (parts) {
        for (let i = 0; i < parts.length; i++) {
          let part = parts[i];
          if (DEBUG) {
            debug("MMS: part[" + i + "]: " + JSON.stringify(part));
          }
          // Sometimes the part is incomplete because the device reboots when
          // downloading MMS. Don't need to expose this part to the content.
          if (!part) {
            continue;
          }

          let partHeaders = part["headers"];
          let partContent = part["content"];
          // Don't need to make the SMIL part if it's present.
          if (partHeaders["content-type"]["media"] == "application/smil") {
            smil = partContent;
            continue;
          }
          attachments.push({
            "id": partHeaders["content-id"],
            "location": partHeaders["content-location"],
            "content": partContent
          });
        }
      }
      let expiryDate = 0;
      if (headers["x-mms-expiry"] != undefined) {
        expiryDate = aMessageRecord.timestamp + headers["x-mms-expiry"] * 1000;
      }
      let readReportRequested = headers["x-mms-read-report"] || false;
      return gMobileMessageService.createMmsMessage(aMessageRecord.id,
                                                    aMessageRecord.threadId,
                                                    aMessageRecord.iccId,
                                                    aMessageRecord.delivery,
                                                    aMessageRecord.deliveryInfo,
                                                    aMessageRecord.sender,
                                                    aMessageRecord.receivers,
                                                    aMessageRecord.timestamp,
                                                    aMessageRecord.sentTimestamp,
                                                    aMessageRecord.read,
                                                    subject,
                                                    smil,
                                                    attachments,
                                                    expiryDate,
                                                    readReportRequested);
    }
  },

  createParticipantRecord: function(aParticipantStore, aAddresses, aCallback) {
    let participantRecord = { addresses: aAddresses };
    let addRequest = aParticipantStore.add(participantRecord);
    addRequest.onsuccess = function(event) {
      participantRecord.id = event.target.result;
      if (DEBUG) {
        debug("createParticipantRecord: " + JSON.stringify(participantRecord));
      }
      aCallback(participantRecord);
    };
  },

  findParticipantRecordByPlmnAddress: function(aParticipantStore, aAddress,
                                               aCreate, aCallback) {
    if (DEBUG) {
      debug("findParticipantRecordByPlmnAddress("
            + JSON.stringify(aAddress) + ", " + aCreate + ")");
    }

    // Two types of input number to match here, international(+886987654321),
    // and local(0987654321) types. The "nationalNumber" parsed from
    // phonenumberutils will be "987654321" in this case.

    // Normalize address before searching for participant record.
    let normalizedAddress = PhoneNumberUtils.normalize(aAddress, false);
    let allPossibleAddresses = [normalizedAddress];
    let parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
    if (parsedAddress && parsedAddress.internationalNumber &&
        allPossibleAddresses.indexOf(parsedAddress.internationalNumber) < 0) {
      // We only stores international numbers into participant store because
      // the parsed national number doesn't contain country info and may
      // duplicate in different country.
      allPossibleAddresses.push(parsedAddress.internationalNumber);
    }
    if (DEBUG) {
      debug("findParticipantRecordByPlmnAddress: allPossibleAddresses = " +
            JSON.stringify(allPossibleAddresses));
    }

    // Make a copy here because we may need allPossibleAddresses again.
    let needles = allPossibleAddresses.slice(0);
    let request = aParticipantStore.index("addresses").get(needles.pop());
    request.onsuccess = (function onsuccess(event) {
      let participantRecord = event.target.result;
      // 1) First try matching through "addresses" index of participant store.
      //    If we're lucky, return the fetched participant record.
      if (participantRecord) {
        if (DEBUG) {
          debug("findParticipantRecordByPlmnAddress: got "
                + JSON.stringify(participantRecord));
        }
        aCallback(participantRecord);
        return;
      }

      // Try next possible address again.
      if (needles.length) {
        let request = aParticipantStore.index("addresses").get(needles.pop());
        request.onsuccess = onsuccess.bind(this);
        return;
      }

      // 2) Traverse throught all participants and check all alias addresses.
      aParticipantStore.openCursor().onsuccess = (function(event) {
        let cursor = event.target.result;
        if (!cursor) {
          // Have traversed whole object store but still in vain.
          if (!aCreate) {
            aCallback(null);
            return;
          }

          this.createParticipantRecord(aParticipantStore, [normalizedAddress],
                                       aCallback);
          return;
        }

        let participantRecord = cursor.value;
        for (let storedAddress of participantRecord.addresses) {
          let parsedStoredAddress = PhoneNumberUtils.parseWithMCC(storedAddress, null);
          let match = this.matchPhoneNumbers(normalizedAddress, parsedAddress,
                                             storedAddress, parsedStoredAddress);
          if (!match) {
            // 3) Else we fail to match current stored participant record.
            continue;
          }
          // Match!
          if (aCreate) {
            // In a READ-WRITE transaction, append one more possible address for
            // this participant record.
            participantRecord.addresses =
              participantRecord.addresses.concat(allPossibleAddresses);
            cursor.update(participantRecord);
          }

          if (DEBUG) {
            debug("findParticipantRecordByPlmnAddress: match "
                  + JSON.stringify(cursor.value));
          }
          aCallback(participantRecord);
          return;
        }

        // Check next participant record if available.
        cursor.continue();
      }).bind(this);
    }).bind(this);
  },

  findParticipantRecordByOtherAddress: function(aParticipantStore, aAddress,
                                                aCreate, aCallback) {
    if (DEBUG) {
      debug("findParticipantRecordByOtherAddress(" +
            JSON.stringify(aAddress) + ", " + aCreate + ")");
    }

    // Go full match.
    let request = aParticipantStore.index("addresses").get(aAddress);
    request.onsuccess = (function(event) {
      let participantRecord = event.target.result;
      if (participantRecord) {
        if (DEBUG) {
          debug("findParticipantRecordByOtherAddress: got "
                + JSON.stringify(participantRecord));
        }
        aCallback(participantRecord);
        return;
      }
      if (aCreate) {
        this.createParticipantRecord(aParticipantStore, [aAddress], aCallback);
        return;
      }
      aCallback(null);
    }).bind(this);
  },

  findParticipantRecordByTypedAddress: function(aParticipantStore,
                                                aTypedAddress, aCreate,
                                                aCallback) {
    if (aTypedAddress.type == "PLMN") {
      this.findParticipantRecordByPlmnAddress(aParticipantStore,
                                              aTypedAddress.address, aCreate,
                                              aCallback);
    } else {
      this.findParticipantRecordByOtherAddress(aParticipantStore,
                                               aTypedAddress.address, aCreate,
                                               aCallback);
    }
  },

  // For upgradeSchema13 usage.
  findParticipantIdsByPlmnAddresses: function(aParticipantStore, aAddresses,
                                              aCreate, aSkipNonexistent, aCallback) {
    if (DEBUG) {
      debug("findParticipantIdsByPlmnAddresses("
            + JSON.stringify(aAddresses) + ", "
            + aCreate + ", " + aSkipNonexistent + ")");
    }

    if (!aAddresses || !aAddresses.length) {
      if (DEBUG) debug("findParticipantIdsByPlmnAddresses: returning null");
      aCallback(null);
      return;
    }

    let self = this;
    (function findParticipantId(index, result) {
      if (index >= aAddresses.length) {
        // Sort numerically.
        result.sort(function(a, b) {
          return a - b;
        });
        if (DEBUG) debug("findParticipantIdsByPlmnAddresses: returning " + result);
        aCallback(result);
        return;
      }

      self.findParticipantRecordByPlmnAddress(aParticipantStore,
                                              aAddresses[index++], aCreate,
                                              function(participantRecord) {
        if (!participantRecord) {
          if (!aSkipNonexistent) {
            if (DEBUG) debug("findParticipantIdsByPlmnAddresses: returning null");
            aCallback(null);
            return;
          }
        } else if (result.indexOf(participantRecord.id) < 0) {
          result.push(participantRecord.id);
        }
        findParticipantId(index, result);
      });
    }) (0, []);
  },

  findParticipantIdsByTypedAddresses: function(aParticipantStore,
                                               aTypedAddresses, aCreate,
                                               aSkipNonexistent, aCallback) {
    if (DEBUG) {
      debug("findParticipantIdsByTypedAddresses(" +
            JSON.stringify(aTypedAddresses) + ", " +
            aCreate + ", " + aSkipNonexistent + ")");
    }

    if (!aTypedAddresses || !aTypedAddresses.length) {
      if (DEBUG) debug("findParticipantIdsByTypedAddresses: returning null");
      aCallback(null);
      return;
    }

    let self = this;
    (function findParticipantId(index, result) {
      if (index >= aTypedAddresses.length) {
        // Sort numerically.
        result.sort(function(a, b) {
          return a - b;
        });
        if (DEBUG) {
          debug("findParticipantIdsByTypedAddresses: returning " + result);
        }
        aCallback(result);
        return;
      }

      self.findParticipantRecordByTypedAddress(aParticipantStore,
                                               aTypedAddresses[index++],
                                               aCreate,
                                               function(participantRecord) {
        if (!participantRecord) {
          if (!aSkipNonexistent) {
            if (DEBUG) {
              debug("findParticipantIdsByTypedAddresses: returning null");
            }
            aCallback(null);
            return;
          }
        } else if (result.indexOf(participantRecord.id) < 0) {
          result.push(participantRecord.id);
        }
        findParticipantId(index, result);
      });
    }) (0, []);
  },

  // For upgradeSchema13 usage.
  findThreadRecordByPlmnAddresses: function(aThreadStore, aParticipantStore,
                                            aAddresses, aCreateParticipants,
                                            aCallback) {
    if (DEBUG) {
      debug("findThreadRecordByPlmnAddresses(" + JSON.stringify(aAddresses)
            + ", " + aCreateParticipants + ")");
    }
    this.findParticipantIdsByPlmnAddresses(aParticipantStore, aAddresses,
                                           aCreateParticipants, false,
                                           function(participantIds) {
      if (!participantIds) {
        if (DEBUG) debug("findThreadRecordByPlmnAddresses: returning null");
        aCallback(null, null);
        return;
      }
      // Find record from thread store.
      let request = aThreadStore.index("participantIds").get(participantIds);
      request.onsuccess = function(event) {
        let threadRecord = event.target.result;
        if (DEBUG) {
          debug("findThreadRecordByPlmnAddresses: return "
                + JSON.stringify(threadRecord));
        }
        aCallback(threadRecord, participantIds);
      };
    });
  },

  findThreadRecordByTypedAddresses: function(aThreadStore, aParticipantStore,
                                             aTypedAddresses,
                                             aCreateParticipants, aCallback) {
    if (DEBUG) {
      debug("findThreadRecordByTypedAddresses(" +
          JSON.stringify(aTypedAddresses) + ", " + aCreateParticipants + ")");
    }
    this.findParticipantIdsByTypedAddresses(aParticipantStore, aTypedAddresses,
                                            aCreateParticipants, false,
                                            function(participantIds) {
      if (!participantIds) {
        if (DEBUG) debug("findThreadRecordByTypedAddresses: returning null");
        aCallback(null, null);
        return;
      }
      // Find record from thread store.
      let request = aThreadStore.index("participantIds").get(participantIds);
      request.onsuccess = function(event) {
        let threadRecord = event.target.result;
        if (DEBUG) {
          debug("findThreadRecordByTypedAddresses: return " +
                JSON.stringify(threadRecord));
        }
        aCallback(threadRecord, participantIds);
      };
    });
  },

  newTxnWithCallback: function(aCallback, aFunc, aStoreNames) {
    let self = this;
    this.newTxn(READ_WRITE, function(aError, aTransaction, aStores) {
      let notifyResult = function(aRv, aMessageRecord) {
        if (!aCallback) {
          return;
        }
        let domMessage =
          aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
        aCallback.notify(aRv, domMessage);
      };

      if (aError) {
        notifyResult(aError, null);
        return;
      }

      let capture = {};
      aTransaction.oncomplete = function(event) {
        notifyResult(Cr.NS_OK, capture.messageRecord);
      };
      aTransaction.onabort = function(event) {
        if (DEBUG) debug("transaction abort due to " + event.target.error.name);
        let error = (event.target.error.name === 'QuotaExceededError')
                    ? Cr.NS_ERROR_FILE_NO_DEVICE_SPACE
                    : Cr.NS_ERROR_FAILURE;
        notifyResult(error, null);
      };

      aFunc(capture, aStores);
    }, aStoreNames);
  },

  saveRecord: function(aMessageRecord, aThreadParticipants, aCallback) {
    if (DEBUG) debug("Going to store " + JSON.stringify(aMessageRecord));

    let self = this;
    this.newTxn(READ_WRITE, function(error, txn, stores) {
      let notifyResult = function(aRv, aMessageRecord) {
        if (!aCallback) {
          return;
        }
        let domMessage =
          aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
        aCallback.notify(aRv, domMessage);
      };

      if (error) {
        notifyResult(error, null);
        return;
      }

      let deletedInfo = { messageIds: [], threadIds: [] };

      txn.oncomplete = function(event) {
        if (aMessageRecord.id > self.lastMessageId) {
          self.lastMessageId = aMessageRecord.id;
        }
        notifyResult(Cr.NS_OK, aMessageRecord);
        self.notifyDeletedInfo(deletedInfo);
      };
      txn.onabort = function(event) {
        if (DEBUG) debug("transaction abort due to " + event.target.error.name);
        let error = (event.target.error.name === 'QuotaExceededError')
                    ? Cr.NS_ERROR_FILE_NO_DEVICE_SPACE
                    : Cr.NS_ERROR_FAILURE;
        notifyResult(error, null);
      };

      let messageStore = stores[0];
      let participantStore = stores[1];
      let threadStore = stores[2];
      self.replaceShortMessageOnSave(txn, messageStore, participantStore,
                                     threadStore, aMessageRecord,
                                     aThreadParticipants, deletedInfo);
    }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME, THREAD_STORE_NAME]);
  },

  replaceShortMessageOnSave: function(aTransaction, aMessageStore,
                                      aParticipantStore, aThreadStore,
                                      aMessageRecord, aThreadParticipants,
                                      aDeletedInfo) {
    let isReplaceTypePid = (aMessageRecord.pid) &&
                           ((aMessageRecord.pid >= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_1 &&
                             aMessageRecord.pid <= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_7) ||
                            aMessageRecord.pid == RIL.PDU_PID_RETURN_CALL_MESSAGE);

    if (aMessageRecord.type != "sms" ||
        aMessageRecord.delivery != DELIVERY_RECEIVED ||
        !isReplaceTypePid) {
      this.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
                          aThreadStore, aMessageRecord, aThreadParticipants,
                          aDeletedInfo);
      return;
    }

    // 3GPP TS 23.040 subclause 9.2.3.9 "TP-Protocol-Identifier (TP-PID)":
    //
    //   ... the MS shall check the originating address and replace any
    //   existing stored message having the same Protocol Identifier code
    //   and originating address with the new short message and other
    //   parameter values. If there is no message to be replaced, the MS
    //   shall store the message in the normal way. ... it is recommended
    //   that the SC address should not be checked by the MS."
    let self = this;
    let typedSender = {
      address: aMessageRecord.sender,
      type: MMS.Address.resolveType(aMessageRecord.sender)
    };
    this.findParticipantRecordByTypedAddress(aParticipantStore, typedSender,
                                             false,
                                             function(participantRecord) {
      if (!participantRecord) {
        self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
                            aThreadStore, aMessageRecord, aThreadParticipants,
                            aDeletedInfo);
        return;
      }

      let participantId = participantRecord.id;
      let range = IDBKeyRange.bound([participantId, 0], [participantId, ""]);
      let request = aMessageStore.index("participantIds").openCursor(range);
      request.onsuccess = function(event) {
        let cursor = event.target.result;
        if (!cursor) {
          self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
                              aThreadStore, aMessageRecord, aThreadParticipants,
                              aDeletedInfo);
          return;
        }

        // A message record with same participantId found.
        // Verify matching criteria.
        let foundMessageRecord = cursor.value;
        if (foundMessageRecord.type != "sms" ||
            foundMessageRecord.sender != aMessageRecord.sender ||
            foundMessageRecord.pid != aMessageRecord.pid) {
          cursor.continue();
          return;
        }

        // Match! Now replace that found message record with current one.
        aMessageRecord.id = foundMessageRecord.id;
        self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
                            aThreadStore, aMessageRecord, aThreadParticipants,
                            aDeletedInfo);
      };
    });
  },

  realSaveRecord: function(aTransaction, aMessageStore, aParticipantStore,
                           aThreadStore, aMessageRecord, aThreadParticipants,
                           aDeletedInfo) {
    let self = this;
    this.findThreadRecordByTypedAddresses(aThreadStore, aParticipantStore,
                                          aThreadParticipants, true,
                                          function(threadRecord,
                                                   participantIds) {
      if (!participantIds) {
        aTransaction.abort();
        return;
      }

      let isOverriding = (aMessageRecord.id !== undefined);
      if (!isOverriding) {
        // |self.lastMessageId| is only updated in |txn.oncomplete|.
        aMessageRecord.id = self.lastMessageId + 1;
      }

      let timestamp = aMessageRecord.timestamp;
      let insertMessageRecord = function(threadId) {
        // Setup threadId & threadIdIndex.
        aMessageRecord.threadId = threadId;
        aMessageRecord.threadIdIndex = [threadId, timestamp];
        // Setup participantIdsIndex.
        aMessageRecord.participantIdsIndex = [];
        for each (let id in participantIds) {
          aMessageRecord.participantIdsIndex.push([id, timestamp]);
        }

        if (!isOverriding) {
          // Really add to message store.
          aMessageStore.put(aMessageRecord);
          return;
        }

        // If we're going to override an old message, we need to update the
        // info of the original thread containing the overridden message.
        // To get the original thread ID and read status of the overridden
        // message record, we need to retrieve it before overriding it.
        aMessageStore.get(aMessageRecord.id).onsuccess = function(event) {
          let oldMessageRecord = event.target.result;
          aMessageStore.put(aMessageRecord);
          if (oldMessageRecord) {
            self.updateThreadByMessageChange(aMessageStore,
                                             aThreadStore,
                                             oldMessageRecord.threadId,
                                             [aMessageRecord.id],
                                             oldMessageRecord.read ? 0 : 1,
                                             aDeletedInfo);
          }
        };
      };

      if (threadRecord) {
        let needsUpdate = false;

        if (threadRecord.lastTimestamp <= timestamp) {
          let lastMessageSubject;
          if (aMessageRecord.type == "mms") {
            lastMessageSubject = aMessageRecord.headers.subject;
          }
          threadRecord.lastMessageSubject = lastMessageSubject || null;
          threadRecord.lastTimestamp = timestamp;
          threadRecord.body = aMessageRecord.body;
          threadRecord.lastMessageId = aMessageRecord.id;
          threadRecord.lastMessageType = aMessageRecord.type;
          needsUpdate = true;
        }

        if (!aMessageRecord.read) {
          threadRecord.unreadCount++;
          needsUpdate = true;
        }

        if (needsUpdate) {
          aThreadStore.put(threadRecord);
        }

        insertMessageRecord(threadRecord.id);
        return;
      }

      let lastMessageSubject;
      if (aMessageRecord.type == "mms") {
        lastMessageSubject = aMessageRecord.headers.subject;
      }

      threadRecord = {
        participantIds: participantIds,
        participantAddresses: aThreadParticipants.map(function(typedAddress) {
          return typedAddress.address;
        }),
        lastMessageId: aMessageRecord.id,
        lastTimestamp: timestamp,
        lastMessageSubject: lastMessageSubject || null,
        body: aMessageRecord.body,
        unreadCount: aMessageRecord.read ? 0 : 1,
        lastMessageType: aMessageRecord.type,
      };
      aThreadStore.add(threadRecord).onsuccess = function(event) {
        let threadId = event.target.result;
        insertMessageRecord(threadId);
      };
    });
  },

  forEachMatchedMmsDeliveryInfo: function(aDeliveryInfo, aNeedle, aCallback) {

    let typedAddress = {
      type: MMS.Address.resolveType(aNeedle),
      address: aNeedle
    };
    let normalizedAddress, parsedAddress;
    if (typedAddress.type === "PLMN") {
      normalizedAddress = PhoneNumberUtils.normalize(aNeedle, false);
      parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
    }

    for (let element of aDeliveryInfo) {
      let typedStoredAddress = {
        type: MMS.Address.resolveType(element.receiver),
        address: element.receiver
      };
      if (typedAddress.type !== typedStoredAddress.type) {
        // Not even my type.  Skip.
        continue;
      }

      if (typedAddress.address == typedStoredAddress.address) {
        // Have a direct match.
        aCallback(element);
        continue;
      }

      if (typedAddress.type !== "PLMN") {
        // Address type other than "PLMN" must have direct match.  Or, skip.
        continue;
      }

      // Both are of "PLMN" type.
      let normalizedStoredAddress =
        PhoneNumberUtils.normalize(element.receiver, false);
      let parsedStoredAddress =
        PhoneNumberUtils.parseWithMCC(normalizedStoredAddress, null);
      if (this.matchPhoneNumbers(normalizedAddress, parsedAddress,
                                 normalizedStoredAddress, parsedStoredAddress)) {
        aCallback(element);
      }
    }
  },

  updateMessageDeliveryById: function(id, type, receiver, delivery,
                                      deliveryStatus, envelopeId, callback) {
    if (DEBUG) {
      debug("Setting message's delivery by " + type + " = "+ id
            + " receiver: " + receiver
            + " delivery: " + delivery
            + " deliveryStatus: " + deliveryStatus
            + " envelopeId: " + envelopeId);
    }

    let self = this;
    this.newTxnWithCallback(callback, function(aCapture, aMessageStore) {
      let getRequest;
      if (type === "messageId") {
        getRequest = aMessageStore.get(id);
      } else if (type === "envelopeId") {
        getRequest = aMessageStore.index("envelopeId").get(id);
      }

      getRequest.onsuccess = function(event) {
        let messageRecord = event.target.result;
        if (!messageRecord) {
          if (DEBUG) debug("type = " + id + " is not found");
          throw Cr.NS_ERROR_FAILURE;
        }

        let isRecordUpdated = false;

        // Update |messageRecord.delivery| if needed.
        if (delivery && messageRecord.delivery != delivery) {
          messageRecord.delivery = delivery;
          messageRecord.deliveryIndex = [delivery, messageRecord.timestamp];
          isRecordUpdated = true;

          // When updating an message's delivey state to 'sent', we also update
          // its |sentTimestamp| by the current device timestamp to represent
          // when the message is successfully sent.
          if (delivery == DELIVERY_SENT) {
            messageRecord.sentTimestamp = Date.now();
          }
        }

        // Attempt to update |deliveryStatus| and |deliveryTimestamp| of:
        // - the |messageRecord| for SMS.
        // - the element(s) in |messageRecord.deliveryInfo| for MMS.
        if (deliveryStatus) {
          // A callback for updating the deliveyStatus/deliveryTimestamp of
          // each target.
          let updateFunc = function(aTarget) {
            if (aTarget.deliveryStatus == deliveryStatus) {
              return;
            }

            aTarget.deliveryStatus = deliveryStatus;

            // Update |deliveryTimestamp| if it's successfully delivered.
            if (deliveryStatus == DELIVERY_STATUS_SUCCESS) {
              aTarget.deliveryTimestamp = Date.now();
            }

            isRecordUpdated = true;
          };

          if (messageRecord.type == "sms") {
            updateFunc(messageRecord);
          } else if (messageRecord.type == "mms") {
            if (!receiver) {
              // If the receiver is specified, we only need to update the
              // element(s) in deliveryInfo that match the same receiver.
              messageRecord.deliveryInfo.forEach(updateFunc);
            } else {
              self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
                                                 receiver, updateFunc);
            }
          }
        }

        // Update |messageRecord.envelopeIdIndex| if needed.
        if (envelopeId) {
          if (messageRecord.envelopeIdIndex != envelopeId) {
            messageRecord.envelopeIdIndex = envelopeId;
            isRecordUpdated = true;
          }
        }

        aCapture.messageRecord = messageRecord;
        if (!isRecordUpdated) {
          if (DEBUG) {
            debug("The values of delivery, deliveryStatus and envelopeId " +
                  "don't need to be updated.");
          }
          return;
        }

        if (DEBUG) {
          debug("The delivery, deliveryStatus or envelopeId are updated.");
        }
        aMessageStore.put(messageRecord);
      };
    });
  },

  fillReceivedMmsThreadParticipants: function(aMessage, threadParticipants) {
    let receivers = aMessage.receivers;
    // If we don't want to disable the MMS grouping for receiving, we need to
    // add the receivers (excluding the user's own number) to the participants
    // for creating the thread. Some cases might be investigated as below:
    //
    // 1. receivers.length == 0
    //    This usually happens when receiving an MMS notification indication
    //    which doesn't carry any receivers.
    // 2. receivers.length == 1
    //    If the receivers contain single phone number, we don't need to
    //    add it into participants because we know that number is our own.
    // 3. receivers.length >= 2
    //    If the receivers contain multiple phone numbers, we need to add all
    //    of them but not the user's own number into participants.
    if (DISABLE_MMS_GROUPING_FOR_RECEIVING || receivers.length < 2) {
      return;
    }
    let isSuccess = false;
    let slicedReceivers = receivers.slice();
    if (aMessage.msisdn) {
      let found = slicedReceivers.indexOf(aMessage.msisdn);
      if (found !== -1) {
        isSuccess = true;
        slicedReceivers.splice(found, 1);
      }
    }

    if (!isSuccess) {
      // For some SIMs we cannot retrieve the vaild MSISDN (i.e. the user's
      // own phone number), so we cannot correcly exclude the user's own
      // number from the receivers, thus wrongly building the thread index.
      if (DEBUG) debug("Error! Cannot strip out user's own phone number!");
    }

    threadParticipants =
      threadParticipants.concat(slicedReceivers).map(function(aAddress) {
        return {
          address: aAddress,
          type: MMS.Address.resolveType(aAddress)
        };
      });
  },

  updateThreadByMessageChange: function(messageStore, threadStore, threadId,
                                        removedMsgIds, ignoredUnreadCount, deletedInfo) {
    let self = this;
    threadStore.get(threadId).onsuccess = function(event) {
      // This must exist.
      let threadRecord = event.target.result;
      if (DEBUG) debug("Updating thread record " + JSON.stringify(threadRecord));

      if (ignoredUnreadCount > 0) {
        if (DEBUG) {
          debug("Updating unread count : " + threadRecord.unreadCount +
                " -> " + (threadRecord.unreadCount - ignoredUnreadCount));
        }
        threadRecord.unreadCount -= ignoredUnreadCount;
      }

      if (removedMsgIds.indexOf(threadRecord.lastMessageId) >= 0) {
        if (DEBUG) debug("MRU entry was deleted.");
        // Check most recent sender/receiver.
        let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
        let request = messageStore.index("threadId")
                                  .openCursor(range, PREV);
        request.onsuccess = function(event) {
          let cursor = event.target.result;
          if (!cursor) {
            if (DEBUG) {
              debug("All messages were deleted. Delete this thread.");
            }
            threadStore.delete(threadId);
            if (deletedInfo) {
              deletedInfo.threadIds.push(threadId);
            }
            return;
          }

          let nextMsg = cursor.value;
          let lastMessageSubject;
          if (nextMsg.type == "mms") {
            lastMessageSubject = nextMsg.headers.subject;
          }
          threadRecord.lastMessageSubject = lastMessageSubject || null;
          threadRecord.lastMessageId = nextMsg.id;
          threadRecord.lastTimestamp = nextMsg.timestamp;
          threadRecord.body = nextMsg.body;
          threadRecord.lastMessageType = nextMsg.type;
          if (DEBUG) {
            debug("Updating mru entry: " +
                  JSON.stringify(threadRecord));
          }
          threadStore.put(threadRecord);
        };
      } else if (ignoredUnreadCount > 0) {
        if (DEBUG) debug("Shortcut, just update the unread count.");
        threadStore.put(threadRecord);
      }
    };
  },

  notifyDeletedInfo: function(info) {
    if (!info ||
        (info.messageIds.length === 0 && info.threadIds.length === 0)) {
      return;
    }

    let deletedInfo =
      gMobileMessageService
      .createDeletedMessageInfo(info.messageIds,
                                info.messageIds.length,
                                info.threadIds,
                                info.threadIds.length);
    Services.obs.notifyObservers(deletedInfo, "sms-deleted", null);
  },

  /**
   * nsIGonkMobileMessageDatabaseService API
   */

  saveReceivedMessage: function(aMessage, aCallback) {
    if ((aMessage.type != "sms" && aMessage.type != "mms") ||
        (aMessage.type == "sms" && (aMessage.messageClass == undefined ||
                                    aMessage.sender == undefined)) ||
        (aMessage.type == "mms" && (aMessage.delivery == undefined ||
                                    aMessage.deliveryStatus == undefined ||
                                    !Array.isArray(aMessage.receivers))) ||
        aMessage.timestamp == undefined) {
      if (aCallback) {
        aCallback.notify(Cr.NS_ERROR_FAILURE, null);
      }
      return;
    }

    let threadParticipants;
    if (aMessage.type == "mms") {
      if (aMessage.headers.from) {
        aMessage.sender = aMessage.headers.from.address;
      } else {
        aMessage.sender = "";
      }

      threadParticipants = [{
        address: aMessage.sender,
        type: MMS.Address.resolveType(aMessage.sender)
      }];
      this.fillReceivedMmsThreadParticipants(aMessage, threadParticipants);
    } else { // SMS
      threadParticipants = [{
        address: aMessage.sender,
        type: MMS.Address.resolveType(aMessage.sender)
      }];
    }

    let timestamp = aMessage.timestamp;

    // Adding needed indexes and extra attributes for internal use.
    // threadIdIndex & participantIdsIndex are filled in saveRecord().
    aMessage.readIndex = [FILTER_READ_UNREAD, timestamp];
    aMessage.read = FILTER_READ_UNREAD;

    // If |sentTimestamp| is not specified, use 0 as default.
    if (aMessage.sentTimestamp == undefined) {
      aMessage.sentTimestamp = 0;
    }

    if (aMessage.type == "mms") {
      aMessage.transactionIdIndex = aMessage.headers["x-mms-transaction-id"];
      aMessage.isReadReportSent = false;

      // As a receiver, we don't need to care about the delivery status of
      // others, so we put a single element with self's phone number in the
      // |deliveryInfo| array.
      aMessage.deliveryInfo = [{
        receiver: aMessage.phoneNumber,
        deliveryStatus: aMessage.deliveryStatus,
        deliveryTimestamp: 0,
        readStatus: MMS.DOM_READ_STATUS_NOT_APPLICABLE,
        readTimestamp: 0,
      }];

      delete aMessage.deliveryStatus;
    }

    if (aMessage.type == "sms") {
      aMessage.delivery = DELIVERY_RECEIVED;
      aMessage.deliveryStatus = DELIVERY_STATUS_SUCCESS;
      aMessage.deliveryTimestamp = 0;

      if (aMessage.pid == undefined) {
        aMessage.pid = RIL.PDU_PID_DEFAULT;
      }
    }
    aMessage.deliveryIndex = [aMessage.delivery, timestamp];

    this.saveRecord(aMessage, threadParticipants, aCallback);
  },

  saveSendingMessage: function(aMessage, aCallback) {
    if ((aMessage.type != "sms" && aMessage.type != "mms") ||
        (aMessage.type == "sms" && aMessage.receiver == undefined) ||
        (aMessage.type == "mms" && !Array.isArray(aMessage.receivers)) ||
        aMessage.deliveryStatusRequested == undefined ||
        aMessage.timestamp == undefined) {
      if (aCallback) {
        aCallback.notify(Cr.NS_ERROR_FAILURE, null);
      }
      return;
    }

    // Set |aMessage.deliveryStatus|. Note that for MMS record
    // it must be an array of strings; For SMS, it's a string.
    let deliveryStatus = aMessage.deliveryStatusRequested
                       ? DELIVERY_STATUS_PENDING
                       : DELIVERY_STATUS_NOT_APPLICABLE;
    if (aMessage.type == "sms") {
      aMessage.deliveryStatus = deliveryStatus;
      // If |deliveryTimestamp| is not specified, use 0 as default.
      if (aMessage.deliveryTimestamp == undefined) {
        aMessage.deliveryTimestamp = 0;
      }
    } else if (aMessage.type == "mms") {
      let receivers = aMessage.receivers;
      let readStatus = aMessage.headers["x-mms-read-report"]
                     ? MMS.DOM_READ_STATUS_PENDING
                     : MMS.DOM_READ_STATUS_NOT_APPLICABLE;
      aMessage.deliveryInfo = [];
      for (let i = 0; i < receivers.length; i++) {
        aMessage.deliveryInfo.push({
          receiver: receivers[i],
          deliveryStatus: deliveryStatus,
          deliveryTimestamp: 0,
          readStatus: readStatus,
          readTimestamp: 0,
        });
      }
    }

    let timestamp = aMessage.timestamp;

    // Adding needed indexes and extra attributes for internal use.
    // threadIdIndex & participantIdsIndex are filled in saveRecord().
    aMessage.deliveryIndex = [DELIVERY_SENDING, timestamp];
    aMessage.readIndex = [FILTER_READ_READ, timestamp];
    aMessage.delivery = DELIVERY_SENDING;
    aMessage.messageClass = MESSAGE_CLASS_NORMAL;
    aMessage.read = FILTER_READ_READ;

    // |sentTimestamp| is not available when the message is still sedning.
    aMessage.sentTimestamp = 0;

    let threadParticipants;
    if (aMessage.type == "sms") {
      threadParticipants = [{
        address: aMessage.receiver,
        type :MMS.Address.resolveType(aMessage.receiver)
      }];
    } else if (aMessage.type == "mms") {
      threadParticipants = aMessage.headers.to;
    }
    this.saveRecord(aMessage, threadParticipants, aCallback);
  },

  setMessageDeliveryByMessageId: function(messageId, receiver, delivery,
                                          deliveryStatus, envelopeId, callback) {
    this.updateMessageDeliveryById(messageId, "messageId",
                                   receiver, delivery, deliveryStatus,
                                   envelopeId, callback);

  },

  setMessageDeliveryStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
                                                 aDeliveryStatus, aCallback) {
    this.updateMessageDeliveryById(aEnvelopeId, "envelopeId", aReceiver, null,
                                   aDeliveryStatus, null, aCallback);
  },

  setMessageReadStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
                                             aReadStatus, aCallback) {
    if (DEBUG) {
      debug("Setting message's read status by envelopeId = " + aEnvelopeId +
            ", receiver: " + aReceiver + ", readStatus: " + aReadStatus);
    }

    let self = this;
    this.newTxnWithCallback(aCallback, function(aCapture, aMessageStore) {
      let getRequest = aMessageStore.index("envelopeId").get(aEnvelopeId);
      getRequest.onsuccess = function(event) {
        let messageRecord = event.target.result;
        if (!messageRecord) {
          if (DEBUG) debug("envelopeId '" + aEnvelopeId + "' not found");
          throw Cr.NS_ERROR_FAILURE;
        }

        aCapture.messageRecord = messageRecord;

        let isRecordUpdated = false;
        self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
                                           aReceiver, function(aEntry) {
          if (aEntry.readStatus == aReadStatus) {
            return;
          }

          aEntry.readStatus = aReadStatus;
          if (aReadStatus == MMS.DOM_READ_STATUS_SUCCESS) {
            aEntry.readTimestamp = Date.now();
          } else {
            aEntry.readTimestamp = 0;
          }
          isRecordUpdated = true;
        });

        if (!isRecordUpdated) {
          if (DEBUG) {
            debug("The values of readStatus don't need to be updated.");
          }
          return;
        }

        if (DEBUG) {
          debug("The readStatus is updated.");
        }
        aMessageStore.put(messageRecord);
      };
    });
  },

  getMessageRecordByTransactionId: function(aTransactionId, aCallback) {
    if (DEBUG) debug("Retrieving message with transaction ID " + aTransactionId);
    let self = this;
    this.newTxn(READ_ONLY, function(error, txn, messageStore) {
      if (error) {
        if (DEBUG) debug(error);
        aCallback.notify(error, null, null);
        return;
      }
      let request = messageStore.index("transactionId").get(aTransactionId);

      txn.oncomplete = function(event) {
        if (DEBUG) debug("Transaction " + txn + " completed.");
        let messageRecord = request.result;
        if (!messageRecord) {
          if (DEBUG) debug("Transaction ID " + aTransactionId + " not found");
          aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
          return;
        }
        // In this case, we don't need a dom message. Just pass null to the
        // third argument.
        aCallback.notify(Cr.NS_OK, messageRecord, null);
      };

      txn.onerror = function(event) {
        if (DEBUG) {
          if (event.target) {
            debug("Caught error on transaction", event.target.error.name);
          }
        }
        aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
      };
    });
  },

  getMessageRecordById: function(aMessageId, aCallback) {
    if (DEBUG) debug("Retrieving message with ID " + aMessageId);
    let self = this;
    this.newTxn(READ_ONLY, function(error, txn, messageStore) {
      if (error) {
        if (DEBUG) debug(error);
        aCallback.notify(error, null, null);
        return;
      }
      let request = messageStore.mozGetAll(aMessageId);

      txn.oncomplete = function() {
        if (DEBUG) debug("Transaction " + txn + " completed.");
        if (request.result.length > 1) {
          if (DEBUG) debug("Got too many results for id " + aMessageId);
          aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
          return;
        }
        let messageRecord = request.result[0];
        if (!messageRecord) {
          if (DEBUG) debug("Message ID " + aMessageId + " not found");
          aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
          return;
        }
        if (messageRecord.id != aMessageId) {
          if (DEBUG) {
            debug("Requested message ID (" + aMessageId + ") is " +
                  "different from the one we got");
          }
          aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
          return;
        }
        let domMessage = self.createDomMessageFromRecord(messageRecord);
        aCallback.notify(Cr.NS_OK, messageRecord, domMessage);
      };

      txn.onerror = function(event) {
        if (DEBUG) {
          if (event.target) {
            debug("Caught error on transaction", event.target.error.name);
          }
        }
        aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
      };
    });
  },

  translateCrErrorToMessageCallbackError: function(aCrError) {
    switch(aCrError) {
      case Cr.NS_OK:
        return Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR;
      case Cr.NS_ERROR_UNEXPECTED:
        return Ci.nsIMobileMessageCallback.UNKNOWN_ERROR;
      case Cr.NS_ERROR_FILE_NOT_FOUND:
        return Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR;
      case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE:
        return Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR;
      default:
        return Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
    }
  },

  saveSmsSegment: function(aSmsSegment, aCallback) {
    let completeMessage = null;
    this.newTxn(READ_WRITE, function(error, txn, segmentStore) {
      if (error) {
        if (DEBUG) debug(error);
        aCallback.notify(error, null);
        return;
      }

      txn.oncomplete = function(event) {
        if (DEBUG) debug("Transaction " + txn + " completed.");
        if (completeMessage) {
          // Rebuild full body
          if (completeMessage.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
            // Uint8Array doesn't have `concat`, so
            // we have to merge all segements by hand.
            let fullDataLen = 0;
            for (let i = 1; i <= completeMessage.segmentMaxSeq; i++) {
              fullDataLen += completeMessage.segments[i].length;
            }

            completeMessage.fullData = new Uint8Array(fullDataLen);
            for (let d = 0, i = 1; i <= completeMessage.segmentMaxSeq; i++) {
              let data = completeMessage.segments[i];
              for (let j = 0; j < data.length; j++) {
                completeMessage.fullData[d++] = data[j];
              }
            }
          } else {
            completeMessage.fullBody = completeMessage.segments.join("");
          }

          // Remove handy fields after completing the concatenation.
          delete completeMessage.id;
          delete completeMessage.hash;
          delete completeMessage.receivedSegments;
          delete completeMessage.segments;
        }
        aCallback.notify(Cr.NS_OK, completeMessage);
      };

      txn.onabort = function(event) {
        if (DEBUG) debug("transaction abort due to " + event.target.error.name);
        let error = (event.target.error.name === 'QuotaExceededError')
                    ? Cr.NS_ERROR_FILE_NO_DEVICE_SPACE
                    : Cr.NS_ERROR_FAILURE;
        aCallback.notify(error, null);
      };

      aSmsSegment.hash = aSmsSegment.sender + ":" +
                         aSmsSegment.segmentRef + ":" +
                         aSmsSegment.segmentMaxSeq + ":" +
                         aSmsSegment.iccId;
      let seq = aSmsSegment.segmentSeq;
      if (DEBUG) {
        debug("Saving SMS Segment: " + aSmsSegment.hash + ", seq: " + seq);
      }
      let getRequest = segmentStore.index("hash").get(aSmsSegment.hash);
      getRequest.onsuccess = function(event) {
        let segmentRecord = event.target.result;
        if (!segmentRecord) {
          if (DEBUG) {
            debug("Not found! Create a new record to store the segments.");
          }
          aSmsSegment.receivedSegments = 1;
          aSmsSegment.segments = [];
          if (aSmsSegment.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
            aSmsSegment.segments[seq] = aSmsSegment.data;
          } else {
            aSmsSegment.segments[seq] = aSmsSegment.body;
          }

          segmentStore.add(aSmsSegment);

          return;
        }

        if (DEBUG) {
          debug("Append SMS Segment into existed message object: " + segmentRecord.id);
        }

        if (segmentRecord.segments[seq]) {
          if (DEBUG) debug("Got duplicated segment no. " + seq);
          return;
        }

        segmentRecord.timestamp = aSmsSegment.timestamp;

        if (segmentRecord.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
          segmentRecord.segments[seq] = aSmsSegment.data;
        } else {
          segmentRecord.segments[seq] = aSmsSegment.body;
        }
        segmentRecord.receivedSegments++;

        // The port information is only available in 1st segment for CDMA WAP Push.
        // If the segments of a WAP Push are not received in sequence
        // (e.g., SMS with seq == 1 is not the 1st segment received by the device),
        // we have to retrieve the port information from 1st segment and
        // save it into the segmentRecord.
        if (aSmsSegment.teleservice === RIL.PDU_CDMA_MSG_TELESERIVCIE_ID_WAP
            && seq === 1) {
          if (aSmsSegment.originatorPort !== Ci.nsIGonkSmsService.SMS_APPLICATION_PORT_INVALID) {
            segmentRecord.originatorPort = aSmsSegment.originatorPort;
          }

          if (aSmsSegment.destinationPort !== Ci.nsIGonkSmsService.SMS_APPLICATION_PORT_INVALID) {
            segmentRecord.destinationPort = aSmsSegment.destinationPort;
          }
        }

        if (segmentRecord.receivedSegments < segmentRecord.segmentMaxSeq) {
          if (DEBUG) debug("Message is incomplete.");
          segmentStore.put(segmentRecord);
          return;
        }

        completeMessage = segmentRecord;

        // Delete Record in DB
        segmentStore.delete(segmentRecord.id);
      };
    }, [SMS_SEGMENT_STORE_NAME]);
  },

  /**
   * nsIMobileMessageDatabaseService API
   */

  getMessage: function(aMessageId, aRequest) {
    if (DEBUG) debug("Retrieving message with ID " + aMessageId);
    let self = this;
    let notifyCallback = {
      notify: function(aRv, aMessageRecord, aDomMessage) {
        if (Cr.NS_OK == aRv) {
          aRequest.notifyMessageGot(aDomMessage);
          return;
        }
        aRequest.notifyGetMessageFailed(
          self.translateCrErrorToMessageCallbackError(aRv), null);
      }
    };
    this.getMessageRecordById(aMessageId, notifyCallback);
  },

  deleteMessage: function(messageIds, length, aRequest) {
    if (DEBUG) debug("deleteMessage: message ids " + JSON.stringify(messageIds));
    let deleted = [];
    let self = this;
    this.newTxn(READ_WRITE, function(error, txn, stores) {
      if (error) {
        if (DEBUG) debug("deleteMessage: failed to open transaction");
        aRequest.notifyDeleteMessageFailed(
          self.translateCrErrorToMessageCallbackError(error));
        return;
      }

      let deletedInfo = { messageIds: [], threadIds: [] };

      txn.onabort = function(event) {
        if (DEBUG) debug("transaction abort due to " + event.target.error.name);
        let error = (event.target.error.name === 'QuotaExceededError')
                    ? Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR
                    : Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
        aRequest.notifyDeleteMessageFailed(error);
      };

      const messageStore = stores[0];
      const threadStore = stores[1];

      txn.oncomplete = function(event) {
        if (DEBUG) debug("Transaction " + txn + " completed.");
        aRequest.notifyMessageDeleted(deleted, length);
        self.notifyDeletedInfo(deletedInfo);
      };

      let threadsToUpdate = {};
      let numOfMessagesToDelete = length;
      let updateThreadInfo = function() {
        for (let threadId in threadsToUpdate) {
          let threadInfo = threadsToUpdate[threadId];
          self.updateThreadByMessageChange(messageStore,
                                           threadStore,
                                           threadInfo.threadId,
                                           threadInfo.removedMsgIds,
                                           threadInfo.ignoredUnreadCount,
                                           deletedInfo);
        }
      };

      for (let i = 0; i < length; i++) {
        let messageId = messageIds[i];
        deleted[i] = false;
        messageStore.get(messageId).onsuccess = function(messageIndex, event) {
          let messageRecord = event.target.result;
          let messageId = messageIds[messageIndex];
          if (messageRecord) {
            if (DEBUG) debug("Deleting message id " + messageId);

            // First actually delete the message.
            messageStore.delete(messageId).onsuccess = function(event) {
              if (DEBUG) debug("Message id " + messageId + " deleted");

              numOfMessagesToDelete--;
              deleted[messageIndex] = true;
              deletedInfo.messageIds.push(messageId);

              // Cache thread info to be updated.
              let threadId = messageRecord.threadId;
              if (!threadsToUpdate[threadId]) {
                threadsToUpdate[threadId] = {
                  threadId: threadId,
                  removedMsgIds: [messageId],
                  ignoredUnreadCount: (!messageRecord.read) ? 1 : 0
                };
              } else {
                let threadInfo = threadsToUpdate[threadId];
                threadInfo.removedMsgIds.push(messageId);
                if (!messageRecord.read) {
                  threadInfo.ignoredUnreadCount++;
                }
              }

              // After all messsages are deleted, update unread count and most
              // recent message of related threads at once.
              if (!numOfMessagesToDelete) {
                updateThreadInfo();
              }
            };
          } else {
            if (DEBUG) debug("Message id " + messageId + " does not exist");

            numOfMessagesToDelete--;
            if (!numOfMessagesToDelete) {
              updateThreadInfo();
            }
          }
        }.bind(null, i);
      }
    }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
  },

  createMessageCursor: function(aHasStartDate, aStartDate, aHasEndDate,
                                aEndDate, aNumbers, aNumbersCount, aDelivery,
                                aHasRead, aRead, aThreadId, aReverse, aCallback) {
    if (DEBUG) {
      debug("Creating a message cursor. Filters:" +
            " startDate: " + (aHasStartDate ? aStartDate : "(null)") +
            " endDate: " + (aHasEndDate ? aEndDate : "(null)") +
            " delivery: " + aDelivery +
            " numbers: " + (aNumbersCount ? aNumbers : "(null)") +
            " read: " + (aHasRead ? aRead : "(null)") +
            " threadId: " + aThreadId +
            " reverse: " + aReverse);
    }

    let filter = {};
    if (aHasStartDate) {
      filter.startDate = aStartDate;
    }
    if (aHasEndDate) {
      filter.endDate = aEndDate;
    }
    if (aNumbersCount) {
      filter.numbers = aNumbers.slice();
    }
    if (aDelivery !== null) {
      filter.delivery = aDelivery;
    }
    if (aHasRead) {
      filter.read = aRead;
    }
    if (aThreadId) {
      filter.threadId = aThreadId;
    }

    let cursor = new GetMessagesCursor(this, aCallback);

    let self = this;
    self.newTxn(READ_ONLY, function(error, txn, stores) {
      let collector = cursor.collector.idCollector;
      let collect = collector.collect.bind(collector);
      FilterSearcherHelper.transact(self, txn, error, filter, aReverse, collect);
    }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME]);

    return cursor;
  },

  markMessageRead: function(messageId, value, aSendReadReport, aRequest) {
    if (DEBUG) debug("Setting message " + messageId + " read to " + value);
    let self = this;
    this.newTxn(READ_WRITE, function(error, txn, stores) {
      if (error) {
        if (DEBUG) debug(error);
        aRequest.notifyMarkMessageReadFailed(
          self.translateCrErrorToMessageCallbackError(error));
        return;
      }

      txn.onabort = function(event) {
        if (DEBUG) debug("transaction abort due to " + event.target.error.name);
        let error = (event.target.error.name === 'QuotaExceededError')
                    ? Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR
                    : Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
        aRequest.notifyMarkMessageReadFailed(error);
      };

      let messageStore = stores[0];
      let threadStore = stores[1];
      messageStore.get(messageId).onsuccess = function(event) {
        let messageRecord = event.target.result;
        if (!messageRecord) {
          if (DEBUG) debug("Message ID " + messageId + " not found");
          aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR);
          return;
        }

        if (messageRecord.id != messageId) {
          if (DEBUG) {
            debug("Retrieve message ID (" + messageId + ") is " +
                  "different from the one we got");
          }
          aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.UNKNOWN_ERROR);
          return;
        }

        // If the value to be set is the same as the current message `read`
        // value, we just notify successfully.
        if (messageRecord.read == value) {
          if (DEBUG) debug("The value of messageRecord.read is already " + value);
          aRequest.notifyMessageMarkedRead(messageRecord.read);
          return;
        }

        messageRecord.read = value ? FILTER_READ_READ : FILTER_READ_UNREAD;
        messageRecord.readIndex = [messageRecord.read, messageRecord.timestamp];
        let readReportMessageId, readReportTo;
        if (aSendReadReport &&
            messageRecord.type == "mms" &&
            messageRecord.delivery == DELIVERY_RECEIVED &&
            messageRecord.read == FILTER_READ_READ &&
            messageRecord.headers["x-mms-read-report"] &&
            !messageRecord.isReadReportSent) {
          messageRecord.isReadReportSent = true;

          let from = messageRecord.headers["from"];
          readReportTo = from && from.address;
          readReportMessageId = messageRecord.headers["message-id"];
        }

        if (DEBUG) debug("Message.read set to: " + value);
        messageStore.put(messageRecord).onsuccess = function(event) {
          if (DEBUG) {
            debug("Update successfully completed. Message: " +
                  JSON.stringify(event.target.result));
          }

          // Now update the unread count.
          let threadId = messageRecord.threadId;

          threadStore.get(threadId).onsuccess = function(event) {
            let threadRecord = event.target.result;
            threadRecord.unreadCount += value ? -1 : 1;
            if (DEBUG) {
              debug("Updating unreadCount for thread id " + threadId + ": " +
                    (value ?
                     threadRecord.unreadCount + 1 :
                     threadRecord.unreadCount - 1) +
                     " -> " + threadRecord.unreadCount);
            }
            threadStore.put(threadRecord).onsuccess = function(event) {
              if(readReportMessageId && readReportTo) {
                gMMSService.sendReadReport(readReportMessageId,
                                           readReportTo,
                                           messageRecord.iccId);
              }
              aRequest.notifyMessageMarkedRead(messageRecord.read);
            };
          };
        };
      };
    }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
  },

  createThreadCursor: function(callback) {
    if (DEBUG) debug("Getting thread list");

    let cursor = new GetThreadsCursor(this, callback);
    this.newTxn(READ_ONLY, function(error, txn, threadStore) {
      let collector = cursor.collector.idCollector;
      if (error) {
        collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
        return;
      }
      txn.onerror = function(event) {
        if (DEBUG) debug("Caught error on transaction ", event.target.error.name);
        collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
      };
      let request = threadStore.index("lastTimestamp").openKeyCursor(null, PREV);
      request.onsuccess = function(event) {
        let cursor = event.target.result;
        if (cursor) {
          if (collector.collect(txn, cursor.primaryKey, cursor.key)) {
            cursor.continue();
          }
        } else {
          collector.collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
        }
      };
    }, [THREAD_STORE_NAME]);

    return cursor;
  }
};

let FilterSearcherHelper = {

  /**
   * @param index
   *        The name of a message store index to filter on.
   * @param range
   *        A IDBKeyRange.
   * @param direction
   *        NEXT or PREV.
   * @param txn
   *        Ongoing IDBTransaction context object.
   * @param collect
   *        Result colletor function. It takes three parameters -- txn, message
   *        id, and message timestamp.
   */
  filterIndex: function(index, range, direction, txn, collect) {
    let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
    let request = messageStore.index(index).openKeyCursor(range, direction);
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      // Once the cursor has retrieved all keys that matches its key range,
      // the filter search is done.
      if (cursor) {
        let timestamp = Array.isArray(cursor.key) ? cursor.key[1] : cursor.key;
        if (collect(txn, cursor.primaryKey, timestamp)) {
          cursor.continue();
        }
      } else {
        collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
      }
    };
    request.onerror = function(event) {
      if (DEBUG && event) debug("IDBRequest error " + event.target.error.name);
      collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
    };
  },

  /**
   * Explicitly fiter message on the timestamp index.
   *
   * @param startDate
   *        Timestamp of the starting date.
   * @param endDate
   *        Timestamp of the ending date.
   * @param direction
   *        NEXT or PREV.
   * @param txn
   *        Ongoing IDBTransaction context object.
   * @param collect
   *        Result colletor function. It takes three parameters -- txn, message
   *        id, and message timestamp.
   */
  filterTimestamp: function(startDate, endDate, direction, txn, collect) {
    let range = null;
    if (startDate != null && endDate != null) {
      range = IDBKeyRange.bound(startDate, endDate);
    } else if (startDate != null) {
      range = IDBKeyRange.lowerBound(startDate);
    } else if (endDate != null) {
      range = IDBKeyRange.upperBound(endDate);
    }
    this.filterIndex("timestamp", range, direction, txn, collect);
  },

  /**
   * Instanciate a filtering transaction.
   *
   * @param mmdb
   *        A MobileMessageDB.
   * @param txn
   *        Ongoing IDBTransaction context object.
   * @param error
   *        Previous error while creating the transaction.
   * @param filter
   *        A MobileMessageFilter dictionary.
   * @param reverse
   *        A boolean value indicating whether we should filter message in
   *        reversed order.
   * @param collect
   *        Result colletor function. It takes three parameters -- txn, message
   *        id, and message timestamp.
   */
  transact: function(mmdb, txn, error, filter, reverse, collect) {
    if (error) {
      // TODO look at event.target.error.name, pick appropriate error constant.
      if (DEBUG) debug("IDBRequest error " + event.target.error.name);
      collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
      return;
    }

    let direction = reverse ? PREV : NEXT;

    // We support filtering by date range only (see `else` block below) or by
    // number/delivery status/read status with an optional date range.
    if (filter.delivery == null &&
        filter.numbers == null &&
        filter.read == null &&
        filter.threadId == null) {
      // Filtering by date range only.
      if (DEBUG) {
        debug("filter.timestamp " + filter.startDate + ", " + filter.endDate);
      }

      this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
                           collect);
      return;
    }

    // Numeric 0 is smaller than any time stamp, and empty string is larger
    // than all numeric values.
    let startDate = 0, endDate = "";
    if (filter.startDate != null) {
      startDate = filter.startDate;
    }
    if (filter.endDate != null) {
      endDate = filter.endDate;
    }

    let single, intersectionCollector;
    {
      let num = 0;
      if (filter.delivery) num++;
      if (filter.numbers) num++;
      if (filter.read != undefined) num++;
      if (filter.threadId != undefined) num++;
      single = (num == 1);
    }

    if (!single) {
      intersectionCollector = new IntersectionResultsCollector(collect, reverse);
    }

    // Retrieve the keys from the 'delivery' index that matches the value of
    // filter.delivery.
    if (filter.delivery) {
      if (DEBUG) debug("filter.delivery " + filter.delivery);
      let delivery = filter.delivery;
      let range = IDBKeyRange.bound([delivery, startDate], [delivery, endDate]);
      this.filterIndex("delivery", range, direction, txn,
                       single ? collect : intersectionCollector.newContext());
    }

    // Retrieve the keys from the 'read' index that matches the value of
    // filter.read.
    if (filter.read != undefined) {
      if (DEBUG) debug("filter.read " + filter.read);
      let read = filter.read ? FILTER_READ_READ : FILTER_READ_UNREAD;
      let range = IDBKeyRange.bound([read, startDate], [read, endDate]);
      this.filterIndex("read", range, direction, txn,
                       single ? collect : intersectionCollector.newContext());
    }

    // Retrieve the keys from the 'threadId' index that matches the value of
    // filter.threadId.
    if (filter.threadId != undefined) {
      if (DEBUG) debug("filter.threadId " + filter.threadId);
      let threadId = filter.threadId;
      let range = IDBKeyRange.bound([threadId, startDate], [threadId, endDate]);
      this.filterIndex("threadId", range, direction, txn,
                       single ? collect : intersectionCollector.newContext());
    }

    // Retrieve the keys from the 'sender' and 'receiver' indexes that
    // match the values of filter.numbers
    if (filter.numbers) {
      if (DEBUG) debug("filter.numbers " + filter.numbers.join(", "));

      if (!single) {
        collect = intersectionCollector.newContext();
      }

      let participantStore = txn.objectStore(PARTICIPANT_STORE_NAME);
      let typedAddresses = filter.numbers.map(function(number) {
        return {
          address: number,
          type: MMS.Address.resolveType(number)
        };
      });
      mmdb.findParticipantIdsByTypedAddresses(participantStore, typedAddresses,
                                              false, true,
                                              (function(participantIds) {
        if (!participantIds || !participantIds.length) {
          // Oops! No such participant at all.

          collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
          return;
        }

        if (participantIds.length == 1) {
          let id = participantIds[0];
          let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
          this.filterIndex("participantIds", range, direction, txn, collect);
          return;
        }

        let unionCollector = new UnionResultsCollector(collect);

        this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
                             unionCollector.newTimestampContext());

        for (let i = 0; i < participantIds.length; i++) {
          let id = participantIds[i];
          let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
          this.filterIndex("participantIds", range, direction, txn,
                           unionCollector.newContext());
        }
      }).bind(this));
    }
  }
};

/**
 * Collector class for read-ahead result objects. Mmdb may now try to fetch
 * message/thread records before it's requested explicitly.
 *
 * The read ahead behavior can be controlled by an integer mozSettings entry
 * "ril.sms.maxReadAheadEntries" as well as an integer holding preference
 * "dom.sms.maxReadAheadEntries". The meanings are:
 *
 *   positive: finite read-ahead entries,
 *   0: don't read ahead unless explicitly requested, (default)
 *   negative: read ahead all IDs if possible.
 *
 * The order of ID filtering objects are now:
 *
 *   [UnionResultsCollector]
 *   +-> [IntersectionResultsCollector]
 *       +-> IDsCollector
 *           +-> ResultsCollector
 *
 * ResultsCollector has basically similar behaviour with IDsCollector. When
 * RC::squeeze() is called, either RC::drip() is called instantly if we have
 * already fetched results available, or the request is kept and IC::squeeze()
 * is called.
 *
 * When RC::collect is called by IC::drip, it proceeds to fetch the
 * corresponding record given that collected ID is neither an error nor an end
 * mark. After the message/thread record being fetched, ResultsCollector::drip
 * is called if we have pending request. Anyway, RC::maybeSqueezeIdCollector is
 * called to determine whether we need to call IC::squeeze again.
 *
 * RC::squeeze is called when nsICursorContinueCallback::handleContinue() is
 * called. ResultsCollector::drip will call to
 * nsIMobileMessageCursorCallback::notifyFoo.
 *
 * In summary, the major call paths are:
 *
 *   RC::squeeze
 *   o-> RC::drip
 *       +-> RC::notifyCallback
 *           +-> nsIMobileMessageCursorCallback::notifyFoo
 *   +-> RC::maybeSqueezeIdCollector
 *       o-> IC::squeeze
 *           o-> IC::drip
 *               +-> RC::collect
 *                   o-> RC::readAhead
 *                       +-> RC::notifyResult
 *                           o-> RC::drip ...
 *                           +-> RC::maybeSqueezeIdCollector ...
 *                   o-> RC::notifyResult ...
 */
function ResultsCollector(readAheadFunc) {
  this.idCollector = new IDsCollector();
  this.results = [];
  this.readAhead = readAheadFunc;

  this.maxReadAhead = DEFAULT_READ_AHEAD_ENTRIES;
  try {
    // positive: finite read-ahead entries,
    // 0: don't read ahead unless explicitly requested,
    // negative: read ahead all IDs if possible.
    this.maxReadAhead =
      Services.prefs.getIntPref("dom.sms.maxReadAheadEntries");
  } catch (e) {}
}
ResultsCollector.prototype = {
  /**
   * Underlying ID collector object.
   */
  idCollector: null,

  /**
   * An array keeping fetched result objects. Replaced by a new empty array
   * every time when |this.drip| is called.
   */
  results: null,

  /**
   * A function that takes (<txn>, <id>, <collector>). It fetches the object
   * specified by <id> and notify <collector> with that by calling
   * |<collector>.notifyResult()|. If <txn> is null, this function should
   * create a new read-only transaction itself. The returned result object may
   * be null to indicate an error during the fetch process.
   */
  readAhead: null,

  /**
   * A boolean value inidicating a readAhead call is ongoing. Set before calling
   * |this.readAhead| and reset in |this.notifyResult|.
   */
  readingAhead: false,

  /**
   * A numeric value read from preference "dom.sms.maxReadAheadEntries".
   */
  maxReadAhead: 0,

  /**
   * An active IDBTransaction object to be reused.
   */
  activeTxn: null,

  /**
   * A nsIMobileMessageCursorCallback.
   */
  requestWaiting: null,

  /**
   * A boolean value indicating either a COLLECT_ID_END or COLLECT_ID_ERROR has
   * been received.
   */
  done: false,

  /**
   * When |this.done|, it's either COLLECT_ID_END or COLLECT_ID_ERROR.
   */
  lastId: null,

  /**
   * Receive collected id from IDsCollector and fetch the correspond result
   * object if necessary.
   *
   * @param txn
   *        An IDBTransaction object. Null if there is no active transaction in
   *        IDsCollector. That is, the ID collecting transaction is completed.
   * @param id
   *        A positive numeric id, COLLECT_ID_END(0), or COLLECT_ID_ERROR(-1).
   */
  collect: function(txn, id) {
    if (this.done) {
      // If this callector has been terminated because of previous errors in
      // |this.readAhead|, ignore any further IDs from IDsCollector.
      return;
    }

    if (DEBUG) debug("ResultsCollector::collect ID = " + id);

    // Reuse the active transaction cached if IDsCollector has no active
    // transaction.
    txn = txn || this.activeTxn;

    if (id > 0) {
      this.readingAhead = true;
      this.readAhead(txn, id, this);
    } else {
      this.notifyResult(txn, id, null);
    }
  },

  /**
   * Callback function for |this.readAhead|.
   *
   * This function pushes result object to |this.results| or updates
   * |this.done|, |this.lastId| if an end mark or an error is found. Since we
   * have already a valid result entry, check |this.requestWaiting| and deal
   * with it. At last, call to |this.maybeSqueezeIdCollector| to ask more id
   * again if necessary.
   *
   * @param txn
   *        An IDBTransaction object. Null if caller has no active transaction.
   * @param id
   *        A positive numeric id, COLLECT_ID_END(0), or COLLECT_ID_ERROR(-1).
   * @param result
   *        An object associated with id. Null if |this.readAhead| failed.
   */
  notifyResult: function(txn, id, result) {
    if (DEBUG) debug("notifyResult(txn, " + id + ", <result>)");

    this.readingAhead = false;

    if (id > 0) {
      if (result != null) {
        this.results.push(result);
      } else {
        id = COLLECT_ID_ERROR;
      }
    }

    if (id <= 0) {
      this.lastId = id;
      this.done = true;
    }

    if (!this.requestWaiting) {
      if (DEBUG) debug("notifyResult: cursor.continue() not called yet");
    } else {
      let callback = this.requestWaiting;
      this.requestWaiting = null;

      this.drip(callback);
    }

    this.maybeSqueezeIdCollector(txn);
  },

  /**
   * Request for one more ID if necessary.
   *
   * @param txn
   *        An IDBTransaction object. Null if caller has no active transaction.
   */
  maybeSqueezeIdCollector: function(txn) {
    if (this.done || // Nothing to be read.
        this.readingAhead || // Already in progress.
        this.idCollector.requestWaiting) { // Already requested.
      return;
    }

    let max = this.maxReadAhead;
    if (!max && this.requestWaiting) {
      // If |this.requestWaiting| is set, try to read ahead at least once.
      max = 1;
    }
    if (max >= 0 && this.results.length >= max) {
      // More-equal than <max> entries has been read. Stop.
      if (DEBUG) debug("maybeSqueezeIdCollector: max " + max + " entries read. Stop.");
      return;
    }

    // A hack to pass current txn to |this.collect| when it's called directly by
    // |IDsCollector.squeeze|.
    this.activeTxn = txn;
    this.idCollector.squeeze(this.collect.bind(this));
    this.activeTxn = null;
  },

  /**
   * Request to pass available results or wait.
   *
   * @param callback
   *        A nsIMobileMessageCursorCallback.
   */
  squeeze: function(callback) {
    if (this.requestWaiting) {
      throw new Error("Already waiting for another request!");
    }

    if (this.results.length || this.done) {
      // If |this.results.length| is non-zero, we have already some results to
      // pass. Otherwise, if |this.done| evaluates to true, we have also a
      // confirmed result to pass.
      this.drip(callback);
    } else {
      this.requestWaiting = callback;
    }

    // If we called |this.drip| in the last step, the fetched results have been
    // consumed and we should ask some more for read-ahead now.
    //
    // Otherwise, kick start read-ahead again because it may be stopped
    // previously because of |this.maxReadAhead| had been reached.
    this.maybeSqueezeIdCollector(null);
  },

  /**
   * Consume fetched resutls.
   *
   * @param callback
   *        A nsIMobileMessageCursorCallback.
   */
  drip: function(callback) {
    let results = this.results;
    this.results = [];

    let func = this.notifyCallback.bind(this, callback, results, this.lastId);
    Services.tm.currentThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
  },

  /**
   * Notify a nsIMobileMessageCursorCallback.
   *
   * @param callback
   *        A nsIMobileMessageCursorCallback.
   * @param results
   *        An array of result objects.
   * @param lastId
   *        Since we only call |this.drip| when either there are results
   *        available or the read-ahead has done, so lastId here will be
   *        COLLECT_ID_END or COLLECT_ID_ERROR when results is empty and null
   *        otherwise.
   */
  notifyCallback: function(callback, results, lastId) {
    if (DEBUG) {
      debug("notifyCallback(results[" + results.length + "], " + lastId + ")");
    }

    if (results.length) {
      callback.notifyCursorResult(results, results.length);
    } else if (lastId == COLLECT_ID_END) {
      callback.notifyCursorDone();
    } else {
      callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
    }
  }
};

function IDsCollector() {
  this.results = [];
  this.done = false;
}
IDsCollector.prototype = {
  results: null,
  requestWaiting: null,
  done: null,

  /**
   * Queue up passed id, reply if necessary.
   *
   * @param txn
   *        Ongoing IDBTransaction context object.
   * @param id
   *        COLLECT_ID_END(0) for no more results, COLLECT_ID_ERROR(-1) for
   *        errors and valid otherwise.
   * @param timestamp
   *        We assume this function is always called in timestamp order. So
   *        this parameter is actually unused.
   *
   * @return true if expects more. false otherwise.
   */
  collect: function(txn, id, timestamp) {
    if (this.done) {
      return false;
    }

    if (DEBUG) debug("IDsCollector::collect ID = " + id);
    // Queue up any id.
    this.results.push(id);
    if (id <= 0) {
      // No more processing on '0' or negative values passed.
      this.done = true;
    }

    if (!this.requestWaiting) {
      if (DEBUG) debug("IDsCollector::squeeze() not called yet");
      return !this.done;
    }

    // We assume there is only one request waiting throughout the message list
    // retrieving process. So we don't bother continuing to process further
    // waiting requests here. This assumption comes from DOMCursor::Continue()
    // implementation.
    let callback = this.requestWaiting;
    this.requestWaiting = null;

    this.drip(txn, callback);

    return !this.done;
  },

  /**
   * Callback right away with the first queued result entry if the filtering is
   * done. Or queue up the request and callback when a new entry is available.
   *
   * @param callback
   *        A callback function that accepts a numeric id.
   */
  squeeze: function(callback) {
    if (this.requestWaiting) {
      throw new Error("Already waiting for another request!");
    }

    if (!this.done) {
      // Database transaction ongoing, let it reply for us so that we won't get
      // blocked by the existing transaction.
      this.requestWaiting = callback;
      return;
    }

    this.drip(null, callback);
  },

  /**
   * @param txn
   *        Ongoing IDBTransaction context object or null.
   * @param callback
   *        A callback function that accepts a numeric id.
   */
  drip: function(txn, callback) {
    let firstId = this.results[0];
    if (firstId > 0) {
      this.results.shift();
    }
    callback(txn, firstId);
  }
};

function IntersectionResultsCollector(collect, reverse) {
  this.cascadedCollect = collect;
  this.reverse = reverse;
  this.contexts = [];
}
IntersectionResultsCollector.prototype = {
  cascadedCollect: null,
  reverse: false,
  contexts: null,

  /**
   * Queue up {id, timestamp} pairs, find out intersections and report to
   * |cascadedCollect|. Return true if it is still possible to have another match.
   */
  collect: function(contextIndex, txn, id, timestamp) {
    if (DEBUG) {
      debug("IntersectionResultsCollector: "
            + contextIndex + ", " + id + ", " + timestamp);
    }

    let contexts = this.contexts;
    let context = contexts[contextIndex];

    if (id < 0) {
      // Act as no more matched records.
      id = 0;
    }
    if (!id) {
      context.done = true;

      if (!context.results.length) {
        // Already empty, can't have further intersection results.
        return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
      }

      for (let i = 0; i < contexts.length; i++) {
        if (!contexts[i].done) {
          // Don't call |this.cascadedCollect| because |context.results| might not
          // be empty, so other contexts might still have a chance here.
          return false;
        }
      }

      // It was the last processing context and is no more processing.
      return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
    }

    // Search id in other existing results. If no other results has it,
    // and A) the last timestamp is smaller-equal to current timestamp,
    // we wait for further results; either B) record timestamp is larger
    // then current timestamp or C) no more processing for a filter, then we
    // drop this id because there can't be a match anymore.
    for (let i = 0; i < contexts.length; i++) {
      if (i == contextIndex) {
        continue;
      }

      let ctx = contexts[i];
      let results = ctx.results;
      let found = false;
      for (let j = 0; j < results.length; j++) {
        let result = results[j];
        if (result.id == id) {
          found = true;
          break;
        }
        if ((!this.reverse && (result.timestamp > timestamp)) ||
            (this.reverse && (result.timestamp < timestamp))) {
          // B) Cannot find a match anymore. Drop.
          return true;
        }
      }

      if (!found) {
        if (ctx.done) {
          // C) Cannot find a match anymore. Drop.
          if (results.length) {
            let lastResult = results[results.length - 1];
            if ((!this.reverse && (lastResult.timestamp >= timestamp)) ||
                (this.reverse && (lastResult.timestamp <= timestamp))) {
              // Still have a chance to get another match. Return true.
              return true;
            }
          }

          // Impossible to find another match because all results in ctx have
          // timestamps smaller than timestamp.
          context.done = true;
          return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
        }

        // A) Pending.
        context.results.push({
          id: id,
          timestamp: timestamp
        });
        return true;
      }
    }

    // Now id is found in all other results. Report it.
    return this.cascadedCollect(txn, id, timestamp);
  },

  newContext: function() {
    let contextIndex = this.contexts.length;
    this.contexts.push({
      results: [],
      done: false
    });
    return this.collect.bind(this, contextIndex);
  }
};

function UnionResultsCollector(collect) {
  this.cascadedCollect = collect;
  this.contexts = [{
    // Timestamp.
    processing: 1,
    results: []
  }, {
    processing: 0,
    results: []
  }];
}
UnionResultsCollector.prototype = {
  cascadedCollect: null,
  contexts: null,

  collect: function(contextIndex, txn, id, timestamp) {
    if (DEBUG) {
      debug("UnionResultsCollector: "
            + contextIndex + ", " + id + ", " + timestamp);
    }

    let contexts = this.contexts;
    let context = contexts[contextIndex];

    if (id < 0) {
      // Act as no more matched records.
      id = 0;
    }
    if (id) {
      if (!contextIndex) {
        // Timestamp.
        context.results.push({
          id: id,
          timestamp: timestamp
        });
      } else {
        context.results.push(id);
      }
      return true;
    }

    context.processing -= 1;
    if (contexts[0].processing || contexts[1].processing) {
      // At least one queue is still processing, but we got here because
      // current cursor gives 0 as id meaning no more messages are
      // available. Return false here to stop further cursor.continue() calls.
      return false;
    }

    let tres = contexts[0].results;
    let qres = contexts[1].results;
    tres = tres.filter(function(element) {
      return qres.indexOf(element.id) != -1;
    });

    for (let i = 0; i < tres.length; i++) {
      this.cascadedCollect(txn, tres[i].id, tres[i].timestamp);
    }
    this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);

    return false;
  },

  newTimestampContext: function() {
    return this.collect.bind(this, 0);
  },

  newContext: function() {
    this.contexts[1].processing++;
    return this.collect.bind(this, 1);
  }
};

function GetMessagesCursor(mmdb, callback) {
  this.mmdb = mmdb;
  this.callback = callback;
  this.collector = new ResultsCollector(this.getMessage.bind(this));

  this.handleContinue(); // Trigger first run.
}
GetMessagesCursor.prototype = {
  classID: RIL_GETMESSAGESCURSOR_CID,
  QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),

  mmdb: null,
  callback: null,
  collector: null,

  getMessageTxn: function(txn, messageStore, messageId, collector) {
    if (DEBUG) debug ("Fetching message " + messageId);

    let getRequest = messageStore.get(messageId);
    let self = this;
    getRequest.onsuccess = function(event) {
      if (DEBUG) {
        debug("notifyNextMessageInListGot - messageId: " + messageId);
      }
      let domMessage =
        self.mmdb.createDomMessageFromRecord(event.target.result);
      collector.notifyResult(txn, messageId, domMessage);
    };
    getRequest.onerror = function(event) {
      // Error reporting is done in ResultsCollector.notifyCallback.
      event.stopPropagation();
      event.preventDefault();

      if (DEBUG) {
        debug("notifyCursorError - messageId: " + messageId);
      }
      collector.notifyResult(txn, messageId, null);
    };
  },

  getMessage: function(txn, messageId, collector) {
    // When filter transaction is not yet completed, we're called with current
    // ongoing transaction object.
    if (txn) {
      let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
      this.getMessageTxn(txn, messageStore, messageId, collector);
      return;
    }

    // Or, we have to open another transaction ourselves.
    let self = this;
    this.mmdb.newTxn(READ_ONLY, function(error, txn, messageStore) {
      if (error) {
        debug("getMessage: failed to create new transaction");
        collector.notifyResult(null, messageId, null);
      } else {
        self.getMessageTxn(txn, messageStore, messageId, collector);
      }
    }, [MESSAGE_STORE_NAME]);
  },

  // nsICursorContinueCallback

  handleContinue: function() {
    if (DEBUG) debug("Getting next message in list");
    this.collector.squeeze(this.callback);
  }
};

function GetThreadsCursor(mmdb, callback) {
  this.mmdb = mmdb;
  this.callback = callback;
  this.collector = new ResultsCollector(this.getThread.bind(this));

  this.handleContinue(); // Trigger first run.
}
GetThreadsCursor.prototype = {
  classID: RIL_GETTHREADSCURSOR_CID,
  QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),

  mmdb: null,
  callback: null,
  collector: null,

  getThreadTxn: function(txn, threadStore, threadId, collector) {
    if (DEBUG) debug ("Fetching thread " + threadId);

    let getRequest = threadStore.get(threadId);
    getRequest.onsuccess = function(event) {
      let threadRecord = event.target.result;
      if (DEBUG) {
        debug("notifyCursorResult: " + JSON.stringify(threadRecord));
      }
      let thread =
        gMobileMessageService.createThread(threadRecord.id,
                                           threadRecord.participantAddresses,
                                           threadRecord.lastTimestamp,
                                           threadRecord.lastMessageSubject || "",
                                           threadRecord.body,
                                           threadRecord.unreadCount,
                                           threadRecord.lastMessageType);
      collector.notifyResult(txn, threadId, thread);
    };
    getRequest.onerror = function(event) {
      // Error reporting is done in ResultsCollector.notifyCallback.
      event.stopPropagation();
      event.preventDefault();

      if (DEBUG) {
        debug("notifyCursorError - threadId: " + threadId);
      }
      collector.notifyResult(txn, threadId, null);
    };
  },

  getThread: function(txn, threadId, collector) {
    // When filter transaction is not yet completed, we're called with current
    // ongoing transaction object.
    if (txn) {
      let threadStore = txn.objectStore(THREAD_STORE_NAME);
      this.getThreadTxn(txn, threadStore, threadId, collector);
      return;
    }

    // Or, we have to open another transaction ourselves.
    let self = this;
    this.mmdb.newTxn(READ_ONLY, function(error, txn, threadStore) {
      if (error) {
        collector.notifyResult(null, threadId, null);
      } else {
        self.getThreadTxn(txn, threadStore, threadId, collector);
      }
    }, [THREAD_STORE_NAME]);
  },

  // nsICursorContinueCallback

  handleContinue: function() {
    if (DEBUG) debug("Getting next thread in list");
    this.collector.squeeze(this.callback);
  }
}

this.EXPORTED_SYMBOLS = [
  'MobileMessageDB'
];

function debug() {
  dump("MobileMessageDB: " + Array.slice(arguments).join(" ") + "\n");
}