mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-09-10 11:54:40 +00:00
5207 lines
189 KiB
JavaScript
5207 lines
189 KiB
JavaScript
/* 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");
|
||
}
|