michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/PhoneNumberUtils.jsm"); michael@0: Cu.importGlobalProperties(["indexedDB"]); michael@0: michael@0: var RIL = {}; michael@0: Cu.import("resource://gre/modules/ril_consts.js", RIL); michael@0: michael@0: const RIL_GETMESSAGESCURSOR_CID = michael@0: Components.ID("{484d1ad8-840e-4782-9dc4-9ebc4d914937}"); michael@0: const RIL_GETTHREADSCURSOR_CID = michael@0: Components.ID("{95ee7c3e-d6f2-4ec4-ade5-0c453c036d35}"); michael@0: michael@0: const DEBUG = false; michael@0: const DISABLE_MMS_GROUPING_FOR_RECEIVING = true; michael@0: michael@0: michael@0: const DB_VERSION = 22; michael@0: const MESSAGE_STORE_NAME = "sms"; michael@0: const THREAD_STORE_NAME = "thread"; michael@0: const PARTICIPANT_STORE_NAME = "participant"; michael@0: const MOST_RECENT_STORE_NAME = "most-recent"; michael@0: const SMS_SEGMENT_STORE_NAME = "sms-segment"; michael@0: michael@0: const DELIVERY_SENDING = "sending"; michael@0: const DELIVERY_SENT = "sent"; michael@0: const DELIVERY_RECEIVED = "received"; michael@0: const DELIVERY_NOT_DOWNLOADED = "not-downloaded"; michael@0: const DELIVERY_ERROR = "error"; michael@0: michael@0: const DELIVERY_STATUS_NOT_APPLICABLE = "not-applicable"; michael@0: const DELIVERY_STATUS_SUCCESS = "success"; michael@0: const DELIVERY_STATUS_PENDING = "pending"; michael@0: const DELIVERY_STATUS_ERROR = "error"; michael@0: michael@0: const MESSAGE_CLASS_NORMAL = "normal"; michael@0: michael@0: const FILTER_TIMESTAMP = "timestamp"; michael@0: const FILTER_NUMBERS = "numbers"; michael@0: const FILTER_DELIVERY = "delivery"; michael@0: const FILTER_READ = "read"; michael@0: michael@0: // We can“t create an IDBKeyCursor with a boolean, so we need to use numbers michael@0: // instead. michael@0: const FILTER_READ_UNREAD = 0; michael@0: const FILTER_READ_READ = 1; michael@0: michael@0: const READ_ONLY = "readonly"; michael@0: const READ_WRITE = "readwrite"; michael@0: const PREV = "prev"; michael@0: const NEXT = "next"; michael@0: michael@0: const COLLECT_ID_END = 0; michael@0: const COLLECT_ID_ERROR = -1; michael@0: const COLLECT_TIMESTAMP_UNUSED = 0; michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService", michael@0: "@mozilla.org/mobilemessage/mobilemessageservice;1", michael@0: "nsIMobileMessageService"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gMMSService", michael@0: "@mozilla.org/mms/rilmmsservice;1", michael@0: "nsIMmsService"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "MMS", function() { michael@0: let MMS = {}; michael@0: Cu.import("resource://gre/modules/MmsPduHelper.jsm", MMS); michael@0: return MMS; michael@0: }); michael@0: michael@0: /** michael@0: * MobileMessageDB michael@0: */ michael@0: this.MobileMessageDB = function() {}; michael@0: MobileMessageDB.prototype = { michael@0: dbName: null, michael@0: dbVersion: null, michael@0: michael@0: /** michael@0: * Cache the DB here. michael@0: */ michael@0: db: null, michael@0: michael@0: /** michael@0: * Last sms/mms object store key value in the database. michael@0: */ michael@0: lastMessageId: 0, michael@0: michael@0: /** michael@0: * An optional hook to check if device storage is full. michael@0: * michael@0: * @return true if full. michael@0: */ michael@0: isDiskFull: null, michael@0: michael@0: /** michael@0: * Prepare the database. This may include opening the database and upgrading michael@0: * it to the latest schema version. michael@0: * michael@0: * @param callback michael@0: * Function that takes an error and db argument. It is called when michael@0: * the database is ready to use or if an error occurs while preparing michael@0: * the database. michael@0: * michael@0: * @return (via callback) a database ready for use. michael@0: */ michael@0: ensureDB: function(callback) { michael@0: if (this.db) { michael@0: if (DEBUG) debug("ensureDB: already have a database, returning early."); michael@0: callback(null, this.db); michael@0: return; michael@0: } michael@0: michael@0: let self = this; michael@0: function gotDB(db) { michael@0: self.db = db; michael@0: callback(null, db); michael@0: } michael@0: michael@0: let request = indexedDB.open(this.dbName, this.dbVersion); michael@0: request.onsuccess = function(event) { michael@0: if (DEBUG) debug("Opened database:", self.dbName, self.dbVersion); michael@0: gotDB(event.target.result); michael@0: }; michael@0: request.onupgradeneeded = function(event) { michael@0: if (DEBUG) { michael@0: debug("Database needs upgrade:", self.dbName, michael@0: event.oldVersion, event.newVersion); michael@0: debug("Correct new database version:", event.newVersion == self.dbVersion); michael@0: } michael@0: michael@0: let db = event.target.result; michael@0: michael@0: let currentVersion = event.oldVersion; michael@0: michael@0: function update(currentVersion) { michael@0: let next = update.bind(self, currentVersion + 1); michael@0: michael@0: switch (currentVersion) { michael@0: case 0: michael@0: if (DEBUG) debug("New database"); michael@0: self.createSchema(db, next); michael@0: break; michael@0: case 1: michael@0: if (DEBUG) debug("Upgrade to version 2. Including `read` index"); michael@0: self.upgradeSchema(event.target.transaction, next); michael@0: break; michael@0: case 2: michael@0: if (DEBUG) debug("Upgrade to version 3. Fix existing entries."); michael@0: self.upgradeSchema2(event.target.transaction, next); michael@0: break; michael@0: case 3: michael@0: if (DEBUG) debug("Upgrade to version 4. Add quick threads view."); michael@0: self.upgradeSchema3(db, event.target.transaction, next); michael@0: break; michael@0: case 4: michael@0: if (DEBUG) debug("Upgrade to version 5. Populate quick threads view."); michael@0: self.upgradeSchema4(event.target.transaction, next); michael@0: break; michael@0: case 5: michael@0: if (DEBUG) debug("Upgrade to version 6. Use PhonenumberJS."); michael@0: self.upgradeSchema5(event.target.transaction, next); michael@0: break; michael@0: case 6: michael@0: if (DEBUG) debug("Upgrade to version 7. Use multiple entry indexes."); michael@0: self.upgradeSchema6(event.target.transaction, next); michael@0: break; michael@0: case 7: michael@0: if (DEBUG) debug("Upgrade to version 8. Add participant/thread stores."); michael@0: self.upgradeSchema7(db, event.target.transaction, next); michael@0: break; michael@0: case 8: michael@0: if (DEBUG) debug("Upgrade to version 9. Add transactionId index for incoming MMS."); michael@0: self.upgradeSchema8(event.target.transaction, next); michael@0: break; michael@0: case 9: michael@0: if (DEBUG) debug("Upgrade to version 10. Upgrade type if it's not existing."); michael@0: self.upgradeSchema9(event.target.transaction, next); michael@0: break; michael@0: case 10: michael@0: if (DEBUG) debug("Upgrade to version 11. Add last message type into threadRecord."); michael@0: self.upgradeSchema10(event.target.transaction, next); michael@0: break; michael@0: case 11: michael@0: if (DEBUG) debug("Upgrade to version 12. Add envelopeId index for outgoing MMS."); michael@0: self.upgradeSchema11(event.target.transaction, next); michael@0: break; michael@0: case 12: michael@0: if (DEBUG) debug("Upgrade to version 13. Replaced deliveryStatus by deliveryInfo."); michael@0: self.upgradeSchema12(event.target.transaction, next); michael@0: break; michael@0: case 13: michael@0: if (DEBUG) debug("Upgrade to version 14. Fix the wrong participants."); michael@0: // A workaround to check if we need to re-upgrade the DB schema 12. We missed this michael@0: // because we didn't properly uplift that logic to b2g_v1.2 and errors could happen michael@0: // when migrating b2g_v1.2 to b2g_v1.3. Please see Bug 960741 for details. michael@0: self.needReUpgradeSchema12(event.target.transaction, function(isNeeded) { michael@0: if (isNeeded) { michael@0: self.upgradeSchema12(event.target.transaction, function() { michael@0: self.upgradeSchema13(event.target.transaction, next); michael@0: }); michael@0: } else { michael@0: self.upgradeSchema13(event.target.transaction, next); michael@0: } michael@0: }); michael@0: break; michael@0: case 14: michael@0: if (DEBUG) debug("Upgrade to version 15. Add deliveryTimestamp."); michael@0: self.upgradeSchema14(event.target.transaction, next); michael@0: break; michael@0: case 15: michael@0: if (DEBUG) debug("Upgrade to version 16. Add ICC ID for each message."); michael@0: self.upgradeSchema15(event.target.transaction, next); michael@0: break; michael@0: case 16: michael@0: if (DEBUG) debug("Upgrade to version 17. Add isReadReportSent for incoming MMS."); michael@0: self.upgradeSchema16(event.target.transaction, next); michael@0: break; michael@0: case 17: michael@0: if (DEBUG) debug("Upgrade to version 18. Add last message subject into threadRecord."); michael@0: self.upgradeSchema17(event.target.transaction, next); michael@0: break; michael@0: case 18: michael@0: if (DEBUG) debug("Upgrade to version 19. Add pid for incoming SMS."); michael@0: self.upgradeSchema18(event.target.transaction, next); michael@0: break; michael@0: case 19: michael@0: if (DEBUG) debug("Upgrade to version 20. Add readStatus and readTimestamp."); michael@0: self.upgradeSchema19(event.target.transaction, next); michael@0: break; michael@0: case 20: michael@0: if (DEBUG) debug("Upgrade to version 21. Add sentTimestamp."); michael@0: self.upgradeSchema20(event.target.transaction, next); michael@0: break; michael@0: case 21: michael@0: if (DEBUG) debug("Upgrade to version 22. Add sms-segment store."); michael@0: self.upgradeSchema21(db, event.target.transaction, next); michael@0: break; michael@0: case 22: michael@0: // This will need to be moved for each new version michael@0: if (DEBUG) debug("Upgrade finished."); michael@0: break; michael@0: default: michael@0: event.target.transaction.abort(); michael@0: if (DEBUG) debug("unexpected db version: " + event.oldVersion); michael@0: callback(Cr.NS_ERROR_FAILURE, null); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: update(currentVersion); michael@0: }; michael@0: request.onerror = function(event) { michael@0: //TODO look at event.target.Code and change error constant accordingly michael@0: if (DEBUG) debug("Error opening database!"); michael@0: callback(Cr.NS_ERROR_FAILURE, null); michael@0: }; michael@0: request.onblocked = function(event) { michael@0: if (DEBUG) debug("Opening database request is blocked."); michael@0: callback(Cr.NS_ERROR_FAILURE, null); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Start a new transaction. michael@0: * michael@0: * @param txn_type michael@0: * Type of transaction (e.g. READ_WRITE) michael@0: * @param callback michael@0: * Function to call when the transaction is available. It will michael@0: * be invoked with the transaction and opened object stores. michael@0: * @param storeNames michael@0: * Names of the stores to open. michael@0: */ michael@0: newTxn: function(txn_type, callback, storeNames) { michael@0: if (!storeNames) { michael@0: storeNames = [MESSAGE_STORE_NAME]; michael@0: } michael@0: if (DEBUG) debug("Opening transaction for object stores: " + storeNames); michael@0: let self = this; michael@0: this.ensureDB(function(error, db) { michael@0: if (!error && michael@0: txn_type === READ_WRITE && michael@0: self.isDiskFull && self.isDiskFull()) { michael@0: error = Cr.NS_ERROR_FILE_NO_DEVICE_SPACE; michael@0: } michael@0: if (error) { michael@0: if (DEBUG) debug("Could not open database: " + error); michael@0: callback(error); michael@0: return; michael@0: } michael@0: let txn = db.transaction(storeNames, txn_type); michael@0: if (DEBUG) debug("Started transaction " + txn + " of type " + txn_type); michael@0: if (DEBUG) { michael@0: txn.oncomplete = function oncomplete(event) { michael@0: debug("Transaction " + txn + " completed."); michael@0: }; michael@0: txn.onerror = function onerror(event) { michael@0: //TODO check event.target.errorCode and show an appropiate error michael@0: // message according to it. michael@0: debug("Error occurred during transaction: " + event.target.errorCode); michael@0: }; michael@0: } michael@0: let stores; michael@0: if (storeNames.length == 1) { michael@0: if (DEBUG) debug("Retrieving object store " + storeNames[0]); michael@0: stores = txn.objectStore(storeNames[0]); michael@0: } else { michael@0: stores = []; michael@0: for each (let storeName in storeNames) { michael@0: if (DEBUG) debug("Retrieving object store " + storeName); michael@0: stores.push(txn.objectStore(storeName)); michael@0: } michael@0: } michael@0: callback(null, txn, stores); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Initialize this MobileMessageDB. michael@0: * michael@0: * @param aDbName michael@0: * A string name for that database. michael@0: * @param aDbVersion michael@0: * The version that mmdb should upgrade to. 0 for the lastest version. michael@0: * @param aCallback michael@0: * A function when either the initialization transaction is completed michael@0: * or any error occurs. Should take only one argument -- null when michael@0: * initialized with success or the error object otherwise. michael@0: */ michael@0: init: function(aDbName, aDbVersion, aCallback) { michael@0: this.dbName = aDbName; michael@0: this.dbVersion = aDbVersion || DB_VERSION; michael@0: michael@0: let self = this; michael@0: this.newTxn(READ_ONLY, function(error, txn, messageStore){ michael@0: if (error) { michael@0: if (aCallback) { michael@0: aCallback(error); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (aCallback) { michael@0: txn.oncomplete = function() { michael@0: aCallback(null); michael@0: }; michael@0: } michael@0: michael@0: // In order to get the highest key value, we open a key cursor in reverse michael@0: // order and get only the first pointed value. michael@0: let request = messageStore.openCursor(null, PREV); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: if (DEBUG) { michael@0: debug("Could not get the last key from mobile message database. " + michael@0: "Probably empty database"); michael@0: } michael@0: return; michael@0: } michael@0: self.lastMessageId = cursor.key || 0; michael@0: if (DEBUG) debug("Last assigned message ID was " + self.lastMessageId); michael@0: }; michael@0: request.onerror = function onerror(event) { michael@0: if (DEBUG) { michael@0: debug("Could not get the last key from mobile message database " + michael@0: event.target.errorCode); michael@0: } michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: close: function() { michael@0: if (!this.db) { michael@0: return; michael@0: } michael@0: michael@0: this.db.close(); michael@0: this.db = null; michael@0: this.lastMessageId = 0; michael@0: }, michael@0: michael@0: /** michael@0: * Sometimes user might reboot or remove battery while sending/receiving michael@0: * message. This is function set the status of message records to error. michael@0: */ michael@0: updatePendingTransactionToError: function(aError) { michael@0: if (aError) { michael@0: return; michael@0: } michael@0: michael@0: this.newTxn(READ_WRITE, function(error, txn, messageStore) { michael@0: if (error) { michael@0: return; michael@0: } michael@0: michael@0: let deliveryIndex = messageStore.index("delivery"); michael@0: michael@0: // Set all 'delivery: sending' records to 'delivery: error' and 'deliveryStatus: michael@0: // error'. michael@0: let keyRange = IDBKeyRange.bound([DELIVERY_SENDING, 0], [DELIVERY_SENDING, ""]); michael@0: let cursorRequestSending = deliveryIndex.openCursor(keyRange); michael@0: cursorRequestSending.onsuccess = function(event) { michael@0: let messageCursor = event.target.result; michael@0: if (!messageCursor) { michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = messageCursor.value; michael@0: michael@0: // Set delivery to error. michael@0: messageRecord.delivery = DELIVERY_ERROR; michael@0: messageRecord.deliveryIndex = [DELIVERY_ERROR, messageRecord.timestamp]; michael@0: michael@0: if (messageRecord.type == "sms") { michael@0: messageRecord.deliveryStatus = DELIVERY_STATUS_ERROR; michael@0: } else { michael@0: // Set delivery status to error. michael@0: for (let i = 0; i < messageRecord.deliveryInfo.length; i++) { michael@0: messageRecord.deliveryInfo[i].deliveryStatus = DELIVERY_STATUS_ERROR; michael@0: } michael@0: } michael@0: michael@0: messageCursor.update(messageRecord); michael@0: messageCursor.continue(); michael@0: }; michael@0: michael@0: // Set all 'delivery: not-downloaded' and 'deliveryStatus: pending' michael@0: // records to 'delivery: not-downloaded' and 'deliveryStatus: error'. michael@0: keyRange = IDBKeyRange.bound([DELIVERY_NOT_DOWNLOADED, 0], [DELIVERY_NOT_DOWNLOADED, ""]); michael@0: let cursorRequestNotDownloaded = deliveryIndex.openCursor(keyRange); michael@0: cursorRequestNotDownloaded.onsuccess = function(event) { michael@0: let messageCursor = event.target.result; michael@0: if (!messageCursor) { michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = messageCursor.value; michael@0: michael@0: // We have no "not-downloaded" SMS messages. michael@0: if (messageRecord.type == "sms") { michael@0: messageCursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // Set delivery status to error. michael@0: let deliveryInfo = messageRecord.deliveryInfo; michael@0: if (deliveryInfo.length == 1 && michael@0: deliveryInfo[0].deliveryStatus == DELIVERY_STATUS_PENDING) { michael@0: deliveryInfo[0].deliveryStatus = DELIVERY_STATUS_ERROR; michael@0: } michael@0: michael@0: messageCursor.update(messageRecord); michael@0: messageCursor.continue(); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Create the initial database schema. michael@0: * michael@0: * TODO need to worry about number normalization somewhere... michael@0: * TODO full text search on body??? michael@0: */ michael@0: createSchema: function(db, next) { michael@0: // This messageStore holds the main mobile message data. michael@0: let messageStore = db.createObjectStore(MESSAGE_STORE_NAME, { keyPath: "id" }); michael@0: messageStore.createIndex("timestamp", "timestamp", { unique: false }); michael@0: if (DEBUG) debug("Created object stores and indexes"); michael@0: next(); michael@0: }, michael@0: michael@0: /** michael@0: * Upgrade to the corresponding database schema version. michael@0: */ michael@0: upgradeSchema: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: messageStore.createIndex("read", "read", { unique: false }); michael@0: next(); michael@0: }, michael@0: michael@0: upgradeSchema2: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: messageRecord.messageClass = MESSAGE_CLASS_NORMAL; michael@0: messageRecord.deliveryStatus = DELIVERY_STATUS_NOT_APPLICABLE; michael@0: cursor.update(messageRecord); michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: upgradeSchema3: function(db, transaction, next) { michael@0: // Delete redundant "id" index. michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: if (messageStore.indexNames.contains("id")) { michael@0: messageStore.deleteIndex("id"); michael@0: } michael@0: michael@0: /** michael@0: * This mostRecentStore can be used to quickly construct a thread view of michael@0: * the mobile message database. Each entry looks like this: michael@0: * michael@0: * { senderOrReceiver: (primary key), michael@0: * id: , michael@0: * timestamp: , michael@0: * body: , michael@0: * unreadCount: } michael@0: * michael@0: */ michael@0: let mostRecentStore = db.createObjectStore(MOST_RECENT_STORE_NAME, michael@0: { keyPath: "senderOrReceiver" }); michael@0: mostRecentStore.createIndex("timestamp", "timestamp"); michael@0: next(); michael@0: }, michael@0: michael@0: upgradeSchema4: function(transaction, next) { michael@0: let threads = {}; michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME); michael@0: michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: for (let thread in threads) { michael@0: mostRecentStore.put(threads[thread]); michael@0: } michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: let contact = messageRecord.sender || messageRecord.receiver; michael@0: michael@0: if (contact in threads) { michael@0: let thread = threads[contact]; michael@0: if (!messageRecord.read) { michael@0: thread.unreadCount++; michael@0: } michael@0: if (messageRecord.timestamp > thread.timestamp) { michael@0: thread.id = messageRecord.id; michael@0: thread.body = messageRecord.body; michael@0: thread.timestamp = messageRecord.timestamp; michael@0: } michael@0: } else { michael@0: threads[contact] = { michael@0: senderOrReceiver: contact, michael@0: id: messageRecord.id, michael@0: timestamp: messageRecord.timestamp, michael@0: body: messageRecord.body, michael@0: unreadCount: messageRecord.read ? 0 : 1 michael@0: }; michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: upgradeSchema5: function(transaction, next) { michael@0: // Don't perform any upgrade. See Bug 819560. michael@0: next(); michael@0: }, michael@0: michael@0: upgradeSchema6: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: // Delete "delivery" index. michael@0: if (messageStore.indexNames.contains("delivery")) { michael@0: messageStore.deleteIndex("delivery"); michael@0: } michael@0: // Delete "sender" index. michael@0: if (messageStore.indexNames.contains("sender")) { michael@0: messageStore.deleteIndex("sender"); michael@0: } michael@0: // Delete "receiver" index. michael@0: if (messageStore.indexNames.contains("receiver")) { michael@0: messageStore.deleteIndex("receiver"); michael@0: } michael@0: // Delete "read" index. michael@0: if (messageStore.indexNames.contains("read")) { michael@0: messageStore.deleteIndex("read"); michael@0: } michael@0: michael@0: // Create new "delivery", "number" and "read" indexes. michael@0: messageStore.createIndex("delivery", "deliveryIndex"); michael@0: messageStore.createIndex("number", "numberIndex", { multiEntry: true }); michael@0: messageStore.createIndex("read", "readIndex"); michael@0: michael@0: // Populate new "deliverIndex", "numberIndex" and "readIndex" attributes. michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: let timestamp = messageRecord.timestamp; michael@0: messageRecord.deliveryIndex = [messageRecord.delivery, timestamp]; michael@0: messageRecord.numberIndex = [ michael@0: [messageRecord.sender, timestamp], michael@0: [messageRecord.receiver, timestamp] michael@0: ]; michael@0: messageRecord.readIndex = [messageRecord.read, timestamp]; michael@0: cursor.update(messageRecord); michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add participant/thread stores. michael@0: * michael@0: * The message store now saves original phone numbers/addresses input from michael@0: * content to message records. No normalization is made. michael@0: * michael@0: * For filtering messages by phone numbers, it first looks up corresponding michael@0: * participant IDs from participant table and fetch message records with michael@0: * matching keys defined in per record "participantIds" field. michael@0: * michael@0: * For message threading, messages with the same participant ID array are put michael@0: * in the same thread. So updating "unreadCount", "lastMessageId" and michael@0: * "lastTimestamp" are through the "threadId" carried by per message record. michael@0: * Fetching threads list is now simply walking through the thread sotre. The michael@0: * "mostRecentStore" is dropped. michael@0: */ michael@0: upgradeSchema7: function(db, transaction, next) { michael@0: /** michael@0: * This "participant" object store keeps mappings of multiple phone numbers michael@0: * of the same recipient to an integer participant id. Each entry looks michael@0: * like: michael@0: * michael@0: * { id: (primary key), michael@0: * addresses: } michael@0: */ michael@0: let participantStore = db.createObjectStore(PARTICIPANT_STORE_NAME, michael@0: { keyPath: "id", michael@0: autoIncrement: true }); michael@0: participantStore.createIndex("addresses", "addresses", { multiEntry: true }); michael@0: michael@0: /** michael@0: * This "threads" object store keeps mappings from an integer thread id to michael@0: * ids of the participants of that message thread. Each entry looks like: michael@0: * michael@0: * { id: (primary key), michael@0: * participantIds: , michael@0: * participantAddresses: , michael@0: * lastMessageId: , michael@0: * lastTimestamp: , michael@0: * subject: , michael@0: * unreadCount: } michael@0: * michael@0: */ michael@0: let threadStore = db.createObjectStore(THREAD_STORE_NAME, michael@0: { keyPath: "id", michael@0: autoIncrement: true }); michael@0: threadStore.createIndex("participantIds", "participantIds"); michael@0: threadStore.createIndex("lastTimestamp", "lastTimestamp"); michael@0: michael@0: /** michael@0: * Replace "numberIndex" with "participantIdsIndex" and create an additional michael@0: * "threadId". "numberIndex" will be removed later. michael@0: */ michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: messageStore.createIndex("threadId", "threadIdIndex"); michael@0: messageStore.createIndex("participantIds", "participantIdsIndex", michael@0: { multiEntry: true }); michael@0: michael@0: // Now populate participantStore & threadStore. michael@0: let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME); michael@0: let self = this; michael@0: let mostRecentRequest = mostRecentStore.openCursor(); michael@0: mostRecentRequest.onsuccess = function(event) { michael@0: let mostRecentCursor = event.target.result; michael@0: if (!mostRecentCursor) { michael@0: db.deleteObjectStore(MOST_RECENT_STORE_NAME); michael@0: michael@0: // No longer need the "number" index in messageStore, use michael@0: // "participantIds" index instead. michael@0: messageStore.deleteIndex("number"); michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let mostRecentRecord = mostRecentCursor.value; michael@0: michael@0: // Each entry in mostRecentStore is supposed to be a unique thread, so we michael@0: // retrieve the records out and insert its "senderOrReceiver" column as a michael@0: // new record in participantStore. michael@0: let number = mostRecentRecord.senderOrReceiver; michael@0: self.findParticipantRecordByAddress(participantStore, number, true, michael@0: function(participantRecord) { michael@0: // Also create a new record in threadStore. michael@0: let threadRecord = { michael@0: participantIds: [participantRecord.id], michael@0: participantAddresses: [number], michael@0: lastMessageId: mostRecentRecord.id, michael@0: lastTimestamp: mostRecentRecord.timestamp, michael@0: subject: mostRecentRecord.body, michael@0: unreadCount: mostRecentRecord.unreadCount, michael@0: }; michael@0: let addThreadRequest = threadStore.add(threadRecord); michael@0: addThreadRequest.onsuccess = function(event) { michael@0: threadRecord.id = event.target.result; michael@0: michael@0: let numberRange = IDBKeyRange.bound([number, 0], [number, ""]); michael@0: let messageRequest = messageStore.index("number") michael@0: .openCursor(numberRange, NEXT); michael@0: messageRequest.onsuccess = function(event) { michael@0: let messageCursor = event.target.result; michael@0: if (!messageCursor) { michael@0: // No more message records, check next most recent record. michael@0: mostRecentCursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = messageCursor.value; michael@0: // Check whether the message really belongs to this thread. michael@0: let matchSenderOrReceiver = false; michael@0: if (messageRecord.delivery == DELIVERY_RECEIVED) { michael@0: if (messageRecord.sender == number) { michael@0: matchSenderOrReceiver = true; michael@0: } michael@0: } else if (messageRecord.receiver == number) { michael@0: matchSenderOrReceiver = true; michael@0: } michael@0: if (!matchSenderOrReceiver) { michael@0: // Check next message record. michael@0: messageCursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: messageRecord.threadId = threadRecord.id; michael@0: messageRecord.threadIdIndex = [threadRecord.id, michael@0: messageRecord.timestamp]; michael@0: messageRecord.participantIdsIndex = [ michael@0: [participantRecord.id, messageRecord.timestamp] michael@0: ]; michael@0: messageCursor.update(messageRecord); michael@0: // Check next message record. michael@0: messageCursor.continue(); michael@0: }; michael@0: messageRequest.onerror = function() { michael@0: // Error in fetching message records, check next most recent record. michael@0: mostRecentCursor.continue(); michael@0: }; michael@0: }; michael@0: addThreadRequest.onerror = function() { michael@0: // Error in fetching message records, check next most recent record. michael@0: mostRecentCursor.continue(); michael@0: }; michael@0: }); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add transactionId index for MMS. michael@0: */ michael@0: upgradeSchema8: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: // Delete "transactionId" index. michael@0: if (messageStore.indexNames.contains("transactionId")) { michael@0: messageStore.deleteIndex("transactionId"); michael@0: } michael@0: michael@0: // Create new "transactionId" indexes. michael@0: messageStore.createIndex("transactionId", "transactionIdIndex", { unique: true }); michael@0: michael@0: // Populate new "transactionIdIndex" attributes. michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if ("mms" == messageRecord.type && michael@0: (DELIVERY_NOT_DOWNLOADED == messageRecord.delivery || michael@0: DELIVERY_RECEIVED == messageRecord.delivery)) { michael@0: messageRecord.transactionIdIndex = michael@0: messageRecord.headers["x-mms-transaction-id"]; michael@0: cursor.update(messageRecord); michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: upgradeSchema9: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: // Update type attributes. michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == undefined) { michael@0: messageRecord.type = "sms"; michael@0: cursor.update(messageRecord); michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: upgradeSchema10: function(transaction, next) { michael@0: let threadStore = transaction.objectStore(THREAD_STORE_NAME); michael@0: michael@0: // Add 'lastMessageType' to each thread record. michael@0: threadStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let threadRecord = cursor.value; michael@0: let lastMessageId = threadRecord.lastMessageId; michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: let request = messageStore.mozGetAll(lastMessageId); michael@0: michael@0: request.onsuccess = function onsuccess() { michael@0: let messageRecord = request.result[0]; michael@0: if (!messageRecord) { michael@0: if (DEBUG) debug("Message ID " + lastMessageId + " not found"); michael@0: return; michael@0: } michael@0: if (messageRecord.id != lastMessageId) { michael@0: if (DEBUG) { michael@0: debug("Requested message ID (" + lastMessageId + ") is different from" + michael@0: " the one we got"); michael@0: } michael@0: return; michael@0: } michael@0: threadRecord.lastMessageType = messageRecord.type; michael@0: cursor.update(threadRecord); michael@0: cursor.continue(); michael@0: }; michael@0: michael@0: request.onerror = function onerror(event) { michael@0: if (DEBUG) { michael@0: if (event.target) { michael@0: debug("Caught error on transaction", event.target.errorCode); michael@0: } michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add envelopeId index for MMS. michael@0: */ michael@0: upgradeSchema11: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: // Delete "envelopeId" index. michael@0: if (messageStore.indexNames.contains("envelopeId")) { michael@0: messageStore.deleteIndex("envelopeId"); michael@0: } michael@0: michael@0: // Create new "envelopeId" indexes. michael@0: messageStore.createIndex("envelopeId", "envelopeIdIndex", { unique: true }); michael@0: michael@0: // Populate new "envelopeIdIndex" attributes. michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == "mms" && michael@0: messageRecord.delivery == DELIVERY_SENT) { michael@0: messageRecord.envelopeIdIndex = messageRecord.headers["message-id"]; michael@0: cursor.update(messageRecord); michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Replace deliveryStatus by deliveryInfo. michael@0: */ michael@0: upgradeSchema12: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == "mms") { michael@0: messageRecord.deliveryInfo = []; michael@0: michael@0: if (messageRecord.deliveryStatus.length == 1 && michael@0: (messageRecord.delivery == DELIVERY_NOT_DOWNLOADED || michael@0: messageRecord.delivery == DELIVERY_RECEIVED)) { michael@0: messageRecord.deliveryInfo.push({ michael@0: receiver: null, michael@0: deliveryStatus: messageRecord.deliveryStatus[0] }); michael@0: } else { michael@0: for (let i = 0; i < messageRecord.deliveryStatus.length; i++) { michael@0: messageRecord.deliveryInfo.push({ michael@0: receiver: messageRecord.receivers[i], michael@0: deliveryStatus: messageRecord.deliveryStatus[i] }); michael@0: } michael@0: } michael@0: delete messageRecord.deliveryStatus; michael@0: cursor.update(messageRecord); michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Check if we need to re-upgrade the DB schema 12. michael@0: */ michael@0: needReUpgradeSchema12: function(transaction, callback) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: callback(false); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == "mms" && michael@0: messageRecord.deliveryInfo === undefined) { michael@0: callback(true); michael@0: return; michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Fix the wrong participants. michael@0: */ michael@0: upgradeSchema13: function(transaction, next) { michael@0: let participantStore = transaction.objectStore(PARTICIPANT_STORE_NAME); michael@0: let threadStore = transaction.objectStore(THREAD_STORE_NAME); michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: let self = this; michael@0: michael@0: let isInvalid = function(participantRecord) { michael@0: let entries = []; michael@0: for (let addr of participantRecord.addresses) { michael@0: entries.push({ michael@0: normalized: addr, michael@0: parsed: PhoneNumberUtils.parseWithMCC(addr, null) michael@0: }) michael@0: } michael@0: for (let ix = 0 ; ix < entries.length - 1; ix++) { michael@0: let entry1 = entries[ix]; michael@0: for (let iy = ix + 1 ; iy < entries.length; iy ++) { michael@0: let entry2 = entries[iy]; michael@0: if (!self.matchPhoneNumbers(entry1.normalized, entry1.parsed, michael@0: entry2.normalized, entry2.parsed)) { michael@0: return true; michael@0: } michael@0: } michael@0: } michael@0: return false; michael@0: }; michael@0: michael@0: let invalidParticipantIds = []; michael@0: participantStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: let participantRecord = cursor.value; michael@0: // Check if this participant record is valid michael@0: if (isInvalid(participantRecord)) { michael@0: invalidParticipantIds.push(participantRecord.id); michael@0: cursor.delete(); michael@0: } michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // Participant store cursor iteration done. michael@0: if (!invalidParticipantIds.length) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: // Find affected thread. michael@0: let wrongThreads = []; michael@0: threadStore.openCursor().onsuccess = function(event) { michael@0: let threadCursor = event.target.result; michael@0: if (threadCursor) { michael@0: let threadRecord = threadCursor.value; michael@0: let participantIds = threadRecord.participantIds; michael@0: let foundInvalid = false; michael@0: for (let invalidParticipantId of invalidParticipantIds) { michael@0: if (participantIds.indexOf(invalidParticipantId) != -1) { michael@0: foundInvalid = true; michael@0: break; michael@0: } michael@0: } michael@0: if (foundInvalid) { michael@0: wrongThreads.push(threadRecord.id); michael@0: threadCursor.delete(); michael@0: } michael@0: threadCursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: if (!wrongThreads.length) { michael@0: next(); michael@0: return; michael@0: } michael@0: // Use recursive function to avoid we add participant twice. michael@0: (function createUpdateThreadAndParticipant(ix) { michael@0: let threadId = wrongThreads[ix]; michael@0: let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]); michael@0: messageStore.index("threadId").openCursor(range).onsuccess = function(event) { michael@0: let messageCursor = event.target.result; michael@0: if (!messageCursor) { michael@0: ix++; michael@0: if (ix === wrongThreads.length) { michael@0: next(); michael@0: return; michael@0: } michael@0: createUpdateThreadAndParticipant(ix); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = messageCursor.value; michael@0: let timestamp = messageRecord.timestamp; michael@0: let threadParticipants = []; michael@0: // Recaculate the thread participants of received message. michael@0: if (messageRecord.delivery === DELIVERY_RECEIVED || michael@0: messageRecord.delivery === DELIVERY_NOT_DOWNLOADED) { michael@0: threadParticipants.push(messageRecord.sender); michael@0: if (messageRecord.type == "mms") { michael@0: this.fillReceivedMmsThreadParticipants(messageRecord, threadParticipants); michael@0: } michael@0: } michael@0: // Recaculate the thread participants of sent messages and error michael@0: // messages. In error sms messages, we don't have error received sms. michael@0: // In received MMS, we don't update the error to deliver field but michael@0: // deliverStatus. So we only consider sent message in DELIVERY_ERROR. michael@0: else if (messageRecord.delivery === DELIVERY_SENT || michael@0: messageRecord.delivery === DELIVERY_ERROR) { michael@0: if (messageRecord.type == "sms") { michael@0: threadParticipants = [messageRecord.receiver]; michael@0: } else if (messageRecord.type == "mms") { michael@0: threadParticipants = messageRecord.receivers; michael@0: } michael@0: } michael@0: self.findThreadRecordByParticipants(threadStore, participantStore, michael@0: threadParticipants, true, michael@0: function(threadRecord, michael@0: participantIds) { michael@0: if (!participantIds) { michael@0: debug("participantIds is empty!"); michael@0: return; michael@0: } michael@0: michael@0: let timestamp = messageRecord.timestamp; michael@0: // Setup participantIdsIndex. michael@0: messageRecord.participantIdsIndex = []; michael@0: for each (let id in participantIds) { michael@0: messageRecord.participantIdsIndex.push([id, timestamp]); michael@0: } michael@0: if (threadRecord) { michael@0: let needsUpdate = false; michael@0: michael@0: if (threadRecord.lastTimestamp <= timestamp) { michael@0: threadRecord.lastTimestamp = timestamp; michael@0: threadRecord.subject = messageRecord.body; michael@0: threadRecord.lastMessageId = messageRecord.id; michael@0: threadRecord.lastMessageType = messageRecord.type; michael@0: needsUpdate = true; michael@0: } michael@0: michael@0: if (!messageRecord.read) { michael@0: threadRecord.unreadCount++; michael@0: needsUpdate = true; michael@0: } michael@0: michael@0: if (needsUpdate) { michael@0: threadStore.put(threadRecord); michael@0: } michael@0: messageRecord.threadId = threadRecord.id; michael@0: messageRecord.threadIdIndex = [threadRecord.id, timestamp]; michael@0: messageCursor.update(messageRecord); michael@0: messageCursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: let threadRecord = { michael@0: participantIds: participantIds, michael@0: participantAddresses: threadParticipants, michael@0: lastMessageId: messageRecord.id, michael@0: lastTimestamp: timestamp, michael@0: subject: messageRecord.body, michael@0: unreadCount: messageRecord.read ? 0 : 1, michael@0: lastMessageType: messageRecord.type michael@0: }; michael@0: threadStore.add(threadRecord).onsuccess = function(event) { michael@0: let threadId = event.target.result; michael@0: // Setup threadId & threadIdIndex. michael@0: messageRecord.threadId = threadId; michael@0: messageRecord.threadIdIndex = [threadId, timestamp]; michael@0: messageCursor.update(messageRecord); michael@0: messageCursor.continue(); michael@0: }; michael@0: }); michael@0: }; michael@0: })(0); michael@0: }; michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add deliveryTimestamp. michael@0: */ michael@0: upgradeSchema14: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == "sms") { michael@0: messageRecord.deliveryTimestamp = 0; michael@0: } else if (messageRecord.type == "mms") { michael@0: let deliveryInfo = messageRecord.deliveryInfo; michael@0: for (let i = 0; i < deliveryInfo.length; i++) { michael@0: deliveryInfo[i].deliveryTimestamp = 0; michael@0: } michael@0: } michael@0: cursor.update(messageRecord); michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add ICC ID. michael@0: */ michael@0: upgradeSchema15: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: messageRecord.iccId = null; michael@0: cursor.update(messageRecord); michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add isReadReportSent for incoming MMS. michael@0: */ michael@0: upgradeSchema16: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: // Update type attributes. michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == "mms") { michael@0: messageRecord.isReadReportSent = false; michael@0: cursor.update(messageRecord); michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: upgradeSchema17: function(transaction, next) { michael@0: let threadStore = transaction.objectStore(THREAD_STORE_NAME); michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: // Add 'lastMessageSubject' to each thread record. michael@0: threadStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let threadRecord = cursor.value; michael@0: // We have defined 'threadRecord.subject' in upgradeSchema7(), but it michael@0: // actually means 'threadRecord.body'. Swap the two values first. michael@0: threadRecord.body = threadRecord.subject; michael@0: delete threadRecord.subject; michael@0: michael@0: // Only MMS supports subject so assign null for non-MMS one. michael@0: if (threadRecord.lastMessageType != "mms") { michael@0: threadRecord.lastMessageSubject = null; michael@0: cursor.update(threadRecord); michael@0: michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: messageStore.get(threadRecord.lastMessageId).onsuccess = function(event) { michael@0: let messageRecord = event.target.result; michael@0: let subject = messageRecord.headers.subject; michael@0: threadRecord.lastMessageSubject = subject || null; michael@0: cursor.update(threadRecord); michael@0: michael@0: cursor.continue(); michael@0: }; michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add pid for incoming SMS. michael@0: */ michael@0: upgradeSchema18: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == "sms") { michael@0: messageRecord.pid = RIL.PDU_PID_DEFAULT; michael@0: cursor.update(messageRecord); michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add readStatus and readTimestamp. michael@0: */ michael@0: upgradeSchema19: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: if (messageRecord.type == "sms") { michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // We can always retrieve transaction id from michael@0: // |messageRecord.headers["x-mms-transaction-id"]|. michael@0: if (messageRecord.hasOwnProperty("transactionId")) { michael@0: delete messageRecord.transactionId; michael@0: } michael@0: michael@0: // xpconnect gives "undefined" for an unassigned argument of an interface michael@0: // method. michael@0: if (messageRecord.envelopeIdIndex === "undefined") { michael@0: delete messageRecord.envelopeIdIndex; michael@0: } michael@0: michael@0: // Convert some header fields that were originally decoded as BooleanValue michael@0: // to numeric enums. michael@0: for (let field of ["x-mms-cancel-status", michael@0: "x-mms-sender-visibility", michael@0: "x-mms-read-status"]) { michael@0: let value = messageRecord.headers[field]; michael@0: if (value !== undefined) { michael@0: messageRecord.headers[field] = value ? 128 : 129; michael@0: } michael@0: } michael@0: michael@0: // For all sent and received MMS messages, we have to add their michael@0: // |readStatus| and |readTimestamp| attributes in |deliveryInfo| array. michael@0: let readReportRequested = michael@0: messageRecord.headers["x-mms-read-report"] || false; michael@0: for (let element of messageRecord.deliveryInfo) { michael@0: element.readStatus = readReportRequested michael@0: ? MMS.DOM_READ_STATUS_PENDING michael@0: : MMS.DOM_READ_STATUS_NOT_APPLICABLE; michael@0: element.readTimestamp = 0; michael@0: } michael@0: michael@0: cursor.update(messageRecord); michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add sentTimestamp. michael@0: */ michael@0: upgradeSchema20: function(transaction, next) { michael@0: let messageStore = transaction.objectStore(MESSAGE_STORE_NAME); michael@0: messageStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: let messageRecord = cursor.value; michael@0: messageRecord.sentTimestamp = 0; michael@0: michael@0: // We can still have changes to assign |sentTimestamp| for the existing michael@0: // MMS message records. michael@0: if (messageRecord.type == "mms" && messageRecord.headers["date"]) { michael@0: messageRecord.sentTimestamp = messageRecord.headers["date"].getTime(); michael@0: } michael@0: michael@0: cursor.update(messageRecord); michael@0: cursor.continue(); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Add smsSegmentStore to store uncomplete SMS segments. michael@0: */ michael@0: upgradeSchema21: function(db, transaction, next) { michael@0: /** michael@0: * This smsSegmentStore is used to store uncomplete SMS segments. michael@0: * Each entry looks like this: michael@0: * michael@0: * { michael@0: * [Common fields in SMS segment] michael@0: * messageType: , michael@0: * teleservice: , michael@0: * SMSC: , michael@0: * sentTimestamp: , michael@0: * timestamp: , michael@0: * sender: , michael@0: * pid: , michael@0: * encoding: , michael@0: * messageClass: , michael@0: * iccId: , michael@0: * michael@0: * [Concatenation Info] michael@0: * segmentRef: , michael@0: * segmentSeq: , michael@0: * segmentMaxSeq: , michael@0: * michael@0: * [Application Port Info] michael@0: * originatorPort: , michael@0: * destinationPort: , michael@0: * michael@0: * [MWI status] michael@0: * mwiPresent: , michael@0: * mwiDiscard: , michael@0: * mwiMsgCount: , michael@0: * mwiActive: , michael@0: * michael@0: * [CDMA Cellbroadcast related fields] michael@0: * serviceCategory: , michael@0: * language: , michael@0: * michael@0: * [Message Body] michael@0: * data: , (available if it's 8bit encoding) michael@0: * body: , (normal text body) michael@0: * michael@0: * [Handy fields created by DB for concatenation] michael@0: * id: , keypath of this objectStore. michael@0: * hash: , Use to identify the segments to the same SMS. michael@0: * receivedSegments: , michael@0: * segments: [] michael@0: * } michael@0: * michael@0: */ michael@0: let smsSegmentStore = db.createObjectStore(SMS_SEGMENT_STORE_NAME, michael@0: { keyPath: "id", michael@0: autoIncrement: true }); michael@0: smsSegmentStore.createIndex("hash", "hash", { unique: true }); michael@0: next(); michael@0: }, michael@0: michael@0: matchParsedPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) { michael@0: if ((parsedAddr1.internationalNumber && michael@0: parsedAddr1.internationalNumber === parsedAddr2.internationalNumber) || michael@0: (parsedAddr1.nationalNumber && michael@0: parsedAddr1.nationalNumber === parsedAddr2.nationalNumber)) { michael@0: return true; michael@0: } michael@0: michael@0: if (parsedAddr1.countryName != parsedAddr2.countryName) { michael@0: return false; michael@0: } michael@0: michael@0: let ssPref = "dom.phonenumber.substringmatching." + parsedAddr1.countryName; michael@0: if (Services.prefs.getPrefType(ssPref) != Ci.nsIPrefBranch.PREF_INT) { michael@0: return false; michael@0: } michael@0: michael@0: let val = Services.prefs.getIntPref(ssPref); michael@0: return addr1.length > val && michael@0: addr2.length > val && michael@0: addr1.slice(-val) === addr2.slice(-val); michael@0: }, michael@0: michael@0: matchPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) { michael@0: if (parsedAddr1 && parsedAddr2) { michael@0: return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2); michael@0: } michael@0: michael@0: if (parsedAddr1) { michael@0: parsedAddr2 = PhoneNumberUtils.parseWithCountryName(addr2, parsedAddr1.countryName); michael@0: if (parsedAddr2) { michael@0: return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2); michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: if (parsedAddr2) { michael@0: parsedAddr1 = PhoneNumberUtils.parseWithCountryName(addr1, parsedAddr2.countryName); michael@0: if (parsedAddr1) { michael@0: return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2); michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: createDomMessageFromRecord: function(aMessageRecord) { michael@0: if (DEBUG) { michael@0: debug("createDomMessageFromRecord: " + JSON.stringify(aMessageRecord)); michael@0: } michael@0: if (aMessageRecord.type == "sms") { michael@0: return gMobileMessageService.createSmsMessage(aMessageRecord.id, michael@0: aMessageRecord.threadId, michael@0: aMessageRecord.iccId, michael@0: aMessageRecord.delivery, michael@0: aMessageRecord.deliveryStatus, michael@0: aMessageRecord.sender, michael@0: aMessageRecord.receiver, michael@0: aMessageRecord.body, michael@0: aMessageRecord.messageClass, michael@0: aMessageRecord.timestamp, michael@0: aMessageRecord.sentTimestamp, michael@0: aMessageRecord.deliveryTimestamp, michael@0: aMessageRecord.read); michael@0: } else if (aMessageRecord.type == "mms") { michael@0: let headers = aMessageRecord["headers"]; michael@0: if (DEBUG) { michael@0: debug("MMS: headers: " + JSON.stringify(headers)); michael@0: } michael@0: michael@0: let subject = headers["subject"]; michael@0: if (subject == undefined) { michael@0: subject = ""; michael@0: } michael@0: michael@0: let smil = ""; michael@0: let attachments = []; michael@0: let parts = aMessageRecord.parts; michael@0: if (parts) { michael@0: for (let i = 0; i < parts.length; i++) { michael@0: let part = parts[i]; michael@0: if (DEBUG) { michael@0: debug("MMS: part[" + i + "]: " + JSON.stringify(part)); michael@0: } michael@0: // Sometimes the part is incomplete because the device reboots when michael@0: // downloading MMS. Don't need to expose this part to the content. michael@0: if (!part) { michael@0: continue; michael@0: } michael@0: michael@0: let partHeaders = part["headers"]; michael@0: let partContent = part["content"]; michael@0: // Don't need to make the SMIL part if it's present. michael@0: if (partHeaders["content-type"]["media"] == "application/smil") { michael@0: smil = partContent; michael@0: continue; michael@0: } michael@0: attachments.push({ michael@0: "id": partHeaders["content-id"], michael@0: "location": partHeaders["content-location"], michael@0: "content": partContent michael@0: }); michael@0: } michael@0: } michael@0: let expiryDate = 0; michael@0: if (headers["x-mms-expiry"] != undefined) { michael@0: expiryDate = aMessageRecord.timestamp + headers["x-mms-expiry"] * 1000; michael@0: } michael@0: let readReportRequested = headers["x-mms-read-report"] || false; michael@0: return gMobileMessageService.createMmsMessage(aMessageRecord.id, michael@0: aMessageRecord.threadId, michael@0: aMessageRecord.iccId, michael@0: aMessageRecord.delivery, michael@0: aMessageRecord.deliveryInfo, michael@0: aMessageRecord.sender, michael@0: aMessageRecord.receivers, michael@0: aMessageRecord.timestamp, michael@0: aMessageRecord.sentTimestamp, michael@0: aMessageRecord.read, michael@0: subject, michael@0: smil, michael@0: attachments, michael@0: expiryDate, michael@0: readReportRequested); michael@0: } michael@0: }, michael@0: michael@0: findParticipantRecordByAddress: function(aParticipantStore, aAddress, michael@0: aCreate, aCallback) { michael@0: if (DEBUG) { michael@0: debug("findParticipantRecordByAddress(" michael@0: + JSON.stringify(aAddress) + ", " + aCreate + ")"); michael@0: } michael@0: michael@0: // Two types of input number to match here, international(+886987654321), michael@0: // and local(0987654321) types. The "nationalNumber" parsed from michael@0: // phonenumberutils will be "987654321" in this case. michael@0: michael@0: // Normalize address before searching for participant record. michael@0: let normalizedAddress = PhoneNumberUtils.normalize(aAddress, false); michael@0: let allPossibleAddresses = [normalizedAddress]; michael@0: let parsedAddress = PhoneNumberUtils.parse(normalizedAddress); michael@0: if (parsedAddress && parsedAddress.internationalNumber && michael@0: allPossibleAddresses.indexOf(parsedAddress.internationalNumber) < 0) { michael@0: // We only stores international numbers into participant store because michael@0: // the parsed national number doesn't contain country info and may michael@0: // duplicate in different country. michael@0: allPossibleAddresses.push(parsedAddress.internationalNumber); michael@0: } michael@0: if (DEBUG) { michael@0: debug("findParticipantRecordByAddress: allPossibleAddresses = " + michael@0: JSON.stringify(allPossibleAddresses)); michael@0: } michael@0: michael@0: // Make a copy here because we may need allPossibleAddresses again. michael@0: let needles = allPossibleAddresses.slice(0); michael@0: let request = aParticipantStore.index("addresses").get(needles.pop()); michael@0: request.onsuccess = (function onsuccess(event) { michael@0: let participantRecord = event.target.result; michael@0: // 1) First try matching through "addresses" index of participant store. michael@0: // If we're lucky, return the fetched participant record. michael@0: if (participantRecord) { michael@0: if (DEBUG) { michael@0: debug("findParticipantRecordByAddress: got " michael@0: + JSON.stringify(participantRecord)); michael@0: } michael@0: aCallback(participantRecord); michael@0: return; michael@0: } michael@0: michael@0: // Try next possible address again. michael@0: if (needles.length) { michael@0: let request = aParticipantStore.index("addresses").get(needles.pop()); michael@0: request.onsuccess = onsuccess.bind(this); michael@0: return; michael@0: } michael@0: michael@0: // 2) Traverse throught all participants and check all alias addresses. michael@0: aParticipantStore.openCursor().onsuccess = (function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: // Have traversed whole object store but still in vain. michael@0: if (!aCreate) { michael@0: aCallback(null); michael@0: return; michael@0: } michael@0: michael@0: let participantRecord = { addresses: [normalizedAddress] }; michael@0: let addRequest = aParticipantStore.add(participantRecord); michael@0: addRequest.onsuccess = function(event) { michael@0: participantRecord.id = event.target.result; michael@0: if (DEBUG) { michael@0: debug("findParticipantRecordByAddress: created " michael@0: + JSON.stringify(participantRecord)); michael@0: } michael@0: aCallback(participantRecord); michael@0: }; michael@0: return; michael@0: } michael@0: michael@0: let participantRecord = cursor.value; michael@0: for (let storedAddress of participantRecord.addresses) { michael@0: let parsedStoredAddress = PhoneNumberUtils.parseWithMCC(storedAddress, null); michael@0: let match = this.matchPhoneNumbers(normalizedAddress, parsedAddress, michael@0: storedAddress, parsedStoredAddress); michael@0: if (!match) { michael@0: // 3) Else we fail to match current stored participant record. michael@0: continue; michael@0: } michael@0: // Match! michael@0: if (aCreate) { michael@0: // In a READ-WRITE transaction, append one more possible address for michael@0: // this participant record. michael@0: participantRecord.addresses = michael@0: participantRecord.addresses.concat(allPossibleAddresses); michael@0: cursor.update(participantRecord); michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: debug("findParticipantRecordByAddress: match " michael@0: + JSON.stringify(cursor.value)); michael@0: } michael@0: aCallback(participantRecord); michael@0: return; michael@0: } michael@0: michael@0: // Check next participant record if available. michael@0: cursor.continue(); michael@0: }).bind(this); michael@0: }).bind(this); michael@0: }, michael@0: michael@0: findParticipantIdsByAddresses: function(aParticipantStore, aAddresses, michael@0: aCreate, aSkipNonexistent, aCallback) { michael@0: if (DEBUG) { michael@0: debug("findParticipantIdsByAddresses(" michael@0: + JSON.stringify(aAddresses) + ", " michael@0: + aCreate + ", " + aSkipNonexistent + ")"); michael@0: } michael@0: michael@0: if (!aAddresses || !aAddresses.length) { michael@0: if (DEBUG) debug("findParticipantIdsByAddresses: returning null"); michael@0: aCallback(null); michael@0: return; michael@0: } michael@0: michael@0: let self = this; michael@0: (function findParticipantId(index, result) { michael@0: if (index >= aAddresses.length) { michael@0: // Sort numerically. michael@0: result.sort(function(a, b) { michael@0: return a - b; michael@0: }); michael@0: if (DEBUG) debug("findParticipantIdsByAddresses: returning " + result); michael@0: aCallback(result); michael@0: return; michael@0: } michael@0: michael@0: self.findParticipantRecordByAddress(aParticipantStore, michael@0: aAddresses[index++], aCreate, michael@0: function(participantRecord) { michael@0: if (!participantRecord) { michael@0: if (!aSkipNonexistent) { michael@0: if (DEBUG) debug("findParticipantIdsByAddresses: returning null"); michael@0: aCallback(null); michael@0: return; michael@0: } michael@0: } else if (result.indexOf(participantRecord.id) < 0) { michael@0: result.push(participantRecord.id); michael@0: } michael@0: findParticipantId(index, result); michael@0: }); michael@0: }) (0, []); michael@0: }, michael@0: michael@0: findThreadRecordByParticipants: function(aThreadStore, aParticipantStore, michael@0: aAddresses, aCreateParticipants, michael@0: aCallback) { michael@0: if (DEBUG) { michael@0: debug("findThreadRecordByParticipants(" + JSON.stringify(aAddresses) michael@0: + ", " + aCreateParticipants + ")"); michael@0: } michael@0: this.findParticipantIdsByAddresses(aParticipantStore, aAddresses, michael@0: aCreateParticipants, false, michael@0: function(participantIds) { michael@0: if (!participantIds) { michael@0: if (DEBUG) debug("findThreadRecordByParticipants: returning null"); michael@0: aCallback(null, null); michael@0: return; michael@0: } michael@0: // Find record from thread store. michael@0: let request = aThreadStore.index("participantIds").get(participantIds); michael@0: request.onsuccess = function(event) { michael@0: let threadRecord = event.target.result; michael@0: if (DEBUG) { michael@0: debug("findThreadRecordByParticipants: return " michael@0: + JSON.stringify(threadRecord)); michael@0: } michael@0: aCallback(threadRecord, participantIds); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: newTxnWithCallback: function(aCallback, aFunc, aStoreNames) { michael@0: let self = this; michael@0: this.newTxn(READ_WRITE, function(aError, aTransaction, aStores) { michael@0: let notifyResult = function(aRv, aMessageRecord) { michael@0: if (!aCallback) { michael@0: return; michael@0: } michael@0: let domMessage = michael@0: aMessageRecord && self.createDomMessageFromRecord(aMessageRecord); michael@0: aCallback.notify(aRv, domMessage); michael@0: }; michael@0: michael@0: if (aError) { michael@0: notifyResult(aError, null); michael@0: return; michael@0: } michael@0: michael@0: let capture = {}; michael@0: aTransaction.oncomplete = function(event) { michael@0: notifyResult(Cr.NS_OK, capture.messageRecord); michael@0: }; michael@0: aTransaction.onabort = function(event) { michael@0: // TODO bug 832140 check event.target.errorCode michael@0: notifyResult(Cr.NS_ERROR_FAILURE, null); michael@0: }; michael@0: michael@0: aFunc(capture, aStores); michael@0: }, aStoreNames); michael@0: }, michael@0: michael@0: saveRecord: function(aMessageRecord, aAddresses, aCallback) { michael@0: if (DEBUG) debug("Going to store " + JSON.stringify(aMessageRecord)); michael@0: michael@0: let self = this; michael@0: this.newTxn(READ_WRITE, function(error, txn, stores) { michael@0: let notifyResult = function(aRv, aMessageRecord) { michael@0: if (!aCallback) { michael@0: return; michael@0: } michael@0: let domMessage = michael@0: aMessageRecord && self.createDomMessageFromRecord(aMessageRecord); michael@0: aCallback.notify(aRv, domMessage); michael@0: }; michael@0: michael@0: if (error) { michael@0: notifyResult(error, null); michael@0: return; michael@0: } michael@0: michael@0: txn.oncomplete = function oncomplete(event) { michael@0: if (aMessageRecord.id > self.lastMessageId) { michael@0: self.lastMessageId = aMessageRecord.id; michael@0: } michael@0: notifyResult(Cr.NS_OK, aMessageRecord); michael@0: }; michael@0: txn.onabort = function onabort(event) { michael@0: // TODO bug 832140 check event.target.errorCode michael@0: notifyResult(Cr.NS_ERROR_FAILURE, null); michael@0: }; michael@0: michael@0: let messageStore = stores[0]; michael@0: let participantStore = stores[1]; michael@0: let threadStore = stores[2]; michael@0: self.replaceShortMessageOnSave(txn, messageStore, participantStore, michael@0: threadStore, aMessageRecord, aAddresses); michael@0: }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME, THREAD_STORE_NAME]); michael@0: }, michael@0: michael@0: replaceShortMessageOnSave: function(aTransaction, aMessageStore, michael@0: aParticipantStore, aThreadStore, michael@0: aMessageRecord, aAddresses) { michael@0: let isReplaceTypePid = (aMessageRecord.pid) && michael@0: ((aMessageRecord.pid >= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_1 && michael@0: aMessageRecord.pid <= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_7) || michael@0: aMessageRecord.pid == RIL.PDU_PID_RETURN_CALL_MESSAGE); michael@0: michael@0: if (aMessageRecord.type != "sms" || michael@0: aMessageRecord.delivery != DELIVERY_RECEIVED || michael@0: !isReplaceTypePid) { michael@0: this.realSaveRecord(aTransaction, aMessageStore, aParticipantStore, michael@0: aThreadStore, aMessageRecord, aAddresses); michael@0: return; michael@0: } michael@0: michael@0: // 3GPP TS 23.040 subclause 9.2.3.9 "TP-Protocol-Identifier (TP-PID)": michael@0: // michael@0: // ... the MS shall check the originating address and replace any michael@0: // existing stored message having the same Protocol Identifier code michael@0: // and originating address with the new short message and other michael@0: // parameter values. If there is no message to be replaced, the MS michael@0: // shall store the message in the normal way. ... it is recommended michael@0: // that the SC address should not be checked by the MS." michael@0: let self = this; michael@0: this.findParticipantRecordByAddress(aParticipantStore, michael@0: aMessageRecord.sender, false, michael@0: function(participantRecord) { michael@0: if (!participantRecord) { michael@0: self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore, michael@0: aThreadStore, aMessageRecord, aAddresses); michael@0: return; michael@0: } michael@0: michael@0: let participantId = participantRecord.id; michael@0: let range = IDBKeyRange.bound([participantId, 0], [participantId, ""]); michael@0: let request = aMessageStore.index("participantIds").openCursor(range); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore, michael@0: aThreadStore, aMessageRecord, aAddresses); michael@0: return; michael@0: } michael@0: michael@0: // A message record with same participantId found. michael@0: // Verify matching criteria. michael@0: let foundMessageRecord = cursor.value; michael@0: if (foundMessageRecord.type != "sms" || michael@0: foundMessageRecord.sender != aMessageRecord.sender || michael@0: foundMessageRecord.pid != aMessageRecord.pid) { michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // Match! Now replace that found message record with current one. michael@0: aMessageRecord.id = foundMessageRecord.id; michael@0: self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore, michael@0: aThreadStore, aMessageRecord, aAddresses); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: realSaveRecord: function(aTransaction, aMessageStore, aParticipantStore, michael@0: aThreadStore, aMessageRecord, aAddresses) { michael@0: let self = this; michael@0: this.findThreadRecordByParticipants(aThreadStore, aParticipantStore, michael@0: aAddresses, true, michael@0: function(threadRecord, participantIds) { michael@0: if (!participantIds) { michael@0: aTransaction.abort(); michael@0: return; michael@0: } michael@0: michael@0: let isOverriding = (aMessageRecord.id !== undefined); michael@0: if (!isOverriding) { michael@0: // |self.lastMessageId| is only updated in |txn.oncomplete|. michael@0: aMessageRecord.id = self.lastMessageId + 1; michael@0: } michael@0: michael@0: let timestamp = aMessageRecord.timestamp; michael@0: let insertMessageRecord = function(threadId) { michael@0: // Setup threadId & threadIdIndex. michael@0: aMessageRecord.threadId = threadId; michael@0: aMessageRecord.threadIdIndex = [threadId, timestamp]; michael@0: // Setup participantIdsIndex. michael@0: aMessageRecord.participantIdsIndex = []; michael@0: for each (let id in participantIds) { michael@0: aMessageRecord.participantIdsIndex.push([id, timestamp]); michael@0: } michael@0: michael@0: if (!isOverriding) { michael@0: // Really add to message store. michael@0: aMessageStore.put(aMessageRecord); michael@0: return; michael@0: } michael@0: michael@0: // If we're going to override an old message, we need to update the michael@0: // info of the original thread containing the overridden message. michael@0: // To get the original thread ID and read status of the overridden michael@0: // message record, we need to retrieve it before overriding it. michael@0: aMessageStore.get(aMessageRecord.id).onsuccess = function(event) { michael@0: let oldMessageRecord = event.target.result; michael@0: aMessageStore.put(aMessageRecord); michael@0: if (oldMessageRecord) { michael@0: self.updateThreadByMessageChange(aMessageStore, michael@0: aThreadStore, michael@0: oldMessageRecord.threadId, michael@0: aMessageRecord.id, michael@0: oldMessageRecord.read); michael@0: } michael@0: }; michael@0: }; michael@0: michael@0: if (threadRecord) { michael@0: let needsUpdate = false; michael@0: michael@0: if (threadRecord.lastTimestamp <= timestamp) { michael@0: let lastMessageSubject; michael@0: if (aMessageRecord.type == "mms") { michael@0: lastMessageSubject = aMessageRecord.headers.subject; michael@0: } michael@0: threadRecord.lastMessageSubject = lastMessageSubject || null; michael@0: threadRecord.lastTimestamp = timestamp; michael@0: threadRecord.body = aMessageRecord.body; michael@0: threadRecord.lastMessageId = aMessageRecord.id; michael@0: threadRecord.lastMessageType = aMessageRecord.type; michael@0: needsUpdate = true; michael@0: } michael@0: michael@0: if (!aMessageRecord.read) { michael@0: threadRecord.unreadCount++; michael@0: needsUpdate = true; michael@0: } michael@0: michael@0: if (needsUpdate) { michael@0: aThreadStore.put(threadRecord); michael@0: } michael@0: michael@0: insertMessageRecord(threadRecord.id); michael@0: return; michael@0: } michael@0: michael@0: let lastMessageSubject; michael@0: if (aMessageRecord.type == "mms") { michael@0: lastMessageSubject = aMessageRecord.headers.subject; michael@0: } michael@0: michael@0: threadRecord = { michael@0: participantIds: participantIds, michael@0: participantAddresses: aAddresses, michael@0: lastMessageId: aMessageRecord.id, michael@0: lastTimestamp: timestamp, michael@0: lastMessageSubject: lastMessageSubject || null, michael@0: body: aMessageRecord.body, michael@0: unreadCount: aMessageRecord.read ? 0 : 1, michael@0: lastMessageType: aMessageRecord.type, michael@0: }; michael@0: aThreadStore.add(threadRecord).onsuccess = function(event) { michael@0: let threadId = event.target.result; michael@0: insertMessageRecord(threadId); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: forEachMatchedMmsDeliveryInfo: function(aDeliveryInfo, aNeedle, aCallback) { michael@0: michael@0: let typedAddress = { michael@0: type: MMS.Address.resolveType(aNeedle), michael@0: address: aNeedle michael@0: }; michael@0: let normalizedAddress, parsedAddress; michael@0: if (typedAddress.type === "PLMN") { michael@0: normalizedAddress = PhoneNumberUtils.normalize(aNeedle, false); michael@0: parsedAddress = PhoneNumberUtils.parse(normalizedAddress); michael@0: } michael@0: michael@0: for (let element of aDeliveryInfo) { michael@0: let typedStoredAddress = { michael@0: type: MMS.Address.resolveType(element.receiver), michael@0: address: element.receiver michael@0: }; michael@0: if (typedAddress.type !== typedStoredAddress.type) { michael@0: // Not even my type. Skip. michael@0: continue; michael@0: } michael@0: michael@0: if (typedAddress.address == typedStoredAddress.address) { michael@0: // Have a direct match. michael@0: aCallback(element); michael@0: continue; michael@0: } michael@0: michael@0: if (typedAddress.type !== "PLMN") { michael@0: // Address type other than "PLMN" must have direct match. Or, skip. michael@0: continue; michael@0: } michael@0: michael@0: // Both are of "PLMN" type. michael@0: let normalizedStoredAddress = michael@0: PhoneNumberUtils.normalize(element.receiver, false); michael@0: let parsedStoredAddress = michael@0: PhoneNumberUtils.parseWithMCC(normalizedStoredAddress, null); michael@0: if (this.matchPhoneNumbers(normalizedAddress, parsedAddress, michael@0: normalizedStoredAddress, parsedStoredAddress)) { michael@0: aCallback(element); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: updateMessageDeliveryById: function(id, type, receiver, delivery, michael@0: deliveryStatus, envelopeId, callback) { michael@0: if (DEBUG) { michael@0: debug("Setting message's delivery by " + type + " = "+ id michael@0: + " receiver: " + receiver michael@0: + " delivery: " + delivery michael@0: + " deliveryStatus: " + deliveryStatus michael@0: + " envelopeId: " + envelopeId); michael@0: } michael@0: michael@0: let self = this; michael@0: this.newTxnWithCallback(callback, function(aCapture, aMessageStore) { michael@0: let getRequest; michael@0: if (type === "messageId") { michael@0: getRequest = aMessageStore.get(id); michael@0: } else if (type === "envelopeId") { michael@0: getRequest = aMessageStore.index("envelopeId").get(id); michael@0: } michael@0: michael@0: getRequest.onsuccess = function onsuccess(event) { michael@0: let messageRecord = event.target.result; michael@0: if (!messageRecord) { michael@0: if (DEBUG) debug("type = " + id + " is not found"); michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: let isRecordUpdated = false; michael@0: michael@0: // Update |messageRecord.delivery| if needed. michael@0: if (delivery && messageRecord.delivery != delivery) { michael@0: messageRecord.delivery = delivery; michael@0: messageRecord.deliveryIndex = [delivery, messageRecord.timestamp]; michael@0: isRecordUpdated = true; michael@0: michael@0: // When updating an message's delivey state to 'sent', we also update michael@0: // its |sentTimestamp| by the current device timestamp to represent michael@0: // when the message is successfully sent. michael@0: if (delivery == DELIVERY_SENT) { michael@0: messageRecord.sentTimestamp = Date.now(); michael@0: } michael@0: } michael@0: michael@0: // Attempt to update |deliveryStatus| and |deliveryTimestamp| of: michael@0: // - the |messageRecord| for SMS. michael@0: // - the element(s) in |messageRecord.deliveryInfo| for MMS. michael@0: if (deliveryStatus) { michael@0: // A callback for updating the deliveyStatus/deliveryTimestamp of michael@0: // each target. michael@0: let updateFunc = function(aTarget) { michael@0: if (aTarget.deliveryStatus == deliveryStatus) { michael@0: return; michael@0: } michael@0: michael@0: aTarget.deliveryStatus = deliveryStatus; michael@0: michael@0: // Update |deliveryTimestamp| if it's successfully delivered. michael@0: if (deliveryStatus == DELIVERY_STATUS_SUCCESS) { michael@0: aTarget.deliveryTimestamp = Date.now(); michael@0: } michael@0: michael@0: isRecordUpdated = true; michael@0: }; michael@0: michael@0: if (messageRecord.type == "sms") { michael@0: updateFunc(messageRecord); michael@0: } else if (messageRecord.type == "mms") { michael@0: if (!receiver) { michael@0: // If the receiver is specified, we only need to update the michael@0: // element(s) in deliveryInfo that match the same receiver. michael@0: messageRecord.deliveryInfo.forEach(updateFunc); michael@0: } else { michael@0: self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo, michael@0: receiver, updateFunc); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Update |messageRecord.envelopeIdIndex| if needed. michael@0: if (envelopeId) { michael@0: if (messageRecord.envelopeIdIndex != envelopeId) { michael@0: messageRecord.envelopeIdIndex = envelopeId; michael@0: isRecordUpdated = true; michael@0: } michael@0: } michael@0: michael@0: aCapture.messageRecord = messageRecord; michael@0: if (!isRecordUpdated) { michael@0: if (DEBUG) { michael@0: debug("The values of delivery, deliveryStatus and envelopeId " + michael@0: "don't need to be updated."); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: debug("The delivery, deliveryStatus or envelopeId are updated."); michael@0: } michael@0: aMessageStore.put(messageRecord); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: fillReceivedMmsThreadParticipants: function(aMessage, threadParticipants) { michael@0: let receivers = aMessage.receivers; michael@0: // If we don't want to disable the MMS grouping for receiving, we need to michael@0: // add the receivers (excluding the user's own number) to the participants michael@0: // for creating the thread. Some cases might be investigated as below: michael@0: // michael@0: // 1. receivers.length == 0 michael@0: // This usually happens when receiving an MMS notification indication michael@0: // which doesn't carry any receivers. michael@0: // 2. receivers.length == 1 michael@0: // If the receivers contain single phone number, we don't need to michael@0: // add it into participants because we know that number is our own. michael@0: // 3. receivers.length >= 2 michael@0: // If the receivers contain multiple phone numbers, we need to add all michael@0: // of them but not the user's own number into participants. michael@0: if (DISABLE_MMS_GROUPING_FOR_RECEIVING || receivers.length < 2) { michael@0: return; michael@0: } michael@0: let isSuccess = false; michael@0: let slicedReceivers = receivers.slice(); michael@0: if (aMessage.msisdn) { michael@0: let found = slicedReceivers.indexOf(aMessage.msisdn); michael@0: if (found !== -1) { michael@0: isSuccess = true; michael@0: slicedReceivers.splice(found, 1); michael@0: } michael@0: } michael@0: michael@0: if (!isSuccess) { michael@0: // For some SIMs we cannot retrieve the vaild MSISDN (i.e. the user's michael@0: // own phone number), so we cannot correcly exclude the user's own michael@0: // number from the receivers, thus wrongly building the thread index. michael@0: if (DEBUG) debug("Error! Cannot strip out user's own phone number!"); michael@0: } michael@0: michael@0: threadParticipants = threadParticipants.concat(slicedReceivers); michael@0: }, michael@0: michael@0: updateThreadByMessageChange: function(messageStore, threadStore, threadId, michael@0: messageId, messageRead) { michael@0: threadStore.get(threadId).onsuccess = function(event) { michael@0: // This must exist. michael@0: let threadRecord = event.target.result; michael@0: if (DEBUG) debug("Updating thread record " + JSON.stringify(threadRecord)); michael@0: michael@0: if (!messageRead) { michael@0: threadRecord.unreadCount--; michael@0: } michael@0: michael@0: if (threadRecord.lastMessageId == messageId) { michael@0: // Check most recent sender/receiver. michael@0: let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]); michael@0: let request = messageStore.index("threadId") michael@0: .openCursor(range, PREV); michael@0: request.onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: if (DEBUG) { michael@0: debug("Deleting mru entry for thread id " + threadId); michael@0: } michael@0: threadStore.delete(threadId); michael@0: return; michael@0: } michael@0: michael@0: let nextMsg = cursor.value; michael@0: let lastMessageSubject; michael@0: if (nextMsg.type == "mms") { michael@0: lastMessageSubject = nextMsg.headers.subject; michael@0: } michael@0: threadRecord.lastMessageSubject = lastMessageSubject || null; michael@0: threadRecord.lastMessageId = nextMsg.id; michael@0: threadRecord.lastTimestamp = nextMsg.timestamp; michael@0: threadRecord.body = nextMsg.body; michael@0: threadRecord.lastMessageType = nextMsg.type; michael@0: if (DEBUG) { michael@0: debug("Updating mru entry: " + michael@0: JSON.stringify(threadRecord)); michael@0: } michael@0: threadStore.put(threadRecord); michael@0: }; michael@0: } else if (!messageRead) { michael@0: // Shortcut, just update the unread count. michael@0: if (DEBUG) { michael@0: debug("Updating unread count for thread id " + threadId + ": " + michael@0: (threadRecord.unreadCount + 1) + " -> " + michael@0: threadRecord.unreadCount); michael@0: } michael@0: threadStore.put(threadRecord); michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * nsIRilMobileMessageDatabaseService API michael@0: */ michael@0: michael@0: saveReceivedMessage: function(aMessage, aCallback) { michael@0: if ((aMessage.type != "sms" && aMessage.type != "mms") || michael@0: (aMessage.type == "sms" && (aMessage.messageClass == undefined || michael@0: aMessage.sender == undefined)) || michael@0: (aMessage.type == "mms" && (aMessage.delivery == undefined || michael@0: aMessage.deliveryStatus == undefined || michael@0: !Array.isArray(aMessage.receivers))) || michael@0: aMessage.timestamp == undefined) { michael@0: if (aCallback) { michael@0: aCallback.notify(Cr.NS_ERROR_FAILURE, null); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let threadParticipants; michael@0: if (aMessage.type == "mms") { michael@0: if (aMessage.headers.from) { michael@0: aMessage.sender = aMessage.headers.from.address; michael@0: } else { michael@0: aMessage.sender = "anonymous"; michael@0: } michael@0: michael@0: threadParticipants = [aMessage.sender]; michael@0: this.fillReceivedMmsThreadParticipants(aMessage, threadParticipants); michael@0: } else { // SMS michael@0: threadParticipants = [aMessage.sender]; michael@0: } michael@0: michael@0: let timestamp = aMessage.timestamp; michael@0: michael@0: // Adding needed indexes and extra attributes for internal use. michael@0: // threadIdIndex & participantIdsIndex are filled in saveRecord(). michael@0: aMessage.readIndex = [FILTER_READ_UNREAD, timestamp]; michael@0: aMessage.read = FILTER_READ_UNREAD; michael@0: michael@0: // If |sentTimestamp| is not specified, use 0 as default. michael@0: if (aMessage.sentTimestamp == undefined) { michael@0: aMessage.sentTimestamp = 0; michael@0: } michael@0: michael@0: if (aMessage.type == "mms") { michael@0: aMessage.transactionIdIndex = aMessage.headers["x-mms-transaction-id"]; michael@0: aMessage.isReadReportSent = false; michael@0: michael@0: // As a receiver, we don't need to care about the delivery status of michael@0: // others, so we put a single element with self's phone number in the michael@0: // |deliveryInfo| array. michael@0: aMessage.deliveryInfo = [{ michael@0: receiver: aMessage.phoneNumber, michael@0: deliveryStatus: aMessage.deliveryStatus, michael@0: deliveryTimestamp: 0, michael@0: readStatus: MMS.DOM_READ_STATUS_NOT_APPLICABLE, michael@0: readTimestamp: 0, michael@0: }]; michael@0: michael@0: delete aMessage.deliveryStatus; michael@0: } michael@0: michael@0: if (aMessage.type == "sms") { michael@0: aMessage.delivery = DELIVERY_RECEIVED; michael@0: aMessage.deliveryStatus = DELIVERY_STATUS_SUCCESS; michael@0: aMessage.deliveryTimestamp = 0; michael@0: michael@0: if (aMessage.pid == undefined) { michael@0: aMessage.pid = RIL.PDU_PID_DEFAULT; michael@0: } michael@0: } michael@0: aMessage.deliveryIndex = [aMessage.delivery, timestamp]; michael@0: michael@0: this.saveRecord(aMessage, threadParticipants, aCallback); michael@0: }, michael@0: michael@0: saveSendingMessage: function(aMessage, aCallback) { michael@0: if ((aMessage.type != "sms" && aMessage.type != "mms") || michael@0: (aMessage.type == "sms" && aMessage.receiver == undefined) || michael@0: (aMessage.type == "mms" && !Array.isArray(aMessage.receivers)) || michael@0: aMessage.deliveryStatusRequested == undefined || michael@0: aMessage.timestamp == undefined) { michael@0: if (aCallback) { michael@0: aCallback.notify(Cr.NS_ERROR_FAILURE, null); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // Set |aMessage.deliveryStatus|. Note that for MMS record michael@0: // it must be an array of strings; For SMS, it's a string. michael@0: let deliveryStatus = aMessage.deliveryStatusRequested michael@0: ? DELIVERY_STATUS_PENDING michael@0: : DELIVERY_STATUS_NOT_APPLICABLE; michael@0: if (aMessage.type == "sms") { michael@0: aMessage.deliveryStatus = deliveryStatus; michael@0: // If |deliveryTimestamp| is not specified, use 0 as default. michael@0: if (aMessage.deliveryTimestamp == undefined) { michael@0: aMessage.deliveryTimestamp = 0; michael@0: } michael@0: } else if (aMessage.type == "mms") { michael@0: let receivers = aMessage.receivers michael@0: if (!Array.isArray(receivers)) { michael@0: if (DEBUG) { michael@0: debug("Need receivers for MMS. Fail to save the sending message."); michael@0: } michael@0: if (aCallback) { michael@0: aCallback.notify(Cr.NS_ERROR_FAILURE, null); michael@0: } michael@0: return; michael@0: } michael@0: let readStatus = aMessage.headers["x-mms-read-report"] michael@0: ? MMS.DOM_READ_STATUS_PENDING michael@0: : MMS.DOM_READ_STATUS_NOT_APPLICABLE; michael@0: aMessage.deliveryInfo = []; michael@0: for (let i = 0; i < receivers.length; i++) { michael@0: aMessage.deliveryInfo.push({ michael@0: receiver: receivers[i], michael@0: deliveryStatus: deliveryStatus, michael@0: deliveryTimestamp: 0, michael@0: readStatus: readStatus, michael@0: readTimestamp: 0, michael@0: }); michael@0: } michael@0: } michael@0: michael@0: let timestamp = aMessage.timestamp; michael@0: michael@0: // Adding needed indexes and extra attributes for internal use. michael@0: // threadIdIndex & participantIdsIndex are filled in saveRecord(). michael@0: aMessage.deliveryIndex = [DELIVERY_SENDING, timestamp]; michael@0: aMessage.readIndex = [FILTER_READ_READ, timestamp]; michael@0: aMessage.delivery = DELIVERY_SENDING; michael@0: aMessage.messageClass = MESSAGE_CLASS_NORMAL; michael@0: aMessage.read = FILTER_READ_READ; michael@0: michael@0: // |sentTimestamp| is not available when the message is still sedning. michael@0: aMessage.sentTimestamp = 0; michael@0: michael@0: let addresses; michael@0: if (aMessage.type == "sms") { michael@0: addresses = [aMessage.receiver]; michael@0: } else if (aMessage.type == "mms") { michael@0: addresses = aMessage.receivers; michael@0: } michael@0: this.saveRecord(aMessage, addresses, aCallback); michael@0: }, michael@0: michael@0: setMessageDeliveryByMessageId: function(messageId, receiver, delivery, michael@0: deliveryStatus, envelopeId, callback) { michael@0: this.updateMessageDeliveryById(messageId, "messageId", michael@0: receiver, delivery, deliveryStatus, michael@0: envelopeId, callback); michael@0: michael@0: }, michael@0: michael@0: setMessageDeliveryStatusByEnvelopeId: function(aEnvelopeId, aReceiver, michael@0: aDeliveryStatus, aCallback) { michael@0: this.updateMessageDeliveryById(aEnvelopeId, "envelopeId", aReceiver, null, michael@0: aDeliveryStatus, null, aCallback); michael@0: }, michael@0: michael@0: setMessageReadStatusByEnvelopeId: function(aEnvelopeId, aReceiver, michael@0: aReadStatus, aCallback) { michael@0: if (DEBUG) { michael@0: debug("Setting message's read status by envelopeId = " + aEnvelopeId + michael@0: ", receiver: " + aReceiver + ", readStatus: " + aReadStatus); michael@0: } michael@0: michael@0: let self = this; michael@0: this.newTxnWithCallback(aCallback, function(aCapture, aMessageStore) { michael@0: let getRequest = aMessageStore.index("envelopeId").get(aEnvelopeId); michael@0: getRequest.onsuccess = function onsuccess(event) { michael@0: let messageRecord = event.target.result; michael@0: if (!messageRecord) { michael@0: if (DEBUG) debug("envelopeId '" + aEnvelopeId + "' not found"); michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: aCapture.messageRecord = messageRecord; michael@0: michael@0: let isRecordUpdated = false; michael@0: self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo, michael@0: aReceiver, function(aEntry) { michael@0: if (aEntry.readStatus == aReadStatus) { michael@0: return; michael@0: } michael@0: michael@0: aEntry.readStatus = aReadStatus; michael@0: if (aReadStatus == MMS.DOM_READ_STATUS_SUCCESS) { michael@0: aEntry.readTimestamp = Date.now(); michael@0: } else { michael@0: aEntry.readTimestamp = 0; michael@0: } michael@0: isRecordUpdated = true; michael@0: }); michael@0: michael@0: if (!isRecordUpdated) { michael@0: if (DEBUG) { michael@0: debug("The values of readStatus don't need to be updated."); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: debug("The readStatus is updated."); michael@0: } michael@0: aMessageStore.put(messageRecord); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: getMessageRecordByTransactionId: function(aTransactionId, aCallback) { michael@0: if (DEBUG) debug("Retrieving message with transaction ID " + aTransactionId); michael@0: let self = this; michael@0: this.newTxn(READ_ONLY, function(error, txn, messageStore) { michael@0: if (error) { michael@0: if (DEBUG) debug(error); michael@0: aCallback.notify(error, null, null); michael@0: return; michael@0: } michael@0: let request = messageStore.index("transactionId").get(aTransactionId); michael@0: michael@0: txn.oncomplete = function oncomplete(event) { michael@0: if (DEBUG) debug("Transaction " + txn + " completed."); michael@0: let messageRecord = request.result; michael@0: if (!messageRecord) { michael@0: if (DEBUG) debug("Transaction ID " + aTransactionId + " not found"); michael@0: aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null); michael@0: return; michael@0: } michael@0: // In this case, we don't need a dom message. Just pass null to the michael@0: // third argument. michael@0: aCallback.notify(Cr.NS_OK, messageRecord, null); michael@0: }; michael@0: michael@0: txn.onerror = function onerror(event) { michael@0: if (DEBUG) { michael@0: if (event.target) { michael@0: debug("Caught error on transaction", event.target.errorCode); michael@0: } michael@0: } michael@0: aCallback.notify(Cr.NS_ERROR_FAILURE, null, null); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: getMessageRecordById: function(aMessageId, aCallback) { michael@0: if (DEBUG) debug("Retrieving message with ID " + aMessageId); michael@0: let self = this; michael@0: this.newTxn(READ_ONLY, function(error, txn, messageStore) { michael@0: if (error) { michael@0: if (DEBUG) debug(error); michael@0: aCallback.notify(error, null, null); michael@0: return; michael@0: } michael@0: let request = messageStore.mozGetAll(aMessageId); michael@0: michael@0: txn.oncomplete = function oncomplete() { michael@0: if (DEBUG) debug("Transaction " + txn + " completed."); michael@0: if (request.result.length > 1) { michael@0: if (DEBUG) debug("Got too many results for id " + aMessageId); michael@0: aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null); michael@0: return; michael@0: } michael@0: let messageRecord = request.result[0]; michael@0: if (!messageRecord) { michael@0: if (DEBUG) debug("Message ID " + aMessageId + " not found"); michael@0: aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null); michael@0: return; michael@0: } michael@0: if (messageRecord.id != aMessageId) { michael@0: if (DEBUG) { michael@0: debug("Requested message ID (" + aMessageId + ") is " + michael@0: "different from the one we got"); michael@0: } michael@0: aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null); michael@0: return; michael@0: } michael@0: let domMessage = self.createDomMessageFromRecord(messageRecord); michael@0: aCallback.notify(Cr.NS_OK, messageRecord, domMessage); michael@0: }; michael@0: michael@0: txn.onerror = function onerror(event) { michael@0: if (DEBUG) { michael@0: if (event.target) { michael@0: debug("Caught error on transaction", event.target.errorCode); michael@0: } michael@0: } michael@0: aCallback.notify(Cr.NS_ERROR_FAILURE, null, null); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: translateCrErrorToMessageCallbackError: function(aCrError) { michael@0: switch(aCrError) { michael@0: case Cr.NS_OK: michael@0: return Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR; michael@0: case Cr.NS_ERROR_UNEXPECTED: michael@0: return Ci.nsIMobileMessageCallback.UNKNOWN_ERROR; michael@0: case Cr.NS_ERROR_FILE_NOT_FOUND: michael@0: return Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR; michael@0: case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE: michael@0: return Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR; michael@0: default: michael@0: return Ci.nsIMobileMessageCallback.INTERNAL_ERROR; michael@0: } michael@0: }, michael@0: michael@0: saveSmsSegment: function(aSmsSegment, aCallback) { michael@0: let completeMessage = null; michael@0: this.newTxn(READ_WRITE, function(error, txn, segmentStore) { michael@0: if (error) { michael@0: if (DEBUG) debug(error); michael@0: aCallback.notify(error, null); michael@0: return; michael@0: } michael@0: michael@0: txn.oncomplete = function oncomplete(event) { michael@0: if (DEBUG) debug("Transaction " + txn + " completed."); michael@0: if (completeMessage) { michael@0: // Rebuild full body michael@0: if (completeMessage.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { michael@0: // Uint8Array doesn't have `concat`, so michael@0: // we have to merge all segements by hand. michael@0: let fullDataLen = 0; michael@0: for (let i = 1; i <= completeMessage.segmentMaxSeq; i++) { michael@0: fullDataLen += completeMessage.segments[i].length; michael@0: } michael@0: michael@0: completeMessage.fullData = new Uint8Array(fullDataLen); michael@0: for (let d = 0, i = 1; i <= completeMessage.segmentMaxSeq; i++) { michael@0: let data = completeMessage.segments[i]; michael@0: for (let j = 0; j < data.length; j++) { michael@0: completeMessage.fullData[d++] = data[j]; michael@0: } michael@0: } michael@0: } else { michael@0: completeMessage.fullBody = completeMessage.segments.join(""); michael@0: } michael@0: michael@0: // Remove handy fields after completing the concatenation. michael@0: delete completeMessage.id; michael@0: delete completeMessage.hash; michael@0: delete completeMessage.receivedSegments; michael@0: delete completeMessage.segments; michael@0: } michael@0: aCallback.notify(Cr.NS_OK, completeMessage); michael@0: }; michael@0: michael@0: txn.onabort = function onerror(event) { michael@0: if (DEBUG) debug("Caught error on transaction", event.target.errorCode); michael@0: aCallback.notify(Cr.NS_ERROR_FAILURE, null, null); michael@0: }; michael@0: michael@0: aSmsSegment.hash = aSmsSegment.sender + ":" + michael@0: aSmsSegment.segmentRef + ":" + michael@0: aSmsSegment.segmentMaxSeq + ":" + michael@0: aSmsSegment.iccId; michael@0: let seq = aSmsSegment.segmentSeq; michael@0: if (DEBUG) { michael@0: debug("Saving SMS Segment: " + aSmsSegment.hash + ", seq: " + seq); michael@0: } michael@0: let getRequest = segmentStore.index("hash").get(aSmsSegment.hash); michael@0: getRequest.onsuccess = function(event) { michael@0: let segmentRecord = event.target.result; michael@0: if (!segmentRecord) { michael@0: if (DEBUG) { michael@0: debug("Not found! Create a new record to store the segments."); michael@0: } michael@0: aSmsSegment.receivedSegments = 1; michael@0: aSmsSegment.segments = []; michael@0: if (aSmsSegment.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { michael@0: aSmsSegment.segments[seq] = aSmsSegment.data; michael@0: } else { michael@0: aSmsSegment.segments[seq] = aSmsSegment.body; michael@0: } michael@0: michael@0: segmentStore.add(aSmsSegment); michael@0: michael@0: return; michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: debug("Append SMS Segment into existed message object: " + segmentRecord.id); michael@0: } michael@0: michael@0: if (segmentRecord.segments[seq]) { michael@0: if (DEBUG) debug("Got duplicated segment no. " + seq); michael@0: return; michael@0: } michael@0: michael@0: segmentRecord.timestamp = aSmsSegment.timestamp; michael@0: michael@0: if (segmentRecord.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { michael@0: segmentRecord.segments[seq] = aSmsSegment.data; michael@0: } else { michael@0: segmentRecord.segments[seq] = aSmsSegment.body; michael@0: } michael@0: segmentRecord.receivedSegments++; michael@0: michael@0: // The port information is only available in 1st segment for CDMA WAP Push. michael@0: // If the segments of a WAP Push are not received in sequence michael@0: // (e.g., SMS with seq == 1 is not the 1st segment received by the device), michael@0: // we have to retrieve the port information from 1st segment and michael@0: // save it into the segmentRecord. michael@0: if (aSmsSegment.teleservice === RIL.PDU_CDMA_MSG_TELESERIVCIE_ID_WAP michael@0: && seq === 1) { michael@0: if (aSmsSegment.originatorPort) { michael@0: segmentRecord.originatorPort = aSmsSegment.originatorPort; michael@0: } michael@0: michael@0: if (aSmsSegment.destinationPort) { michael@0: segmentRecord.destinationPort = aSmsSegment.destinationPort; michael@0: } michael@0: } michael@0: michael@0: if (segmentRecord.receivedSegments < segmentRecord.segmentMaxSeq) { michael@0: if (DEBUG) debug("Message is incomplete."); michael@0: segmentStore.put(segmentRecord); michael@0: return; michael@0: } michael@0: michael@0: completeMessage = segmentRecord; michael@0: michael@0: // Delete Record in DB michael@0: segmentStore.delete(segmentRecord.id); michael@0: }; michael@0: }, [SMS_SEGMENT_STORE_NAME]); michael@0: }, michael@0: michael@0: /** michael@0: * nsIMobileMessageDatabaseService API michael@0: */ michael@0: michael@0: getMessage: function(aMessageId, aRequest) { michael@0: if (DEBUG) debug("Retrieving message with ID " + aMessageId); michael@0: let self = this; michael@0: let notifyCallback = { michael@0: notify: function(aRv, aMessageRecord, aDomMessage) { michael@0: if (Cr.NS_OK == aRv) { michael@0: aRequest.notifyMessageGot(aDomMessage); michael@0: return; michael@0: } michael@0: aRequest.notifyGetMessageFailed( michael@0: self.translateCrErrorToMessageCallbackError(aRv), null); michael@0: } michael@0: }; michael@0: this.getMessageRecordById(aMessageId, notifyCallback); michael@0: }, michael@0: michael@0: deleteMessage: function(messageIds, length, aRequest) { michael@0: if (DEBUG) debug("deleteMessage: message ids " + JSON.stringify(messageIds)); michael@0: let deleted = []; michael@0: let self = this; michael@0: this.newTxn(READ_WRITE, function(error, txn, stores) { michael@0: if (error) { michael@0: if (DEBUG) debug("deleteMessage: failed to open transaction"); michael@0: aRequest.notifyDeleteMessageFailed( michael@0: self.translateCrErrorToMessageCallbackError(error)); michael@0: return; michael@0: } michael@0: txn.onerror = function onerror(event) { michael@0: if (DEBUG) debug("Caught error on transaction", event.target.errorCode); michael@0: //TODO look at event.target.errorCode, pick appropriate error constant michael@0: aRequest.notifyDeleteMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: }; michael@0: michael@0: const messageStore = stores[0]; michael@0: const threadStore = stores[1]; michael@0: michael@0: txn.oncomplete = function oncomplete(event) { michael@0: if (DEBUG) debug("Transaction " + txn + " completed."); michael@0: aRequest.notifyMessageDeleted(deleted, length); michael@0: }; michael@0: michael@0: for (let i = 0; i < length; i++) { michael@0: let messageId = messageIds[i]; michael@0: deleted[i] = false; michael@0: messageStore.get(messageId).onsuccess = function(messageIndex, event) { michael@0: let messageRecord = event.target.result; michael@0: let messageId = messageIds[messageIndex]; michael@0: if (messageRecord) { michael@0: if (DEBUG) debug("Deleting message id " + messageId); michael@0: michael@0: // First actually delete the message. michael@0: messageStore.delete(messageId).onsuccess = function(event) { michael@0: if (DEBUG) debug("Message id " + messageId + " deleted"); michael@0: deleted[messageIndex] = true; michael@0: michael@0: // Then update unread count and most recent message. michael@0: self.updateThreadByMessageChange(messageStore, michael@0: threadStore, michael@0: messageRecord.threadId, michael@0: messageId, michael@0: messageRecord.read); michael@0: michael@0: Services.obs.notifyObservers(null, michael@0: "mobile-message-deleted", michael@0: JSON.stringify({ id: messageId })); michael@0: }; michael@0: } else if (DEBUG) { michael@0: debug("Message id " + messageId + " does not exist"); michael@0: } michael@0: }.bind(null, i); michael@0: } michael@0: }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]); michael@0: }, michael@0: michael@0: createMessageCursor: function(filter, reverse, callback) { michael@0: if (DEBUG) { michael@0: debug("Creating a message cursor. Filters:" + michael@0: " startDate: " + filter.startDate + michael@0: " endDate: " + filter.endDate + michael@0: " delivery: " + filter.delivery + michael@0: " numbers: " + filter.numbers + michael@0: " read: " + filter.read + michael@0: " threadId: " + filter.threadId + michael@0: " reverse: " + reverse); michael@0: } michael@0: michael@0: let cursor = new GetMessagesCursor(this, callback); michael@0: michael@0: let self = this; michael@0: self.newTxn(READ_ONLY, function(error, txn, stores) { michael@0: let collector = cursor.collector; michael@0: let collect = collector.collect.bind(collector); michael@0: FilterSearcherHelper.transact(self, txn, error, filter, reverse, collect); michael@0: }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME]); michael@0: michael@0: return cursor; michael@0: }, michael@0: michael@0: markMessageRead: function(messageId, value, aSendReadReport, aRequest) { michael@0: if (DEBUG) debug("Setting message " + messageId + " read to " + value); michael@0: let self = this; michael@0: this.newTxn(READ_WRITE, function(error, txn, stores) { michael@0: if (error) { michael@0: if (DEBUG) debug(error); michael@0: aRequest.notifyMarkMessageReadFailed( michael@0: self.translateCrErrorToMessageCallbackError(error)); michael@0: return; michael@0: } michael@0: michael@0: txn.onerror = function onerror(event) { michael@0: if (DEBUG) debug("Caught error on transaction ", event.target.errorCode); michael@0: aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: }; michael@0: michael@0: let messageStore = stores[0]; michael@0: let threadStore = stores[1]; michael@0: messageStore.get(messageId).onsuccess = function onsuccess(event) { michael@0: let messageRecord = event.target.result; michael@0: if (!messageRecord) { michael@0: if (DEBUG) debug("Message ID " + messageId + " not found"); michael@0: aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR); michael@0: return; michael@0: } michael@0: michael@0: if (messageRecord.id != messageId) { michael@0: if (DEBUG) { michael@0: debug("Retrieve message ID (" + messageId + ") is " + michael@0: "different from the one we got"); michael@0: } michael@0: aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.UNKNOWN_ERROR); michael@0: return; michael@0: } michael@0: michael@0: // If the value to be set is the same as the current message `read` michael@0: // value, we just notify successfully. michael@0: if (messageRecord.read == value) { michael@0: if (DEBUG) debug("The value of messageRecord.read is already " + value); michael@0: aRequest.notifyMessageMarkedRead(messageRecord.read); michael@0: return; michael@0: } michael@0: michael@0: messageRecord.read = value ? FILTER_READ_READ : FILTER_READ_UNREAD; michael@0: messageRecord.readIndex = [messageRecord.read, messageRecord.timestamp]; michael@0: let readReportMessageId, readReportTo; michael@0: if (aSendReadReport && michael@0: messageRecord.type == "mms" && michael@0: messageRecord.delivery == DELIVERY_RECEIVED && michael@0: messageRecord.read == FILTER_READ_READ && michael@0: !messageRecord.isReadReportSent) { michael@0: messageRecord.isReadReportSent = true; michael@0: michael@0: let from = messageRecord.headers["from"]; michael@0: readReportTo = from && from.address; michael@0: readReportMessageId = messageRecord.headers["message-id"]; michael@0: } michael@0: michael@0: if (DEBUG) debug("Message.read set to: " + value); michael@0: messageStore.put(messageRecord).onsuccess = function onsuccess(event) { michael@0: if (DEBUG) { michael@0: debug("Update successfully completed. Message: " + michael@0: JSON.stringify(event.target.result)); michael@0: } michael@0: michael@0: // Now update the unread count. michael@0: let threadId = messageRecord.threadId; michael@0: michael@0: threadStore.get(threadId).onsuccess = function(event) { michael@0: let threadRecord = event.target.result; michael@0: threadRecord.unreadCount += value ? -1 : 1; michael@0: if (DEBUG) { michael@0: debug("Updating unreadCount for thread id " + threadId + ": " + michael@0: (value ? michael@0: threadRecord.unreadCount + 1 : michael@0: threadRecord.unreadCount - 1) + michael@0: " -> " + threadRecord.unreadCount); michael@0: } michael@0: threadStore.put(threadRecord).onsuccess = function(event) { michael@0: if(readReportMessageId && readReportTo) { michael@0: gMMSService.sendReadReport(readReportMessageId, michael@0: readReportTo, michael@0: messageRecord.iccId); michael@0: } michael@0: aRequest.notifyMessageMarkedRead(messageRecord.read); michael@0: }; michael@0: }; michael@0: }; michael@0: }; michael@0: }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]); michael@0: }, michael@0: michael@0: createThreadCursor: function(callback) { michael@0: if (DEBUG) debug("Getting thread list"); michael@0: michael@0: let cursor = new GetThreadsCursor(this, callback); michael@0: this.newTxn(READ_ONLY, function(error, txn, threadStore) { michael@0: let collector = cursor.collector; michael@0: if (error) { michael@0: collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED); michael@0: return; michael@0: } michael@0: txn.onerror = function onerror(event) { michael@0: if (DEBUG) debug("Caught error on transaction ", event.target.errorCode); michael@0: collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED); michael@0: }; michael@0: let request = threadStore.index("lastTimestamp").openKeyCursor(null, PREV); michael@0: request.onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (collector.collect(txn, cursor.primaryKey, cursor.key)) { michael@0: cursor.continue(); michael@0: } michael@0: } else { michael@0: collector.collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED); michael@0: } michael@0: }; michael@0: }, [THREAD_STORE_NAME]); michael@0: michael@0: return cursor; michael@0: } michael@0: }; michael@0: michael@0: let FilterSearcherHelper = { michael@0: michael@0: /** michael@0: * @param index michael@0: * The name of a message store index to filter on. michael@0: * @param range michael@0: * A IDBKeyRange. michael@0: * @param direction michael@0: * NEXT or PREV. michael@0: * @param txn michael@0: * Ongoing IDBTransaction context object. michael@0: * @param collect michael@0: * Result colletor function. It takes three parameters -- txn, message michael@0: * id, and message timestamp. michael@0: */ michael@0: filterIndex: function(index, range, direction, txn, collect) { michael@0: let messageStore = txn.objectStore(MESSAGE_STORE_NAME); michael@0: let request = messageStore.index(index).openKeyCursor(range, direction); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: // Once the cursor has retrieved all keys that matches its key range, michael@0: // the filter search is done. michael@0: if (cursor) { michael@0: let timestamp = Array.isArray(cursor.key) ? cursor.key[1] : cursor.key; michael@0: if (collect(txn, cursor.primaryKey, timestamp)) { michael@0: cursor.continue(); michael@0: } michael@0: } else { michael@0: collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED); michael@0: } michael@0: }; michael@0: request.onerror = function onerror(event) { michael@0: if (DEBUG && event) debug("IDBRequest error " + event.target.errorCode); michael@0: collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Explicitly fiter message on the timestamp index. michael@0: * michael@0: * @param startDate michael@0: * Timestamp of the starting date. michael@0: * @param endDate michael@0: * Timestamp of the ending date. michael@0: * @param direction michael@0: * NEXT or PREV. michael@0: * @param txn michael@0: * Ongoing IDBTransaction context object. michael@0: * @param collect michael@0: * Result colletor function. It takes three parameters -- txn, message michael@0: * id, and message timestamp. michael@0: */ michael@0: filterTimestamp: function(startDate, endDate, direction, txn, collect) { michael@0: let range = null; michael@0: if (startDate != null && endDate != null) { michael@0: range = IDBKeyRange.bound(startDate.getTime(), endDate.getTime()); michael@0: } else if (startDate != null) { michael@0: range = IDBKeyRange.lowerBound(startDate.getTime()); michael@0: } else if (endDate != null) { michael@0: range = IDBKeyRange.upperBound(endDate.getTime()); michael@0: } michael@0: this.filterIndex("timestamp", range, direction, txn, collect); michael@0: }, michael@0: michael@0: /** michael@0: * Instanciate a filtering transaction. michael@0: * michael@0: * @param mmdb michael@0: * A MobileMessageDB. michael@0: * @param txn michael@0: * Ongoing IDBTransaction context object. michael@0: * @param error michael@0: * Previous error while creating the transaction. michael@0: * @param filter michael@0: * A SmsFilter object. michael@0: * @param reverse michael@0: * A boolean value indicating whether we should filter message in michael@0: * reversed order. michael@0: * @param collect michael@0: * Result colletor function. It takes three parameters -- txn, message michael@0: * id, and message timestamp. michael@0: */ michael@0: transact: function(mmdb, txn, error, filter, reverse, collect) { michael@0: if (error) { michael@0: //TODO look at event.target.errorCode, pick appropriate error constant. michael@0: if (DEBUG) debug("IDBRequest error " + error.target.errorCode); michael@0: collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED); michael@0: return; michael@0: } michael@0: michael@0: let direction = reverse ? PREV : NEXT; michael@0: michael@0: // We support filtering by date range only (see `else` block below) or by michael@0: // number/delivery status/read status with an optional date range. michael@0: if (filter.delivery == null && michael@0: filter.numbers == null && michael@0: filter.read == null && michael@0: filter.threadId == null) { michael@0: // Filtering by date range only. michael@0: if (DEBUG) { michael@0: debug("filter.timestamp " + filter.startDate + ", " + filter.endDate); michael@0: } michael@0: michael@0: this.filterTimestamp(filter.startDate, filter.endDate, direction, txn, michael@0: collect); michael@0: return; michael@0: } michael@0: michael@0: // Numeric 0 is smaller than any time stamp, and empty string is larger michael@0: // than all numeric values. michael@0: let startDate = 0, endDate = ""; michael@0: if (filter.startDate != null) { michael@0: startDate = filter.startDate.getTime(); michael@0: } michael@0: if (filter.endDate != null) { michael@0: endDate = filter.endDate.getTime(); michael@0: } michael@0: michael@0: let single, intersectionCollector; michael@0: { michael@0: let num = 0; michael@0: if (filter.delivery) num++; michael@0: if (filter.numbers) num++; michael@0: if (filter.read != undefined) num++; michael@0: if (filter.threadId != undefined) num++; michael@0: single = (num == 1); michael@0: } michael@0: michael@0: if (!single) { michael@0: intersectionCollector = new IntersectionResultsCollector(collect, reverse); michael@0: } michael@0: michael@0: // Retrieve the keys from the 'delivery' index that matches the value of michael@0: // filter.delivery. michael@0: if (filter.delivery) { michael@0: if (DEBUG) debug("filter.delivery " + filter.delivery); michael@0: let delivery = filter.delivery; michael@0: let range = IDBKeyRange.bound([delivery, startDate], [delivery, endDate]); michael@0: this.filterIndex("delivery", range, direction, txn, michael@0: single ? collect : intersectionCollector.newContext()); michael@0: } michael@0: michael@0: // Retrieve the keys from the 'read' index that matches the value of michael@0: // filter.read. michael@0: if (filter.read != undefined) { michael@0: if (DEBUG) debug("filter.read " + filter.read); michael@0: let read = filter.read ? FILTER_READ_READ : FILTER_READ_UNREAD; michael@0: let range = IDBKeyRange.bound([read, startDate], [read, endDate]); michael@0: this.filterIndex("read", range, direction, txn, michael@0: single ? collect : intersectionCollector.newContext()); michael@0: } michael@0: michael@0: // Retrieve the keys from the 'threadId' index that matches the value of michael@0: // filter.threadId. michael@0: if (filter.threadId != undefined) { michael@0: if (DEBUG) debug("filter.threadId " + filter.threadId); michael@0: let threadId = filter.threadId; michael@0: let range = IDBKeyRange.bound([threadId, startDate], [threadId, endDate]); michael@0: this.filterIndex("threadId", range, direction, txn, michael@0: single ? collect : intersectionCollector.newContext()); michael@0: } michael@0: michael@0: // Retrieve the keys from the 'sender' and 'receiver' indexes that michael@0: // match the values of filter.numbers michael@0: if (filter.numbers) { michael@0: if (DEBUG) debug("filter.numbers " + filter.numbers.join(", ")); michael@0: michael@0: if (!single) { michael@0: collect = intersectionCollector.newContext(); michael@0: } michael@0: michael@0: let participantStore = txn.objectStore(PARTICIPANT_STORE_NAME); michael@0: mmdb.findParticipantIdsByAddresses(participantStore, filter.numbers, michael@0: false, true, michael@0: (function(participantIds) { michael@0: if (!participantIds || !participantIds.length) { michael@0: // Oops! No such participant at all. michael@0: michael@0: collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED); michael@0: return; michael@0: } michael@0: michael@0: if (participantIds.length == 1) { michael@0: let id = participantIds[0]; michael@0: let range = IDBKeyRange.bound([id, startDate], [id, endDate]); michael@0: this.filterIndex("participantIds", range, direction, txn, collect); michael@0: return; michael@0: } michael@0: michael@0: let unionCollector = new UnionResultsCollector(collect); michael@0: michael@0: this.filterTimestamp(filter.startDate, filter.endDate, direction, txn, michael@0: unionCollector.newTimestampContext()); michael@0: michael@0: for (let i = 0; i < participantIds.length; i++) { michael@0: let id = participantIds[i]; michael@0: let range = IDBKeyRange.bound([id, startDate], [id, endDate]); michael@0: this.filterIndex("participantIds", range, direction, txn, michael@0: unionCollector.newContext()); michael@0: } michael@0: }).bind(this)); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: function ResultsCollector() { michael@0: this.results = []; michael@0: this.done = false; michael@0: } michael@0: ResultsCollector.prototype = { michael@0: results: null, michael@0: requestWaiting: null, michael@0: done: null, michael@0: michael@0: /** michael@0: * Queue up passed id, reply if necessary. michael@0: * michael@0: * @param txn michael@0: * Ongoing IDBTransaction context object. michael@0: * @param id michael@0: * COLLECT_ID_END(0) for no more results, COLLECT_ID_ERROR(-1) for michael@0: * errors and valid otherwise. michael@0: * @param timestamp michael@0: * We assume this function is always called in timestamp order. So michael@0: * this parameter is actually unused. michael@0: * michael@0: * @return true if expects more. false otherwise. michael@0: */ michael@0: collect: function(txn, id, timestamp) { michael@0: if (this.done) { michael@0: return false; michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: debug("collect: message ID = " + id); michael@0: } michael@0: if (id) { michael@0: // Queue up any id but '0' and replies later accordingly. michael@0: this.results.push(id); michael@0: } michael@0: if (id <= 0) { michael@0: // No more processing on '0' or negative values passed. michael@0: this.done = true; michael@0: } michael@0: michael@0: if (!this.requestWaiting) { michael@0: if (DEBUG) debug("Cursor.continue() not called yet"); michael@0: return !this.done; michael@0: } michael@0: michael@0: // We assume there is only one request waiting throughout the message list michael@0: // retrieving process. So we don't bother continuing to process further michael@0: // waiting requests here. This assumption comes from DOMCursor::Continue() michael@0: // implementation. michael@0: let callback = this.requestWaiting; michael@0: this.requestWaiting = null; michael@0: michael@0: this.drip(txn, callback); michael@0: michael@0: return !this.done; michael@0: }, michael@0: michael@0: /** michael@0: * Callback right away with the first queued result entry if the filtering is michael@0: * done. Or queue up the request and callback when a new entry is available. michael@0: * michael@0: * @param callback michael@0: * A callback function that accepts a numeric id. michael@0: */ michael@0: squeeze: function(callback) { michael@0: if (this.requestWaiting) { michael@0: throw new Error("Already waiting for another request!"); michael@0: } michael@0: michael@0: if (!this.done) { michael@0: // Database transaction ongoing, let it reply for us so that we won't get michael@0: // blocked by the existing transaction. michael@0: this.requestWaiting = callback; michael@0: return; michael@0: } michael@0: michael@0: this.drip(null, callback); michael@0: }, michael@0: michael@0: /** michael@0: * @param txn michael@0: * Ongoing IDBTransaction context object or null. michael@0: * @param callback michael@0: * A callback function that accepts a numeric id. michael@0: */ michael@0: drip: function(txn, callback) { michael@0: if (!this.results.length) { michael@0: if (DEBUG) debug("No messages matching the filter criteria"); michael@0: callback(txn, COLLECT_ID_END); michael@0: return; michael@0: } michael@0: michael@0: if (this.results[0] < 0) { michael@0: // An previous error found. Keep the answer in results so that we can michael@0: // reply INTERNAL_ERROR for further requests. michael@0: if (DEBUG) debug("An previous error found"); michael@0: callback(txn, COLLECT_ID_ERROR); michael@0: return; michael@0: } michael@0: michael@0: let firstMessageId = this.results.shift(); michael@0: callback(txn, firstMessageId); michael@0: } michael@0: }; michael@0: michael@0: function IntersectionResultsCollector(collect, reverse) { michael@0: this.cascadedCollect = collect; michael@0: this.reverse = reverse; michael@0: this.contexts = []; michael@0: } michael@0: IntersectionResultsCollector.prototype = { michael@0: cascadedCollect: null, michael@0: reverse: false, michael@0: contexts: null, michael@0: michael@0: /** michael@0: * Queue up {id, timestamp} pairs, find out intersections and report to michael@0: * |cascadedCollect|. Return true if it is still possible to have another match. michael@0: */ michael@0: collect: function(contextIndex, txn, id, timestamp) { michael@0: if (DEBUG) { michael@0: debug("IntersectionResultsCollector: " michael@0: + contextIndex + ", " + id + ", " + timestamp); michael@0: } michael@0: michael@0: let contexts = this.contexts; michael@0: let context = contexts[contextIndex]; michael@0: michael@0: if (id < 0) { michael@0: // Act as no more matched records. michael@0: id = 0; michael@0: } michael@0: if (!id) { michael@0: context.done = true; michael@0: michael@0: if (!context.results.length) { michael@0: // Already empty, can't have further intersection results. michael@0: return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED); michael@0: } michael@0: michael@0: for (let i = 0; i < contexts.length; i++) { michael@0: if (!contexts[i].done) { michael@0: // Don't call |this.cascadedCollect| because |context.results| might not michael@0: // be empty, so other contexts might still have a chance here. michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: // It was the last processing context and is no more processing. michael@0: return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED); michael@0: } michael@0: michael@0: // Search id in other existing results. If no other results has it, michael@0: // and A) the last timestamp is smaller-equal to current timestamp, michael@0: // we wait for further results; either B) record timestamp is larger michael@0: // then current timestamp or C) no more processing for a filter, then we michael@0: // drop this id because there can't be a match anymore. michael@0: for (let i = 0; i < contexts.length; i++) { michael@0: if (i == contextIndex) { michael@0: continue; michael@0: } michael@0: michael@0: let ctx = contexts[i]; michael@0: let results = ctx.results; michael@0: let found = false; michael@0: for (let j = 0; j < results.length; j++) { michael@0: let result = results[j]; michael@0: if (result.id == id) { michael@0: found = true; michael@0: break; michael@0: } michael@0: if ((!this.reverse && (result.timestamp > timestamp)) || michael@0: (this.reverse && (result.timestamp < timestamp))) { michael@0: // B) Cannot find a match anymore. Drop. michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: if (!found) { michael@0: if (ctx.done) { michael@0: // C) Cannot find a match anymore. Drop. michael@0: if (results.length) { michael@0: let lastResult = results[results.length - 1]; michael@0: if ((!this.reverse && (lastResult.timestamp >= timestamp)) || michael@0: (this.reverse && (lastResult.timestamp <= timestamp))) { michael@0: // Still have a chance to get another match. Return true. michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: // Impossible to find another match because all results in ctx have michael@0: // timestamps smaller than timestamp. michael@0: context.done = true; michael@0: return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED); michael@0: } michael@0: michael@0: // A) Pending. michael@0: context.results.push({ michael@0: id: id, michael@0: timestamp: timestamp michael@0: }); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: // Now id is found in all other results. Report it. michael@0: return this.cascadedCollect(txn, id, timestamp); michael@0: }, michael@0: michael@0: newContext: function() { michael@0: let contextIndex = this.contexts.length; michael@0: this.contexts.push({ michael@0: results: [], michael@0: done: false michael@0: }); michael@0: return this.collect.bind(this, contextIndex); michael@0: } michael@0: }; michael@0: michael@0: function UnionResultsCollector(collect) { michael@0: this.cascadedCollect = collect; michael@0: this.contexts = [{ michael@0: // Timestamp. michael@0: processing: 1, michael@0: results: [] michael@0: }, { michael@0: processing: 0, michael@0: results: [] michael@0: }]; michael@0: } michael@0: UnionResultsCollector.prototype = { michael@0: cascadedCollect: null, michael@0: contexts: null, michael@0: michael@0: collect: function(contextIndex, txn, id, timestamp) { michael@0: if (DEBUG) { michael@0: debug("UnionResultsCollector: " michael@0: + contextIndex + ", " + id + ", " + timestamp); michael@0: } michael@0: michael@0: let contexts = this.contexts; michael@0: let context = contexts[contextIndex]; michael@0: michael@0: if (id < 0) { michael@0: // Act as no more matched records. michael@0: id = 0; michael@0: } michael@0: if (id) { michael@0: if (!contextIndex) { michael@0: // Timestamp. michael@0: context.results.push({ michael@0: id: id, michael@0: timestamp: timestamp michael@0: }); michael@0: } else { michael@0: context.results.push(id); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: context.processing -= 1; michael@0: if (contexts[0].processing || contexts[1].processing) { michael@0: // At least one queue is still processing, but we got here because michael@0: // current cursor gives 0 as id meaning no more messages are michael@0: // available. Return false here to stop further cursor.continue() calls. michael@0: return false; michael@0: } michael@0: michael@0: let tres = contexts[0].results; michael@0: let qres = contexts[1].results; michael@0: tres = tres.filter(function(element) { michael@0: return qres.indexOf(element.id) != -1; michael@0: }); michael@0: michael@0: for (let i = 0; i < tres.length; i++) { michael@0: this.cascadedCollect(txn, tres[i].id, tres[i].timestamp); michael@0: } michael@0: this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED); michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: newTimestampContext: function() { michael@0: return this.collect.bind(this, 0); michael@0: }, michael@0: michael@0: newContext: function() { michael@0: this.contexts[1].processing++; michael@0: return this.collect.bind(this, 1); michael@0: } michael@0: }; michael@0: michael@0: function GetMessagesCursor(mmdb, callback) { michael@0: this.mmdb = mmdb; michael@0: this.callback = callback; michael@0: this.collector = new ResultsCollector(); michael@0: michael@0: this.handleContinue(); // Trigger first run. michael@0: } michael@0: GetMessagesCursor.prototype = { michael@0: classID: RIL_GETMESSAGESCURSOR_CID, michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]), michael@0: michael@0: mmdb: null, michael@0: callback: null, michael@0: collector: null, michael@0: michael@0: getMessageTxn: function(messageStore, messageId) { michael@0: if (DEBUG) debug ("Fetching message " + messageId); michael@0: michael@0: let getRequest = messageStore.get(messageId); michael@0: let self = this; michael@0: getRequest.onsuccess = function onsuccess(event) { michael@0: if (DEBUG) { michael@0: debug("notifyNextMessageInListGot - messageId: " + messageId); michael@0: } michael@0: let domMessage = michael@0: self.mmdb.createDomMessageFromRecord(event.target.result); michael@0: self.callback.notifyCursorResult(domMessage); michael@0: }; michael@0: getRequest.onerror = function onerror(event) { michael@0: if (DEBUG) { michael@0: debug("notifyCursorError - messageId: " + messageId); michael@0: } michael@0: self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: }; michael@0: }, michael@0: michael@0: notify: function(txn, messageId) { michael@0: if (!messageId) { michael@0: this.callback.notifyCursorDone(); michael@0: return; michael@0: } michael@0: michael@0: if (messageId < 0) { michael@0: this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: return; michael@0: } michael@0: michael@0: // When filter transaction is not yet completed, we're called with current michael@0: // ongoing transaction object. michael@0: if (txn) { michael@0: let messageStore = txn.objectStore(MESSAGE_STORE_NAME); michael@0: this.getMessageTxn(messageStore, messageId); michael@0: return; michael@0: } michael@0: michael@0: // Or, we have to open another transaction ourselves. michael@0: let self = this; michael@0: this.mmdb.newTxn(READ_ONLY, function(error, txn, messageStore) { michael@0: if (error) { michael@0: self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: return; michael@0: } michael@0: self.getMessageTxn(messageStore, messageId); michael@0: }, [MESSAGE_STORE_NAME]); michael@0: }, michael@0: michael@0: // nsICursorContinueCallback michael@0: michael@0: handleContinue: function() { michael@0: if (DEBUG) debug("Getting next message in list"); michael@0: this.collector.squeeze(this.notify.bind(this)); michael@0: } michael@0: }; michael@0: michael@0: function GetThreadsCursor(mmdb, callback) { michael@0: this.mmdb = mmdb; michael@0: this.callback = callback; michael@0: this.collector = new ResultsCollector(); michael@0: michael@0: this.handleContinue(); // Trigger first run. michael@0: } michael@0: GetThreadsCursor.prototype = { michael@0: classID: RIL_GETTHREADSCURSOR_CID, michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]), michael@0: michael@0: mmdb: null, michael@0: callback: null, michael@0: collector: null, michael@0: michael@0: getThreadTxn: function(threadStore, threadId) { michael@0: if (DEBUG) debug ("Fetching thread " + threadId); michael@0: michael@0: let getRequest = threadStore.get(threadId); michael@0: let self = this; michael@0: getRequest.onsuccess = function onsuccess(event) { michael@0: let threadRecord = event.target.result; michael@0: if (DEBUG) { michael@0: debug("notifyCursorResult: " + JSON.stringify(threadRecord)); michael@0: } michael@0: let thread = michael@0: gMobileMessageService.createThread(threadRecord.id, michael@0: threadRecord.participantAddresses, michael@0: threadRecord.lastTimestamp, michael@0: threadRecord.lastMessageSubject || "", michael@0: threadRecord.body, michael@0: threadRecord.unreadCount, michael@0: threadRecord.lastMessageType); michael@0: self.callback.notifyCursorResult(thread); michael@0: }; michael@0: getRequest.onerror = function onerror(event) { michael@0: if (DEBUG) { michael@0: debug("notifyCursorError - threadId: " + threadId); michael@0: } michael@0: self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: }; michael@0: }, michael@0: michael@0: notify: function(txn, threadId) { michael@0: if (!threadId) { michael@0: this.callback.notifyCursorDone(); michael@0: return; michael@0: } michael@0: michael@0: if (threadId < 0) { michael@0: this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: return; michael@0: } michael@0: michael@0: // When filter transaction is not yet completed, we're called with current michael@0: // ongoing transaction object. michael@0: if (txn) { michael@0: let threadStore = txn.objectStore(THREAD_STORE_NAME); michael@0: this.getThreadTxn(threadStore, threadId); michael@0: return; michael@0: } michael@0: michael@0: // Or, we have to open another transaction ourselves. michael@0: let self = this; michael@0: this.mmdb.newTxn(READ_ONLY, function(error, txn, threadStore) { michael@0: if (error) { michael@0: self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR); michael@0: return; michael@0: } michael@0: self.getThreadTxn(threadStore, threadId); michael@0: }, [THREAD_STORE_NAME]); michael@0: }, michael@0: michael@0: // nsICursorContinueCallback michael@0: michael@0: handleContinue: function() { michael@0: if (DEBUG) debug("Getting next thread in list"); michael@0: this.collector.squeeze(this.notify.bind(this)); michael@0: } michael@0: } michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: 'MobileMessageDB' michael@0: ]; michael@0: michael@0: function debug() { michael@0: dump("MobileMessageDB: " + Array.slice(arguments).join(" ") + "\n"); michael@0: }