gecko-dev/dom/mobilemessage/gonk/MobileMessageDB.jsm

5207 lines
189 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* This file is documented in JSDoc format. To generate the document:
*
* 1. Follow the instruction of JSDoc project to install it. See
* https://github.com/jsdoc3/jsdoc for details.
*
* 2. Since JSDoc does not recognize ES6 syntax and XPCOM components, you should
* enable the "commentsOnly" plugin in your conf.json to strip all code out
* before generating the document. You'll need to change source.includePattern
* as well to include "*.jsm" since it's not included by default. Here's a
* minimal example of conf.json you need:
*
* {
* "source": {
* "includePattern": ".+\\.js(m)?$"
* },
* "plugins": ["plugins/commentsOnly"]
* }
*
* 3. Run jsdoc:
*
* $ jsdoc -c <path-to-conf-json> -d <output-directory> MobileMessageDB.jsm
*/
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
Cu.importGlobalProperties(["indexedDB"]);
XPCOMUtils.defineLazyGetter(this, "RIL", function () {
let obj = {};
Cu.import("resource://gre/modules/ril_consts.js", obj);
return obj;
});
const RIL_GETMESSAGESCURSOR_CID =
Components.ID("{484d1ad8-840e-4782-9dc4-9ebc4d914937}");
const RIL_GETTHREADSCURSOR_CID =
Components.ID("{95ee7c3e-d6f2-4ec4-ade5-0c453c036d35}");
const DEBUG = false;
const DISABLE_MMS_GROUPING_FOR_RECEIVING = true;
const DB_VERSION = 23;
/**
* @typedef {string} MobileMessageDB.MESSAGE_STORE_NAME
*
* The name of the object store for messages.
*/
const MESSAGE_STORE_NAME = "sms";
/**
* @typedef {string} MobileMessageDB.THREAD_STORE_NAME
*
* The name of the object store for threads.
*/
const THREAD_STORE_NAME = "thread";
/**
* @typedef {string} MobileMessageDB.PARTICIPANT_STORE_NAME
*
* The name of the object store for participants.
*/
const PARTICIPANT_STORE_NAME = "participant";
/**
* @typedef {string} MobileMessageDB.MOST_RECENT_STORE_NAME
* @deprecated
*/
const MOST_RECENT_STORE_NAME = "most-recent";
/**
* @typedef {string} MobileMessageDB.SMS_SEGMENT_STORE_NAME
*
* The name of the object store for incoming SMS segments.
*/
const SMS_SEGMENT_STORE_NAME = "sms-segment";
const DELIVERY_SENDING = "sending";
const DELIVERY_SENT = "sent";
const DELIVERY_RECEIVED = "received";
const DELIVERY_NOT_DOWNLOADED = "not-downloaded";
const DELIVERY_ERROR = "error";
const DELIVERY_STATUS_NOT_APPLICABLE = "not-applicable";
const DELIVERY_STATUS_SUCCESS = "success";
const DELIVERY_STATUS_PENDING = "pending";
const DELIVERY_STATUS_ERROR = "error";
const MESSAGE_CLASS_NORMAL = "normal";
const FILTER_TIMESTAMP = "timestamp";
const FILTER_NUMBERS = "numbers";
const FILTER_DELIVERY = "delivery";
const FILTER_READ = "read";
// We can´t create an IDBKeyCursor with a boolean, so we need to use numbers
// instead.
const FILTER_READ_UNREAD = 0;
const FILTER_READ_READ = 1;
const READ_ONLY = "readonly";
const READ_WRITE = "readwrite";
const PREV = "prev";
const NEXT = "next";
const COLLECT_ID_END = 0;
const COLLECT_ID_ERROR = -1;
const COLLECT_TIMESTAMP_UNUSED = 0;
// Default value for integer preference "dom.sms.maxReadAheadEntries".
const DEFAULT_READ_AHEAD_ENTRIES = 7;
XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService",
"@mozilla.org/mobilemessage/mobilemessageservice;1",
"nsIMobileMessageService");
XPCOMUtils.defineLazyServiceGetter(this, "gMMSService",
"@mozilla.org/mms/gonkmmsservice;1",
"nsIMmsService");
XPCOMUtils.defineLazyGetter(this, "MMS", function() {
let MMS = {};
Cu.import("resource://gre/modules/MmsPduHelper.jsm", MMS);
return MMS;
});
/**
* @typedef {Object} MobileMessageDB.MessageRecord
*
* Represents a SMS or MMS message.
*
* <pre>
* +--------------------------------------------------------------------------+
* | MessageRecord |
* +--------------------------------------------------------------------------+
* | id: Number (primary-key) |
* | |
* | [SMS / MMS Common] |
* | type: String |
* | read: Number // Works as boolean, only use 0 or 1. |
* | iccId: String |
* | sender: String |
* | delivery: String |
* | timestamp: Number |
* | sentTimestamp: Number |
* | |
* | [Database Foreign Keys] |
* | threadId: Number |
* | |
* | [Common Indices] |
* | threadIdIndex: Array // [threadId, timestamp] |
* | deliveryIndex: Array // [delivery, timestamp] |
* | readIndex: Array // [read, timpstamp] |
* | participantIdsIndex: Array of Array // [[participantId, timestamp], ...] |
* | |
* | [SMS / Common Fields] |
* | pid: Number |
* | SMSC: String |
* | receiver: String |
* | encoding: Number |
* | messageType: Number |
* | teleservice: Number |
* | messageClass: String |
* | deliveryStatus: String |
* | deliveryTimestamp: Number |
* | |
* | [SMS / Application Port Info] |
* | originatorPort: Number |
* | destinationPort: Number |
* | |
* | [SMS / MWI status] |
* | mwiPresent: Boolean |
* | mwiDiscard: Boolean |
* | mwiMsgCount: Number |
* | mwiActive: Boolean |
* | |
* | [SMS / Message Body] |
* | data: Array of Uint8 (available if it's 8bit encoding) |
* | body: String (normal text body) |
* | fullBody: String |
* | |
* | [SMS / CDMA Cellbroadcast Related] |
* | serviceCategory: Number |
* | language: String |
* | |
* | [MMS Info] |
* | receivers: Array of String |
* | phoneNumber: String |
* | transactionIdIndex: String |
* | envelopeIdIndex: String |
* | isReadReportSent: Boolean |
* | deliveryInfo: Array of { |
* | receiver: String |
* | deliveryStatus: String |
* | deliveryTimestamp: Number |
* | readStatus: String |
* | readTimestamp: Number |
* | } |
* | headers: { |
* | x-mms-message-type: Number |
* | x-mms-transaction-id: String |
* | x-mms-mms-version: Number |
* | from: { |
* | address: String |
* | type: String |
* | } |
* | subject: String |
* | x-mms-message-class: String |
* | x-mms-message-size: Number |
* | x-mms-expiry: Number |
* | x-mms-content-location: { |
* | uri: String |
* | } |
* | to: Array of { |
* | address: String |
* | type: String |
* | } |
* | x-mms-read-report: Boolean |
* | x-mms-priority: Number |
* | message-id: String |
* | date: String |
* | x-mms-delivery-report: Boolean |
* | content-type: { |
* | media: String |
* | params: { |
* | type: String |
* | start: String |
* | } |
* | } |
* | } |
* | parts: Array of { |
* | index: Number |
* | headers: { |
* | content-type: { |
* | media: String |
* | params: { |
* | name: String |
* | charset: { |
* | charset: String |
* | } |
* | } |
* | content-length: Number |
* | content-location: String |
* | content-id: String |
* | } |
* | content: String |
* | } |
* +--------------------------------------------------------------------------+
* </pre>
*/
/**
* @typedef {Object} MobileMessageDB.ThreadRecord
*
* Represents a message thread.
*
* <pre>
* +---------------------------------------+
* | ThreadRecord |
* +---------------------------------------+
* | id: Number (primary-key) |
* | participantIds: Array of Number |
* | participantAddresses: Array of String |
* | lastMessageId: Number |
* | lastTimestamp: Number |
* | unreadCount: Number |
* | lastMessageType: String |
* | |
* | [SMS Only] |
* | body: String |
* | |
* | [MMS Only] |
* | lastMessageSubject: String |
* +---------------------------------------+
* </pre>
*/
/**
* @typedef {Object} MobileMessageDB.ParticipantRecord
*
* Represents the mapping of a participant and one or multiple addresses.
* (National and Int'l numbers)
*
* <pre>
* +----------------------------+
* | ParticipantRecord |
* +----------------------------+
* | id: Number (primary-key) |
* | addresses: Array of String |
* +----------------------------+
* </pre>
*/
/**
* @typedef {Object} MobileMessageDB.SmsSegmentRecord
*
* Represents a SMS segment.
*
* <pre>
* +---------------------------------------------------------------+
* | SmsSegmentRecord |
* +---------------------------------------------------------------+
* | [Common Fields in SMS segment] |
* | messageType: Number |
* | teleservice: Number |
* | SMSC: String |
* | sentTimestamp: Number |
* | timestamp: Number |
* | sender: String |
* | pid: Number |
* | encoding: Number |
* | messageClass: String |
* | iccId: String |
* | |
* | [Concatenation Info] |
* | segmentRef: Number |
* | segmentSeq: Number |
* | segmentMaxSeq: Number |
* | |
* | [Application Port Info] |
* | originatorPort: Number |
* | destinationPort: Number |
* | |
* | [MWI Status] |
* | mwiPresent: Boolean |
* | mwiDiscard: Boolean |
* | mwiMsgCount: Number |
* | mwiActive: Boolean |
* | |
* | [CDMA Cell Broadcast Related Fields] |
* | serviceCategory: Number |
* | language: String |
* | |
* | [Message Body] |
* | data: Array of Uint8 (available if it's 8bit encoding) |
* | body: String (normal text body) |
* | |
* | [Handy Fields Created by DB for Concatenation] |
* | id: Number (primary-key) |
* | hash: String // Use to identify the segments to the same SMS. |
* | receivedSegments: Number |
* | segments: Array |
* +---------------------------------------------------------------+
* </pre>
*/
/**
* @class MobileMessageDB
* @classdesc
*
* <p>
* MobileMessageDB is used to store all SMS / MMS messages, as well as the
* threads those messages belong to, and the participants of those messages.
* </p>
*
* <p>
* The relations between threads, messages and participants can be described as
* the following ERD -- each thread consists of one or many messages, and
* consists of one or many participants. A participant resolves to one or many
* (usually up to 2) addresses -- which represent different formats of the same
* address, for example a national number and an international number.
* </p>
*
* <pre>
* X
* / \
* +-----------+ / \ +-----------+
* | | / \ /| |
* | thread |-|--|consist|--|--|participant|
* | | \ of / \| |
* +-----------+ \ / +-----------+
* | \ / |
* - V -
* | |
* | |
* X X
* / \ / \
* / \ / \
* / \ / \
* |consist| |resolve|
* \ of / \ to /
* \ / \ /
* \ / \ /
* V V
* | |
* | |
* - -
* | |
* /|\ /|\
* +-----------+ +-----------+
* | | | |
* | message | | address |
* | | | |
* +-----------+ +-----------+
* </pre>
*
* <p>
* There are 4 object stores in use: </br>
* 1. MESSAGE_STORE: stores {@link MobileMessageDB.MessageRecord}. </br>
* 2. THREAD_STORE: stores {@link MobileMessageDB.ThreadRecord}. </br>
* 3. PARTICIPANT_STORE: stores {@link MobileMessageDB.ParticipantRecord}. </br>
* 4. SMS_SEGMENT_STORE: stores partial incoming SMS segments defined in
* {@link MobileMessageDB.SmsSegmentRecord}. The records are deleted as soon as
* it's enough to compose a complete SMS message.
* </p>
*
* <p>
* Besides all object stores mentioned above, there was a MOST_RECENT_STORE
* which is deprecated and no longer in use.
* </p>
*/
this.MobileMessageDB = function() {};
MobileMessageDB.prototype = {
dbName: null,
dbVersion: null,
/**
* Cache the DB instance.
*
* @member {IDBDatabase} MobileMessageDB.db
* @private
*/
db: null,
/**
* Last sms/mms object store key value in the database.
*
* @member {number} MobileMessageDB.lastMessageId
* @private
*/
lastMessageId: 0,
/**
* @callback MobileMessageDB.EnsureDBCallback
* @param {number} aErrorCode
* The error code on failure, or <code>null</code> on success.
* @param {IDBDatabase} aDatabase
* The ready-to-use database object on success.
*/
/**
* Prepare the database. This may include opening the database and upgrading
* it to the latest schema version.
*
* @function MobileMessageDB.ensureDB
* @param {MobileMessageDB.EnsureDBCallback} callback
* Function that takes an error and db argument. It is called when
* the database is ready to use or if an error occurs while preparing
* the database.
*/
ensureDB: function(callback) {
if (this.db) {
if (DEBUG) debug("ensureDB: already have a database, returning early.");
callback(null, this.db);
return;
}
let self = this;
function gotDB(db) {
self.db = db;
callback(null, db);
}
let request = indexedDB.open(this.dbName, this.dbVersion);
request.onsuccess = function(event) {
if (DEBUG) debug("Opened database:", self.dbName, self.dbVersion);
gotDB(event.target.result);
};
request.onupgradeneeded = function(event) {
if (DEBUG) {
debug("Database needs upgrade:", self.dbName,
event.oldVersion, event.newVersion);
debug("Correct new database version:", event.newVersion == self.dbVersion);
}
let db = event.target.result;
let currentVersion = event.oldVersion;
function update(currentVersion) {
if (currentVersion >= self.dbVersion) {
if (DEBUG) debug("Upgrade finished.");
return;
}
let next = update.bind(self, currentVersion + 1);
switch (currentVersion) {
case 0:
if (DEBUG) debug("New database");
self.createSchema(db, next);
break;
case 1:
if (DEBUG) debug("Upgrade to version 2. Including `read` index");
self.upgradeSchema(event.target.transaction, next);
break;
case 2:
if (DEBUG) debug("Upgrade to version 3. Fix existing entries.");
self.upgradeSchema2(event.target.transaction, next);
break;
case 3:
if (DEBUG) debug("Upgrade to version 4. Add quick threads view.");
self.upgradeSchema3(db, event.target.transaction, next);
break;
case 4:
if (DEBUG) debug("Upgrade to version 5. Populate quick threads view.");
self.upgradeSchema4(event.target.transaction, next);
break;
case 5:
if (DEBUG) debug("Upgrade to version 6. Use PhonenumberJS.");
self.upgradeSchema5(event.target.transaction, next);
break;
case 6:
if (DEBUG) debug("Upgrade to version 7. Use multiple entry indexes.");
self.upgradeSchema6(event.target.transaction, next);
break;
case 7:
if (DEBUG) debug("Upgrade to version 8. Add participant/thread stores.");
self.upgradeSchema7(db, event.target.transaction, next);
break;
case 8:
if (DEBUG) debug("Upgrade to version 9. Add transactionId index for incoming MMS.");
self.upgradeSchema8(event.target.transaction, next);
break;
case 9:
if (DEBUG) debug("Upgrade to version 10. Upgrade type if it's not existing.");
self.upgradeSchema9(event.target.transaction, next);
break;
case 10:
if (DEBUG) debug("Upgrade to version 11. Add last message type into threadRecord.");
self.upgradeSchema10(event.target.transaction, next);
break;
case 11:
if (DEBUG) debug("Upgrade to version 12. Add envelopeId index for outgoing MMS.");
self.upgradeSchema11(event.target.transaction, next);
break;
case 12:
if (DEBUG) debug("Upgrade to version 13. Replaced deliveryStatus by deliveryInfo.");
self.upgradeSchema12(event.target.transaction, next);
break;
case 13:
if (DEBUG) debug("Upgrade to version 14. Fix the wrong participants.");
// A workaround to check if we need to re-upgrade the DB schema 12. We missed this
// because we didn't properly uplift that logic to b2g_v1.2 and errors could happen
// when migrating b2g_v1.2 to b2g_v1.3. Please see Bug 960741 for details.
self.needReUpgradeSchema12(event.target.transaction, function(isNeeded) {
if (isNeeded) {
self.upgradeSchema12(event.target.transaction, function() {
self.upgradeSchema13(event.target.transaction, next);
});
} else {
self.upgradeSchema13(event.target.transaction, next);
}
});
break;
case 14:
if (DEBUG) debug("Upgrade to version 15. Add deliveryTimestamp.");
self.upgradeSchema14(event.target.transaction, next);
break;
case 15:
if (DEBUG) debug("Upgrade to version 16. Add ICC ID for each message.");
self.upgradeSchema15(event.target.transaction, next);
break;
case 16:
if (DEBUG) debug("Upgrade to version 17. Add isReadReportSent for incoming MMS.");
self.upgradeSchema16(event.target.transaction, next);
break;
case 17:
if (DEBUG) debug("Upgrade to version 18. Add last message subject into threadRecord.");
self.upgradeSchema17(event.target.transaction, next);
break;
case 18:
if (DEBUG) debug("Upgrade to version 19. Add pid for incoming SMS.");
self.upgradeSchema18(event.target.transaction, next);
break;
case 19:
if (DEBUG) debug("Upgrade to version 20. Add readStatus and readTimestamp.");
self.upgradeSchema19(event.target.transaction, next);
break;
case 20:
if (DEBUG) debug("Upgrade to version 21. Add sentTimestamp.");
self.upgradeSchema20(event.target.transaction, next);
break;
case 21:
if (DEBUG) debug("Upgrade to version 22. Add sms-segment store.");
self.upgradeSchema21(db, event.target.transaction, next);
break;
case 22:
if (DEBUG) debug("Upgrade to version 23. Add type information to receivers and to");
self.upgradeSchema22(event.target.transaction, next);
break;
default:
event.target.transaction.abort();
if (DEBUG) debug("unexpected db version: " + event.oldVersion);
callback(Cr.NS_ERROR_FAILURE, null);
break;
}
}
update(currentVersion);
};
request.onerror = function(event) {
// TODO look at event.target.Code and change error constant accordingly.
if (DEBUG) debug("Error opening database!");
callback(Cr.NS_ERROR_FAILURE, null);
};
request.onblocked = function(event) {
if (DEBUG) debug("Opening database request is blocked.");
callback(Cr.NS_ERROR_FAILURE, null);
};
},
/**
* @callback MobileMessageDB.NewTxnCallback
* @param {number} aErrorCode
* The error code on failure, or <code>null</code> on success.
* @param {IDBTransaction} aTransaction
* The transaction object to operate the indexedDB on success.
* @param {IDBObjectStore|IDBObjectStore[]} aObjectStores
* The object store(s) on success. If only one object store is passed,
* it's passed as an <code>IDBObjectStore</code>; Otherwise, it's
* <code>IDBObjectStore[]</code>.
*/
/**
* Start a new transaction.
*
* @function MobileMessageDB.newTxn
* @param {string} txn_type
* Type of transaction (e.g. READ_WRITE)
* @param {MobileMessageDB.NewTxnCallback} callback
* Function to call when the transaction is available. It will
* be invoked with the transaction and opened object stores.
* @param {string[]} [storeNames=[{@link MobileMessageDB.MESSAGE_STORE_NAME}]]
* Names of the stores to open.
*/
newTxn: function(txn_type, callback, storeNames) {
if (!storeNames) {
storeNames = [MESSAGE_STORE_NAME];
}
if (DEBUG) debug("Opening transaction for object stores: " + storeNames);
let self = this;
this.ensureDB(function(error, db) {
if (error) {
if (DEBUG) debug("Could not open database: " + error);
callback(error);
return;
}
let txn = db.transaction(storeNames, txn_type);
if (DEBUG) debug("Started transaction " + txn + " of type " + txn_type);
if (DEBUG) {
txn.oncomplete = function(event) {
debug("Transaction " + txn + " completed.");
};
txn.onerror = function(event) {
// TODO check event.target.error.name and show an appropiate error
// message according to it.
debug("Error occurred during transaction: " + event.target.error.name);
};
}
let stores;
if (storeNames.length == 1) {
if (DEBUG) debug("Retrieving object store " + storeNames[0]);
stores = txn.objectStore(storeNames[0]);
} else {
stores = [];
for (let storeName of storeNames) {
if (DEBUG) debug("Retrieving object store " + storeName);
stores.push(txn.objectStore(storeName));
}
}
callback(null, txn, stores);
});
},
/**
* @callback MobileMessageDB.InitCallback
* @param {number} aErrorCode
* The error code on failure, or <code>null</code> on success.
*/
/**
* Initialize this MobileMessageDB.
*
* @function MobileMessageDB.init
* @param {string} aDbName
* A string name for that database.
* @param {number} aDbVersion
* The version that mmdb should upgrade to. 0 for the latest version.
* @param {MobileMessageDB.InitCallback} aCallback
* A function when either the initialization transaction is completed
* or any error occurs. Should take only one argument -- null when
* initialized with success or the error object otherwise.
*/
init: function(aDbName, aDbVersion, aCallback) {
this.dbName = aDbName;
this.dbVersion = aDbVersion || DB_VERSION;
let self = this;
this.newTxn(READ_ONLY, function(error, txn, messageStore){
if (error) {
if (aCallback) {
aCallback(error);
}
return;
}
if (aCallback) {
txn.oncomplete = function() {
aCallback(null);
};
}
// In order to get the highest key value, we open a key cursor in reverse
// order and get only the first pointed value.
let request = messageStore.openCursor(null, PREV);
request.onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
if (DEBUG) {
debug("Could not get the last key from mobile message database. " +
"Probably empty database");
}
return;
}
self.lastMessageId = cursor.key || 0;
if (DEBUG) debug("Last assigned message ID was " + self.lastMessageId);
};
request.onerror = function(event) {
if (DEBUG) {
debug("Could not get the last key from mobile message database " +
event.target.error.name);
}
};
});
},
/**
* Close the MobileMessageDB.
*
* @function MobileMessageDB.close
*/
close: function() {
if (!this.db) {
return;
}
this.db.close();
this.db = null;
this.lastMessageId = 0;
},
/**
* Sometimes user might reboot or remove battery while sending/receiving
* message. This function set the status of message records to error. The
* function can be used as the callback of {@link MobileMessageDB.init}.
*
* @function MobileMessageDB.updatePendingTransactionToError
* @param {number} aError
* The function does nothing if <code>aError</code> is not
* <code>null</code>.
*/
updatePendingTransactionToError: function(aError) {
if (aError) {
return;
}
this.newTxn(READ_WRITE, function(error, txn, messageStore) {
if (error) {
return;
}
let deliveryIndex = messageStore.index("delivery");
// Set all 'delivery: sending' records to 'delivery: error' and 'deliveryStatus:
// error'.
let keyRange = IDBKeyRange.bound([DELIVERY_SENDING, 0], [DELIVERY_SENDING, ""]);
let cursorRequestSending = deliveryIndex.openCursor(keyRange);
cursorRequestSending.onsuccess = function(event) {
let messageCursor = event.target.result;
if (!messageCursor) {
return;
}
let messageRecord = messageCursor.value;
// Set delivery to error.
messageRecord.delivery = DELIVERY_ERROR;
messageRecord.deliveryIndex = [DELIVERY_ERROR, messageRecord.timestamp];
if (messageRecord.type == "sms") {
messageRecord.deliveryStatus = DELIVERY_STATUS_ERROR;
} else {
// Set delivery status to error.
for (let i = 0; i < messageRecord.deliveryInfo.length; i++) {
messageRecord.deliveryInfo[i].deliveryStatus = DELIVERY_STATUS_ERROR;
}
}
messageCursor.update(messageRecord);
messageCursor.continue();
};
// Set all 'delivery: not-downloaded' and 'deliveryStatus: pending'
// records to 'delivery: not-downloaded' and 'deliveryStatus: error'.
keyRange = IDBKeyRange.bound([DELIVERY_NOT_DOWNLOADED, 0], [DELIVERY_NOT_DOWNLOADED, ""]);
let cursorRequestNotDownloaded = deliveryIndex.openCursor(keyRange);
cursorRequestNotDownloaded.onsuccess = function(event) {
let messageCursor = event.target.result;
if (!messageCursor) {
return;
}
let messageRecord = messageCursor.value;
// We have no "not-downloaded" SMS messages.
if (messageRecord.type == "sms") {
messageCursor.continue();
return;
}
// Set delivery status to error.
let deliveryInfo = messageRecord.deliveryInfo;
if (deliveryInfo.length == 1 &&
deliveryInfo[0].deliveryStatus == DELIVERY_STATUS_PENDING) {
deliveryInfo[0].deliveryStatus = DELIVERY_STATUS_ERROR;
}
messageCursor.update(messageRecord);
messageCursor.continue();
};
});
},
/**
* Create the initial database schema.
*
* TODO need to worry about number normalization somewhere...
* TODO full text search on body???
*/
createSchema: function(db, next) {
// This messageStore holds the main mobile message data.
let messageStore = db.createObjectStore(MESSAGE_STORE_NAME, { keyPath: "id" });
messageStore.createIndex("timestamp", "timestamp", { unique: false });
if (DEBUG) debug("Created object stores and indexes");
next();
},
/**
* Upgrade to the corresponding database schema version.
*/
upgradeSchema: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.createIndex("read", "read", { unique: false });
next();
},
upgradeSchema2: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
messageRecord.messageClass = MESSAGE_CLASS_NORMAL;
messageRecord.deliveryStatus = DELIVERY_STATUS_NOT_APPLICABLE;
cursor.update(messageRecord);
cursor.continue();
};
},
upgradeSchema3: function(db, transaction, next) {
// Delete redundant "id" index.
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
if (messageStore.indexNames.contains("id")) {
messageStore.deleteIndex("id");
}
/**
* This mostRecentStore can be used to quickly construct a thread view of
* the mobile message database. Each entry looks like this:
*
* { senderOrReceiver: <String> (primary key),
* id: <Number>,
* timestamp: <Date>,
* body: <String>,
* unreadCount: <Number> }
*
*/
let mostRecentStore = db.createObjectStore(MOST_RECENT_STORE_NAME,
{ keyPath: "senderOrReceiver" });
mostRecentStore.createIndex("timestamp", "timestamp");
next();
},
upgradeSchema4: function(transaction, next) {
let threads = {};
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
for (let thread in threads) {
mostRecentStore.put(threads[thread]);
}
next();
return;
}
let messageRecord = cursor.value;
let contact = messageRecord.sender || messageRecord.receiver;
if (contact in threads) {
let thread = threads[contact];
if (!messageRecord.read) {
thread.unreadCount++;
}
if (messageRecord.timestamp > thread.timestamp) {
thread.id = messageRecord.id;
thread.body = messageRecord.body;
thread.timestamp = messageRecord.timestamp;
}
} else {
threads[contact] = {
senderOrReceiver: contact,
id: messageRecord.id,
timestamp: messageRecord.timestamp,
body: messageRecord.body,
unreadCount: messageRecord.read ? 0 : 1
};
}
cursor.continue();
};
},
upgradeSchema5: function(transaction, next) {
// Don't perform any upgrade. See Bug 819560.
next();
},
upgradeSchema6: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
// Delete "delivery" index.
if (messageStore.indexNames.contains("delivery")) {
messageStore.deleteIndex("delivery");
}
// Delete "sender" index.
if (messageStore.indexNames.contains("sender")) {
messageStore.deleteIndex("sender");
}
// Delete "receiver" index.
if (messageStore.indexNames.contains("receiver")) {
messageStore.deleteIndex("receiver");
}
// Delete "read" index.
if (messageStore.indexNames.contains("read")) {
messageStore.deleteIndex("read");
}
// Create new "delivery", "number" and "read" indexes.
messageStore.createIndex("delivery", "deliveryIndex");
messageStore.createIndex("number", "numberIndex", { multiEntry: true });
messageStore.createIndex("read", "readIndex");
// Populate new "deliverIndex", "numberIndex" and "readIndex" attributes.
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
let timestamp = messageRecord.timestamp;
messageRecord.deliveryIndex = [messageRecord.delivery, timestamp];
messageRecord.numberIndex = [
[messageRecord.sender, timestamp],
[messageRecord.receiver, timestamp]
];
messageRecord.readIndex = [messageRecord.read, timestamp];
cursor.update(messageRecord);
cursor.continue();
};
},
/**
* Add participant/thread stores.
*
* The message store now saves original phone numbers/addresses input from
* content to message records. No normalization is made.
*
* For filtering messages by phone numbers, it first looks up corresponding
* participant IDs from participant table and fetch message records with
* matching keys defined in per record "participantIds" field.
*
* For message threading, messages with the same participant ID array are put
* in the same thread. So updating "unreadCount", "lastMessageId" and
* "lastTimestamp" are through the "threadId" carried by per message record.
* Fetching threads list is now simply walking through the thread sotre. The
* "mostRecentStore" is dropped.
*/
upgradeSchema7: function(db, transaction, next) {
/**
* This "participant" object store keeps mappings of multiple phone numbers
* of the same recipient to an integer participant id. Each entry looks
* like:
*
* { id: <Number> (primary key),
* addresses: <Array of strings> }
*/
let participantStore = db.createObjectStore(PARTICIPANT_STORE_NAME,
{ keyPath: "id",
autoIncrement: true });
participantStore.createIndex("addresses", "addresses", { multiEntry: true });
/**
* This "threads" object store keeps mappings from an integer thread id to
* ids of the participants of that message thread. Each entry looks like:
*
* { id: <Number> (primary key),
* participantIds: <Array of participant IDs>,
* participantAddresses: <Array of the first addresses of the participants>,
* lastMessageId: <Number>,
* lastTimestamp: <Date>,
* subject: <String>,
* unreadCount: <Number> }
*
*/
let threadStore = db.createObjectStore(THREAD_STORE_NAME,
{ keyPath: "id",
autoIncrement: true });
threadStore.createIndex("participantIds", "participantIds");
threadStore.createIndex("lastTimestamp", "lastTimestamp");
/**
* Replace "numberIndex" with "participantIdsIndex" and create an additional
* "threadId". "numberIndex" will be removed later.
*/
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.createIndex("threadId", "threadIdIndex");
messageStore.createIndex("participantIds", "participantIdsIndex",
{ multiEntry: true });
// Now populate participantStore & threadStore.
let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);
let self = this;
let mostRecentRequest = mostRecentStore.openCursor();
mostRecentRequest.onsuccess = function(event) {
let mostRecentCursor = event.target.result;
if (!mostRecentCursor) {
db.deleteObjectStore(MOST_RECENT_STORE_NAME);
// No longer need the "number" index in messageStore, use
// "participantIds" index instead.
messageStore.deleteIndex("number");
next();
return;
}
let mostRecentRecord = mostRecentCursor.value;
// Each entry in mostRecentStore is supposed to be a unique thread, so we
// retrieve the records out and insert its "senderOrReceiver" column as a
// new record in participantStore.
let number = mostRecentRecord.senderOrReceiver;
self.findParticipantRecordByPlmnAddress(participantStore, number, true,
function(participantRecord) {
// Also create a new record in threadStore.
let threadRecord = {
participantIds: [participantRecord.id],
participantAddresses: [number],
lastMessageId: mostRecentRecord.id,
lastTimestamp: mostRecentRecord.timestamp,
subject: mostRecentRecord.body,
unreadCount: mostRecentRecord.unreadCount,
};
let addThreadRequest = threadStore.add(threadRecord);
addThreadRequest.onsuccess = function(event) {
threadRecord.id = event.target.result;
let numberRange = IDBKeyRange.bound([number, 0], [number, ""]);
let messageRequest = messageStore.index("number")
.openCursor(numberRange, NEXT);
messageRequest.onsuccess = function(event) {
let messageCursor = event.target.result;
if (!messageCursor) {
// No more message records, check next most recent record.
mostRecentCursor.continue();
return;
}
let messageRecord = messageCursor.value;
// Check whether the message really belongs to this thread.
let matchSenderOrReceiver = false;
if (messageRecord.delivery == DELIVERY_RECEIVED) {
if (messageRecord.sender == number) {
matchSenderOrReceiver = true;
}
} else if (messageRecord.receiver == number) {
matchSenderOrReceiver = true;
}
if (!matchSenderOrReceiver) {
// Check next message record.
messageCursor.continue();
return;
}
messageRecord.threadId = threadRecord.id;
messageRecord.threadIdIndex = [threadRecord.id,
messageRecord.timestamp];
messageRecord.participantIdsIndex = [
[participantRecord.id, messageRecord.timestamp]
];
messageCursor.update(messageRecord);
// Check next message record.
messageCursor.continue();
};
messageRequest.onerror = function() {
// Error in fetching message records, check next most recent record.
mostRecentCursor.continue();
};
};
addThreadRequest.onerror = function() {
// Error in fetching message records, check next most recent record.
mostRecentCursor.continue();
};
});
};
},
/**
* Add transactionId index for MMS.
*/
upgradeSchema8: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
// Delete "transactionId" index.
if (messageStore.indexNames.contains("transactionId")) {
messageStore.deleteIndex("transactionId");
}
// Create new "transactionId" indexes.
messageStore.createIndex("transactionId", "transactionIdIndex", { unique: true });
// Populate new "transactionIdIndex" attributes.
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if ("mms" == messageRecord.type &&
(DELIVERY_NOT_DOWNLOADED == messageRecord.delivery ||
DELIVERY_RECEIVED == messageRecord.delivery)) {
messageRecord.transactionIdIndex =
messageRecord.headers["x-mms-transaction-id"];
cursor.update(messageRecord);
}
cursor.continue();
};
},
upgradeSchema9: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
// Update type attributes.
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == undefined) {
messageRecord.type = "sms";
cursor.update(messageRecord);
}
cursor.continue();
};
},
upgradeSchema10: function(transaction, next) {
let threadStore = transaction.objectStore(THREAD_STORE_NAME);
// Add 'lastMessageType' to each thread record.
threadStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let threadRecord = cursor.value;
let lastMessageId = threadRecord.lastMessageId;
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
let request = messageStore.mozGetAll(lastMessageId);
request.onsuccess = function() {
let messageRecord = request.result[0];
if (!messageRecord) {
if (DEBUG) debug("Message ID " + lastMessageId + " not found");
return;
}
if (messageRecord.id != lastMessageId) {
if (DEBUG) {
debug("Requested message ID (" + lastMessageId + ") is different from" +
" the one we got");
}
return;
}
threadRecord.lastMessageType = messageRecord.type;
cursor.update(threadRecord);
cursor.continue();
};
request.onerror = function(event) {
if (DEBUG) {
if (event.target) {
debug("Caught error on transaction", event.target.error.name);
}
}
cursor.continue();
};
};
},
/**
* Add envelopeId index for MMS.
*/
upgradeSchema11: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
// Delete "envelopeId" index.
if (messageStore.indexNames.contains("envelopeId")) {
messageStore.deleteIndex("envelopeId");
}
// Create new "envelopeId" indexes.
messageStore.createIndex("envelopeId", "envelopeIdIndex", { unique: true });
// Populate new "envelopeIdIndex" attributes.
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == "mms" &&
messageRecord.delivery == DELIVERY_SENT) {
messageRecord.envelopeIdIndex = messageRecord.headers["message-id"];
cursor.update(messageRecord);
}
cursor.continue();
};
},
/**
* Replace deliveryStatus by deliveryInfo.
*/
upgradeSchema12: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == "mms") {
messageRecord.deliveryInfo = [];
if (messageRecord.deliveryStatus.length == 1 &&
(messageRecord.delivery == DELIVERY_NOT_DOWNLOADED ||
messageRecord.delivery == DELIVERY_RECEIVED)) {
messageRecord.deliveryInfo.push({
receiver: null,
deliveryStatus: messageRecord.deliveryStatus[0] });
} else {
for (let i = 0; i < messageRecord.deliveryStatus.length; i++) {
messageRecord.deliveryInfo.push({
receiver: messageRecord.receivers[i],
deliveryStatus: messageRecord.deliveryStatus[i] });
}
}
delete messageRecord.deliveryStatus;
cursor.update(messageRecord);
}
cursor.continue();
};
},
/**
* Check if we need to re-upgrade the DB schema 12.
*/
needReUpgradeSchema12: function(transaction, callback) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
callback(false);
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == "mms" &&
messageRecord.deliveryInfo === undefined) {
callback(true);
return;
}
cursor.continue();
};
},
/**
* Fix the wrong participants.
*/
upgradeSchema13: function(transaction, next) {
let participantStore = transaction.objectStore(PARTICIPANT_STORE_NAME);
let threadStore = transaction.objectStore(THREAD_STORE_NAME);
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
let self = this;
let isInvalid = function(participantRecord) {
let entries = [];
for (let addr of participantRecord.addresses) {
entries.push({
normalized: addr,
parsed: PhoneNumberUtils.parseWithMCC(addr, null)
})
}
for (let ix = 0 ; ix < entries.length - 1; ix++) {
let entry1 = entries[ix];
for (let iy = ix + 1 ; iy < entries.length; iy ++) {
let entry2 = entries[iy];
if (!self.matchPhoneNumbers(entry1.normalized, entry1.parsed,
entry2.normalized, entry2.parsed)) {
return true;
}
}
}
return false;
};
let invalidParticipantIds = [];
participantStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
let participantRecord = cursor.value;
// Check if this participant record is valid
if (isInvalid(participantRecord)) {
invalidParticipantIds.push(participantRecord.id);
cursor.delete();
}
cursor.continue();
return;
}
// Participant store cursor iteration done.
if (!invalidParticipantIds.length) {
next();
return;
}
// Find affected thread.
let wrongThreads = [];
threadStore.openCursor().onsuccess = function(event) {
let threadCursor = event.target.result;
if (threadCursor) {
let threadRecord = threadCursor.value;
let participantIds = threadRecord.participantIds;
let foundInvalid = false;
for (let invalidParticipantId of invalidParticipantIds) {
if (participantIds.indexOf(invalidParticipantId) != -1) {
foundInvalid = true;
break;
}
}
if (foundInvalid) {
wrongThreads.push(threadRecord.id);
threadCursor.delete();
}
threadCursor.continue();
return;
}
if (!wrongThreads.length) {
next();
return;
}
// Use recursive function to avoid we add participant twice.
(function createUpdateThreadAndParticipant(ix) {
let threadId = wrongThreads[ix];
let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
messageStore.index("threadId").openCursor(range).onsuccess = function(event) {
let messageCursor = event.target.result;
if (!messageCursor) {
ix++;
if (ix === wrongThreads.length) {
next();
return;
}
createUpdateThreadAndParticipant(ix);
return;
}
let messageRecord = messageCursor.value;
let timestamp = messageRecord.timestamp;
let threadParticipants = [];
// Recaculate the thread participants of received message.
if (messageRecord.delivery === DELIVERY_RECEIVED ||
messageRecord.delivery === DELIVERY_NOT_DOWNLOADED) {
threadParticipants.push(messageRecord.sender);
if (messageRecord.type == "mms") {
this.fillReceivedMmsThreadParticipants(messageRecord, threadParticipants);
}
}
// Recaculate the thread participants of sent messages and error
// messages. In error sms messages, we don't have error received sms.
// In received MMS, we don't update the error to deliver field but
// deliverStatus. So we only consider sent message in DELIVERY_ERROR.
else if (messageRecord.delivery === DELIVERY_SENT ||
messageRecord.delivery === DELIVERY_ERROR) {
if (messageRecord.type == "sms") {
threadParticipants = [messageRecord.receiver];
} else if (messageRecord.type == "mms") {
threadParticipants = messageRecord.receivers;
}
}
self.findThreadRecordByPlmnAddresses(threadStore, participantStore,
threadParticipants, true,
function(threadRecord,
participantIds) {
if (!participantIds) {
debug("participantIds is empty!");
return;
}
let timestamp = messageRecord.timestamp;
// Setup participantIdsIndex.
messageRecord.participantIdsIndex = [];
for (let id of participantIds) {
messageRecord.participantIdsIndex.push([id, timestamp]);
}
if (threadRecord) {
let needsUpdate = false;
if (threadRecord.lastTimestamp <= timestamp) {
threadRecord.lastTimestamp = timestamp;
threadRecord.subject = messageRecord.body;
threadRecord.lastMessageId = messageRecord.id;
threadRecord.lastMessageType = messageRecord.type;
needsUpdate = true;
}
if (!messageRecord.read) {
threadRecord.unreadCount++;
needsUpdate = true;
}
if (needsUpdate) {
threadStore.put(threadRecord);
}
messageRecord.threadId = threadRecord.id;
messageRecord.threadIdIndex = [threadRecord.id, timestamp];
messageCursor.update(messageRecord);
messageCursor.continue();
return;
}
threadRecord = {
participantIds: participantIds,
participantAddresses: threadParticipants,
lastMessageId: messageRecord.id,
lastTimestamp: timestamp,
subject: messageRecord.body,
unreadCount: messageRecord.read ? 0 : 1,
lastMessageType: messageRecord.type
};
threadStore.add(threadRecord).onsuccess = function(event) {
let threadId = event.target.result;
// Setup threadId & threadIdIndex.
messageRecord.threadId = threadId;
messageRecord.threadIdIndex = [threadId, timestamp];
messageCursor.update(messageRecord);
messageCursor.continue();
};
});
};
})(0);
};
};
},
/**
* Add deliveryTimestamp.
*/
upgradeSchema14: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == "sms") {
messageRecord.deliveryTimestamp = 0;
} else if (messageRecord.type == "mms") {
let deliveryInfo = messageRecord.deliveryInfo;
for (let i = 0; i < deliveryInfo.length; i++) {
deliveryInfo[i].deliveryTimestamp = 0;
}
}
cursor.update(messageRecord);
cursor.continue();
};
},
/**
* Add ICC ID.
*/
upgradeSchema15: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
messageRecord.iccId = null;
cursor.update(messageRecord);
cursor.continue();
};
},
/**
* Add isReadReportSent for incoming MMS.
*/
upgradeSchema16: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
// Update type attributes.
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == "mms") {
messageRecord.isReadReportSent = false;
cursor.update(messageRecord);
}
cursor.continue();
};
},
upgradeSchema17: function(transaction, next) {
let threadStore = transaction.objectStore(THREAD_STORE_NAME);
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
// Add 'lastMessageSubject' to each thread record.
threadStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let threadRecord = cursor.value;
// We have defined 'threadRecord.subject' in upgradeSchema7(), but it
// actually means 'threadRecord.body'. Swap the two values first.
threadRecord.body = threadRecord.subject;
delete threadRecord.subject;
// Only MMS supports subject so assign null for non-MMS one.
if (threadRecord.lastMessageType != "mms") {
threadRecord.lastMessageSubject = null;
cursor.update(threadRecord);
cursor.continue();
return;
}
messageStore.get(threadRecord.lastMessageId).onsuccess = function(event) {
let messageRecord = event.target.result;
let subject = messageRecord.headers.subject;
threadRecord.lastMessageSubject = subject || null;
cursor.update(threadRecord);
cursor.continue();
};
};
},
/**
* Add pid for incoming SMS.
*/
upgradeSchema18: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == "sms") {
messageRecord.pid = RIL.PDU_PID_DEFAULT;
cursor.update(messageRecord);
}
cursor.continue();
};
},
/**
* Add readStatus and readTimestamp.
*/
upgradeSchema19: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
if (messageRecord.type == "sms") {
cursor.continue();
return;
}
// We can always retrieve transaction id from
// |messageRecord.headers["x-mms-transaction-id"]|.
if (messageRecord.hasOwnProperty("transactionId")) {
delete messageRecord.transactionId;
}
// xpconnect gives "undefined" for an unassigned argument of an interface
// method.
if (messageRecord.envelopeIdIndex === "undefined") {
delete messageRecord.envelopeIdIndex;
}
// Convert some header fields that were originally decoded as BooleanValue
// to numeric enums.
for (let field of ["x-mms-cancel-status",
"x-mms-sender-visibility",
"x-mms-read-status"]) {
let value = messageRecord.headers[field];
if (value !== undefined) {
messageRecord.headers[field] = value ? 128 : 129;
}
}
// For all sent and received MMS messages, we have to add their
// |readStatus| and |readTimestamp| attributes in |deliveryInfo| array.
let readReportRequested =
messageRecord.headers["x-mms-read-report"] || false;
for (let element of messageRecord.deliveryInfo) {
element.readStatus = readReportRequested
? MMS.DOM_READ_STATUS_PENDING
: MMS.DOM_READ_STATUS_NOT_APPLICABLE;
element.readTimestamp = 0;
}
cursor.update(messageRecord);
cursor.continue();
};
},
/**
* Add sentTimestamp.
*/
upgradeSchema20: function(transaction, next) {
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
messageStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
next();
return;
}
let messageRecord = cursor.value;
messageRecord.sentTimestamp = 0;
// We can still have changes to assign |sentTimestamp| for the existing
// MMS message records.
if (messageRecord.type == "mms" && messageRecord.headers["date"]) {
messageRecord.sentTimestamp = messageRecord.headers["date"].getTime();
}
cursor.update(messageRecord);
cursor.continue();
};
},
/**
* Add smsSegmentStore to store uncomplete SMS segments.
*/
upgradeSchema21: function(db, transaction, next) {
/**
* This smsSegmentStore is used to store uncomplete SMS segments.
* Each entry looks like this:
*
* {
* [Common fields in SMS segment]
* messageType: <Number>,
* teleservice: <Number>,
* SMSC: <String>,
* sentTimestamp: <Number>,
* timestamp: <Number>,
* sender: <String>,
* pid: <Number>,
* encoding: <Number>,
* messageClass: <String>,
* iccId: <String>,
*
* [Concatenation Info]
* segmentRef: <Number>,
* segmentSeq: <Number>,
* segmentMaxSeq: <Number>,
*
* [Application Port Info]
* originatorPort: <Number>,
* destinationPort: <Number>,
*
* [MWI status]
* mwiPresent: <Boolean>,
* mwiDiscard: <Boolean>,
* mwiMsgCount: <Number>,
* mwiActive: <Boolean>,
*
* [CDMA Cellbroadcast related fields]
* serviceCategory: <Number>,
* language: <String>,
*
* [Message Body]
* data: <Uint8Array>, (available if it's 8bit encoding)
* body: <String>, (normal text body)
*
* [Handy fields created by DB for concatenation]
* id: <Number>, keypath of this objectStore.
* hash: <String>, Use to identify the segments to the same SMS.
* receivedSegments: <Number>,
* segments: []
* }
*
*/
let smsSegmentStore = db.createObjectStore(SMS_SEGMENT_STORE_NAME,
{ keyPath: "id",
autoIncrement: true });
smsSegmentStore.createIndex("hash", "hash", { unique: true });
next();
},
/**
* Change receivers format to address and type.
*/
upgradeSchema22: function(transaction, next) {
// Since bug 871433 (DB_VERSION 11), we normalize addresses before really
// diving into participant store in findParticipantRecordByPlmnAddress.
// This also follows that all addresses stored in participant store are
// normalized phone numbers, although they might not be phone numbers at the
// first place. So addresses in participant store are not reliable.
//
// |participantAddresses| in a thread record are reliable, but several
// distinct threads can be wrongly mapped into one. For example, an IPv4
// address "55.252.255.54" was normalized as US phone number "5525225554".
// So beginning with thread store is not really a good idea.
//
// The only correct way is to begin with all messages records and check if
// the findThreadRecordByTypedAddresses() call using a message record's
// thread participants returns the same thread record with the one it
// currently belong to.
function getThreadParticipantsFromMessageRecord(aMessageRecord) {
let threadParticipants;
if (aMessageRecord.type == "sms") {
let address;
if (aMessageRecord.delivery == DELIVERY_RECEIVED) {
address = aMessageRecord.sender;
} else {
address = aMessageRecord.receiver;
}
threadParticipants = [{
address: address,
type: MMS.Address.resolveType(address)
}];
} else { // MMS
if ((aMessageRecord.delivery == DELIVERY_RECEIVED) ||
(aMessageRecord.delivery == DELIVERY_NOT_DOWNLOADED)) {
// DISABLE_MMS_GROUPING_FOR_RECEIVING is set to true at the time, so
// we consider only |aMessageRecord.sender|.
threadParticipants = [{
address: aMessageRecord.sender,
type: MMS.Address.resolveType(aMessageRecord.sender)
}];
} else {
threadParticipants = aMessageRecord.headers.to;
}
}
return threadParticipants;
}
let participantStore = transaction.objectStore(PARTICIPANT_STORE_NAME);
let threadStore = transaction.objectStore(THREAD_STORE_NAME);
let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
let invalidThreadIds = [];
let self = this;
let messageCursorReq = messageStore.openCursor();
messageCursorReq.onsuccess = function(aEvent) {
let messageCursor = aEvent.target.result;
if (messageCursor) {
let messageRecord = messageCursor.value;
let threadParticipants =
getThreadParticipantsFromMessageRecord(messageRecord);
// 1. If thread ID of this message record has been marked as invalid,
// skip further checks and go ahead for the next one.
if (invalidThreadIds.indexOf(messageRecord.threadId) >= 0) {
messageCursor.continue();
return;
}
// 2. Check if the thread record found with the new algorithm matches
// the original one.
self.findThreadRecordByTypedAddresses(threadStore, participantStore,
threadParticipants, true,
function(aThreadRecord,
aParticipantIds) {
if (!aThreadRecord || aThreadRecord.id !== messageRecord.threadId) {
invalidThreadIds.push(messageRecord.threadId);
}
messageCursor.continue();
});
// Only calls |messageCursor.continue()| inside the callback of
// findThreadRecordByTypedAddresses() because that may inserts new
// participant records and hurt concurrency.
return;
} // End of |if (messageCursor)|.
// 3. If there is no any mis-grouped message found, go on to next upgrade.
if (!invalidThreadIds.length) {
next();
return;
}
// 4. Remove invalid thread records first, so that we don't have
// unexpected match in findThreadRecordByTypedAddresses().
invalidThreadIds.forEach(function(aInvalidThreadId) {
threadStore.delete(aInvalidThreadId);
});
// 5. For each affected thread, re-create a valid thread record for it.
(function redoThreading(aInvalidThreadId) {
// 5-1. For each message record originally belongs to this thread, find
// a new home for it.
let range = IDBKeyRange.bound([aInvalidThreadId, 0],
[aInvalidThreadId, ""]);
let threadMessageCursorReq = messageStore.index("threadId")
.openCursor(range, NEXT);
threadMessageCursorReq.onsuccess = function(aEvent) {
let messageCursor = aEvent.target.result;
// 5-2. If no more message records to process in this invalid thread,
// go on to next invalid thread if available, or pass to next
// upgradeSchema function.
if (!messageCursor) {
if (invalidThreadIds.length) {
redoThreading(invalidThreadIds.shift());
} else {
next();
}
return;
}
let messageRecord = messageCursor.value;
let threadParticipants =
getThreadParticipantsFromMessageRecord(messageRecord);
// 5-3. Assign a thread record for this message record. Basically
// copied from |realSaveRecord|, but we don't have to worry
// about |updateThreadByMessageChange| because we've removed
// affected threads.
self.findThreadRecordByTypedAddresses(threadStore, participantStore,
threadParticipants, true,
function(aThreadRecord,
aParticipantIds) {
// Setup participantIdsIndex.
messageRecord.participantIdsIndex =
aParticipantIds.map(function(aParticipantId) {
return [aParticipantId, messageRecord.timestamp];
});
let threadExists = aThreadRecord ? true : false;
if (!threadExists) {
aThreadRecord = {
participantIds: aParticipantIds,
participantAddresses:
threadParticipants.map(function(aTypedAddress) {
return aTypedAddress.address;
}),
unreadCount: 0,
lastTimestamp: -1
};
}
let needsUpdate = false;
if (aThreadRecord.lastTimestamp <= messageRecord.timestamp) {
let lastMessageSubject;
if (messageRecord.type == "mms") {
lastMessageSubject = messageRecord.headers.subject;
}
aThreadRecord.lastMessageSubject = lastMessageSubject || null;
aThreadRecord.lastTimestamp = messageRecord.timestamp;
aThreadRecord.body = messageRecord.body;
aThreadRecord.lastMessageId = messageRecord.id;
aThreadRecord.lastMessageType = messageRecord.type;
needsUpdate = true;
}
if (!messageRecord.read) {
aThreadRecord.unreadCount++;
needsUpdate = true;
}
let updateMessageRecordThreadId = function(aThreadId) {
// Setup threadId & threadIdIndex.
messageRecord.threadId = aThreadId;
messageRecord.threadIdIndex = [aThreadId, messageRecord.timestamp];
messageCursor.update(messageRecord);
messageCursor.continue();
};
if (threadExists) {
if (needsUpdate) {
threadStore.put(aThreadRecord);
}
updateMessageRecordThreadId(aThreadRecord.id);
} else {
threadStore.add(aThreadRecord).onsuccess = function(aEvent) {
let threadId = aEvent.target.result;
updateMessageRecordThreadId(threadId);
};
}
}); // End of findThreadRecordByTypedAddresses().
}; // End of threadMessageCursorReq.onsuccess.
})(invalidThreadIds.shift()); // End of function redoThreading.
}; // End of messageStore.openCursor().onsuccess
},
/**
* Check if <code>addr1</code> matches <code>addr2</code>.
*
* @function MobileMessageDB.matchParsedPhoneNumbers
* @param {string} addr1
* Normalized address 1.
* @param {Object} parsedAddr1
* Parsed address 1.
* @param {string} addr2
* Normalized address 2.
* @param {Object} parsedAddr2
* Parsed address 2.
* @return {boolean}
* <code>true</code> if the 2 addresses match.
*/
matchParsedPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
if ((parsedAddr1.internationalNumber &&
parsedAddr1.internationalNumber === parsedAddr2.internationalNumber) ||
(parsedAddr1.nationalNumber &&
parsedAddr1.nationalNumber === parsedAddr2.nationalNumber)) {
return true;
}
if (parsedAddr1.countryName != parsedAddr2.countryName) {
return false;
}
let ssPref = "dom.phonenumber.substringmatching." + parsedAddr1.countryName;
if (Services.prefs.getPrefType(ssPref) != Ci.nsIPrefBranch.PREF_INT) {
return false;
}
let val = Services.prefs.getIntPref(ssPref);
return addr1.length > val &&
addr2.length > val &&
addr1.slice(-val) === addr2.slice(-val);
},
/**
* Check if <code>addr1</code> matches <code>addr2</code>.
*
* @function MobileMessageDB.matchPhoneNumbers
* @param {string} addr1
* Normalized address 1.
* @param {Object} parsedAddr1
* Parsed address 1. Try to parse from <code>addr1</code> if not given.
* @param {string} addr2
* Normalized address 2.
* @param {Object} parsedAddr2
* Parsed address 2. Try to parse from <code>addr2</code> if not given.
* @return {boolean}
* <code>true</code> if the 2 addresses match.
*/
matchPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
if (parsedAddr1 && parsedAddr2) {
return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
}
if (parsedAddr1) {
parsedAddr2 = PhoneNumberUtils.parseWithCountryName(addr2, parsedAddr1.countryName);
if (parsedAddr2) {
return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
}
return false;
}
if (parsedAddr2) {
parsedAddr1 = PhoneNumberUtils.parseWithCountryName(addr1, parsedAddr2.countryName);
if (parsedAddr1) {
return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
}
}
return false;
},
/**
* Generate a <code>nsISmsMessage</code> or
* <code>nsIMmsMessage</code> instance from a stored message record.
*
* @function MobileMessageDB.createDomMessageFromRecord
* @param {MobileMessageDB.MessageRecord} aMessageRecord
* The stored message record.
* @return {nsISmsMessage|nsIMmsMessage}
*/
createDomMessageFromRecord: function(aMessageRecord) {
if (DEBUG) {
debug("createDomMessageFromRecord: " + JSON.stringify(aMessageRecord));
}
if (aMessageRecord.type == "sms") {
return gMobileMessageService.createSmsMessage(aMessageRecord.id,
aMessageRecord.threadId,
aMessageRecord.iccId,
aMessageRecord.delivery,
aMessageRecord.deliveryStatus,
aMessageRecord.sender,
aMessageRecord.receiver,
aMessageRecord.body,
aMessageRecord.messageClass,
aMessageRecord.timestamp,
aMessageRecord.sentTimestamp,
aMessageRecord.deliveryTimestamp,
aMessageRecord.read);
} else if (aMessageRecord.type == "mms") {
let headers = aMessageRecord["headers"];
if (DEBUG) {
debug("MMS: headers: " + JSON.stringify(headers));
}
let subject = headers["subject"];
if (subject == undefined) {
subject = "";
}
let smil = "";
let attachments = [];
let parts = aMessageRecord.parts;
if (parts) {
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
if (DEBUG) {
debug("MMS: part[" + i + "]: " + JSON.stringify(part));
}
// Sometimes the part is incomplete because the device reboots when
// downloading MMS. Don't need to expose this part to the content.
if (!part) {
continue;
}
let partHeaders = part["headers"];
let partContent = part["content"];
// Don't need to make the SMIL part if it's present.
if (partHeaders["content-type"]["media"] == "application/smil") {
smil = partContent;
continue;
}
attachments.push({
"id": partHeaders["content-id"],
"location": partHeaders["content-location"],
"content": partContent
});
}
}
let expiryDate = 0;
if (headers["x-mms-expiry"] != undefined) {
expiryDate = aMessageRecord.timestamp + headers["x-mms-expiry"] * 1000;
}
let readReportRequested = headers["x-mms-read-report"] || false;
return gMobileMessageService.createMmsMessage(aMessageRecord.id,
aMessageRecord.threadId,
aMessageRecord.iccId,
aMessageRecord.delivery,
aMessageRecord.deliveryInfo,
aMessageRecord.sender,
aMessageRecord.receivers,
aMessageRecord.timestamp,
aMessageRecord.sentTimestamp,
aMessageRecord.read,
subject,
smil,
attachments,
expiryDate,
readReportRequested);
}
},
/**
* @callback MobileMessageDB.ParticipantRecordCallback
* @param {MobileMessageDB.ParticipantRecord} aParticipantRecord
* The stored participant record.
*/
/**
* Create a participant record with the given addresses, and add it into the
* participant object store immediately.
*
* @function MobileMessageDB.createParticipantRecord
* @param {IDBObjectStore} aParticipantStore
* Object store for participants.
* @param {string[]} aAddresses
* The addresses associated to the participant.
* @param {MobileMessageDB.ParticipantRecordCallback} aCallback
* The callback function to invoke when the request finishes.
*/
createParticipantRecord: function(aParticipantStore, aAddresses, aCallback) {
let participantRecord = { addresses: aAddresses };
let addRequest = aParticipantStore.add(participantRecord);
addRequest.onsuccess = function(event) {
participantRecord.id = event.target.result;
if (DEBUG) {
debug("createParticipantRecord: " + JSON.stringify(participantRecord));
}
aCallback(participantRecord);
};
},
/**
* Find or create the participant record associated to the given PLMN address.
*
* @function MobileMessageDB.findParticipantRecordByPlmnAddress
* @param {IDBObjectStore} aParticipantStore
* The object store for participants.
* @param {string} aAddress
* The PLMN address to look up with.
* @param {boolean} aCreate
* <code>true</code> to create a new participant record if not exists
* yet, otherwise return <code>null</code> to the callback if record
* not found.
* @param {MobileMessageDB.ParticipantRecordCallback} aCallback
* The callback function to invoke when the request finishes.
*/
findParticipantRecordByPlmnAddress: function(aParticipantStore, aAddress,
aCreate, aCallback) {
if (DEBUG) {
debug("findParticipantRecordByPlmnAddress("
+ JSON.stringify(aAddress) + ", " + aCreate + ")");
}
// Two types of input number to match here, international(+886987654321),
// and local(0987654321) types. The "nationalNumber" parsed from
// phonenumberutils will be "987654321" in this case.
// Normalize address before searching for participant record.
let normalizedAddress = PhoneNumberUtils.normalize(aAddress, false);
let allPossibleAddresses = [normalizedAddress];
let parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
if (parsedAddress && parsedAddress.internationalNumber &&
allPossibleAddresses.indexOf(parsedAddress.internationalNumber) < 0) {
// We only stores international numbers into participant store because
// the parsed national number doesn't contain country info and may
// duplicate in different country.
allPossibleAddresses.push(parsedAddress.internationalNumber);
}
if (DEBUG) {
debug("findParticipantRecordByPlmnAddress: allPossibleAddresses = " +
JSON.stringify(allPossibleAddresses));
}
// Make a copy here because we may need allPossibleAddresses again.
let needles = allPossibleAddresses.slice(0);
let request = aParticipantStore.index("addresses").get(needles.pop());
request.onsuccess = (function onsuccess(event) {
let participantRecord = event.target.result;
// 1) First try matching through "addresses" index of participant store.
// If we're lucky, return the fetched participant record.
if (participantRecord) {
if (DEBUG) {
debug("findParticipantRecordByPlmnAddress: got "
+ JSON.stringify(participantRecord));
}
aCallback(participantRecord);
return;
}
// Try next possible address again.
if (needles.length) {
let request = aParticipantStore.index("addresses").get(needles.pop());
request.onsuccess = onsuccess.bind(this);
return;
}
// 2) Traverse throught all participants and check all alias addresses.
aParticipantStore.openCursor().onsuccess = (function(event) {
let cursor = event.target.result;
if (!cursor) {
// Have traversed whole object store but still in vain.
if (!aCreate) {
aCallback(null);
return;
}
this.createParticipantRecord(aParticipantStore, [normalizedAddress],
aCallback);
return;
}
let participantRecord = cursor.value;
for (let storedAddress of participantRecord.addresses) {
let parsedStoredAddress = PhoneNumberUtils.parseWithMCC(storedAddress, null);
let match = this.matchPhoneNumbers(normalizedAddress, parsedAddress,
storedAddress, parsedStoredAddress);
if (!match) {
// 3) Else we fail to match current stored participant record.
continue;
}
// Match!
if (aCreate) {
// In a READ-WRITE transaction, append one more possible address for
// this participant record.
participantRecord.addresses =
participantRecord.addresses.concat(allPossibleAddresses);
cursor.update(participantRecord);
}
if (DEBUG) {
debug("findParticipantRecordByPlmnAddress: match "
+ JSON.stringify(cursor.value));
}
aCallback(participantRecord);
return;
}
// Check next participant record if available.
cursor.continue();
}).bind(this);
}).bind(this);
},
/**
* Find or create the participant record associated to the given address other
* than PLMN address.
*
* @function MobileMessageDB.findParticipantRecordByOtherAddress
* @param {IDBObjectStore} aParticipantStore
* The object store for participants.
* @param {string} aAddress
* The address to look up with.
* @param {boolean} aCreate
* <code>true</code> to create a new participant record if not exists
* yet, otherwise return <code>null</code> to the callback if record
* not found.
* @param {MobileMessageDB.ParticipantRecordCallback} aCallback
* The callback function to invoke when the request finishes.
*/
findParticipantRecordByOtherAddress: function(aParticipantStore, aAddress,
aCreate, aCallback) {
if (DEBUG) {
debug("findParticipantRecordByOtherAddress(" +
JSON.stringify(aAddress) + ", " + aCreate + ")");
}
// Go full match.
let request = aParticipantStore.index("addresses").get(aAddress);
request.onsuccess = (function(event) {
let participantRecord = event.target.result;
if (participantRecord) {
if (DEBUG) {
debug("findParticipantRecordByOtherAddress: got "
+ JSON.stringify(participantRecord));
}
aCallback(participantRecord);
return;
}
if (aCreate) {
this.createParticipantRecord(aParticipantStore, [aAddress], aCallback);
return;
}
aCallback(null);
}).bind(this);
},
/**
* @typedef {Object} MobileMessageDB.TypedAddress
* @property {string} address Address
* @property {string} type Type of the address, such as "PLMN", "IPv4",
* "IPv6", "email" or "Others"
*/
/**
* Find or create the participant record associated to the given address.
*
* @function MobileMessageDB.findParticipantRecordByTypedAddress
* @param {IDBObjectStore} aParticipantStore
* The object store for participants.
* @param {MobileMessageDB.TypedAddress} aTypedAddress
* The address to look up with.
* @param {boolean} aCreate
* <code>true</code> to create a new participant record if not exists
* yet, otherwise return <code>null</code> to the callback if record
* not found.
* @param {MobileMessageDB.ParticipantRecordCallback} aCallback
* The callback function to invoke when the request finishes.
*/
findParticipantRecordByTypedAddress: function(aParticipantStore,
aTypedAddress, aCreate,
aCallback) {
if (aTypedAddress.type == "PLMN") {
this.findParticipantRecordByPlmnAddress(aParticipantStore,
aTypedAddress.address, aCreate,
aCallback);
} else {
this.findParticipantRecordByOtherAddress(aParticipantStore,
aTypedAddress.address, aCreate,
aCallback);
}
},
// For upgradeSchema13 usage.
findParticipantIdsByPlmnAddresses: function(aParticipantStore, aAddresses,
aCreate, aSkipNonexistent, aCallback) {
if (DEBUG) {
debug("findParticipantIdsByPlmnAddresses("
+ JSON.stringify(aAddresses) + ", "
+ aCreate + ", " + aSkipNonexistent + ")");
}
if (!aAddresses || !aAddresses.length) {
if (DEBUG) debug("findParticipantIdsByPlmnAddresses: returning null");
aCallback(null);
return;
}
let self = this;
(function findParticipantId(index, result) {
if (index >= aAddresses.length) {
// Sort numerically.
result.sort(function(a, b) {
return a - b;
});
if (DEBUG) debug("findParticipantIdsByPlmnAddresses: returning " + result);
aCallback(result);
return;
}
self.findParticipantRecordByPlmnAddress(aParticipantStore,
aAddresses[index++], aCreate,
function(participantRecord) {
if (!participantRecord) {
if (!aSkipNonexistent) {
if (DEBUG) debug("findParticipantIdsByPlmnAddresses: returning null");
aCallback(null);
return;
}
} else if (result.indexOf(participantRecord.id) < 0) {
result.push(participantRecord.id);
}
findParticipantId(index, result);
});
}) (0, []);
},
/**
* @callback MobileMessageDB.ParticipantIdsCallback
* @param {number[]} aParticipantIds
* An array of participant IDs. May be <code>null</code>.
*/
/**
* Find or create participant records associated to the given addresses, and
* return the IDs of the participant records to the caller through the
* callback.
*
* @function MobileMessageDB.findParticipantIdsByTypedAddresses
* @param {IDBObjectStore} aParticipantStore
* The object store for participants.
* @param {MobileMessageDB.TypedAddress[]} aTypedAddresses
* Addresses to look up with.
* @param {boolean} aCreate
* <code>true</code> to create a new participant record associates to
* the given addresses if not exists yet, otherwise return
* <code>null</code> to the callback if no record found.
* @param {boolean} aSkipNonexistent
* <code>true</code> to skip the addresses not exist in the participant
* store, otherwise return <code>null</code> to the callback when one
* or more addresses not found.
* @param {MobileMessageDB.ParticipantIdsCallback} aCallback
* The callback function to invoke when the request finishes.
*/
findParticipantIdsByTypedAddresses: function(aParticipantStore,
aTypedAddresses, aCreate,
aSkipNonexistent, aCallback) {
if (DEBUG) {
debug("findParticipantIdsByTypedAddresses(" +
JSON.stringify(aTypedAddresses) + ", " +
aCreate + ", " + aSkipNonexistent + ")");
}
if (!aTypedAddresses || !aTypedAddresses.length) {
if (DEBUG) debug("findParticipantIdsByTypedAddresses: returning null");
aCallback(null);
return;
}
let self = this;
(function findParticipantId(index, result) {
if (index >= aTypedAddresses.length) {
// Sort numerically.
result.sort(function(a, b) {
return a - b;
});
if (DEBUG) {
debug("findParticipantIdsByTypedAddresses: returning " + result);
}
aCallback(result);
return;
}
self.findParticipantRecordByTypedAddress(aParticipantStore,
aTypedAddresses[index++],
aCreate,
function(participantRecord) {
if (!participantRecord) {
if (!aSkipNonexistent) {
if (DEBUG) {
debug("findParticipantIdsByTypedAddresses: returning null");
}
aCallback(null);
return;
}
} else if (result.indexOf(participantRecord.id) < 0) {
result.push(participantRecord.id);
}
findParticipantId(index, result);
});
}) (0, []);
},
// For upgradeSchema13 usage.
findThreadRecordByPlmnAddresses: function(aThreadStore, aParticipantStore,
aAddresses, aCreateParticipants,
aCallback) {
if (DEBUG) {
debug("findThreadRecordByPlmnAddresses(" + JSON.stringify(aAddresses)
+ ", " + aCreateParticipants + ")");
}
this.findParticipantIdsByPlmnAddresses(aParticipantStore, aAddresses,
aCreateParticipants, false,
function(participantIds) {
if (!participantIds) {
if (DEBUG) debug("findThreadRecordByPlmnAddresses: returning null");
aCallback(null, null);
return;
}
// Find record from thread store.
let request = aThreadStore.index("participantIds").get(participantIds);
request.onsuccess = function(event) {
let threadRecord = event.target.result;
if (DEBUG) {
debug("findThreadRecordByPlmnAddresses: return "
+ JSON.stringify(threadRecord));
}
aCallback(threadRecord, participantIds);
};
});
},
/**
* @callback MobileMessageDB.ThreadRecordCallback
* @param {MobileMessageDB.ThreadRecord} aThreadRecord
* The stored thread record.
* @param {number[]} aParticipantIds
* IDs of participants of the thread.
*/
/**
* Find the thread record associated to the given address.
*
* @function MobileMessageDB.findThreadRecordByTypedAddresses
* @param {IDBObjectStore} aThreadStore
* The object store for threads.
* @param {IDBObjectStore} aParticipantStore
* The object store for participants.
* @param {MobileMessageDB.TypedAddress[]} aTypedAddresses
* Addresses to look up with.
* @param {boolean} aCreateParticipants
* <code>true</code> to create participant record associated to the
* addresses if not exist yet.
* @param {MobileMessageDB.ThreadRecordCallback} aCallback
* The callback function to invoke when the request finishes.
*/
findThreadRecordByTypedAddresses: function(aThreadStore, aParticipantStore,
aTypedAddresses,
aCreateParticipants, aCallback) {
if (DEBUG) {
debug("findThreadRecordByTypedAddresses(" +
JSON.stringify(aTypedAddresses) + ", " + aCreateParticipants + ")");
}
this.findParticipantIdsByTypedAddresses(aParticipantStore, aTypedAddresses,
aCreateParticipants, false,
function(participantIds) {
if (!participantIds) {
if (DEBUG) debug("findThreadRecordByTypedAddresses: returning null");
aCallback(null, null);
return;
}
// Find record from thread store.
let request = aThreadStore.index("participantIds").get(participantIds);
request.onsuccess = function(event) {
let threadRecord = event.target.result;
if (DEBUG) {
debug("findThreadRecordByTypedAddresses: return " +
JSON.stringify(threadRecord));
}
aCallback(threadRecord, participantIds);
};
});
},
/**
* @callback MobileMessageDB.TransactionResultCallback
* @param {number} aErrorCode
* The error code on failure, or <code>NS_OK</code> on success.
* @param {nsISmsMessage|nsIMmsMessage} aDomMessage
* The DOM message instance of the transaction result.
*/
/**
* @callback MobileMessageDB.NewTxnWithCallbackRequestCallback
* @param {Object} aCapture
* An output parameter. The <code>messageRecord</code> property will be
* set on transaction finishes.
* @param {MobileMessageDB.MessageRecord} aCapture.messageRecord
* The stored message record. The property presents if the transaction
* finished successfully.
* @param {IDBObjectStore|IDBObjectStore[]} aObjectStores
* The object store(s) on success. If only one object store is passed,
* it's passed as an <code>IDBObjectStore</code>; Otherwise, it's
* <code>IDBObjectStore[]</code>.
*/
/**
* Start a new transaction with default <code>oncomplete</code> /
* <code>onabort</code> implementation on the <code>IDBTransaction</code>
* object which redirects the error / result to <code>aCallback</code>.
*
* @function MobileMessageDB.newTxnWithCallback
* @param {Object} aCallback
* The object which includes a callback function.
* @param {MobileMessageDB.TransactionResultCallback} aCallback.notify
* The callback function to invoke when the transaction finishes.
* @param {MobileMessageDB.NewTxnWithCallbackRequestCallback} aFunc
* The callback function to invoke when the request finishes.
* @param {string[]} [aStoreNames=[{@link MobileMessageDB.MESSAGE_STORE_NAME}]]
* Names of the stores to open.
*/
newTxnWithCallback: function(aCallback, aFunc, aStoreNames) {
let self = this;
this.newTxn(READ_WRITE, function(aError, aTransaction, aStores) {
let notifyResult = function(aRv, aMessageRecord) {
if (!aCallback) {
return;
}
let domMessage =
aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
aCallback.notify(aRv, domMessage);
};
if (aError) {
notifyResult(aError, null);
return;
}
let capture = {};
aTransaction.oncomplete = function(event) {
notifyResult(Cr.NS_OK, capture.messageRecord);
};
aTransaction.onabort = function(event) {
if (DEBUG) debug("transaction abort due to " + event.target.error.name);
let error = (event.target.error.name === 'QuotaExceededError')
? Cr.NS_ERROR_FILE_NO_DEVICE_SPACE
: Cr.NS_ERROR_FAILURE;
notifyResult(error, null);
};
aFunc(capture, aStores);
}, aStoreNames);
},
/**
* Save a message record.
*
* @function MobileMessageDB.saveRecord
* @param {MobileMessageDB.MessageRecord} aMessageRecord
* Message record to store.
* @param {MobileMessageDB.TypedAddress[]} aThreadParticipants
* Participants of the thread of the message.
* @param {Object} aCallback
* The object which includes a callback function.
* @param {MobileMessageDB.TransactionResultCallback} aCallback.notify
* The callback function to invoke when the transaction finishes.
*/
saveRecord: function(aMessageRecord, aThreadParticipants, aCallback) {
if (DEBUG) debug("Going to store " + JSON.stringify(aMessageRecord));
let self = this;
this.newTxn(READ_WRITE, function(error, txn, stores) {
let notifyResult = function(aRv, aMessageRecord) {
if (!aCallback) {
return;
}
let domMessage =
aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
aCallback.notify(aRv, domMessage);
};
if (error) {
notifyResult(error, null);
return;
}
let deletedInfo = { messageIds: [], threadIds: [] };
txn.oncomplete = function(event) {
if (aMessageRecord.id > self.lastMessageId) {
self.lastMessageId = aMessageRecord.id;
}
notifyResult(Cr.NS_OK, aMessageRecord);
self.notifyDeletedInfo(deletedInfo);
};
txn.onabort = function(event) {
if (DEBUG) debug("transaction abort due to " + event.target.error.name);
let error = (event.target.error.name === 'QuotaExceededError')
? Cr.NS_ERROR_FILE_NO_DEVICE_SPACE
: Cr.NS_ERROR_FAILURE;
notifyResult(error, null);
};
let messageStore = stores[0];
let participantStore = stores[1];
let threadStore = stores[2];
self.replaceShortMessageOnSave(txn, messageStore, participantStore,
threadStore, aMessageRecord,
aThreadParticipants, deletedInfo);
}, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME, THREAD_STORE_NAME]);
},
/**
* @typedef {Object} MobileMessageDB.DeletedInfo
* @property {number[]} messageIds
* IDs of deleted messages.
* @property {number[]} threadIds
* IDs of deleted threads, which indicates all messages within the
* threads have been deleted.
*/
/**
* According to <i>3GPP 23.040 - subclause 9.2.3.9 TP-Protocol-Identifier (TP-PID)</i>,
* if the Protocol Identifier contains a <i>Replace Short Message Type</i> or
* <i>Return Call Message</i> code, it should replace any existing stored
* message having the same Protocol Identifier code and originating address.
*
* This function checks the Protocol Identifier before saving the message
* record to fulfill the feature.
*
* @function MobileMessageDB.replaceShortMessageOnSave
* @param {IDBTransaction} aTransaction
* The transaction object.
* @param {IDBObjectStore} aMessageStore
* The object store for messages.
* @param {IDBObjectStore} aParticipantStore
* The object store for participants.
* @param {IDBObjectStore} aThreadStore
* The object store for threads.
* @param {MobileMessageDB.MessageRecord} aMessageRecord
* The message record to store.
* @param {MobileMessageDB.TypedAddress[]} aThreadParticipants
* Participants of the thread of the message.
* @param {MobileMessageDB.DeletedInfo} aDeletedInfo
* An out parameter indicating which messages have been deleted due to
* the replacement.
*/
replaceShortMessageOnSave: function(aTransaction, aMessageStore,
aParticipantStore, aThreadStore,
aMessageRecord, aThreadParticipants,
aDeletedInfo) {
let isReplaceTypePid = (aMessageRecord.pid) &&
((aMessageRecord.pid >= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_1 &&
aMessageRecord.pid <= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_7) ||
aMessageRecord.pid == RIL.PDU_PID_RETURN_CALL_MESSAGE);
if (aMessageRecord.type != "sms" ||
aMessageRecord.delivery != DELIVERY_RECEIVED ||
!isReplaceTypePid) {
this.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
aThreadStore, aMessageRecord, aThreadParticipants,
aDeletedInfo);
return;
}
// 3GPP TS 23.040 subclause 9.2.3.9 "TP-Protocol-Identifier (TP-PID)":
//
// ... the MS shall check the originating address and replace any
// existing stored message having the same Protocol Identifier code
// and originating address with the new short message and other
// parameter values. If there is no message to be replaced, the MS
// shall store the message in the normal way. ... it is recommended
// that the SC address should not be checked by the MS."
let self = this;
let typedSender = {
address: aMessageRecord.sender,
type: MMS.Address.resolveType(aMessageRecord.sender)
};
this.findParticipantRecordByTypedAddress(aParticipantStore, typedSender,
false,
function(participantRecord) {
if (!participantRecord) {
self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
aThreadStore, aMessageRecord, aThreadParticipants,
aDeletedInfo);
return;
}
let participantId = participantRecord.id;
let range = IDBKeyRange.bound([participantId, 0], [participantId, ""]);
let request = aMessageStore.index("participantIds").openCursor(range);
request.onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
aThreadStore, aMessageRecord, aThreadParticipants,
aDeletedInfo);
return;
}
// A message record with same participantId found.
// Verify matching criteria.
let foundMessageRecord = cursor.value;
if (foundMessageRecord.type != "sms" ||
foundMessageRecord.sender != aMessageRecord.sender ||
foundMessageRecord.pid != aMessageRecord.pid) {
cursor.continue();
return;
}
// Match! Now replace that found message record with current one.
aMessageRecord.id = foundMessageRecord.id;
self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
aThreadStore, aMessageRecord, aThreadParticipants,
aDeletedInfo);
};
});
},
/**
* The function where object store manipulations actually occur.
*
* @function MobileMessageDB.realSaveRecord
* @param {IDBTransaction} aTransaction
* The transaction object.
* @param {IDBObjectStore} aMessageStore
* The object store for messages.
* @param {IDBObjectStore} aParticipantStore
* The object store for participants.
* @param {IDBObjectStore} aThreadStore
* The object store for threads.
* @param {MobileMessageDB.MessageRecord} aMessageRecord
* The message record to store.
* @param {MobileMessageDB.TypedAddress[]} aThreadParticipants
* Participants of the thread of the message.
* @param {MobileMessageDB.DeletedInfo} aDeletedInfo
* An out parameter indicating which messages have been deleted due to
* the replacement.
*/
realSaveRecord: function(aTransaction, aMessageStore, aParticipantStore,
aThreadStore, aMessageRecord, aThreadParticipants,
aDeletedInfo) {
let self = this;
this.findThreadRecordByTypedAddresses(aThreadStore, aParticipantStore,
aThreadParticipants, true,
function(threadRecord,
participantIds) {
if (!participantIds) {
aTransaction.abort();
return;
}
let isOverriding = (aMessageRecord.id !== undefined);
if (!isOverriding) {
// |self.lastMessageId| is only updated in |txn.oncomplete|.
aMessageRecord.id = self.lastMessageId + 1;
}
let timestamp = aMessageRecord.timestamp;
let insertMessageRecord = function(threadId) {
// Setup threadId & threadIdIndex.
aMessageRecord.threadId = threadId;
aMessageRecord.threadIdIndex = [threadId, timestamp];
// Setup participantIdsIndex.
aMessageRecord.participantIdsIndex = [];
for (let id of participantIds) {
aMessageRecord.participantIdsIndex.push([id, timestamp]);
}
if (!isOverriding) {
// Really add to message store.
aMessageStore.put(aMessageRecord);
return;
}
// If we're going to override an old message, we need to update the
// info of the original thread containing the overridden message.
// To get the original thread ID and read status of the overridden
// message record, we need to retrieve it before overriding it.
aMessageStore.get(aMessageRecord.id).onsuccess = function(event) {
let oldMessageRecord = event.target.result;
aMessageStore.put(aMessageRecord);
if (oldMessageRecord) {
self.updateThreadByMessageChange(aMessageStore,
aThreadStore,
oldMessageRecord.threadId,
[aMessageRecord.id],
oldMessageRecord.read ? 0 : 1,
aDeletedInfo);
}
};
};
if (threadRecord) {
let needsUpdate = false;
if (threadRecord.lastTimestamp <= timestamp) {
let lastMessageSubject;
if (aMessageRecord.type == "mms") {
lastMessageSubject = aMessageRecord.headers.subject;
}
threadRecord.lastMessageSubject = lastMessageSubject || null;
threadRecord.lastTimestamp = timestamp;
threadRecord.body = aMessageRecord.body;
threadRecord.lastMessageId = aMessageRecord.id;
threadRecord.lastMessageType = aMessageRecord.type;
needsUpdate = true;
}
if (!aMessageRecord.read) {
threadRecord.unreadCount++;
needsUpdate = true;
}
if (needsUpdate) {
aThreadStore.put(threadRecord);
}
insertMessageRecord(threadRecord.id);
return;
}
let lastMessageSubject;
if (aMessageRecord.type == "mms") {
lastMessageSubject = aMessageRecord.headers.subject;
}
threadRecord = {
participantIds: participantIds,
participantAddresses: aThreadParticipants.map(function(typedAddress) {
return typedAddress.address;
}),
lastMessageId: aMessageRecord.id,
lastTimestamp: timestamp,
lastMessageSubject: lastMessageSubject || null,
body: aMessageRecord.body,
unreadCount: aMessageRecord.read ? 0 : 1,
lastMessageType: aMessageRecord.type,
};
aThreadStore.add(threadRecord).onsuccess = function(event) {
let threadId = event.target.result;
insertMessageRecord(threadId);
};
});
},
/**
* @typedef {Object} MobileMessageDB.MmsDeliveryInfoElement
* @property {string} receiver
* @property {string} deliveryStatus
* @property {number} deliveryTimestamp
* @property {string} readStatus
* @property {number} readTimestamp
*/
/**
* @callback MobileMessageDB.ForEachMatchedMmsDeliveryInfoCallback
* @param {MobileMessageDB.MmsDeliveryInfoElement} aElement
* An element of the MMS <code>deliverInfo</code> of a message record.
*/
/**
* Iterate all elements of <code>aDeliveryInfo</code>, check if the receiver
* address matches <code>aNeedle</code> and invoke <code>aCallback</code> on
* each matched element.
*
* @function MobileMessageDB.forEachMatchedMmsDeliveryInfo
* @param {MobileMessageDB.MmsDeliveryInfoElement[]} aDeliveryInfo
* The MMS <code>deliverInfo</code> of a message record.
* @param {string} aNeedle
* The receiver address to look up with.
* @param {MobileMessageDB.ForEachMatchedMmsDeliveryInfoCallback} aCallback
* The callback function to invoke on each match.
*/
forEachMatchedMmsDeliveryInfo: function(aDeliveryInfo, aNeedle, aCallback) {
let typedAddress = {
type: MMS.Address.resolveType(aNeedle),
address: aNeedle
};
let normalizedAddress, parsedAddress;
if (typedAddress.type === "PLMN") {
normalizedAddress = PhoneNumberUtils.normalize(aNeedle, false);
parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
}
for (let element of aDeliveryInfo) {
let typedStoredAddress = {
type: MMS.Address.resolveType(element.receiver),
address: element.receiver
};
if (typedAddress.type !== typedStoredAddress.type) {
// Not even my type. Skip.
continue;
}
if (typedAddress.address == typedStoredAddress.address) {
// Have a direct match.
aCallback(element);
continue;
}
if (typedAddress.type !== "PLMN") {
// Address type other than "PLMN" must have direct match. Or, skip.
continue;
}
// Both are of "PLMN" type.
let normalizedStoredAddress =
PhoneNumberUtils.normalize(element.receiver, false);
let parsedStoredAddress =
PhoneNumberUtils.parseWithMCC(normalizedStoredAddress, null);
if (this.matchPhoneNumbers(normalizedAddress, parsedAddress,
normalizedStoredAddress, parsedStoredAddress)) {
aCallback(element);
}
}
},
/**
* Find the message of a given message ID or envelope ID. Update its
* <code>delivery</code>, <code>deliveryStatus</code>, and
* <code>envelopeId</code> accordingly.
*
* @function MobileMessageDB.updateMessageDeliveryById
* @param {string} id
* If <code>type</code> is "messageId", it represents the message ID;
* If <code>type</code> is "envelopeId", it represents the envelope ID,
* which is the "x-mms-transaction-id" in the header of an MMS message.
* @param {string} type
* Either "messageId" or "envelopeId".
* @param {string} receiver
* The receiver address.
* @param {string} delivery
* If given, it will be used to update the <code>deliveryIndex</code>
* property of a stored message record.
* @param {string} deliveryStatus
* If given, it will be used to update the <code>deliveryStatus</code>
* property of a stored message record.
* @param {string} envelopeId
* If given, it will be used to update the <code>envelopeIdIndex</code>
* property of a stored message record.
* @param {Object} callback
* The object passed as <code>aCallback</code> to
* {@link MobileMessageDB.newTxnWithCallback}.
*/
updateMessageDeliveryById: function(id, type, receiver, delivery,
deliveryStatus, envelopeId, callback) {
if (DEBUG) {
debug("Setting message's delivery by " + type + " = "+ id
+ " receiver: " + receiver
+ " delivery: " + delivery
+ " deliveryStatus: " + deliveryStatus
+ " envelopeId: " + envelopeId);
}
let self = this;
this.newTxnWithCallback(callback, function(aCapture, aMessageStore) {
let getRequest;
if (type === "messageId") {
getRequest = aMessageStore.get(id);
} else if (type === "envelopeId") {
getRequest = aMessageStore.index("envelopeId").get(id);
}
getRequest.onsuccess = function(event) {
let messageRecord = event.target.result;
if (!messageRecord) {
if (DEBUG) debug("type = " + id + " is not found");
throw Cr.NS_ERROR_FAILURE;
}
let isRecordUpdated = false;
// Update |messageRecord.delivery| if needed.
if (delivery && messageRecord.delivery != delivery) {
messageRecord.delivery = delivery;
messageRecord.deliveryIndex = [delivery, messageRecord.timestamp];
isRecordUpdated = true;
// When updating an message's delivey state to 'sent', we also update
// its |sentTimestamp| by the current device timestamp to represent
// when the message is successfully sent.
if (delivery == DELIVERY_SENT) {
messageRecord.sentTimestamp = Date.now();
}
}
// Attempt to update |deliveryStatus| and |deliveryTimestamp| of:
// - the |messageRecord| for SMS.
// - the element(s) in |messageRecord.deliveryInfo| for MMS.
if (deliveryStatus) {
// A callback for updating the deliveyStatus/deliveryTimestamp of
// each target.
let updateFunc = function(aTarget) {
if (aTarget.deliveryStatus == deliveryStatus) {
return;
}
aTarget.deliveryStatus = deliveryStatus;
// Update |deliveryTimestamp| if it's successfully delivered.
if (deliveryStatus == DELIVERY_STATUS_SUCCESS) {
aTarget.deliveryTimestamp = Date.now();
}
isRecordUpdated = true;
};
if (messageRecord.type == "sms") {
updateFunc(messageRecord);
} else if (messageRecord.type == "mms") {
if (!receiver) {
// If the receiver is specified, we only need to update the
// element(s) in deliveryInfo that match the same receiver.
messageRecord.deliveryInfo.forEach(updateFunc);
} else {
self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
receiver, updateFunc);
}
}
}
// Update |messageRecord.envelopeIdIndex| if needed.
if (envelopeId) {
if (messageRecord.envelopeIdIndex != envelopeId) {
messageRecord.envelopeIdIndex = envelopeId;
isRecordUpdated = true;
}
}
aCapture.messageRecord = messageRecord;
if (!isRecordUpdated) {
if (DEBUG) {
debug("The values of delivery, deliveryStatus and envelopeId " +
"don't need to be updated.");
}
return;
}
if (DEBUG) {
debug("The delivery, deliveryStatus or envelopeId are updated.");
}
aMessageStore.put(messageRecord);
};
});
},
/**
* Map receivers of a MMS message record to the thread participant list if MMS
* grouping is enabled.
*
* @function MobileMessageDB.fillReceivedMmsThreadParticipants
* @param {MobileMessageDB.MessageRecord} aMessage
* The MMS message.
* @param {MobileMessageDB.TypedAddress[]} threadParticipants
* Participants to add.
*/
fillReceivedMmsThreadParticipants: function(aMessage, threadParticipants) {
let receivers = aMessage.receivers;
// If we don't want to disable the MMS grouping for receiving, we need to
// add the receivers (excluding the user's own number) to the participants
// for creating the thread. Some cases might be investigated as below:
//
// 1. receivers.length == 0
// This usually happens when receiving an MMS notification indication
// which doesn't carry any receivers.
// 2. receivers.length == 1
// If the receivers contain single phone number, we don't need to
// add it into participants because we know that number is our own.
// 3. receivers.length >= 2
// If the receivers contain multiple phone numbers, we need to add all
// of them but not the user's own number into participants.
if (DISABLE_MMS_GROUPING_FOR_RECEIVING || receivers.length < 2) {
return;
}
let isSuccess = false;
let slicedReceivers = receivers.slice();
if (aMessage.msisdn) {
let found = slicedReceivers.indexOf(aMessage.msisdn);
if (found !== -1) {
isSuccess = true;
slicedReceivers.splice(found, 1);
}
}
if (!isSuccess) {
// For some SIMs we cannot retrieve the valid MSISDN (i.e. the user's
// own phone number), so we cannot correctly exclude the user's own
// number from the receivers, thus wrongly building the thread index.
if (DEBUG) debug("Error! Cannot strip out user's own phone number!");
}
threadParticipants =
threadParticipants.concat(slicedReceivers).map(function(aAddress) {
return {
address: aAddress,
type: MMS.Address.resolveType(aAddress)
};
});
},
/**
* Update the thread when one or more messages are deleted / replaced.
*
* @function MobileMessageDB.updateThreadByMessageChange
* @param {IDBObjectStore} messageStore
* The object store for messages.
* @param {IDBObjectStore} threadStore
* The object store for threads.
* @param {number} threadId
* The thread ID.
* @param {number[]} removedMsgIds
* The IDs of removed messages.
* @param {number} ignoredUnreadCount
* Negative offset for <code>unreadCount</code>. For example, if the
* <code>unreadCount</code> was 5, given
* <code>ignoredUnreadCount</code> to 3 causes <code>unreadCount</code>
* becomes 2.
* @param {MobileMessageDB.DeletedInfo} deletedInfo
* An out parameter indicating if the thread is deleted after the
* operation.
*/
updateThreadByMessageChange: function(messageStore, threadStore, threadId,
removedMsgIds, ignoredUnreadCount, deletedInfo) {
let self = this;
threadStore.get(threadId).onsuccess = function(event) {
// This must exist.
let threadRecord = event.target.result;
if (DEBUG) debug("Updating thread record " + JSON.stringify(threadRecord));
if (ignoredUnreadCount > 0) {
if (DEBUG) {
debug("Updating unread count : " + threadRecord.unreadCount +
" -> " + (threadRecord.unreadCount - ignoredUnreadCount));
}
threadRecord.unreadCount -= ignoredUnreadCount;
}
if (removedMsgIds.indexOf(threadRecord.lastMessageId) >= 0) {
if (DEBUG) debug("MRU entry was deleted.");
// Check most recent sender/receiver.
let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
let request = messageStore.index("threadId")
.openCursor(range, PREV);
request.onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
if (DEBUG) {
debug("All messages were deleted. Delete this thread.");
}
threadStore.delete(threadId);
if (deletedInfo) {
deletedInfo.threadIds.push(threadId);
}
return;
}
let nextMsg = cursor.value;
let lastMessageSubject;
if (nextMsg.type == "mms") {
lastMessageSubject = nextMsg.headers.subject;
}
threadRecord.lastMessageSubject = lastMessageSubject || null;
threadRecord.lastMessageId = nextMsg.id;
threadRecord.lastTimestamp = nextMsg.timestamp;
threadRecord.body = nextMsg.body;
threadRecord.lastMessageType = nextMsg.type;
if (DEBUG) {
debug("Updating mru entry: " +
JSON.stringify(threadRecord));
}
threadStore.put(threadRecord);
};
} else if (ignoredUnreadCount > 0) {
if (DEBUG) debug("Shortcut, just update the unread count.");
threadStore.put(threadRecord);
}
};
},
/**
* Notify the observers that one or more messages are deleted.
*
* @function MobileMessageDB.notifyDeletedInfo
* @param {MobileMessageDB.DeletedInfo} info
* The IDs of deleted messages and threads.
*/
notifyDeletedInfo: function(info) {
if (!info ||
(info.messageIds.length === 0 && info.threadIds.length === 0)) {
return;
}
let deletedInfo =
gMobileMessageService
.createDeletedMessageInfo(info.messageIds,
info.messageIds.length,
info.threadIds,
info.threadIds.length);
Services.obs.notifyObservers(deletedInfo, "sms-deleted", null);
},
/**
* nsIGonkMobileMessageDatabaseService API
*/
/**
* Store an incoming message.
*
* @function MobileMessageDB.saveReceivedMessage
* @param {MobileMessageDB.MessageRecord} aMessage
* The message record to store.
* @param {Object} aCallback
* The object which includes a callback function.
* @param {MobileMessageDB.TransactionResultCallback} aCallback.notify
* The callback function to invoke when the transaction finishes.
*/
saveReceivedMessage: function(aMessage, aCallback) {
if ((aMessage.type != "sms" && aMessage.type != "mms") ||
(aMessage.type == "sms" && (aMessage.messageClass == undefined ||
aMessage.sender == undefined)) ||
(aMessage.type == "mms" && (aMessage.delivery == undefined ||
aMessage.deliveryStatus == undefined ||
!Array.isArray(aMessage.receivers))) ||
aMessage.timestamp == undefined) {
if (aCallback) {
aCallback.notify(Cr.NS_ERROR_FAILURE, null);
}
return;
}
let threadParticipants;
if (aMessage.type == "mms") {
if (aMessage.headers.from) {
aMessage.sender = aMessage.headers.from.address;
} else {
aMessage.sender = "";
}
threadParticipants = [{
address: aMessage.sender,
type: MMS.Address.resolveType(aMessage.sender)
}];
this.fillReceivedMmsThreadParticipants(aMessage, threadParticipants);
} else { // SMS
threadParticipants = [{
address: aMessage.sender,
type: MMS.Address.resolveType(aMessage.sender)
}];
}
let timestamp = aMessage.timestamp;
// Adding needed indexes and extra attributes for internal use.
// threadIdIndex & participantIdsIndex are filled in saveRecord().
aMessage.readIndex = [FILTER_READ_UNREAD, timestamp];
aMessage.read = FILTER_READ_UNREAD;
// If |sentTimestamp| is not specified, use 0 as default.
if (aMessage.sentTimestamp == undefined) {
aMessage.sentTimestamp = 0;
}
if (aMessage.type == "mms") {
aMessage.transactionIdIndex = aMessage.headers["x-mms-transaction-id"];
aMessage.isReadReportSent = false;
// As a receiver, we don't need to care about the delivery status of
// others, so we put a single element with self's phone number in the
// |deliveryInfo| array.
aMessage.deliveryInfo = [{
receiver: aMessage.phoneNumber,
deliveryStatus: aMessage.deliveryStatus,
deliveryTimestamp: 0,
readStatus: MMS.DOM_READ_STATUS_NOT_APPLICABLE,
readTimestamp: 0,
}];
delete aMessage.deliveryStatus;
}
if (aMessage.type == "sms") {
aMessage.delivery = DELIVERY_RECEIVED;
aMessage.deliveryStatus = DELIVERY_STATUS_SUCCESS;
aMessage.deliveryTimestamp = 0;
if (aMessage.pid == undefined) {
aMessage.pid = RIL.PDU_PID_DEFAULT;
}
}
aMessage.deliveryIndex = [aMessage.delivery, timestamp];
this.saveRecord(aMessage, threadParticipants, aCallback);
},
/**
* Store an outgoing message.
*
* @function MobileMessageDB.saveSendingMessage
* @param {MobileMessageDB.MessageRecord} aMessage
* The message record to store.
* @param {Object} aCallback
* The object which includes a callback function.
* @param {MobileMessageDB.TransactionResultCallback} aCallback.notify
* The callback function to invoke when the transaction finishes.
*/
saveSendingMessage: function(aMessage, aCallback) {
if ((aMessage.type != "sms" && aMessage.type != "mms") ||
(aMessage.type == "sms" && aMessage.receiver == undefined) ||
(aMessage.type == "mms" && !Array.isArray(aMessage.receivers)) ||
aMessage.deliveryStatusRequested == undefined ||
aMessage.timestamp == undefined) {
if (aCallback) {
aCallback.notify(Cr.NS_ERROR_FAILURE, null);
}
return;
}
// Set |aMessage.deliveryStatus|. Note that for MMS record
// it must be an array of strings; For SMS, it's a string.
let deliveryStatus = aMessage.deliveryStatusRequested
? DELIVERY_STATUS_PENDING
: DELIVERY_STATUS_NOT_APPLICABLE;
if (aMessage.type == "sms") {
aMessage.deliveryStatus = deliveryStatus;
// If |deliveryTimestamp| is not specified, use 0 as default.
if (aMessage.deliveryTimestamp == undefined) {
aMessage.deliveryTimestamp = 0;
}
} else if (aMessage.type == "mms") {
let receivers = aMessage.receivers;
let readStatus = aMessage.headers["x-mms-read-report"]
? MMS.DOM_READ_STATUS_PENDING
: MMS.DOM_READ_STATUS_NOT_APPLICABLE;
aMessage.deliveryInfo = [];
for (let i = 0; i < receivers.length; i++) {
aMessage.deliveryInfo.push({
receiver: receivers[i],
deliveryStatus: deliveryStatus,
deliveryTimestamp: 0,
readStatus: readStatus,
readTimestamp: 0,
});
}
}
let timestamp = aMessage.timestamp;
// Adding needed indexes and extra attributes for internal use.
// threadIdIndex & participantIdsIndex are filled in saveRecord().
aMessage.deliveryIndex = [DELIVERY_SENDING, timestamp];
aMessage.readIndex = [FILTER_READ_READ, timestamp];
aMessage.delivery = DELIVERY_SENDING;
aMessage.messageClass = MESSAGE_CLASS_NORMAL;
aMessage.read = FILTER_READ_READ;
// |sentTimestamp| is not available when the message is still sedning.
aMessage.sentTimestamp = 0;
let threadParticipants;
if (aMessage.type == "sms") {
threadParticipants = [{
address: aMessage.receiver,
type :MMS.Address.resolveType(aMessage.receiver)
}];
} else if (aMessage.type == "mms") {
threadParticipants = aMessage.headers.to;
}
this.saveRecord(aMessage, threadParticipants, aCallback);
},
/**
* Update the <code>delivery</code>, <code>deliveryStatus</code>, and
* <code>envelopeId</code> of a stored message record matching the given
* message ID.
*
* @function MobileMessageDB.setMessageDeliveryByMessageId
* @param {number} messageId
* The message ID.
* @param {string} receiver
* The receiver address.
* @param {string} delivery
* If given, it will be used to update the <code>deliveryIndex</code>
* property of a stored message record.
* @param {string} deliveryStatus
* If given, it will be used to update the <code>deliveryStatus</code>
* property of a stored message record.
* @param {string} envelopeId
* If given, it will be used to update the <code>envelopeIdIndex</code>
* property of a stored message record.
* @param {Object} callback
* The object passed as <code>aCallback</code> to
* {@link MobileMessageDB.newTxnWithCallback}.
*/
setMessageDeliveryByMessageId: function(messageId, receiver, delivery,
deliveryStatus, envelopeId, callback) {
this.updateMessageDeliveryById(messageId, "messageId",
receiver, delivery, deliveryStatus,
envelopeId, callback);
},
/**
* Update the <code>deliveryStatus</code> of the specified
* <code>aReceiver</code> within the <code>deliveryInfo</code> of the message
* record retrieved by the given <code>envelopeId</code>.
*
* @function MobileMessageDB.setMessageDeliveryStatusByEnvelopeId
* @param {string} aEnvelopeId
* The envelope ID, which is the "x-mms-transaction-id" in the header
* of an MMS message.
* @param {string} aReceiver
* The receiver address.
* @param {string} aDeliveryStatus
* If given, it will be used to update the <code>deliveryStatus</code>
* property of a stored message record.
* @param {Object} aCallback
* The object passed as <code>aCallback</code> to
* {@link MobileMessageDB.newTxnWithCallback}.
*/
setMessageDeliveryStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
aDeliveryStatus, aCallback) {
this.updateMessageDeliveryById(aEnvelopeId, "envelopeId", aReceiver, null,
aDeliveryStatus, null, aCallback);
},
/**
* Update the <code>readStatus</code> of the specified <code>aReceiver</code>
* within the <code>deliveryInfo</code> of the message record retrieved by the
* given <code>envelopeId</code>.
*
* @function MobileMessageDB.setMessageReadStatusByEnvelopeId
* @param {string} aEnvelopeId
* The envelope ID, which is the "x-mms-transaction-id" in the header
* of an MMS message.
* @param {string} aReceiver
* The receiver address.
* @param {string} aReadStatus
* The updated read status.
* @param {Object} aCallback
* The object passed as <code>aCallback</code> to
* {@link MobileMessageDB.newTxnWithCallback}.
*/
setMessageReadStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
aReadStatus, aCallback) {
if (DEBUG) {
debug("Setting message's read status by envelopeId = " + aEnvelopeId +
", receiver: " + aReceiver + ", readStatus: " + aReadStatus);
}
let self = this;
this.newTxnWithCallback(aCallback, function(aCapture, aMessageStore) {
let getRequest = aMessageStore.index("envelopeId").get(aEnvelopeId);
getRequest.onsuccess = function(event) {
let messageRecord = event.target.result;
if (!messageRecord) {
if (DEBUG) debug("envelopeId '" + aEnvelopeId + "' not found");
throw Cr.NS_ERROR_FAILURE;
}
aCapture.messageRecord = messageRecord;
let isRecordUpdated = false;
self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
aReceiver, function(aEntry) {
if (aEntry.readStatus == aReadStatus) {
return;
}
aEntry.readStatus = aReadStatus;
if (aReadStatus == MMS.DOM_READ_STATUS_SUCCESS) {
aEntry.readTimestamp = Date.now();
} else {
aEntry.readTimestamp = 0;
}
isRecordUpdated = true;
});
if (!isRecordUpdated) {
if (DEBUG) {
debug("The values of readStatus don't need to be updated.");
}
return;
}
if (DEBUG) {
debug("The readStatus is updated.");
}
aMessageStore.put(messageRecord);
};
});
},
/**
* @callback MobileMessageDB.GetMessageRecordCallback
* @param {number} aErrorCode
* The error code on failure, or <code>NS_OK</code> on success.
* @param {MobileMessageDB.MessageRecord} aMessageRecord
* The stored message record.
* @param {nsISmsMessage|nsIMmsMessage} aDomMessage
* The DOM message instance of the message record.
*/
/**
* Get the message record with given transaction ID.
*
* @function MobileMessageDB.getMessageRecordByTransactionId
* @param {string} aTransactionId
* The transaction ID.
* @param {Object} aCallback
* The object which includes a callback function.
* @param {MobileMessageDB.GetMessageRecordCallback} aCallback.notify
* The callback function to invoke when the request finishes.
*/
getMessageRecordByTransactionId: function(aTransactionId, aCallback) {
if (DEBUG) debug("Retrieving message with transaction ID " + aTransactionId);
let self = this;
this.newTxn(READ_ONLY, function(error, txn, messageStore) {
if (error) {
if (DEBUG) debug(error);
aCallback.notify(error, null, null);
return;
}
let request = messageStore.index("transactionId").get(aTransactionId);
txn.oncomplete = function(event) {
if (DEBUG) debug("Transaction " + txn + " completed.");
let messageRecord = request.result;
if (!messageRecord) {
if (DEBUG) debug("Transaction ID " + aTransactionId + " not found");
aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
return;
}
// In this case, we don't need a dom message. Just pass null to the
// third argument.
aCallback.notify(Cr.NS_OK, messageRecord, null);
};
txn.onerror = function(event) {
if (DEBUG) {
if (event.target) {
debug("Caught error on transaction", event.target.error.name);
}
}
aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
};
});
},
/**
* Get the message record with given message ID.
*
* @function MobileMessageDB.getMessageRecordById
* @param {string} aMessageID
* The message ID.
* @param {Object} aCallback
* The object which includes a callback function.
* @param {MobileMessageDB.GetMessageRecordCallback} aCallback.notify
* The callback function to invoke when the request finishes.
*/
getMessageRecordById: function(aMessageId, aCallback) {
if (DEBUG) debug("Retrieving message with ID " + aMessageId);
let self = this;
this.newTxn(READ_ONLY, function(error, txn, messageStore) {
if (error) {
if (DEBUG) debug(error);
aCallback.notify(error, null, null);
return;
}
let request = messageStore.mozGetAll(aMessageId);
txn.oncomplete = function() {
if (DEBUG) debug("Transaction " + txn + " completed.");
if (request.result.length > 1) {
if (DEBUG) debug("Got too many results for id " + aMessageId);
aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
return;
}
let messageRecord = request.result[0];
if (!messageRecord) {
if (DEBUG) debug("Message ID " + aMessageId + " not found");
aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
return;
}
if (messageRecord.id != aMessageId) {
if (DEBUG) {
debug("Requested message ID (" + aMessageId + ") is " +
"different from the one we got");
}
aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
return;
}
let domMessage = self.createDomMessageFromRecord(messageRecord);
aCallback.notify(Cr.NS_OK, messageRecord, domMessage);
};
txn.onerror = function(event) {
if (DEBUG) {
if (event.target) {
debug("Caught error on transaction", event.target.error.name);
}
}
aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
};
});
},
/**
* Helper to translate NS errors to the error causes defined in
* <code>nsIMobileMessageCallback</code>.
*
* @function MobileMessageDB.translateCrErrorToMessageCallbackError
* @param {number} aCrError
* The error code defined in <code>Components.result</code>
* @return {number}
* The error code defined in <code>nsIMobileMessageCallback</code>
*/
translateCrErrorToMessageCallbackError: function(aCrError) {
switch(aCrError) {
case Cr.NS_OK:
return Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR;
case Cr.NS_ERROR_UNEXPECTED:
return Ci.nsIMobileMessageCallback.UNKNOWN_ERROR;
case Cr.NS_ERROR_FILE_NOT_FOUND:
return Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR;
case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE:
return Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR;
default:
return Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
}
},
/**
* @callback MobileMessageDB.SaveSmsSegmentCallback
* @param {number} aErrorCode
* The error code on failure, or <code>NS_OK</code> on success.
* @param {MobileMessageDB.SmsSegmentRecord} aCompleteMessage
* The composing message. It becomes a complete message once the last
* segment is stored.
*/
/**
* Store a single SMS segment.
*
* @function MobileMessageDB.saveSmsSegment
* @param {MobileMessageDB.SmsSegmentRecord} aSmsSegment
* Single SMS segment.
* @param {Object} aCallback
* The object which includes a callback function.
* @param {MobileMessageDB.SaveSmsSegmentCallback} aCallback.notify
* The callback function to invoke when the request finishes.
*/
saveSmsSegment: function(aSmsSegment, aCallback) {
let completeMessage = null;
this.newTxn(READ_WRITE, function(error, txn, segmentStore) {
if (error) {
if (DEBUG) debug(error);
aCallback.notify(error, null);
return;
}
txn.oncomplete = function(event) {
if (DEBUG) debug("Transaction " + txn + " completed.");
if (completeMessage) {
// Rebuild full body
if (completeMessage.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
// Uint8Array doesn't have `concat`, so
// we have to merge all segments by hand.
let fullDataLen = 0;
for (let i = 1; i <= completeMessage.segmentMaxSeq; i++) {
fullDataLen += completeMessage.segments[i].length;
}
completeMessage.fullData = new Uint8Array(fullDataLen);
for (let d = 0, i = 1; i <= completeMessage.segmentMaxSeq; i++) {
let data = completeMessage.segments[i];
for (let j = 0; j < data.length; j++) {
completeMessage.fullData[d++] = data[j];
}
}
} else {
completeMessage.fullBody = completeMessage.segments.join("");
}
// Remove handy fields after completing the concatenation.
delete completeMessage.id;
delete completeMessage.hash;
delete completeMessage.receivedSegments;
delete completeMessage.segments;
}
aCallback.notify(Cr.NS_OK, completeMessage);
};
txn.onabort = function(event) {
if (DEBUG) debug("transaction abort due to " + event.target.error.name);
let error = (event.target.error.name === 'QuotaExceededError')
? Cr.NS_ERROR_FILE_NO_DEVICE_SPACE
: Cr.NS_ERROR_FAILURE;
aCallback.notify(error, null);
};
aSmsSegment.hash = aSmsSegment.sender + ":" +
aSmsSegment.segmentRef + ":" +
aSmsSegment.segmentMaxSeq + ":" +
aSmsSegment.iccId;
let seq = aSmsSegment.segmentSeq;
if (DEBUG) {
debug("Saving SMS Segment: " + aSmsSegment.hash + ", seq: " + seq);
}
let getRequest = segmentStore.index("hash").get(aSmsSegment.hash);
getRequest.onsuccess = function(event) {
let segmentRecord = event.target.result;
if (!segmentRecord) {
if (DEBUG) {
debug("Not found! Create a new record to store the segments.");
}
aSmsSegment.receivedSegments = 1;
aSmsSegment.segments = [];
if (aSmsSegment.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
aSmsSegment.segments[seq] = aSmsSegment.data;
} else {
aSmsSegment.segments[seq] = aSmsSegment.body;
}
segmentStore.add(aSmsSegment);
return;
}
if (DEBUG) {
debug("Append SMS Segment into existed message object: " + segmentRecord.id);
}
if (segmentRecord.segments[seq]) {
if (segmentRecord.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET &&
segmentRecord.encoding == aSmsSegment.encoding &&
segmentRecord.segments[seq].length == aSmsSegment.data.length &&
segmentRecord.segments[seq].every(function(aElement, aIndex) {
return aElement == aSmsSegment.data[aIndex];
})) {
if (DEBUG) {
debug("Got duplicated binary segment no: " + seq);
}
return;
}
if (segmentRecord.encoding != RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET &&
aSmsSegment.encoding != RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET &&
segmentRecord.segments[seq] == aSmsSegment.body) {
if (DEBUG) {
debug("Got duplicated text segment no: " + seq);
}
return;
}
// Update mandatory properties to ensure that the segments could be
// concatenated properly.
segmentRecord.encoding = aSmsSegment.encoding;
segmentRecord.originatorPort = aSmsSegment.originatorPort;
segmentRecord.destinationPort = aSmsSegment.destinationPort;
segmentRecord.teleservice = aSmsSegment.teleservice;
// Decrease the counter for this collided segment.
segmentRecord.receivedSegments--;
}
segmentRecord.timestamp = aSmsSegment.timestamp;
if (segmentRecord.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
segmentRecord.segments[seq] = aSmsSegment.data;
} else {
segmentRecord.segments[seq] = aSmsSegment.body;
}
segmentRecord.receivedSegments++;
// The port information is only available in 1st segment for CDMA WAP Push.
// If the segments of a WAP Push are not received in sequence
// (e.g., SMS with seq == 1 is not the 1st segment received by the device),
// we have to retrieve the port information from 1st segment and
// save it into the segmentRecord.
if (aSmsSegment.teleservice === RIL.PDU_CDMA_MSG_TELESERIVCIE_ID_WAP
&& seq === 1) {
if (aSmsSegment.originatorPort !== Ci.nsIGonkSmsService.SMS_APPLICATION_PORT_INVALID) {
segmentRecord.originatorPort = aSmsSegment.originatorPort;
}
if (aSmsSegment.destinationPort !== Ci.nsIGonkSmsService.SMS_APPLICATION_PORT_INVALID) {
segmentRecord.destinationPort = aSmsSegment.destinationPort;
}
}
if (segmentRecord.receivedSegments < segmentRecord.segmentMaxSeq) {
if (DEBUG) debug("Message is incomplete.");
segmentStore.put(segmentRecord);
return;
}
completeMessage = segmentRecord;
// Delete Record in DB
segmentStore.delete(segmentRecord.id);
};
}, [SMS_SEGMENT_STORE_NAME]);
},
/**
* nsIMobileMessageDatabaseService API
*/
/**
* Get the message record with given message ID.
*
* @function MobileMessageDB.getMessage
* @param {string} aMessageID
* The message ID.
* @param {nsIMobileMessageCallback} aRequest
* The callback object.
*/
getMessage: function(aMessageId, aRequest) {
if (DEBUG) debug("Retrieving message with ID " + aMessageId);
let self = this;
let notifyCallback = {
notify: function(aRv, aMessageRecord, aDomMessage) {
if (Cr.NS_OK == aRv) {
aRequest.notifyMessageGot(aDomMessage);
return;
}
aRequest.notifyGetMessageFailed(
self.translateCrErrorToMessageCallbackError(aRv), null);
}
};
this.getMessageRecordById(aMessageId, notifyCallback);
},
/**
* Delete the message record with given message IDs.
*
* @function MobileMessageDB.deleteMessage
* @param {number[]} messageIds
* The IDs of messages to delete.
* @param {number} length
* The length of the <code>messageIds</code> array.
* @param {nsIMobileMessageCallback} aRequest
* The callback object.
*/
deleteMessage: function(messageIds, length, aRequest) {
if (DEBUG) debug("deleteMessage: message ids " + JSON.stringify(messageIds));
let deleted = [];
let self = this;
this.newTxn(READ_WRITE, function(error, txn, stores) {
if (error) {
if (DEBUG) debug("deleteMessage: failed to open transaction");
aRequest.notifyDeleteMessageFailed(
self.translateCrErrorToMessageCallbackError(error));
return;
}
let deletedInfo = { messageIds: [], threadIds: [] };
txn.onabort = function(event) {
if (DEBUG) debug("transaction abort due to " + event.target.error.name);
let error = (event.target.error.name === 'QuotaExceededError')
? Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR
: Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
aRequest.notifyDeleteMessageFailed(error);
};
const messageStore = stores[0];
const threadStore = stores[1];
txn.oncomplete = function(event) {
if (DEBUG) debug("Transaction " + txn + " completed.");
aRequest.notifyMessageDeleted(deleted, length);
self.notifyDeletedInfo(deletedInfo);
};
let threadsToUpdate = {};
let numOfMessagesToDelete = length;
let updateThreadInfo = function() {
for (let threadId in threadsToUpdate) {
let threadInfo = threadsToUpdate[threadId];
self.updateThreadByMessageChange(messageStore,
threadStore,
threadInfo.threadId,
threadInfo.removedMsgIds,
threadInfo.ignoredUnreadCount,
deletedInfo);
}
};
for (let i = 0; i < length; i++) {
let messageId = messageIds[i];
deleted[i] = false;
messageStore.get(messageId).onsuccess = function(messageIndex, event) {
let messageRecord = event.target.result;
let messageId = messageIds[messageIndex];
if (messageRecord) {
if (DEBUG) debug("Deleting message id " + messageId);
// First actually delete the message.
messageStore.delete(messageId).onsuccess = function(event) {
if (DEBUG) debug("Message id " + messageId + " deleted");
numOfMessagesToDelete--;
deleted[messageIndex] = true;
deletedInfo.messageIds.push(messageId);
// Cache thread info to be updated.
let threadId = messageRecord.threadId;
if (!threadsToUpdate[threadId]) {
threadsToUpdate[threadId] = {
threadId: threadId,
removedMsgIds: [messageId],
ignoredUnreadCount: (!messageRecord.read) ? 1 : 0
};
} else {
let threadInfo = threadsToUpdate[threadId];
threadInfo.removedMsgIds.push(messageId);
if (!messageRecord.read) {
threadInfo.ignoredUnreadCount++;
}
}
// After all messsages are deleted, update unread count and most
// recent message of related threads at once.
if (!numOfMessagesToDelete) {
updateThreadInfo();
}
};
} else {
if (DEBUG) debug("Message id " + messageId + " does not exist");
numOfMessagesToDelete--;
if (!numOfMessagesToDelete) {
updateThreadInfo();
}
}
}.bind(null, i);
}
}, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
},
/**
* Create a cursor to iterate on stored message records.
*
* @function MobileMessageDB.createMessageCursor
* @param {boolean} aHasStartDate
* <code>true</code> to query only messages starts with
* <code>aStartDate</code>
* @param {number} aStartDate
* The timestamp of start date in milliseconds.
* @param {boolean} aHasEndDate
* <code>true</code> to query only messages before the
* <code>aEndDate</code>
* @param {number} aEndDate
* The timestamp of end date in milliseconds.
* @param {string[]} aNumbers
* If not <code>null</code>, query only messages with sender or
* receiver who's number matches one of the numbers listed in the array.
* @param {number} aNumbersCount
* The length of <code>aNumbers</code> array.
* @param {string} aDelivery
* If not <code>null</code>, query only messages matching the delivery
* value.
* @param {boolean} aHasRead
* <code>true</code> to query only messages match the read value
* specified by <code>aRead</code>
* @param {boolean} aRead
* Specify the <code>read</code> query condition.
* @param {number} aThreadId
* If not <code>null</code>, query only messages in the given thread.
* @param {boolean} aReverse
* <code>true</code> to reverse the order.
* @param {nsIMobileMessageCursorCallback} aCallback
* The callback object used by GetMessagesCursor
* @return {GetMessagesCursor}
* The cursor to iterate on messages.
*/
createMessageCursor: function(aHasStartDate, aStartDate, aHasEndDate,
aEndDate, aNumbers, aNumbersCount, aDelivery,
aHasRead, aRead, aHasThreadId, aThreadId,
aReverse, aCallback) {
if (DEBUG) {
debug("Creating a message cursor. Filters:" +
" startDate: " + (aHasStartDate ? aStartDate : "(null)") +
" endDate: " + (aHasEndDate ? aEndDate : "(null)") +
" delivery: " + aDelivery +
" numbers: " + (aNumbersCount ? aNumbers : "(null)") +
" read: " + (aHasRead ? aRead : "(null)") +
" threadId: " + (aHasThreadId ? aThreadId : "(null)") +
" reverse: " + aReverse);
}
let filter = {};
if (aHasStartDate) {
filter.startDate = aStartDate;
}
if (aHasEndDate) {
filter.endDate = aEndDate;
}
if (aNumbersCount) {
filter.numbers = aNumbers.slice();
}
if (aDelivery !== null) {
filter.delivery = aDelivery;
}
if (aHasRead) {
filter.read = aRead;
}
if (aHasThreadId) {
filter.threadId = aThreadId;
}
let cursor = new GetMessagesCursor(this, aCallback);
let self = this;
self.newTxn(READ_ONLY, function(error, txn, stores) {
let collector = cursor.collector.idCollector;
let collect = collector.collect.bind(collector);
FilterSearcherHelper.transact(self, txn, error, filter, aReverse, collect);
}, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME]);
return cursor;
},
/**
* Change the <code>read</code> property of a stored message record.
*
* @function MobileMessageDB.markMessageRead
* @param {number} messageId
* The message ID.
* @param {boolean} value
* The updated <code>read</code> value.
* @param {boolean} aSendReadReport
* <code>true</code> to reply the read report of an incoming MMS
* message whose <code>isReadReportSent</code> is 'false'.
* Note: <code>isReadReportSent</code> will be set to 'true' no
* matter aSendReadReport is true or not when a message was marked
* from UNREAD to READ. See bug 1180470 for the new UX policy.
* @param {nsIMobileMessageCallback} aRequest
* The callback object.
*/
markMessageRead: function(messageId, value, aSendReadReport, aRequest) {
if (DEBUG) debug("Setting message " + messageId + " read to " + value);
let self = this;
this.newTxn(READ_WRITE, function(error, txn, stores) {
if (error) {
if (DEBUG) debug(error);
aRequest.notifyMarkMessageReadFailed(
self.translateCrErrorToMessageCallbackError(error));
return;
}
txn.onabort = function(event) {
if (DEBUG) debug("transaction abort due to " + event.target.error.name);
let error = (event.target.error.name === 'QuotaExceededError')
? Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR
: Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
aRequest.notifyMarkMessageReadFailed(error);
};
let messageStore = stores[0];
let threadStore = stores[1];
messageStore.get(messageId).onsuccess = function(event) {
let messageRecord = event.target.result;
if (!messageRecord) {
if (DEBUG) debug("Message ID " + messageId + " not found");
aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR);
return;
}
if (messageRecord.id != messageId) {
if (DEBUG) {
debug("Retrieve message ID (" + messageId + ") is " +
"different from the one we got");
}
aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.UNKNOWN_ERROR);
return;
}
// If the value to be set is the same as the current message `read`
// value, we just notify successfully.
if (messageRecord.read == value) {
if (DEBUG) debug("The value of messageRecord.read is already " + value);
aRequest.notifyMessageMarkedRead(messageRecord.read);
return;
}
messageRecord.read = value ? FILTER_READ_READ : FILTER_READ_UNREAD;
messageRecord.readIndex = [messageRecord.read, messageRecord.timestamp];
let readReportMessageId, readReportTo;
if (messageRecord.type == "mms" &&
messageRecord.delivery == DELIVERY_RECEIVED &&
messageRecord.read == FILTER_READ_READ &&
messageRecord.headers["x-mms-read-report"] &&
!messageRecord.isReadReportSent) {
messageRecord.isReadReportSent = true;
if (aSendReadReport) {
let from = messageRecord.headers["from"];
readReportTo = from && from.address;
readReportMessageId = messageRecord.headers["message-id"];
}
}
if (DEBUG) debug("Message.read set to: " + value);
messageStore.put(messageRecord).onsuccess = function(event) {
if (DEBUG) {
debug("Update successfully completed. Message: " +
JSON.stringify(event.target.result));
}
// Now update the unread count.
let threadId = messageRecord.threadId;
threadStore.get(threadId).onsuccess = function(event) {
let threadRecord = event.target.result;
threadRecord.unreadCount += value ? -1 : 1;
if (DEBUG) {
debug("Updating unreadCount for thread id " + threadId + ": " +
(value ?
threadRecord.unreadCount + 1 :
threadRecord.unreadCount - 1) +
" -> " + threadRecord.unreadCount);
}
threadStore.put(threadRecord).onsuccess = function(event) {
if(readReportMessageId && readReportTo) {
gMMSService.sendReadReport(readReportMessageId,
readReportTo,
messageRecord.iccId);
}
aRequest.notifyMessageMarkedRead(messageRecord.read);
};
};
};
};
}, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
},
/**
* Create a cursor to iterate on stored threads.
*
* @function MobileMessageDB.createThreadCursor
* @param {nsIMobileMessageCursorCallback} callback
* The callback object used by GetMessagesCursor
* @return {GetThreadsCursor}
* The cursor to iterate on threads.
*/
createThreadCursor: function(callback) {
if (DEBUG) debug("Getting thread list");
let cursor = new GetThreadsCursor(this, callback);
this.newTxn(READ_ONLY, function(error, txn, threadStore) {
let collector = cursor.collector.idCollector;
if (error) {
collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
return;
}
txn.onerror = function(event) {
if (DEBUG) debug("Caught error on transaction ", event.target.error.name);
collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
};
let request = threadStore.index("lastTimestamp").openKeyCursor(null, PREV);
request.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
if (collector.collect(txn, cursor.primaryKey, cursor.key)) {
cursor.continue();
}
} else {
collector.collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
}
};
}, [THREAD_STORE_NAME]);
return cursor;
}
};
var FilterSearcherHelper = {
/**
* @param index
* The name of a message store index to filter on.
* @param range
* A IDBKeyRange.
* @param direction
* NEXT or PREV.
* @param txn
* Ongoing IDBTransaction context object.
* @param collect
* Result colletor function. It takes three parameters -- txn, message
* id, and message timestamp.
*/
filterIndex: function(index, range, direction, txn, collect) {
let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
let request = messageStore.index(index).openKeyCursor(range, direction);
request.onsuccess = function(event) {
let cursor = event.target.result;
// Once the cursor has retrieved all keys that matches its key range,
// the filter search is done.
if (cursor) {
let timestamp = Array.isArray(cursor.key) ? cursor.key[1] : cursor.key;
if (collect(txn, cursor.primaryKey, timestamp)) {
cursor.continue();
}
} else {
collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
}
};
request.onerror = function(event) {
if (DEBUG && event) debug("IDBRequest error " + event.target.error.name);
collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
};
},
/**
* Explicitly filter message on the timestamp index.
*
* @param startDate
* Timestamp of the starting date.
* @param endDate
* Timestamp of the ending date.
* @param direction
* NEXT or PREV.
* @param txn
* Ongoing IDBTransaction context object.
* @param collect
* Result colletor function. It takes three parameters -- txn, message
* id, and message timestamp.
*/
filterTimestamp: function(startDate, endDate, direction, txn, collect) {
let range = null;
if (startDate != null && endDate != null) {
range = IDBKeyRange.bound(startDate, endDate);
} else if (startDate != null) {
range = IDBKeyRange.lowerBound(startDate);
} else if (endDate != null) {
range = IDBKeyRange.upperBound(endDate);
}
this.filterIndex("timestamp", range, direction, txn, collect);
},
/**
* Initiate a filtering transaction.
*
* @param mmdb
* A MobileMessageDB.
* @param txn
* Ongoing IDBTransaction context object.
* @param error
* Previous error while creating the transaction.
* @param filter
* A MobileMessageFilter dictionary.
* @param reverse
* A boolean value indicating whether we should filter message in
* reversed order.
* @param collect
* Result collector function. It takes three parameters -- txn, message
* id, and message timestamp.
*/
transact: function(mmdb, txn, error, filter, reverse, collect) {
if (error) {
// TODO look at event.target.error.name, pick appropriate error constant.
if (DEBUG) debug("IDBRequest error " + event.target.error.name);
collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
return;
}
let direction = reverse ? PREV : NEXT;
// We support filtering by date range only (see `else` block below) or by
// number/delivery status/read status with an optional date range.
if (filter.delivery == null &&
filter.numbers == null &&
filter.read == null &&
filter.threadId == null) {
// Filtering by date range only.
if (DEBUG) {
debug("filter.timestamp " + filter.startDate + ", " + filter.endDate);
}
this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
collect);
return;
}
// Numeric 0 is smaller than any time stamp, and empty string is larger
// than all numeric values.
let startDate = 0, endDate = "";
if (filter.startDate != null) {
startDate = filter.startDate;
}
if (filter.endDate != null) {
endDate = filter.endDate;
}
let single, intersectionCollector;
{
let num = 0;
if (filter.delivery) num++;
if (filter.numbers) num++;
if (filter.read != undefined) num++;
if (filter.threadId != undefined) num++;
single = (num == 1);
}
if (!single) {
intersectionCollector = new IntersectionResultsCollector(collect, reverse);
}
// Retrieve the keys from the 'delivery' index that matches the value of
// filter.delivery.
if (filter.delivery) {
if (DEBUG) debug("filter.delivery " + filter.delivery);
let delivery = filter.delivery;
let range = IDBKeyRange.bound([delivery, startDate], [delivery, endDate]);
this.filterIndex("delivery", range, direction, txn,
single ? collect : intersectionCollector.newContext());
}
// Retrieve the keys from the 'read' index that matches the value of
// filter.read.
if (filter.read != undefined) {
if (DEBUG) debug("filter.read " + filter.read);
let read = filter.read ? FILTER_READ_READ : FILTER_READ_UNREAD;
let range = IDBKeyRange.bound([read, startDate], [read, endDate]);
this.filterIndex("read", range, direction, txn,
single ? collect : intersectionCollector.newContext());
}
// Retrieve the keys from the 'threadId' index that matches the value of
// filter.threadId.
if (filter.threadId != undefined) {
if (DEBUG) debug("filter.threadId " + filter.threadId);
let threadId = filter.threadId;
let range = IDBKeyRange.bound([threadId, startDate], [threadId, endDate]);
this.filterIndex("threadId", range, direction, txn,
single ? collect : intersectionCollector.newContext());
}
// Retrieve the keys from the 'sender' and 'receiver' indexes that
// match the values of filter.numbers
if (filter.numbers) {
if (DEBUG) debug("filter.numbers " + filter.numbers.join(", "));
if (!single) {
collect = intersectionCollector.newContext();
}
let participantStore = txn.objectStore(PARTICIPANT_STORE_NAME);
let typedAddresses = filter.numbers.map(function(number) {
return {
address: number,
type: MMS.Address.resolveType(number)
};
});
mmdb.findParticipantIdsByTypedAddresses(participantStore, typedAddresses,
false, true,
(function(participantIds) {
if (!participantIds || !participantIds.length) {
// Oops! No such participant at all.
collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
return;
}
if (participantIds.length == 1) {
let id = participantIds[0];
let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
this.filterIndex("participantIds", range, direction, txn, collect);
return;
}
let unionCollector = new UnionResultsCollector(collect);
this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
unionCollector.newTimestampContext());
for (let i = 0; i < participantIds.length; i++) {
let id = participantIds[i];
let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
this.filterIndex("participantIds", range, direction, txn,
unionCollector.newContext());
}
}).bind(this));
}
}
};
/**
* Collector class for read-ahead result objects. Mmdb may now try to fetch
* message/thread records before it's requested explicitly.
*
* The read ahead behavior can be controlled by an integer mozSettings entry
* "ril.sms.maxReadAheadEntries" as well as an integer holding preference
* "dom.sms.maxReadAheadEntries". The meanings are:
*
* positive: finite read-ahead entries,
* 0: don't read ahead unless explicitly requested, (default)
* negative: read ahead all IDs if possible.
*
* The order of ID filtering objects are now:
*
* [UnionResultsCollector]
* +-> [IntersectionResultsCollector]
* +-> IDsCollector
* +-> ResultsCollector
*
* ResultsCollector has basically similar behaviour with IDsCollector. When
* RC::squeeze() is called, either RC::drip() is called instantly if we have
* already fetched results available, or the request is kept and IC::squeeze()
* is called.
*
* When RC::collect is called by IC::drip, it proceeds to fetch the
* corresponding record given that collected ID is neither an error nor an end
* mark. After the message/thread record being fetched, ResultsCollector::drip
* is called if we have pending request. Anyway, RC::maybeSqueezeIdCollector is
* called to determine whether we need to call IC::squeeze again.
*
* RC::squeeze is called when nsICursorContinueCallback::handleContinue() is
* called. ResultsCollector::drip will call to
* nsIMobileMessageCursorCallback::notifyFoo.
*
* In summary, the major call paths are:
*
* RC::squeeze
* o-> RC::drip
* +-> RC::notifyCallback
* +-> nsIMobileMessageCursorCallback::notifyFoo
* +-> RC::maybeSqueezeIdCollector
* o-> IC::squeeze
* o-> IC::drip
* +-> RC::collect
* o-> RC::readAhead
* +-> RC::notifyResult
* o-> RC::drip ...
* +-> RC::maybeSqueezeIdCollector ...
* o-> RC::notifyResult ...
*/
function ResultsCollector(readAheadFunc) {
this.idCollector = new IDsCollector();
this.results = [];
this.readAhead = readAheadFunc;
this.maxReadAhead = DEFAULT_READ_AHEAD_ENTRIES;
try {
// positive: finite read-ahead entries,
// 0: don't read ahead unless explicitly requested,
// negative: read ahead all IDs if possible.
this.maxReadAhead =
Services.prefs.getIntPref("dom.sms.maxReadAheadEntries");
} catch (e) {}
}
ResultsCollector.prototype = {
/**
* Underlying ID collector object.
*/
idCollector: null,
/**
* An array keeping fetched result objects. Replaced by a new empty array
* every time when |this.drip| is called.
*/
results: null,
/**
* A function that takes (<txn>, <id>, <collector>). It fetches the object
* specified by <id> and notify <collector> with that by calling
* |<collector>.notifyResult()|. If <txn> is null, this function should
* create a new read-only transaction itself. The returned result object may
* be null to indicate an error during the fetch process.
*/
readAhead: null,
/**
* A boolean value inidicating a readAhead call is ongoing. Set before calling
* |this.readAhead| and reset in |this.notifyResult|.
*/
readingAhead: false,
/**
* A numeric value read from preference "dom.sms.maxReadAheadEntries".
*/
maxReadAhead: 0,
/**
* An active IDBTransaction object to be reused.
*/
activeTxn: null,
/**
* A nsIMobileMessageCursorCallback.
*/
requestWaiting: null,
/**
* A boolean value indicating either a COLLECT_ID_END or COLLECT_ID_ERROR has
* been received.
*/
done: false,
/**
* When |this.done|, it's either COLLECT_ID_END or COLLECT_ID_ERROR.
*/
lastId: null,
/**
* Receive collected id from IDsCollector and fetch the correspond result
* object if necessary.
*
* @param txn
* An IDBTransaction object. Null if there is no active transaction in
* IDsCollector. That is, the ID collecting transaction is completed.
* @param id
* A positive numeric id, COLLECT_ID_END(0), or COLLECT_ID_ERROR(-1).
*/
collect: function(txn, id) {
if (this.done) {
// If this callector has been terminated because of previous errors in
// |this.readAhead|, ignore any further IDs from IDsCollector.
return;
}
if (DEBUG) debug("ResultsCollector::collect ID = " + id);
// Reuse the active transaction cached if IDsCollector has no active
// transaction.
txn = txn || this.activeTxn;
if (id > 0) {
this.readingAhead = true;
this.readAhead(txn, id, this);
} else {
this.notifyResult(txn, id, null);
}
},
/**
* Callback function for |this.readAhead|.
*
* This function pushes result object to |this.results| or updates
* |this.done|, |this.lastId| if an end mark or an error is found. Since we
* have already a valid result entry, check |this.requestWaiting| and deal
* with it. At last, call to |this.maybeSqueezeIdCollector| to ask more id
* again if necessary.
*
* @param txn
* An IDBTransaction object. Null if caller has no active transaction.
* @param id
* A positive numeric id, COLLECT_ID_END(0), or COLLECT_ID_ERROR(-1).
* @param result
* An object associated with id. Null if |this.readAhead| failed.
*/
notifyResult: function(txn, id, result) {
if (DEBUG) debug("notifyResult(txn, " + id + ", <result>)");
this.readingAhead = false;
if (id > 0) {
if (result != null) {
this.results.push(result);
} else {
id = COLLECT_ID_ERROR;
}
}
if (id <= 0) {
this.lastId = id;
this.done = true;
}
if (!this.requestWaiting) {
if (DEBUG) debug("notifyResult: cursor.continue() not called yet");
} else {
let callback = this.requestWaiting;
this.requestWaiting = null;
this.drip(callback);
}
this.maybeSqueezeIdCollector(txn);
},
/**
* Request for one more ID if necessary.
*
* @param txn
* An IDBTransaction object. Null if caller has no active transaction.
*/
maybeSqueezeIdCollector: function(txn) {
if (this.done || // Nothing to be read.
this.readingAhead || // Already in progress.
this.idCollector.requestWaiting) { // Already requested.
return;
}
let max = this.maxReadAhead;
if (!max && this.requestWaiting) {
// If |this.requestWaiting| is set, try to read ahead at least once.
max = 1;
}
if (max >= 0 && this.results.length >= max) {
// More-equal than <max> entries has been read. Stop.
if (DEBUG) debug("maybeSqueezeIdCollector: max " + max + " entries read. Stop.");
return;
}
// A hack to pass current txn to |this.collect| when it's called directly by
// |IDsCollector.squeeze|.
this.activeTxn = txn;
this.idCollector.squeeze(this.collect.bind(this));
this.activeTxn = null;
},
/**
* Request to pass available results or wait.
*
* @param callback
* A nsIMobileMessageCursorCallback.
*/
squeeze: function(callback) {
if (this.requestWaiting) {
throw new Error("Already waiting for another request!");
}
if (this.results.length || this.done) {
// If |this.results.length| is non-zero, we have already some results to
// pass. Otherwise, if |this.done| evaluates to true, we have also a
// confirmed result to pass.
this.drip(callback);
} else {
this.requestWaiting = callback;
}
// If we called |this.drip| in the last step, the fetched results have been
// consumed and we should ask some more for read-ahead now.
//
// Otherwise, kick start read-ahead again because it may be stopped
// previously because of |this.maxReadAhead| had been reached.
this.maybeSqueezeIdCollector(null);
},
/**
* Consume fetched resutls.
*
* @param callback
* A nsIMobileMessageCursorCallback.
*/
drip: function(callback) {
let results = this.results;
this.results = [];
let func = this.notifyCallback.bind(this, callback, results, this.lastId);
Services.tm.currentThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
},
/**
* Notify a nsIMobileMessageCursorCallback.
*
* @param callback
* A nsIMobileMessageCursorCallback.
* @param results
* An array of result objects.
* @param lastId
* Since we only call |this.drip| when either there are results
* available or the read-ahead has done, so lastId here will be
* COLLECT_ID_END or COLLECT_ID_ERROR when results is empty and null
* otherwise.
*/
notifyCallback: function(callback, results, lastId) {
if (DEBUG) {
debug("notifyCallback(results[" + results.length + "], " + lastId + ")");
}
if (results.length) {
callback.notifyCursorResult(results, results.length);
} else if (lastId == COLLECT_ID_END) {
callback.notifyCursorDone();
} else {
callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
}
}
};
function IDsCollector() {
this.results = [];
this.done = false;
}
IDsCollector.prototype = {
results: null,
requestWaiting: null,
done: null,
/**
* Queue up passed id, reply if necessary.
*
* @param txn
* Ongoing IDBTransaction context object.
* @param id
* COLLECT_ID_END(0) for no more results, COLLECT_ID_ERROR(-1) for
* errors and valid otherwise.
* @param timestamp
* We assume this function is always called in timestamp order. So
* this parameter is actually unused.
*
* @return true if expects more. false otherwise.
*/
collect: function(txn, id, timestamp) {
if (this.done) {
return false;
}
if (DEBUG) debug("IDsCollector::collect ID = " + id);
// Queue up any id.
this.results.push(id);
if (id <= 0) {
// No more processing on '0' or negative values passed.
this.done = true;
}
if (!this.requestWaiting) {
if (DEBUG) debug("IDsCollector::squeeze() not called yet");
return !this.done;
}
// We assume there is only one request waiting throughout the message list
// retrieving process. So we don't bother continuing to process further
// waiting requests here. This assumption comes from DOMCursor::Continue()
// implementation.
let callback = this.requestWaiting;
this.requestWaiting = null;
this.drip(txn, callback);
return !this.done;
},
/**
* Callback right away with the first queued result entry if the filtering is
* done. Or queue up the request and callback when a new entry is available.
*
* @param callback
* A callback function that accepts a numeric id.
*/
squeeze: function(callback) {
if (this.requestWaiting) {
throw new Error("Already waiting for another request!");
}
if (!this.done) {
// Database transaction ongoing, let it reply for us so that we won't get
// blocked by the existing transaction.
this.requestWaiting = callback;
return;
}
this.drip(null, callback);
},
/**
* @param txn
* Ongoing IDBTransaction context object or null.
* @param callback
* A callback function that accepts a numeric id.
*/
drip: function(txn, callback) {
let firstId = this.results[0];
if (firstId > 0) {
this.results.shift();
}
callback(txn, firstId);
}
};
function IntersectionResultsCollector(collect, reverse) {
this.cascadedCollect = collect;
this.reverse = reverse;
this.contexts = [];
}
IntersectionResultsCollector.prototype = {
cascadedCollect: null,
reverse: false,
contexts: null,
/**
* Queue up {id, timestamp} pairs, find out intersections and report to
* |cascadedCollect|. Return true if it is still possible to have another match.
*/
collect: function(contextIndex, txn, id, timestamp) {
if (DEBUG) {
debug("IntersectionResultsCollector: "
+ contextIndex + ", " + id + ", " + timestamp);
}
let contexts = this.contexts;
let context = contexts[contextIndex];
if (id < 0) {
// Act as no more matched records.
id = 0;
}
if (!id) {
context.done = true;
if (!context.results.length) {
// Already empty, can't have further intersection results.
return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
}
for (let i = 0; i < contexts.length; i++) {
if (!contexts[i].done) {
// Don't call |this.cascadedCollect| because |context.results| might not
// be empty, so other contexts might still have a chance here.
return false;
}
}
// It was the last processing context and is no more processing.
return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
}
// Search id in other existing results. If no other results has it,
// and A) the last timestamp is smaller-equal to current timestamp,
// we wait for further results; either B) record timestamp is larger
// then current timestamp or C) no more processing for a filter, then we
// drop this id because there can't be a match anymore.
for (let i = 0; i < contexts.length; i++) {
if (i == contextIndex) {
continue;
}
let ctx = contexts[i];
let results = ctx.results;
let found = false;
for (let j = 0; j < results.length; j++) {
let result = results[j];
if (result.id == id) {
found = true;
break;
}
if ((!this.reverse && (result.timestamp > timestamp)) ||
(this.reverse && (result.timestamp < timestamp))) {
// B) Cannot find a match anymore. Drop.
return true;
}
}
if (!found) {
if (ctx.done) {
// C) Cannot find a match anymore. Drop.
if (results.length) {
let lastResult = results[results.length - 1];
if ((!this.reverse && (lastResult.timestamp >= timestamp)) ||
(this.reverse && (lastResult.timestamp <= timestamp))) {
// Still have a chance to get another match. Return true.
return true;
}
}
// Impossible to find another match because all results in ctx have
// timestamps smaller than timestamp.
context.done = true;
return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
}
// A) Pending.
context.results.push({
id: id,
timestamp: timestamp
});
return true;
}
}
// Now id is found in all other results. Report it.
return this.cascadedCollect(txn, id, timestamp);
},
newContext: function() {
let contextIndex = this.contexts.length;
this.contexts.push({
results: [],
done: false
});
return this.collect.bind(this, contextIndex);
}
};
function UnionResultsCollector(collect) {
this.cascadedCollect = collect;
this.contexts = [{
// Timestamp.
processing: 1,
results: []
}, {
processing: 0,
results: []
}];
}
UnionResultsCollector.prototype = {
cascadedCollect: null,
contexts: null,
collect: function(contextIndex, txn, id, timestamp) {
if (DEBUG) {
debug("UnionResultsCollector: "
+ contextIndex + ", " + id + ", " + timestamp);
}
let contexts = this.contexts;
let context = contexts[contextIndex];
if (id < 0) {
// Act as no more matched records.
id = 0;
}
if (id) {
if (!contextIndex) {
// Timestamp.
context.results.push({
id: id,
timestamp: timestamp
});
} else {
context.results.push(id);
}
return true;
}
context.processing -= 1;
if (contexts[0].processing || contexts[1].processing) {
// At least one queue is still processing, but we got here because
// current cursor gives 0 as id meaning no more messages are
// available. Return false here to stop further cursor.continue() calls.
return false;
}
let tres = contexts[0].results;
let qres = contexts[1].results;
tres = tres.filter(function(element) {
return qres.indexOf(element.id) != -1;
});
for (let i = 0; i < tres.length; i++) {
this.cascadedCollect(txn, tres[i].id, tres[i].timestamp);
}
this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
return false;
},
newTimestampContext: function() {
return this.collect.bind(this, 0);
},
newContext: function() {
this.contexts[1].processing++;
return this.collect.bind(this, 1);
}
};
function GetMessagesCursor(mmdb, callback) {
this.mmdb = mmdb;
this.callback = callback;
this.collector = new ResultsCollector(this.getMessage.bind(this));
this.handleContinue(); // Trigger first run.
}
GetMessagesCursor.prototype = {
classID: RIL_GETMESSAGESCURSOR_CID,
QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),
mmdb: null,
callback: null,
collector: null,
getMessageTxn: function(txn, messageStore, messageId, collector) {
if (DEBUG) debug ("Fetching message " + messageId);
let getRequest = messageStore.get(messageId);
let self = this;
getRequest.onsuccess = function(event) {
if (DEBUG) {
debug("notifyNextMessageInListGot - messageId: " + messageId);
}
let domMessage =
self.mmdb.createDomMessageFromRecord(event.target.result);
collector.notifyResult(txn, messageId, domMessage);
};
getRequest.onerror = function(event) {
// Error reporting is done in ResultsCollector.notifyCallback.
event.stopPropagation();
event.preventDefault();
if (DEBUG) {
debug("notifyCursorError - messageId: " + messageId);
}
collector.notifyResult(txn, messageId, null);
};
},
getMessage: function(txn, messageId, collector) {
// When filter transaction is not yet completed, we're called with current
// ongoing transaction object.
if (txn) {
let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
this.getMessageTxn(txn, messageStore, messageId, collector);
return;
}
// Or, we have to open another transaction ourselves.
let self = this;
this.mmdb.newTxn(READ_ONLY, function(error, txn, messageStore) {
if (error) {
debug("getMessage: failed to create new transaction");
collector.notifyResult(null, messageId, null);
} else {
self.getMessageTxn(txn, messageStore, messageId, collector);
}
}, [MESSAGE_STORE_NAME]);
},
// nsICursorContinueCallback
handleContinue: function() {
if (DEBUG) debug("Getting next message in list");
this.collector.squeeze(this.callback);
}
};
function GetThreadsCursor(mmdb, callback) {
this.mmdb = mmdb;
this.callback = callback;
this.collector = new ResultsCollector(this.getThread.bind(this));
this.handleContinue(); // Trigger first run.
}
GetThreadsCursor.prototype = {
classID: RIL_GETTHREADSCURSOR_CID,
QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),
mmdb: null,
callback: null,
collector: null,
getThreadTxn: function(txn, threadStore, threadId, collector) {
if (DEBUG) debug ("Fetching thread " + threadId);
let getRequest = threadStore.get(threadId);
getRequest.onsuccess = function(event) {
let threadRecord = event.target.result;
if (DEBUG) {
debug("notifyCursorResult: " + JSON.stringify(threadRecord));
}
let thread =
gMobileMessageService.createThread(threadRecord.id,
threadRecord.participantAddresses,
threadRecord.lastTimestamp,
threadRecord.lastMessageSubject || "",
threadRecord.body,
threadRecord.unreadCount,
threadRecord.lastMessageType);
collector.notifyResult(txn, threadId, thread);
};
getRequest.onerror = function(event) {
// Error reporting is done in ResultsCollector.notifyCallback.
event.stopPropagation();
event.preventDefault();
if (DEBUG) {
debug("notifyCursorError - threadId: " + threadId);
}
collector.notifyResult(txn, threadId, null);
};
},
getThread: function(txn, threadId, collector) {
// When filter transaction is not yet completed, we're called with current
// ongoing transaction object.
if (txn) {
let threadStore = txn.objectStore(THREAD_STORE_NAME);
this.getThreadTxn(txn, threadStore, threadId, collector);
return;
}
// Or, we have to open another transaction ourselves.
let self = this;
this.mmdb.newTxn(READ_ONLY, function(error, txn, threadStore) {
if (error) {
collector.notifyResult(null, threadId, null);
} else {
self.getThreadTxn(txn, threadStore, threadId, collector);
}
}, [THREAD_STORE_NAME]);
},
// nsICursorContinueCallback
handleContinue: function() {
if (DEBUG) debug("Getting next thread in list");
this.collector.squeeze(this.callback);
}
}
this.EXPORTED_SYMBOLS = [
'MobileMessageDB'
];
function debug() {
dump("MobileMessageDB: " + Array.slice(arguments).join(" ") + "\n");
}