/* 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/. */ /* * This file is documented in JSDoc format. To generate the document: * * 1. Follow the instruction of JSDoc project to install it. See * https://github.com/jsdoc3/jsdoc for details. * * 2. Since JSDoc does not recognize ES6 syntax and XPCOM components, you should * enable the "commentsOnly" plugin in your conf.json to strip all code out * before generating the document. You'll need to change source.includePattern * as well to include "*.jsm" since it's not included by default. Here's a * minimal example of conf.json you need: * * { * "source": { * "includePattern": ".+\\.js(m)?$" * }, * "plugins": ["plugins/commentsOnly"] * } * * 3. Run jsdoc: * * $ jsdoc -c -d MobileMessageDB.jsm */ "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; /** * @typedef {string} MobileMessageDB.MESSAGE_STORE_NAME * * The name of the object store for messages. */ const MESSAGE_STORE_NAME = "sms"; /** * @typedef {string} MobileMessageDB.THREAD_STORE_NAME * * The name of the object store for threads. */ const THREAD_STORE_NAME = "thread"; /** * @typedef {string} MobileMessageDB.PARTICIPANT_STORE_NAME * * The name of the object store for participants. */ const PARTICIPANT_STORE_NAME = "participant"; /** * @typedef {string} MobileMessageDB.MOST_RECENT_STORE_NAME * @deprecated */ const MOST_RECENT_STORE_NAME = "most-recent"; /** * @typedef {string} MobileMessageDB.SMS_SEGMENT_STORE_NAME * * The name of the object store for incoming SMS segments. */ 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; }); /** * @typedef {Object} MobileMessageDB.MessageRecord * * Represents a SMS or MMS message. * *
 * +--------------------------------------------------------------------------+
 * | MessageRecord                                                            |
 * +--------------------------------------------------------------------------+
 * | id: Number (primary-key)                                                 |
 * |                                                                          |
 * | [SMS / MMS Common]                                                       |
 * | type: String                                                             |
 * | read: Number // Works as boolean, only use 0 or 1.                       |
 * | iccId: String                                                            |
 * | sender: String                                                           |
 * | delivery: String                                                         |
 * | timestamp: Number                                                        |
 * | sentTimestamp: Number                                                    |
 * |                                                                          |
 * | [Database Foreign Keys]                                                  |
 * | threadId: Number                                                         |
 * |                                                                          |
 * | [Common Indices]                                                         |
 * | threadIdIndex: Array // [threadId, timestamp]                            |
 * | deliveryIndex: Array // [delivery, timestamp]                            |
 * | readIndex: Array // [read, timpstamp]                                    |
 * | participantIdsIndex: Array of Array // [[participantId, timestamp], ...] |
 * |                                                                          |
 * | [SMS / Common Fields]                                                    |
 * | pid: Number                                                              |
 * | SMSC: String                                                             |
 * | receiver: String                                                         |
 * | encoding: Number                                                         |
 * | messageType: Number                                                      |
 * | teleservice: Number                                                      |
 * | messageClass: String                                                     |
 * | deliveryStatus: String                                                   |
 * | deliveryTimestamp: Number                                                |
 * |                                                                          |
 * | [SMS / Application Port Info]                                            |
 * | originatorPort: Number                                                   |
 * | destinationPort: Number                                                  |
 * |                                                                          |
 * | [SMS / MWI status]                                                       |
 * | mwiPresent: Boolean                                                      |
 * | mwiDiscard: Boolean                                                      |
 * | mwiMsgCount: Number                                                      |
 * | mwiActive: Boolean                                                       |
 * |                                                                          |
 * | [SMS / Message Body]                                                     |
 * | data: Array of Uint8 (available if it's 8bit encoding)                   |
 * | body: String (normal text body)                                          |
 * | fullBody: String                                                         |
 * |                                                                          |
 * | [SMS / CDMA Cellbroadcast Related]                                       |
 * | serviceCategory: Number                                                  |
 * | language: String                                                         |
 * |                                                                          |
 * | [MMS Info]                                                               |
 * | receivers: Array of String                                               |
 * | phoneNumber: String                                                      |
 * | transactionIdIndex: String                                               |
 * | envelopeIdIndex: String                                                  |
 * | isReadReportSent: Boolean                                                |
 * | deliveryInfo: Array of {                                                 |
 * |   receiver: String                                                       |
 * |   deliveryStatus: String                                                 |
 * |   deliveryTimestamp: Number                                              |
 * |   readStatus: String                                                     |
 * |   readTimestamp: Number                                                  |
 * | }                                                                        |
 * | headers: {                                                               |
 * |   x-mms-message-type: Number                                             |
 * |   x-mms-transaction-id: String                                           |
 * |   x-mms-mms-version: Number                                              |
 * |   from: {                                                                |
 * |     address: String                                                      |
 * |     type: String                                                         |
 * |   }                                                                      |
 * |   subject: String                                                        |
 * |   x-mms-message-class: String                                            |
 * |   x-mms-message-size: Number                                             |
 * |   x-mms-expiry: Number                                                   |
 * |   x-mms-content-location: {                                              |
 * |     uri: String                                                          |
 * |   }                                                                      |
 * |   to: Array of {                                                         |
 * |     address: String                                                      |
 * |     type: String                                                         |
 * |   }                                                                      |
 * |   x-mms-read-report: Boolean                                             |
 * |   x-mms-priority: Number                                                 |
 * |   message-id: String                                                     |
 * |   date: String                                                           |
 * |   x-mms-delivery-report: Boolean                                         |
 * |   content-type: {                                                        |
 * |     media: String                                                        |
 * |     params: {                                                            |
 * |       type: String                                                       |
 * |       start: String                                                      |
 * |     }                                                                    |
 * |   }                                                                      |
 * | }                                                                        |
 * | parts: Array of {                                                        |
 * |    index: Number                                                         |
 * |    headers: {                                                            |
 * |      content-type: {                                                     |
 * |        media: String                                                     |
 * |        params: {                                                         |
 * |          name: String                                                    |
 * |          charset: {                                                      |
 * |            charset: String                                               |
 * |          }                                                               |
 * |        }                                                                 |
 * |      content-length: Number                                              |
 * |      content-location: String                                            |
 * |      content-id: String                                                  |
 * |    }                                                                     |
 * |    content: String                                                       |
 * | }                                                                        |
 * +--------------------------------------------------------------------------+
 * 
*/ /** * @typedef {Object} MobileMessageDB.ThreadRecord * * Represents a message thread. * *
 * +---------------------------------------+
 * | ThreadRecord                          |
 * +---------------------------------------+
 * | id: Number (primary-key)              |
 * | participantIds: Array of Number       |
 * | participantAddresses: Array of String |
 * | lastMessageId: Number                 |
 * | lastTimestamp: Number                 |
 * | unreadCount: Number                   |
 * | lastMessageType: String               |
 * |                                       |
 * | [SMS Only]                            |
 * | body: String                          |
 * |                                       |
 * | [MMS Only]                            |
 * | lastMessageSubject: String            |
 * +---------------------------------------+
 * 
*/ /** * @typedef {Object} MobileMessageDB.ParticipantRecord * * Represents the mapping of a participant and one or multiple addresses. * (National and Int'l numbers) * *
 * +----------------------------+
 * | ParticipantRecord          |
 * +----------------------------+
 * | id: Number (primary-key)   |
 * | addresses: Array of String |
 * +----------------------------+
 * 
*/ /** * @typedef {Object} MobileMessageDB.SmsSegmentRecord * * Represents a SMS segment. * *
 * +---------------------------------------------------------------+
 * | SmsSegmentRecord                                              |
 * +---------------------------------------------------------------+
 * | [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 Cell Broadcast Related Fields]                          |
 * | serviceCategory: Number                                       |
 * | language: String                                              |
 * |                                                               |
 * | [Message Body]                                                |
 * | data: Array of Uint8 (available if it's 8bit encoding)        |
 * | body: String (normal text body)                               |
 * |                                                               |
 * | [Handy Fields Created by DB for Concatenation]                |
 * | id: Number (primary-key)                                      |
 * | hash: String // Use to identify the segments to the same SMS. |
 * | receivedSegments: Number                                      |
 * | segments: Array                                               |
 * +---------------------------------------------------------------+
 * 
*/ /** * @class MobileMessageDB * @classdesc * *

* MobileMessageDB is used to store all SMS / MMS messages, as well as the * threads those messages belong to, and the participants of those messages. *

* *

* The relations between threads, messages and participants can be described as * the following ERD -- each thread consists of one or many messages, and * consists of one or many participants. A participant resolves to one or many * (usually up to 2) addresses -- which represent different formats of the same * address, for example a national number and an international number. *

* *
 *                      X
 *                     / \
 * +-----------+      /   \       +-----------+
 * |           |     /     \     /|           |
 * |  thread   |-|--|consist|--|--|participant|
 * |           |     \  of /     \|           |
 * +-----------+      \   /       +-----------+
 *       |             \ /              |
 *       -              V               -
 *       |                              |
 *       |                              |
 *       X                              X
 *      / \                            / \
 *     /   \                          /   \
 *    /     \                        /     \
 *   |consist|                      |resolve|
 *    \  of /                        \  to /
 *     \   /                          \   /
 *      \ /                            \ /
 *       V                              V
 *       |                              |
 *       |                              |
 *       -                              -
 *       |                              |
 *      /|\                            /|\
 * +-----------+                  +-----------+
 * |           |                  |           |
 * |  message  |                  |  address  |
 * |           |                  |           |
 * +-----------+                  +-----------+
 * 
* *

* There are 4 object stores in use:
* 1. MESSAGE_STORE: stores {@link MobileMessageDB.MessageRecord}.
* 2. THREAD_STORE: stores {@link MobileMessageDB.ThreadRecord}.
* 3. PARTICIPANT_STORE: stores {@link MobileMessageDB.ParticipantRecord}.
* 4. SMS_SEGMENT_STORE: stores partial incoming SMS segments defined in * {@link MobileMessageDB.SmsSegmentRecord}. The records are deleted as soon as * it's enough to compose a complete SMS message. *

* *

* Besides all object stores mentioned above, there was a MOST_RECENT_STORE * which is deprecated and no longer in use. *

*/ this.MobileMessageDB = function() {}; MobileMessageDB.prototype = { dbName: null, dbVersion: null, /** * Cache the DB instance. * * @member {IDBDatabase} MobileMessageDB.db * @private */ db: null, /** * Last sms/mms object store key value in the database. * * @member {number} MobileMessageDB.lastMessageId * @private */ lastMessageId: 0, /** * @callback MobileMessageDB.EnsureDBCallback * @param {number} aErrorCode * The error code on failure, or null on success. * @param {IDBDatabase} aDatabase * The ready-to-use database object on success. */ /** * Prepare the database. This may include opening the database and upgrading * it to the latest schema version. * * @function MobileMessageDB.ensureDB * @param {MobileMessageDB.EnsureDBCallback} 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. */ 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); }; }, /** * @callback MobileMessageDB.NewTxnCallback * @param {number} aErrorCode * The error code on failure, or null on success. * @param {IDBTransaction} aTransaction * The transaction object to operate the indexedDB on success. * @param {IDBObjectStore|IDBObjectStore[]} aObjectStores * The object store(s) on success. If only one object store is passed, * it's passed as an IDBObjectStore; Otherwise, it's * IDBObjectStore[]. */ /** * Start a new transaction. * * @function MobileMessageDB.newTxn * @param {string} txn_type * Type of transaction (e.g. READ_WRITE) * @param {MobileMessageDB.NewTxnCallback} callback * Function to call when the transaction is available. It will * be invoked with the transaction and opened object stores. * @param {string[]} [storeNames=[{@link MobileMessageDB.MESSAGE_STORE_NAME}]] * 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 (let storeName of storeNames) { if (DEBUG) debug("Retrieving object store " + storeName); stores.push(txn.objectStore(storeName)); } } callback(null, txn, stores); }); }, /** * @callback MobileMessageDB.InitCallback * @param {number} aErrorCode * The error code on failure, or null on success. */ /** * Initialize this MobileMessageDB. * * @function MobileMessageDB.init * @param {string} aDbName * A string name for that database. * @param {number} aDbVersion * The version that mmdb should upgrade to. 0 for the latest version. * @param {MobileMessageDB.InitCallback} 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 the MobileMessageDB. * * @function MobileMessageDB.close */ 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 function set the status of message records to error. The * function can be used as the callback of {@link MobileMessageDB.init}. * * @function MobileMessageDB.updatePendingTransactionToError * @param {number} aError * The function does nothing if aError is not * null. */ 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: (primary key), * id: , * timestamp: , * body: , * unreadCount: } * */ 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: (primary key), * addresses: } */ 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: (primary key), * participantIds: , * participantAddresses: , * lastMessageId: , * lastTimestamp: , * subject: , * unreadCount: } * */ 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 (let id of 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: , * teleservice: , * SMSC: , * sentTimestamp: , * timestamp: , * sender: , * pid: , * encoding: , * messageClass: , * iccId: , * * [Concatenation Info] * segmentRef: , * segmentSeq: , * segmentMaxSeq: , * * [Application Port Info] * originatorPort: , * destinationPort: , * * [MWI status] * mwiPresent: , * mwiDiscard: , * mwiMsgCount: , * mwiActive: , * * [CDMA Cellbroadcast related fields] * serviceCategory: , * language: , * * [Message Body] * data: , (available if it's 8bit encoding) * body: , (normal text body) * * [Handy fields created by DB for concatenation] * id: , keypath of this objectStore. * hash: , Use to identify the segments to the same SMS. * receivedSegments: , * 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 }, /** * Check if addr1 matches addr2. * * @function MobileMessageDB.matchParsedPhoneNumbers * @param {string} addr1 * Normalized address 1. * @param {Object} parsedAddr1 * Parsed address 1. * @param {string} addr2 * Normalized address 2. * @param {Object} parsedAddr2 * Parsed address 2. * @return {boolean} * true if the 2 addresses match. */ 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); }, /** * Check if addr1 matches addr2. * * @function MobileMessageDB.matchPhoneNumbers * @param {string} addr1 * Normalized address 1. * @param {Object} parsedAddr1 * Parsed address 1. Try to parse from addr1 if not given. * @param {string} addr2 * Normalized address 2. * @param {Object} parsedAddr2 * Parsed address 2. Try to parse from addr2 if not given. * @return {boolean} * true if the 2 addresses match. */ 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; }, /** * Generate a nsISmsMessage or * nsIMmsMessage instance from a stored message record. * * @function MobileMessageDB.createDomMessageFromRecord * @param {MobileMessageDB.MessageRecord} aMessageRecord * The stored message record. * @return {nsISmsMessage|nsIMmsMessage} */ 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); } }, /** * @callback MobileMessageDB.ParticipantRecordCallback * @param {MobileMessageDB.ParticipantRecord} aParticipantRecord * The stored participant record. */ /** * Create a participant record with the given addresses, and add it into the * participant object store immediately. * * @function MobileMessageDB.createParticipantRecord * @param {IDBObjectStore} aParticipantStore * Object store for participants. * @param {string[]} aAddresses * The addresses associated to the participant. * @param {MobileMessageDB.ParticipantRecordCallback} aCallback * The callback function to invoke when the request finishes. */ 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); }; }, /** * Find or create the participant record associated to the given PLMN address. * * @function MobileMessageDB.findParticipantRecordByPlmnAddress * @param {IDBObjectStore} aParticipantStore * The object store for participants. * @param {string} aAddress * The PLMN address to look up with. * @param {boolean} aCreate * true to create a new participant record if not exists * yet, otherwise return null to the callback if record * not found. * @param {MobileMessageDB.ParticipantRecordCallback} aCallback * The callback function to invoke when the request finishes. */ 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); }, /** * Find or create the participant record associated to the given address other * than PLMN address. * * @function MobileMessageDB.findParticipantRecordByOtherAddress * @param {IDBObjectStore} aParticipantStore * The object store for participants. * @param {string} aAddress * The address to look up with. * @param {boolean} aCreate * true to create a new participant record if not exists * yet, otherwise return null to the callback if record * not found. * @param {MobileMessageDB.ParticipantRecordCallback} aCallback * The callback function to invoke when the request finishes. */ 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); }, /** * @typedef {Object} MobileMessageDB.TypedAddress * @property {string} address Address * @property {string} type Type of the address, such as "PLMN", "IPv4", * "IPv6", "email" or "Others" */ /** * Find or create the participant record associated to the given address. * * @function MobileMessageDB.findParticipantRecordByTypedAddress * @param {IDBObjectStore} aParticipantStore * The object store for participants. * @param {MobileMessageDB.TypedAddress} aTypedAddress * The address to look up with. * @param {boolean} aCreate * true to create a new participant record if not exists * yet, otherwise return null to the callback if record * not found. * @param {MobileMessageDB.ParticipantRecordCallback} aCallback * The callback function to invoke when the request finishes. */ 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, []); }, /** * @callback MobileMessageDB.ParticipantIdsCallback * @param {number[]} aParticipantIds * An array of participant IDs. May be null. */ /** * Find or create participant records associated to the given addresses, and * return the IDs of the participant records to the caller through the * callback. * * @function MobileMessageDB.findParticipantIdsByTypedAddresses * @param {IDBObjectStore} aParticipantStore * The object store for participants. * @param {MobileMessageDB.TypedAddress[]} aTypedAddresses * Addresses to look up with. * @param {boolean} aCreate * true to create a new participant record associates to * the given addresses if not exists yet, otherwise return * null to the callback if no record found. * @param {boolean} aSkipNonexistent * true to skip the addresses not exist in the participant * store, otherwise return null to the callback when one * or more addresses not found. * @param {MobileMessageDB.ParticipantIdsCallback} aCallback * The callback function to invoke when the request finishes. */ 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); }; }); }, /** * @callback MobileMessageDB.ThreadRecordCallback * @param {MobileMessageDB.ThreadRecord} aThreadRecord * The stored thread record. * @param {number[]} aParticipantIds * IDs of participants of the thread. */ /** * Find the thread record associated to the given address. * * @function MobileMessageDB.findThreadRecordByTypedAddresses * @param {IDBObjectStore} aThreadStore * The object store for threads. * @param {IDBObjectStore} aParticipantStore * The object store for participants. * @param {MobileMessageDB.TypedAddress[]} aTypedAddresses * Addresses to look up with. * @param {boolean} aCreateParticipants * true to create participant record associated to the * addresses if not exist yet. * @param {MobileMessageDB.ThreadRecordCallback} aCallback * The callback function to invoke when the request finishes. */ 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); }; }); }, /** * @callback MobileMessageDB.TransactionResultCallback * @param {number} aErrorCode * The error code on failure, or NS_OK on success. * @param {nsISmsMessage|nsIMmsMessage} aDomMessage * The DOM message instance of the transaction result. */ /** * @callback MobileMessageDB.NewTxnWithCallbackRequestCallback * @param {Object} aCapture * An output parameter. The messageRecord property will be * set on transaction finishes. * @param {MobileMessageDB.MessageRecord} aCapture.messageRecord * The stored message record. The property presents if the transaction * finished successfully. * @param {IDBObjectStore|IDBObjectStore[]} aObjectStores * The object store(s) on success. If only one object store is passed, * it's passed as an IDBObjectStore; Otherwise, it's * IDBObjectStore[]. */ /** * Start a new transaction with default oncomplete / * onabort implementation on the IDBTransaction * object which redirects the error / result to aCallback. * * @function MobileMessageDB.newTxnWithCallback * @param {Object} aCallback * The object which includes a callback function. * @param {MobileMessageDB.TransactionResultCallback} aCallback.notify * The callback function to invoke when the transaction finishes. * @param {MobileMessageDB.NewTxnWithCallbackRequestCallback} aFunc * The callback function to invoke when the request finishes. * @param {string[]} [aStoreNames=[{@link MobileMessageDB.MESSAGE_STORE_NAME}]] * Names of the stores to open. */ 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); }, /** * Save a message record. * * @function MobileMessageDB.saveRecord * @param {MobileMessageDB.MessageRecord} aMessageRecord * Message record to store. * @param {MobileMessageDB.TypedAddress[]} aThreadParticipants * Participants of the thread of the message. * @param {Object} aCallback * The object which includes a callback function. * @param {MobileMessageDB.TransactionResultCallback} aCallback.notify * The callback function to invoke when the transaction finishes. */ 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]); }, /** * @typedef {Object} MobileMessageDB.DeletedInfo * @property {number[]} messageIds * IDs of deleted messages. * @property {number[]} threadIds * IDs of deleted threads, which indicates all messages within the * threads have been deleted. */ /** * According to 3GPP 23.040 - subclause 9.2.3.9 TP-Protocol-Identifier (TP-PID), * if the Protocol Identifier contains a Replace Short Message Type or * Return Call Message code, it should replace any existing stored * message having the same Protocol Identifier code and originating address. * * This function checks the Protocol Identifier before saving the message * record to fulfill the feature. * * @function MobileMessageDB.replaceShortMessageOnSave * @param {IDBTransaction} aTransaction * The transaction object. * @param {IDBObjectStore} aMessageStore * The object store for messages. * @param {IDBObjectStore} aParticipantStore * The object store for participants. * @param {IDBObjectStore} aThreadStore * The object store for threads. * @param {MobileMessageDB.MessageRecord} aMessageRecord * The message record to store. * @param {MobileMessageDB.TypedAddress[]} aThreadParticipants * Participants of the thread of the message. * @param {MobileMessageDB.DeletedInfo} aDeletedInfo * An out parameter indicating which messages have been deleted due to * the replacement. */ 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); }; }); }, /** * The function where object store manipulations actually occur. * * @function MobileMessageDB.realSaveRecord * @param {IDBTransaction} aTransaction * The transaction object. * @param {IDBObjectStore} aMessageStore * The object store for messages. * @param {IDBObjectStore} aParticipantStore * The object store for participants. * @param {IDBObjectStore} aThreadStore * The object store for threads. * @param {MobileMessageDB.MessageRecord} aMessageRecord * The message record to store. * @param {MobileMessageDB.TypedAddress[]} aThreadParticipants * Participants of the thread of the message. * @param {MobileMessageDB.DeletedInfo} aDeletedInfo * An out parameter indicating which messages have been deleted due to * the replacement. */ 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 (let id of 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); }; }); }, /** * @typedef {Object} MobileMessageDB.MmsDeliveryInfoElement * @property {string} receiver * @property {string} deliveryStatus * @property {number} deliveryTimestamp * @property {string} readStatus * @property {number} readTimestamp */ /** * @callback MobileMessageDB.ForEachMatchedMmsDeliveryInfoCallback * @param {MobileMessageDB.MmsDeliveryInfoElement} aElement * An element of the MMS deliverInfo of a message record. */ /** * Iterate all elements of aDeliveryInfo, check if the receiver * address matches aNeedle and invoke aCallback on * each matched element. * * @function MobileMessageDB.forEachMatchedMmsDeliveryInfo * @param {MobileMessageDB.MmsDeliveryInfoElement[]} aDeliveryInfo * The MMS deliverInfo of a message record. * @param {string} aNeedle * The receiver address to look up with. * @param {MobileMessageDB.ForEachMatchedMmsDeliveryInfoCallback} aCallback * The callback function to invoke on each match. */ 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); } } }, /** * Find the message of a given message ID or envelope ID. Update its * delivery, deliveryStatus, and * envelopeId accordingly. * * @function MobileMessageDB.updateMessageDeliveryById * @param {string} id * If type is "messageId", it represents the message ID; * If type is "envelopeId", it represents the envelope ID, * which is the "x-mms-transaction-id" in the header of an MMS message. * @param {string} type * Either "messageId" or "envelopeId". * @param {string} receiver * The receiver address. * @param {string} delivery * If given, it will be used to update the deliveryIndex * property of a stored message record. * @param {string} deliveryStatus * If given, it will be used to update the deliveryStatus * property of a stored message record. * @param {string} envelopeId * If given, it will be used to update the envelopeIdIndex * property of a stored message record. * @param {Object} callback * The object passed as aCallback to * {@link MobileMessageDB.newTxnWithCallback}. */ 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); }; }); }, /** * Map receivers of a MMS message record to the thread participant list if MMS * grouping is enabled. * * @function MobileMessageDB.fillReceivedMmsThreadParticipants * @param {MobileMessageDB.MessageRecord} aMessage * The MMS message. * @param {MobileMessageDB.TypedAddress[]} threadParticipants * Participants to add. */ 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 valid MSISDN (i.e. the user's // own phone number), so we cannot correctly 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) }; }); }, /** * Update the thread when one or more messages are deleted / replaced. * * @function MobileMessageDB.updateThreadByMessageChange * @param {IDBObjectStore} messageStore * The object store for messages. * @param {IDBObjectStore} threadStore * The object store for threads. * @param {number} threadId * The thread ID. * @param {number[]} removedMsgIds * The IDs of removed messages. * @param {number} ignoredUnreadCount * Negative offset for unreadCount. For example, if the * unreadCount was 5, given * ignoredUnreadCount to 3 causes unreadCount * becomes 2. * @param {MobileMessageDB.DeletedInfo} deletedInfo * An out parameter indicating if the thread is deleted after the * operation. */ 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); } }; }, /** * Notify the observers that one or more messages are deleted. * * @function MobileMessageDB.notifyDeletedInfo * @param {MobileMessageDB.DeletedInfo} info * The IDs of deleted messages and threads. */ 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 */ /** * Store an incoming message. * * @function MobileMessageDB.saveReceivedMessage * @param {MobileMessageDB.MessageRecord} aMessage * The message record to store. * @param {Object} aCallback * The object which includes a callback function. * @param {MobileMessageDB.TransactionResultCallback} aCallback.notify * The callback function to invoke when the transaction finishes. */ 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); }, /** * Store an outgoing message. * * @function MobileMessageDB.saveSendingMessage * @param {MobileMessageDB.MessageRecord} aMessage * The message record to store. * @param {Object} aCallback * The object which includes a callback function. * @param {MobileMessageDB.TransactionResultCallback} aCallback.notify * The callback function to invoke when the transaction finishes. */ 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); }, /** * Update the delivery, deliveryStatus, and * envelopeId of a stored message record matching the given * message ID. * * @function MobileMessageDB.setMessageDeliveryByMessageId * @param {number} messageId * The message ID. * @param {string} receiver * The receiver address. * @param {string} delivery * If given, it will be used to update the deliveryIndex * property of a stored message record. * @param {string} deliveryStatus * If given, it will be used to update the deliveryStatus * property of a stored message record. * @param {string} envelopeId * If given, it will be used to update the envelopeIdIndex * property of a stored message record. * @param {Object} callback * The object passed as aCallback to * {@link MobileMessageDB.newTxnWithCallback}. */ setMessageDeliveryByMessageId: function(messageId, receiver, delivery, deliveryStatus, envelopeId, callback) { this.updateMessageDeliveryById(messageId, "messageId", receiver, delivery, deliveryStatus, envelopeId, callback); }, /** * Update the deliveryStatus of the specified * aReceiver within the deliveryInfo of the message * record retrieved by the given envelopeId. * * @function MobileMessageDB.setMessageDeliveryStatusByEnvelopeId * @param {string} aEnvelopeId * The envelope ID, which is the "x-mms-transaction-id" in the header * of an MMS message. * @param {string} aReceiver * The receiver address. * @param {string} aDeliveryStatus * If given, it will be used to update the deliveryStatus * property of a stored message record. * @param {Object} aCallback * The object passed as aCallback to * {@link MobileMessageDB.newTxnWithCallback}. */ setMessageDeliveryStatusByEnvelopeId: function(aEnvelopeId, aReceiver, aDeliveryStatus, aCallback) { this.updateMessageDeliveryById(aEnvelopeId, "envelopeId", aReceiver, null, aDeliveryStatus, null, aCallback); }, /** * Update the readStatus of the specified aReceiver * within the deliveryInfo of the message record retrieved by the * given envelopeId. * * @function MobileMessageDB.setMessageReadStatusByEnvelopeId * @param {string} aEnvelopeId * The envelope ID, which is the "x-mms-transaction-id" in the header * of an MMS message. * @param {string} aReceiver * The receiver address. * @param {string} aReadStatus * The updated read status. * @param {Object} aCallback * The object passed as aCallback to * {@link MobileMessageDB.newTxnWithCallback}. */ 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); }; }); }, /** * @callback MobileMessageDB.GetMessageRecordCallback * @param {number} aErrorCode * The error code on failure, or NS_OK on success. * @param {MobileMessageDB.MessageRecord} aMessageRecord * The stored message record. * @param {nsISmsMessage|nsIMmsMessage} aDomMessage * The DOM message instance of the message record. */ /** * Get the message record with given transaction ID. * * @function MobileMessageDB.getMessageRecordByTransactionId * @param {string} aTransactionId * The transaction ID. * @param {Object} aCallback * The object which includes a callback function. * @param {MobileMessageDB.GetMessageRecordCallback} aCallback.notify * The callback function to invoke when the request finishes. */ 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); }; }); }, /** * Get the message record with given message ID. * * @function MobileMessageDB.getMessageRecordById * @param {string} aMessageID * The message ID. * @param {Object} aCallback * The object which includes a callback function. * @param {MobileMessageDB.GetMessageRecordCallback} aCallback.notify * The callback function to invoke when the request finishes. */ 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); }; }); }, /** * Helper to translate NS errors to the error causes defined in * nsIMobileMessageCallback. * * @function MobileMessageDB.translateCrErrorToMessageCallbackError * @param {number} aCrError * The error code defined in Components.result * @return {number} * The error code defined in nsIMobileMessageCallback */ 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; } }, /** * @callback MobileMessageDB.SaveSmsSegmentCallback * @param {number} aErrorCode * The error code on failure, or NS_OK on success. * @param {MobileMessageDB.SmsSegmentRecord} aCompleteMessage * The composing message. It becomes a complete message once the last * segment is stored. */ /** * Store a single SMS segment. * * @function MobileMessageDB.saveSmsSegment * @param {MobileMessageDB.SmsSegmentRecord} aSmsSegment * Single SMS segment. * @param {Object} aCallback * The object which includes a callback function. * @param {MobileMessageDB.SaveSmsSegmentCallback} aCallback.notify * The callback function to invoke when the request finishes. */ 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 segments 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 (segmentRecord.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET && segmentRecord.encoding == aSmsSegment.encoding && segmentRecord.segments[seq].length == aSmsSegment.data.length && segmentRecord.segments[seq].every(function(aElement, aIndex) { return aElement == aSmsSegment.data[aIndex]; })) { if (DEBUG) { debug("Got duplicated binary segment no: " + seq); } return; } if (segmentRecord.encoding != RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET && aSmsSegment.encoding != RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET && segmentRecord.segments[seq] == aSmsSegment.body) { if (DEBUG) { debug("Got duplicated text segment no: " + seq); } return; } // Update mandatory properties to ensure that the segments could be // concatenated properly. segmentRecord.encoding = aSmsSegment.encoding; segmentRecord.originatorPort = aSmsSegment.originatorPort; segmentRecord.destinationPort = aSmsSegment.destinationPort; segmentRecord.teleservice = aSmsSegment.teleservice; // Decrease the counter for this collided segment. segmentRecord.receivedSegments--; } 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 */ /** * Get the message record with given message ID. * * @function MobileMessageDB.getMessage * @param {string} aMessageID * The message ID. * @param {nsIMobileMessageCallback} aRequest * The callback object. */ 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); }, /** * Delete the message record with given message IDs. * * @function MobileMessageDB.deleteMessage * @param {number[]} messageIds * The IDs of messages to delete. * @param {number} length * The length of the messageIds array. * @param {nsIMobileMessageCallback} aRequest * The callback object. */ 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]); }, /** * Create a cursor to iterate on stored message records. * * @function MobileMessageDB.createMessageCursor * @param {boolean} aHasStartDate * true to query only messages starts with * aStartDate * @param {number} aStartDate * The timestamp of start date in milliseconds. * @param {boolean} aHasEndDate * true to query only messages before the * aEndDate * @param {number} aEndDate * The timestamp of end date in milliseconds. * @param {string[]} aNumbers * If not null, query only messages with sender or * receiver who's number matches one of the numbers listed in the array. * @param {number} aNumbersCount * The length of aNumbers array. * @param {string} aDelivery * If not null, query only messages matching the delivery * value. * @param {boolean} aHasRead * true to query only messages match the read value * specified by aRead * @param {boolean} aRead * Specify the read query condition. * @param {number} aThreadId * If not null, query only messages in the given thread. * @param {boolean} aReverse * true to reverse the order. * @param {nsIMobileMessageCursorCallback} aCallback * The callback object used by GetMessagesCursor * @return {GetMessagesCursor} * The cursor to iterate on messages. */ createMessageCursor: function(aHasStartDate, aStartDate, aHasEndDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, 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: " + (aHasThreadId ? aThreadId : "(null)") + " 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 (aHasThreadId) { 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; }, /** * Change the read property of a stored message record. * * @function MobileMessageDB.markMessageRead * @param {number} messageId * The message ID. * @param {boolean} value * The updated read value. * @param {boolean} aSendReadReport * true to reply the read report of an incoming MMS * message whose isReadReportSent is 'false'. * Note: isReadReportSent will be set to 'true' no * matter aSendReadReport is true or not when a message was marked * from UNREAD to READ. See bug 1180470 for the new UX policy. * @param {nsIMobileMessageCallback} aRequest * The callback object. */ 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 (messageRecord.type == "mms" && messageRecord.delivery == DELIVERY_RECEIVED && messageRecord.read == FILTER_READ_READ && messageRecord.headers["x-mms-read-report"] && !messageRecord.isReadReportSent) { messageRecord.isReadReportSent = true; if (aSendReadReport) { 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]); }, /** * Create a cursor to iterate on stored threads. * * @function MobileMessageDB.createThreadCursor * @param {nsIMobileMessageCursorCallback} callback * The callback object used by GetMessagesCursor * @return {GetThreadsCursor} * The cursor to iterate on threads. */ 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; } }; var 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 filter 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); }, /** * Initiate 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 collector 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 (, , ). It fetches the object * specified by and notify with that by calling * |.notifyResult()|. If 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 + ", )"); 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 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"); }