dom/mobilemessage/src/gonk/MobileMessageDB.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
michael@0 8
michael@0 9 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 10 Cu.import("resource://gre/modules/Services.jsm");
michael@0 11 Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
michael@0 12 Cu.importGlobalProperties(["indexedDB"]);
michael@0 13
michael@0 14 var RIL = {};
michael@0 15 Cu.import("resource://gre/modules/ril_consts.js", RIL);
michael@0 16
michael@0 17 const RIL_GETMESSAGESCURSOR_CID =
michael@0 18 Components.ID("{484d1ad8-840e-4782-9dc4-9ebc4d914937}");
michael@0 19 const RIL_GETTHREADSCURSOR_CID =
michael@0 20 Components.ID("{95ee7c3e-d6f2-4ec4-ade5-0c453c036d35}");
michael@0 21
michael@0 22 const DEBUG = false;
michael@0 23 const DISABLE_MMS_GROUPING_FOR_RECEIVING = true;
michael@0 24
michael@0 25
michael@0 26 const DB_VERSION = 22;
michael@0 27 const MESSAGE_STORE_NAME = "sms";
michael@0 28 const THREAD_STORE_NAME = "thread";
michael@0 29 const PARTICIPANT_STORE_NAME = "participant";
michael@0 30 const MOST_RECENT_STORE_NAME = "most-recent";
michael@0 31 const SMS_SEGMENT_STORE_NAME = "sms-segment";
michael@0 32
michael@0 33 const DELIVERY_SENDING = "sending";
michael@0 34 const DELIVERY_SENT = "sent";
michael@0 35 const DELIVERY_RECEIVED = "received";
michael@0 36 const DELIVERY_NOT_DOWNLOADED = "not-downloaded";
michael@0 37 const DELIVERY_ERROR = "error";
michael@0 38
michael@0 39 const DELIVERY_STATUS_NOT_APPLICABLE = "not-applicable";
michael@0 40 const DELIVERY_STATUS_SUCCESS = "success";
michael@0 41 const DELIVERY_STATUS_PENDING = "pending";
michael@0 42 const DELIVERY_STATUS_ERROR = "error";
michael@0 43
michael@0 44 const MESSAGE_CLASS_NORMAL = "normal";
michael@0 45
michael@0 46 const FILTER_TIMESTAMP = "timestamp";
michael@0 47 const FILTER_NUMBERS = "numbers";
michael@0 48 const FILTER_DELIVERY = "delivery";
michael@0 49 const FILTER_READ = "read";
michael@0 50
michael@0 51 // We can“t create an IDBKeyCursor with a boolean, so we need to use numbers
michael@0 52 // instead.
michael@0 53 const FILTER_READ_UNREAD = 0;
michael@0 54 const FILTER_READ_READ = 1;
michael@0 55
michael@0 56 const READ_ONLY = "readonly";
michael@0 57 const READ_WRITE = "readwrite";
michael@0 58 const PREV = "prev";
michael@0 59 const NEXT = "next";
michael@0 60
michael@0 61 const COLLECT_ID_END = 0;
michael@0 62 const COLLECT_ID_ERROR = -1;
michael@0 63 const COLLECT_TIMESTAMP_UNUSED = 0;
michael@0 64
michael@0 65 XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService",
michael@0 66 "@mozilla.org/mobilemessage/mobilemessageservice;1",
michael@0 67 "nsIMobileMessageService");
michael@0 68
michael@0 69 XPCOMUtils.defineLazyServiceGetter(this, "gMMSService",
michael@0 70 "@mozilla.org/mms/rilmmsservice;1",
michael@0 71 "nsIMmsService");
michael@0 72
michael@0 73 XPCOMUtils.defineLazyGetter(this, "MMS", function() {
michael@0 74 let MMS = {};
michael@0 75 Cu.import("resource://gre/modules/MmsPduHelper.jsm", MMS);
michael@0 76 return MMS;
michael@0 77 });
michael@0 78
michael@0 79 /**
michael@0 80 * MobileMessageDB
michael@0 81 */
michael@0 82 this.MobileMessageDB = function() {};
michael@0 83 MobileMessageDB.prototype = {
michael@0 84 dbName: null,
michael@0 85 dbVersion: null,
michael@0 86
michael@0 87 /**
michael@0 88 * Cache the DB here.
michael@0 89 */
michael@0 90 db: null,
michael@0 91
michael@0 92 /**
michael@0 93 * Last sms/mms object store key value in the database.
michael@0 94 */
michael@0 95 lastMessageId: 0,
michael@0 96
michael@0 97 /**
michael@0 98 * An optional hook to check if device storage is full.
michael@0 99 *
michael@0 100 * @return true if full.
michael@0 101 */
michael@0 102 isDiskFull: null,
michael@0 103
michael@0 104 /**
michael@0 105 * Prepare the database. This may include opening the database and upgrading
michael@0 106 * it to the latest schema version.
michael@0 107 *
michael@0 108 * @param callback
michael@0 109 * Function that takes an error and db argument. It is called when
michael@0 110 * the database is ready to use or if an error occurs while preparing
michael@0 111 * the database.
michael@0 112 *
michael@0 113 * @return (via callback) a database ready for use.
michael@0 114 */
michael@0 115 ensureDB: function(callback) {
michael@0 116 if (this.db) {
michael@0 117 if (DEBUG) debug("ensureDB: already have a database, returning early.");
michael@0 118 callback(null, this.db);
michael@0 119 return;
michael@0 120 }
michael@0 121
michael@0 122 let self = this;
michael@0 123 function gotDB(db) {
michael@0 124 self.db = db;
michael@0 125 callback(null, db);
michael@0 126 }
michael@0 127
michael@0 128 let request = indexedDB.open(this.dbName, this.dbVersion);
michael@0 129 request.onsuccess = function(event) {
michael@0 130 if (DEBUG) debug("Opened database:", self.dbName, self.dbVersion);
michael@0 131 gotDB(event.target.result);
michael@0 132 };
michael@0 133 request.onupgradeneeded = function(event) {
michael@0 134 if (DEBUG) {
michael@0 135 debug("Database needs upgrade:", self.dbName,
michael@0 136 event.oldVersion, event.newVersion);
michael@0 137 debug("Correct new database version:", event.newVersion == self.dbVersion);
michael@0 138 }
michael@0 139
michael@0 140 let db = event.target.result;
michael@0 141
michael@0 142 let currentVersion = event.oldVersion;
michael@0 143
michael@0 144 function update(currentVersion) {
michael@0 145 let next = update.bind(self, currentVersion + 1);
michael@0 146
michael@0 147 switch (currentVersion) {
michael@0 148 case 0:
michael@0 149 if (DEBUG) debug("New database");
michael@0 150 self.createSchema(db, next);
michael@0 151 break;
michael@0 152 case 1:
michael@0 153 if (DEBUG) debug("Upgrade to version 2. Including `read` index");
michael@0 154 self.upgradeSchema(event.target.transaction, next);
michael@0 155 break;
michael@0 156 case 2:
michael@0 157 if (DEBUG) debug("Upgrade to version 3. Fix existing entries.");
michael@0 158 self.upgradeSchema2(event.target.transaction, next);
michael@0 159 break;
michael@0 160 case 3:
michael@0 161 if (DEBUG) debug("Upgrade to version 4. Add quick threads view.");
michael@0 162 self.upgradeSchema3(db, event.target.transaction, next);
michael@0 163 break;
michael@0 164 case 4:
michael@0 165 if (DEBUG) debug("Upgrade to version 5. Populate quick threads view.");
michael@0 166 self.upgradeSchema4(event.target.transaction, next);
michael@0 167 break;
michael@0 168 case 5:
michael@0 169 if (DEBUG) debug("Upgrade to version 6. Use PhonenumberJS.");
michael@0 170 self.upgradeSchema5(event.target.transaction, next);
michael@0 171 break;
michael@0 172 case 6:
michael@0 173 if (DEBUG) debug("Upgrade to version 7. Use multiple entry indexes.");
michael@0 174 self.upgradeSchema6(event.target.transaction, next);
michael@0 175 break;
michael@0 176 case 7:
michael@0 177 if (DEBUG) debug("Upgrade to version 8. Add participant/thread stores.");
michael@0 178 self.upgradeSchema7(db, event.target.transaction, next);
michael@0 179 break;
michael@0 180 case 8:
michael@0 181 if (DEBUG) debug("Upgrade to version 9. Add transactionId index for incoming MMS.");
michael@0 182 self.upgradeSchema8(event.target.transaction, next);
michael@0 183 break;
michael@0 184 case 9:
michael@0 185 if (DEBUG) debug("Upgrade to version 10. Upgrade type if it's not existing.");
michael@0 186 self.upgradeSchema9(event.target.transaction, next);
michael@0 187 break;
michael@0 188 case 10:
michael@0 189 if (DEBUG) debug("Upgrade to version 11. Add last message type into threadRecord.");
michael@0 190 self.upgradeSchema10(event.target.transaction, next);
michael@0 191 break;
michael@0 192 case 11:
michael@0 193 if (DEBUG) debug("Upgrade to version 12. Add envelopeId index for outgoing MMS.");
michael@0 194 self.upgradeSchema11(event.target.transaction, next);
michael@0 195 break;
michael@0 196 case 12:
michael@0 197 if (DEBUG) debug("Upgrade to version 13. Replaced deliveryStatus by deliveryInfo.");
michael@0 198 self.upgradeSchema12(event.target.transaction, next);
michael@0 199 break;
michael@0 200 case 13:
michael@0 201 if (DEBUG) debug("Upgrade to version 14. Fix the wrong participants.");
michael@0 202 // A workaround to check if we need to re-upgrade the DB schema 12. We missed this
michael@0 203 // because we didn't properly uplift that logic to b2g_v1.2 and errors could happen
michael@0 204 // when migrating b2g_v1.2 to b2g_v1.3. Please see Bug 960741 for details.
michael@0 205 self.needReUpgradeSchema12(event.target.transaction, function(isNeeded) {
michael@0 206 if (isNeeded) {
michael@0 207 self.upgradeSchema12(event.target.transaction, function() {
michael@0 208 self.upgradeSchema13(event.target.transaction, next);
michael@0 209 });
michael@0 210 } else {
michael@0 211 self.upgradeSchema13(event.target.transaction, next);
michael@0 212 }
michael@0 213 });
michael@0 214 break;
michael@0 215 case 14:
michael@0 216 if (DEBUG) debug("Upgrade to version 15. Add deliveryTimestamp.");
michael@0 217 self.upgradeSchema14(event.target.transaction, next);
michael@0 218 break;
michael@0 219 case 15:
michael@0 220 if (DEBUG) debug("Upgrade to version 16. Add ICC ID for each message.");
michael@0 221 self.upgradeSchema15(event.target.transaction, next);
michael@0 222 break;
michael@0 223 case 16:
michael@0 224 if (DEBUG) debug("Upgrade to version 17. Add isReadReportSent for incoming MMS.");
michael@0 225 self.upgradeSchema16(event.target.transaction, next);
michael@0 226 break;
michael@0 227 case 17:
michael@0 228 if (DEBUG) debug("Upgrade to version 18. Add last message subject into threadRecord.");
michael@0 229 self.upgradeSchema17(event.target.transaction, next);
michael@0 230 break;
michael@0 231 case 18:
michael@0 232 if (DEBUG) debug("Upgrade to version 19. Add pid for incoming SMS.");
michael@0 233 self.upgradeSchema18(event.target.transaction, next);
michael@0 234 break;
michael@0 235 case 19:
michael@0 236 if (DEBUG) debug("Upgrade to version 20. Add readStatus and readTimestamp.");
michael@0 237 self.upgradeSchema19(event.target.transaction, next);
michael@0 238 break;
michael@0 239 case 20:
michael@0 240 if (DEBUG) debug("Upgrade to version 21. Add sentTimestamp.");
michael@0 241 self.upgradeSchema20(event.target.transaction, next);
michael@0 242 break;
michael@0 243 case 21:
michael@0 244 if (DEBUG) debug("Upgrade to version 22. Add sms-segment store.");
michael@0 245 self.upgradeSchema21(db, event.target.transaction, next);
michael@0 246 break;
michael@0 247 case 22:
michael@0 248 // This will need to be moved for each new version
michael@0 249 if (DEBUG) debug("Upgrade finished.");
michael@0 250 break;
michael@0 251 default:
michael@0 252 event.target.transaction.abort();
michael@0 253 if (DEBUG) debug("unexpected db version: " + event.oldVersion);
michael@0 254 callback(Cr.NS_ERROR_FAILURE, null);
michael@0 255 break;
michael@0 256 }
michael@0 257 }
michael@0 258
michael@0 259 update(currentVersion);
michael@0 260 };
michael@0 261 request.onerror = function(event) {
michael@0 262 //TODO look at event.target.Code and change error constant accordingly
michael@0 263 if (DEBUG) debug("Error opening database!");
michael@0 264 callback(Cr.NS_ERROR_FAILURE, null);
michael@0 265 };
michael@0 266 request.onblocked = function(event) {
michael@0 267 if (DEBUG) debug("Opening database request is blocked.");
michael@0 268 callback(Cr.NS_ERROR_FAILURE, null);
michael@0 269 };
michael@0 270 },
michael@0 271
michael@0 272 /**
michael@0 273 * Start a new transaction.
michael@0 274 *
michael@0 275 * @param txn_type
michael@0 276 * Type of transaction (e.g. READ_WRITE)
michael@0 277 * @param callback
michael@0 278 * Function to call when the transaction is available. It will
michael@0 279 * be invoked with the transaction and opened object stores.
michael@0 280 * @param storeNames
michael@0 281 * Names of the stores to open.
michael@0 282 */
michael@0 283 newTxn: function(txn_type, callback, storeNames) {
michael@0 284 if (!storeNames) {
michael@0 285 storeNames = [MESSAGE_STORE_NAME];
michael@0 286 }
michael@0 287 if (DEBUG) debug("Opening transaction for object stores: " + storeNames);
michael@0 288 let self = this;
michael@0 289 this.ensureDB(function(error, db) {
michael@0 290 if (!error &&
michael@0 291 txn_type === READ_WRITE &&
michael@0 292 self.isDiskFull && self.isDiskFull()) {
michael@0 293 error = Cr.NS_ERROR_FILE_NO_DEVICE_SPACE;
michael@0 294 }
michael@0 295 if (error) {
michael@0 296 if (DEBUG) debug("Could not open database: " + error);
michael@0 297 callback(error);
michael@0 298 return;
michael@0 299 }
michael@0 300 let txn = db.transaction(storeNames, txn_type);
michael@0 301 if (DEBUG) debug("Started transaction " + txn + " of type " + txn_type);
michael@0 302 if (DEBUG) {
michael@0 303 txn.oncomplete = function oncomplete(event) {
michael@0 304 debug("Transaction " + txn + " completed.");
michael@0 305 };
michael@0 306 txn.onerror = function onerror(event) {
michael@0 307 //TODO check event.target.errorCode and show an appropiate error
michael@0 308 // message according to it.
michael@0 309 debug("Error occurred during transaction: " + event.target.errorCode);
michael@0 310 };
michael@0 311 }
michael@0 312 let stores;
michael@0 313 if (storeNames.length == 1) {
michael@0 314 if (DEBUG) debug("Retrieving object store " + storeNames[0]);
michael@0 315 stores = txn.objectStore(storeNames[0]);
michael@0 316 } else {
michael@0 317 stores = [];
michael@0 318 for each (let storeName in storeNames) {
michael@0 319 if (DEBUG) debug("Retrieving object store " + storeName);
michael@0 320 stores.push(txn.objectStore(storeName));
michael@0 321 }
michael@0 322 }
michael@0 323 callback(null, txn, stores);
michael@0 324 });
michael@0 325 },
michael@0 326
michael@0 327 /**
michael@0 328 * Initialize this MobileMessageDB.
michael@0 329 *
michael@0 330 * @param aDbName
michael@0 331 * A string name for that database.
michael@0 332 * @param aDbVersion
michael@0 333 * The version that mmdb should upgrade to. 0 for the lastest version.
michael@0 334 * @param aCallback
michael@0 335 * A function when either the initialization transaction is completed
michael@0 336 * or any error occurs. Should take only one argument -- null when
michael@0 337 * initialized with success or the error object otherwise.
michael@0 338 */
michael@0 339 init: function(aDbName, aDbVersion, aCallback) {
michael@0 340 this.dbName = aDbName;
michael@0 341 this.dbVersion = aDbVersion || DB_VERSION;
michael@0 342
michael@0 343 let self = this;
michael@0 344 this.newTxn(READ_ONLY, function(error, txn, messageStore){
michael@0 345 if (error) {
michael@0 346 if (aCallback) {
michael@0 347 aCallback(error);
michael@0 348 }
michael@0 349 return;
michael@0 350 }
michael@0 351
michael@0 352 if (aCallback) {
michael@0 353 txn.oncomplete = function() {
michael@0 354 aCallback(null);
michael@0 355 };
michael@0 356 }
michael@0 357
michael@0 358 // In order to get the highest key value, we open a key cursor in reverse
michael@0 359 // order and get only the first pointed value.
michael@0 360 let request = messageStore.openCursor(null, PREV);
michael@0 361 request.onsuccess = function onsuccess(event) {
michael@0 362 let cursor = event.target.result;
michael@0 363 if (!cursor) {
michael@0 364 if (DEBUG) {
michael@0 365 debug("Could not get the last key from mobile message database. " +
michael@0 366 "Probably empty database");
michael@0 367 }
michael@0 368 return;
michael@0 369 }
michael@0 370 self.lastMessageId = cursor.key || 0;
michael@0 371 if (DEBUG) debug("Last assigned message ID was " + self.lastMessageId);
michael@0 372 };
michael@0 373 request.onerror = function onerror(event) {
michael@0 374 if (DEBUG) {
michael@0 375 debug("Could not get the last key from mobile message database " +
michael@0 376 event.target.errorCode);
michael@0 377 }
michael@0 378 };
michael@0 379 });
michael@0 380 },
michael@0 381
michael@0 382 close: function() {
michael@0 383 if (!this.db) {
michael@0 384 return;
michael@0 385 }
michael@0 386
michael@0 387 this.db.close();
michael@0 388 this.db = null;
michael@0 389 this.lastMessageId = 0;
michael@0 390 },
michael@0 391
michael@0 392 /**
michael@0 393 * Sometimes user might reboot or remove battery while sending/receiving
michael@0 394 * message. This is function set the status of message records to error.
michael@0 395 */
michael@0 396 updatePendingTransactionToError: function(aError) {
michael@0 397 if (aError) {
michael@0 398 return;
michael@0 399 }
michael@0 400
michael@0 401 this.newTxn(READ_WRITE, function(error, txn, messageStore) {
michael@0 402 if (error) {
michael@0 403 return;
michael@0 404 }
michael@0 405
michael@0 406 let deliveryIndex = messageStore.index("delivery");
michael@0 407
michael@0 408 // Set all 'delivery: sending' records to 'delivery: error' and 'deliveryStatus:
michael@0 409 // error'.
michael@0 410 let keyRange = IDBKeyRange.bound([DELIVERY_SENDING, 0], [DELIVERY_SENDING, ""]);
michael@0 411 let cursorRequestSending = deliveryIndex.openCursor(keyRange);
michael@0 412 cursorRequestSending.onsuccess = function(event) {
michael@0 413 let messageCursor = event.target.result;
michael@0 414 if (!messageCursor) {
michael@0 415 return;
michael@0 416 }
michael@0 417
michael@0 418 let messageRecord = messageCursor.value;
michael@0 419
michael@0 420 // Set delivery to error.
michael@0 421 messageRecord.delivery = DELIVERY_ERROR;
michael@0 422 messageRecord.deliveryIndex = [DELIVERY_ERROR, messageRecord.timestamp];
michael@0 423
michael@0 424 if (messageRecord.type == "sms") {
michael@0 425 messageRecord.deliveryStatus = DELIVERY_STATUS_ERROR;
michael@0 426 } else {
michael@0 427 // Set delivery status to error.
michael@0 428 for (let i = 0; i < messageRecord.deliveryInfo.length; i++) {
michael@0 429 messageRecord.deliveryInfo[i].deliveryStatus = DELIVERY_STATUS_ERROR;
michael@0 430 }
michael@0 431 }
michael@0 432
michael@0 433 messageCursor.update(messageRecord);
michael@0 434 messageCursor.continue();
michael@0 435 };
michael@0 436
michael@0 437 // Set all 'delivery: not-downloaded' and 'deliveryStatus: pending'
michael@0 438 // records to 'delivery: not-downloaded' and 'deliveryStatus: error'.
michael@0 439 keyRange = IDBKeyRange.bound([DELIVERY_NOT_DOWNLOADED, 0], [DELIVERY_NOT_DOWNLOADED, ""]);
michael@0 440 let cursorRequestNotDownloaded = deliveryIndex.openCursor(keyRange);
michael@0 441 cursorRequestNotDownloaded.onsuccess = function(event) {
michael@0 442 let messageCursor = event.target.result;
michael@0 443 if (!messageCursor) {
michael@0 444 return;
michael@0 445 }
michael@0 446
michael@0 447 let messageRecord = messageCursor.value;
michael@0 448
michael@0 449 // We have no "not-downloaded" SMS messages.
michael@0 450 if (messageRecord.type == "sms") {
michael@0 451 messageCursor.continue();
michael@0 452 return;
michael@0 453 }
michael@0 454
michael@0 455 // Set delivery status to error.
michael@0 456 let deliveryInfo = messageRecord.deliveryInfo;
michael@0 457 if (deliveryInfo.length == 1 &&
michael@0 458 deliveryInfo[0].deliveryStatus == DELIVERY_STATUS_PENDING) {
michael@0 459 deliveryInfo[0].deliveryStatus = DELIVERY_STATUS_ERROR;
michael@0 460 }
michael@0 461
michael@0 462 messageCursor.update(messageRecord);
michael@0 463 messageCursor.continue();
michael@0 464 };
michael@0 465 });
michael@0 466 },
michael@0 467
michael@0 468 /**
michael@0 469 * Create the initial database schema.
michael@0 470 *
michael@0 471 * TODO need to worry about number normalization somewhere...
michael@0 472 * TODO full text search on body???
michael@0 473 */
michael@0 474 createSchema: function(db, next) {
michael@0 475 // This messageStore holds the main mobile message data.
michael@0 476 let messageStore = db.createObjectStore(MESSAGE_STORE_NAME, { keyPath: "id" });
michael@0 477 messageStore.createIndex("timestamp", "timestamp", { unique: false });
michael@0 478 if (DEBUG) debug("Created object stores and indexes");
michael@0 479 next();
michael@0 480 },
michael@0 481
michael@0 482 /**
michael@0 483 * Upgrade to the corresponding database schema version.
michael@0 484 */
michael@0 485 upgradeSchema: function(transaction, next) {
michael@0 486 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 487 messageStore.createIndex("read", "read", { unique: false });
michael@0 488 next();
michael@0 489 },
michael@0 490
michael@0 491 upgradeSchema2: function(transaction, next) {
michael@0 492 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 493 messageStore.openCursor().onsuccess = function(event) {
michael@0 494 let cursor = event.target.result;
michael@0 495 if (!cursor) {
michael@0 496 next();
michael@0 497 return;
michael@0 498 }
michael@0 499
michael@0 500 let messageRecord = cursor.value;
michael@0 501 messageRecord.messageClass = MESSAGE_CLASS_NORMAL;
michael@0 502 messageRecord.deliveryStatus = DELIVERY_STATUS_NOT_APPLICABLE;
michael@0 503 cursor.update(messageRecord);
michael@0 504 cursor.continue();
michael@0 505 };
michael@0 506 },
michael@0 507
michael@0 508 upgradeSchema3: function(db, transaction, next) {
michael@0 509 // Delete redundant "id" index.
michael@0 510 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 511 if (messageStore.indexNames.contains("id")) {
michael@0 512 messageStore.deleteIndex("id");
michael@0 513 }
michael@0 514
michael@0 515 /**
michael@0 516 * This mostRecentStore can be used to quickly construct a thread view of
michael@0 517 * the mobile message database. Each entry looks like this:
michael@0 518 *
michael@0 519 * { senderOrReceiver: <String> (primary key),
michael@0 520 * id: <Number>,
michael@0 521 * timestamp: <Date>,
michael@0 522 * body: <String>,
michael@0 523 * unreadCount: <Number> }
michael@0 524 *
michael@0 525 */
michael@0 526 let mostRecentStore = db.createObjectStore(MOST_RECENT_STORE_NAME,
michael@0 527 { keyPath: "senderOrReceiver" });
michael@0 528 mostRecentStore.createIndex("timestamp", "timestamp");
michael@0 529 next();
michael@0 530 },
michael@0 531
michael@0 532 upgradeSchema4: function(transaction, next) {
michael@0 533 let threads = {};
michael@0 534 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 535 let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);
michael@0 536
michael@0 537 messageStore.openCursor().onsuccess = function(event) {
michael@0 538 let cursor = event.target.result;
michael@0 539 if (!cursor) {
michael@0 540 for (let thread in threads) {
michael@0 541 mostRecentStore.put(threads[thread]);
michael@0 542 }
michael@0 543 next();
michael@0 544 return;
michael@0 545 }
michael@0 546
michael@0 547 let messageRecord = cursor.value;
michael@0 548 let contact = messageRecord.sender || messageRecord.receiver;
michael@0 549
michael@0 550 if (contact in threads) {
michael@0 551 let thread = threads[contact];
michael@0 552 if (!messageRecord.read) {
michael@0 553 thread.unreadCount++;
michael@0 554 }
michael@0 555 if (messageRecord.timestamp > thread.timestamp) {
michael@0 556 thread.id = messageRecord.id;
michael@0 557 thread.body = messageRecord.body;
michael@0 558 thread.timestamp = messageRecord.timestamp;
michael@0 559 }
michael@0 560 } else {
michael@0 561 threads[contact] = {
michael@0 562 senderOrReceiver: contact,
michael@0 563 id: messageRecord.id,
michael@0 564 timestamp: messageRecord.timestamp,
michael@0 565 body: messageRecord.body,
michael@0 566 unreadCount: messageRecord.read ? 0 : 1
michael@0 567 };
michael@0 568 }
michael@0 569 cursor.continue();
michael@0 570 };
michael@0 571 },
michael@0 572
michael@0 573 upgradeSchema5: function(transaction, next) {
michael@0 574 // Don't perform any upgrade. See Bug 819560.
michael@0 575 next();
michael@0 576 },
michael@0 577
michael@0 578 upgradeSchema6: function(transaction, next) {
michael@0 579 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 580
michael@0 581 // Delete "delivery" index.
michael@0 582 if (messageStore.indexNames.contains("delivery")) {
michael@0 583 messageStore.deleteIndex("delivery");
michael@0 584 }
michael@0 585 // Delete "sender" index.
michael@0 586 if (messageStore.indexNames.contains("sender")) {
michael@0 587 messageStore.deleteIndex("sender");
michael@0 588 }
michael@0 589 // Delete "receiver" index.
michael@0 590 if (messageStore.indexNames.contains("receiver")) {
michael@0 591 messageStore.deleteIndex("receiver");
michael@0 592 }
michael@0 593 // Delete "read" index.
michael@0 594 if (messageStore.indexNames.contains("read")) {
michael@0 595 messageStore.deleteIndex("read");
michael@0 596 }
michael@0 597
michael@0 598 // Create new "delivery", "number" and "read" indexes.
michael@0 599 messageStore.createIndex("delivery", "deliveryIndex");
michael@0 600 messageStore.createIndex("number", "numberIndex", { multiEntry: true });
michael@0 601 messageStore.createIndex("read", "readIndex");
michael@0 602
michael@0 603 // Populate new "deliverIndex", "numberIndex" and "readIndex" attributes.
michael@0 604 messageStore.openCursor().onsuccess = function(event) {
michael@0 605 let cursor = event.target.result;
michael@0 606 if (!cursor) {
michael@0 607 next();
michael@0 608 return;
michael@0 609 }
michael@0 610
michael@0 611 let messageRecord = cursor.value;
michael@0 612 let timestamp = messageRecord.timestamp;
michael@0 613 messageRecord.deliveryIndex = [messageRecord.delivery, timestamp];
michael@0 614 messageRecord.numberIndex = [
michael@0 615 [messageRecord.sender, timestamp],
michael@0 616 [messageRecord.receiver, timestamp]
michael@0 617 ];
michael@0 618 messageRecord.readIndex = [messageRecord.read, timestamp];
michael@0 619 cursor.update(messageRecord);
michael@0 620 cursor.continue();
michael@0 621 };
michael@0 622 },
michael@0 623
michael@0 624 /**
michael@0 625 * Add participant/thread stores.
michael@0 626 *
michael@0 627 * The message store now saves original phone numbers/addresses input from
michael@0 628 * content to message records. No normalization is made.
michael@0 629 *
michael@0 630 * For filtering messages by phone numbers, it first looks up corresponding
michael@0 631 * participant IDs from participant table and fetch message records with
michael@0 632 * matching keys defined in per record "participantIds" field.
michael@0 633 *
michael@0 634 * For message threading, messages with the same participant ID array are put
michael@0 635 * in the same thread. So updating "unreadCount", "lastMessageId" and
michael@0 636 * "lastTimestamp" are through the "threadId" carried by per message record.
michael@0 637 * Fetching threads list is now simply walking through the thread sotre. The
michael@0 638 * "mostRecentStore" is dropped.
michael@0 639 */
michael@0 640 upgradeSchema7: function(db, transaction, next) {
michael@0 641 /**
michael@0 642 * This "participant" object store keeps mappings of multiple phone numbers
michael@0 643 * of the same recipient to an integer participant id. Each entry looks
michael@0 644 * like:
michael@0 645 *
michael@0 646 * { id: <Number> (primary key),
michael@0 647 * addresses: <Array of strings> }
michael@0 648 */
michael@0 649 let participantStore = db.createObjectStore(PARTICIPANT_STORE_NAME,
michael@0 650 { keyPath: "id",
michael@0 651 autoIncrement: true });
michael@0 652 participantStore.createIndex("addresses", "addresses", { multiEntry: true });
michael@0 653
michael@0 654 /**
michael@0 655 * This "threads" object store keeps mappings from an integer thread id to
michael@0 656 * ids of the participants of that message thread. Each entry looks like:
michael@0 657 *
michael@0 658 * { id: <Number> (primary key),
michael@0 659 * participantIds: <Array of participant IDs>,
michael@0 660 * participantAddresses: <Array of the first addresses of the participants>,
michael@0 661 * lastMessageId: <Number>,
michael@0 662 * lastTimestamp: <Date>,
michael@0 663 * subject: <String>,
michael@0 664 * unreadCount: <Number> }
michael@0 665 *
michael@0 666 */
michael@0 667 let threadStore = db.createObjectStore(THREAD_STORE_NAME,
michael@0 668 { keyPath: "id",
michael@0 669 autoIncrement: true });
michael@0 670 threadStore.createIndex("participantIds", "participantIds");
michael@0 671 threadStore.createIndex("lastTimestamp", "lastTimestamp");
michael@0 672
michael@0 673 /**
michael@0 674 * Replace "numberIndex" with "participantIdsIndex" and create an additional
michael@0 675 * "threadId". "numberIndex" will be removed later.
michael@0 676 */
michael@0 677 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 678 messageStore.createIndex("threadId", "threadIdIndex");
michael@0 679 messageStore.createIndex("participantIds", "participantIdsIndex",
michael@0 680 { multiEntry: true });
michael@0 681
michael@0 682 // Now populate participantStore & threadStore.
michael@0 683 let mostRecentStore = transaction.objectStore(MOST_RECENT_STORE_NAME);
michael@0 684 let self = this;
michael@0 685 let mostRecentRequest = mostRecentStore.openCursor();
michael@0 686 mostRecentRequest.onsuccess = function(event) {
michael@0 687 let mostRecentCursor = event.target.result;
michael@0 688 if (!mostRecentCursor) {
michael@0 689 db.deleteObjectStore(MOST_RECENT_STORE_NAME);
michael@0 690
michael@0 691 // No longer need the "number" index in messageStore, use
michael@0 692 // "participantIds" index instead.
michael@0 693 messageStore.deleteIndex("number");
michael@0 694 next();
michael@0 695 return;
michael@0 696 }
michael@0 697
michael@0 698 let mostRecentRecord = mostRecentCursor.value;
michael@0 699
michael@0 700 // Each entry in mostRecentStore is supposed to be a unique thread, so we
michael@0 701 // retrieve the records out and insert its "senderOrReceiver" column as a
michael@0 702 // new record in participantStore.
michael@0 703 let number = mostRecentRecord.senderOrReceiver;
michael@0 704 self.findParticipantRecordByAddress(participantStore, number, true,
michael@0 705 function(participantRecord) {
michael@0 706 // Also create a new record in threadStore.
michael@0 707 let threadRecord = {
michael@0 708 participantIds: [participantRecord.id],
michael@0 709 participantAddresses: [number],
michael@0 710 lastMessageId: mostRecentRecord.id,
michael@0 711 lastTimestamp: mostRecentRecord.timestamp,
michael@0 712 subject: mostRecentRecord.body,
michael@0 713 unreadCount: mostRecentRecord.unreadCount,
michael@0 714 };
michael@0 715 let addThreadRequest = threadStore.add(threadRecord);
michael@0 716 addThreadRequest.onsuccess = function(event) {
michael@0 717 threadRecord.id = event.target.result;
michael@0 718
michael@0 719 let numberRange = IDBKeyRange.bound([number, 0], [number, ""]);
michael@0 720 let messageRequest = messageStore.index("number")
michael@0 721 .openCursor(numberRange, NEXT);
michael@0 722 messageRequest.onsuccess = function(event) {
michael@0 723 let messageCursor = event.target.result;
michael@0 724 if (!messageCursor) {
michael@0 725 // No more message records, check next most recent record.
michael@0 726 mostRecentCursor.continue();
michael@0 727 return;
michael@0 728 }
michael@0 729
michael@0 730 let messageRecord = messageCursor.value;
michael@0 731 // Check whether the message really belongs to this thread.
michael@0 732 let matchSenderOrReceiver = false;
michael@0 733 if (messageRecord.delivery == DELIVERY_RECEIVED) {
michael@0 734 if (messageRecord.sender == number) {
michael@0 735 matchSenderOrReceiver = true;
michael@0 736 }
michael@0 737 } else if (messageRecord.receiver == number) {
michael@0 738 matchSenderOrReceiver = true;
michael@0 739 }
michael@0 740 if (!matchSenderOrReceiver) {
michael@0 741 // Check next message record.
michael@0 742 messageCursor.continue();
michael@0 743 return;
michael@0 744 }
michael@0 745
michael@0 746 messageRecord.threadId = threadRecord.id;
michael@0 747 messageRecord.threadIdIndex = [threadRecord.id,
michael@0 748 messageRecord.timestamp];
michael@0 749 messageRecord.participantIdsIndex = [
michael@0 750 [participantRecord.id, messageRecord.timestamp]
michael@0 751 ];
michael@0 752 messageCursor.update(messageRecord);
michael@0 753 // Check next message record.
michael@0 754 messageCursor.continue();
michael@0 755 };
michael@0 756 messageRequest.onerror = function() {
michael@0 757 // Error in fetching message records, check next most recent record.
michael@0 758 mostRecentCursor.continue();
michael@0 759 };
michael@0 760 };
michael@0 761 addThreadRequest.onerror = function() {
michael@0 762 // Error in fetching message records, check next most recent record.
michael@0 763 mostRecentCursor.continue();
michael@0 764 };
michael@0 765 });
michael@0 766 };
michael@0 767 },
michael@0 768
michael@0 769 /**
michael@0 770 * Add transactionId index for MMS.
michael@0 771 */
michael@0 772 upgradeSchema8: function(transaction, next) {
michael@0 773 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 774
michael@0 775 // Delete "transactionId" index.
michael@0 776 if (messageStore.indexNames.contains("transactionId")) {
michael@0 777 messageStore.deleteIndex("transactionId");
michael@0 778 }
michael@0 779
michael@0 780 // Create new "transactionId" indexes.
michael@0 781 messageStore.createIndex("transactionId", "transactionIdIndex", { unique: true });
michael@0 782
michael@0 783 // Populate new "transactionIdIndex" attributes.
michael@0 784 messageStore.openCursor().onsuccess = function(event) {
michael@0 785 let cursor = event.target.result;
michael@0 786 if (!cursor) {
michael@0 787 next();
michael@0 788 return;
michael@0 789 }
michael@0 790
michael@0 791 let messageRecord = cursor.value;
michael@0 792 if ("mms" == messageRecord.type &&
michael@0 793 (DELIVERY_NOT_DOWNLOADED == messageRecord.delivery ||
michael@0 794 DELIVERY_RECEIVED == messageRecord.delivery)) {
michael@0 795 messageRecord.transactionIdIndex =
michael@0 796 messageRecord.headers["x-mms-transaction-id"];
michael@0 797 cursor.update(messageRecord);
michael@0 798 }
michael@0 799 cursor.continue();
michael@0 800 };
michael@0 801 },
michael@0 802
michael@0 803 upgradeSchema9: function(transaction, next) {
michael@0 804 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 805
michael@0 806 // Update type attributes.
michael@0 807 messageStore.openCursor().onsuccess = function(event) {
michael@0 808 let cursor = event.target.result;
michael@0 809 if (!cursor) {
michael@0 810 next();
michael@0 811 return;
michael@0 812 }
michael@0 813
michael@0 814 let messageRecord = cursor.value;
michael@0 815 if (messageRecord.type == undefined) {
michael@0 816 messageRecord.type = "sms";
michael@0 817 cursor.update(messageRecord);
michael@0 818 }
michael@0 819 cursor.continue();
michael@0 820 };
michael@0 821 },
michael@0 822
michael@0 823 upgradeSchema10: function(transaction, next) {
michael@0 824 let threadStore = transaction.objectStore(THREAD_STORE_NAME);
michael@0 825
michael@0 826 // Add 'lastMessageType' to each thread record.
michael@0 827 threadStore.openCursor().onsuccess = function(event) {
michael@0 828 let cursor = event.target.result;
michael@0 829 if (!cursor) {
michael@0 830 next();
michael@0 831 return;
michael@0 832 }
michael@0 833
michael@0 834 let threadRecord = cursor.value;
michael@0 835 let lastMessageId = threadRecord.lastMessageId;
michael@0 836 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 837 let request = messageStore.mozGetAll(lastMessageId);
michael@0 838
michael@0 839 request.onsuccess = function onsuccess() {
michael@0 840 let messageRecord = request.result[0];
michael@0 841 if (!messageRecord) {
michael@0 842 if (DEBUG) debug("Message ID " + lastMessageId + " not found");
michael@0 843 return;
michael@0 844 }
michael@0 845 if (messageRecord.id != lastMessageId) {
michael@0 846 if (DEBUG) {
michael@0 847 debug("Requested message ID (" + lastMessageId + ") is different from" +
michael@0 848 " the one we got");
michael@0 849 }
michael@0 850 return;
michael@0 851 }
michael@0 852 threadRecord.lastMessageType = messageRecord.type;
michael@0 853 cursor.update(threadRecord);
michael@0 854 cursor.continue();
michael@0 855 };
michael@0 856
michael@0 857 request.onerror = function onerror(event) {
michael@0 858 if (DEBUG) {
michael@0 859 if (event.target) {
michael@0 860 debug("Caught error on transaction", event.target.errorCode);
michael@0 861 }
michael@0 862 }
michael@0 863 cursor.continue();
michael@0 864 };
michael@0 865 };
michael@0 866 },
michael@0 867
michael@0 868 /**
michael@0 869 * Add envelopeId index for MMS.
michael@0 870 */
michael@0 871 upgradeSchema11: function(transaction, next) {
michael@0 872 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 873
michael@0 874 // Delete "envelopeId" index.
michael@0 875 if (messageStore.indexNames.contains("envelopeId")) {
michael@0 876 messageStore.deleteIndex("envelopeId");
michael@0 877 }
michael@0 878
michael@0 879 // Create new "envelopeId" indexes.
michael@0 880 messageStore.createIndex("envelopeId", "envelopeIdIndex", { unique: true });
michael@0 881
michael@0 882 // Populate new "envelopeIdIndex" attributes.
michael@0 883 messageStore.openCursor().onsuccess = function(event) {
michael@0 884 let cursor = event.target.result;
michael@0 885 if (!cursor) {
michael@0 886 next();
michael@0 887 return;
michael@0 888 }
michael@0 889
michael@0 890 let messageRecord = cursor.value;
michael@0 891 if (messageRecord.type == "mms" &&
michael@0 892 messageRecord.delivery == DELIVERY_SENT) {
michael@0 893 messageRecord.envelopeIdIndex = messageRecord.headers["message-id"];
michael@0 894 cursor.update(messageRecord);
michael@0 895 }
michael@0 896 cursor.continue();
michael@0 897 };
michael@0 898 },
michael@0 899
michael@0 900 /**
michael@0 901 * Replace deliveryStatus by deliveryInfo.
michael@0 902 */
michael@0 903 upgradeSchema12: function(transaction, next) {
michael@0 904 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 905
michael@0 906 messageStore.openCursor().onsuccess = function(event) {
michael@0 907 let cursor = event.target.result;
michael@0 908 if (!cursor) {
michael@0 909 next();
michael@0 910 return;
michael@0 911 }
michael@0 912
michael@0 913 let messageRecord = cursor.value;
michael@0 914 if (messageRecord.type == "mms") {
michael@0 915 messageRecord.deliveryInfo = [];
michael@0 916
michael@0 917 if (messageRecord.deliveryStatus.length == 1 &&
michael@0 918 (messageRecord.delivery == DELIVERY_NOT_DOWNLOADED ||
michael@0 919 messageRecord.delivery == DELIVERY_RECEIVED)) {
michael@0 920 messageRecord.deliveryInfo.push({
michael@0 921 receiver: null,
michael@0 922 deliveryStatus: messageRecord.deliveryStatus[0] });
michael@0 923 } else {
michael@0 924 for (let i = 0; i < messageRecord.deliveryStatus.length; i++) {
michael@0 925 messageRecord.deliveryInfo.push({
michael@0 926 receiver: messageRecord.receivers[i],
michael@0 927 deliveryStatus: messageRecord.deliveryStatus[i] });
michael@0 928 }
michael@0 929 }
michael@0 930 delete messageRecord.deliveryStatus;
michael@0 931 cursor.update(messageRecord);
michael@0 932 }
michael@0 933 cursor.continue();
michael@0 934 };
michael@0 935 },
michael@0 936
michael@0 937 /**
michael@0 938 * Check if we need to re-upgrade the DB schema 12.
michael@0 939 */
michael@0 940 needReUpgradeSchema12: function(transaction, callback) {
michael@0 941 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 942
michael@0 943 messageStore.openCursor().onsuccess = function(event) {
michael@0 944 let cursor = event.target.result;
michael@0 945 if (!cursor) {
michael@0 946 callback(false);
michael@0 947 return;
michael@0 948 }
michael@0 949
michael@0 950 let messageRecord = cursor.value;
michael@0 951 if (messageRecord.type == "mms" &&
michael@0 952 messageRecord.deliveryInfo === undefined) {
michael@0 953 callback(true);
michael@0 954 return;
michael@0 955 }
michael@0 956 cursor.continue();
michael@0 957 };
michael@0 958 },
michael@0 959
michael@0 960 /**
michael@0 961 * Fix the wrong participants.
michael@0 962 */
michael@0 963 upgradeSchema13: function(transaction, next) {
michael@0 964 let participantStore = transaction.objectStore(PARTICIPANT_STORE_NAME);
michael@0 965 let threadStore = transaction.objectStore(THREAD_STORE_NAME);
michael@0 966 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 967 let self = this;
michael@0 968
michael@0 969 let isInvalid = function(participantRecord) {
michael@0 970 let entries = [];
michael@0 971 for (let addr of participantRecord.addresses) {
michael@0 972 entries.push({
michael@0 973 normalized: addr,
michael@0 974 parsed: PhoneNumberUtils.parseWithMCC(addr, null)
michael@0 975 })
michael@0 976 }
michael@0 977 for (let ix = 0 ; ix < entries.length - 1; ix++) {
michael@0 978 let entry1 = entries[ix];
michael@0 979 for (let iy = ix + 1 ; iy < entries.length; iy ++) {
michael@0 980 let entry2 = entries[iy];
michael@0 981 if (!self.matchPhoneNumbers(entry1.normalized, entry1.parsed,
michael@0 982 entry2.normalized, entry2.parsed)) {
michael@0 983 return true;
michael@0 984 }
michael@0 985 }
michael@0 986 }
michael@0 987 return false;
michael@0 988 };
michael@0 989
michael@0 990 let invalidParticipantIds = [];
michael@0 991 participantStore.openCursor().onsuccess = function(event) {
michael@0 992 let cursor = event.target.result;
michael@0 993 if (cursor) {
michael@0 994 let participantRecord = cursor.value;
michael@0 995 // Check if this participant record is valid
michael@0 996 if (isInvalid(participantRecord)) {
michael@0 997 invalidParticipantIds.push(participantRecord.id);
michael@0 998 cursor.delete();
michael@0 999 }
michael@0 1000 cursor.continue();
michael@0 1001 return;
michael@0 1002 }
michael@0 1003
michael@0 1004 // Participant store cursor iteration done.
michael@0 1005 if (!invalidParticipantIds.length) {
michael@0 1006 next();
michael@0 1007 return;
michael@0 1008 }
michael@0 1009
michael@0 1010 // Find affected thread.
michael@0 1011 let wrongThreads = [];
michael@0 1012 threadStore.openCursor().onsuccess = function(event) {
michael@0 1013 let threadCursor = event.target.result;
michael@0 1014 if (threadCursor) {
michael@0 1015 let threadRecord = threadCursor.value;
michael@0 1016 let participantIds = threadRecord.participantIds;
michael@0 1017 let foundInvalid = false;
michael@0 1018 for (let invalidParticipantId of invalidParticipantIds) {
michael@0 1019 if (participantIds.indexOf(invalidParticipantId) != -1) {
michael@0 1020 foundInvalid = true;
michael@0 1021 break;
michael@0 1022 }
michael@0 1023 }
michael@0 1024 if (foundInvalid) {
michael@0 1025 wrongThreads.push(threadRecord.id);
michael@0 1026 threadCursor.delete();
michael@0 1027 }
michael@0 1028 threadCursor.continue();
michael@0 1029 return;
michael@0 1030 }
michael@0 1031
michael@0 1032 if (!wrongThreads.length) {
michael@0 1033 next();
michael@0 1034 return;
michael@0 1035 }
michael@0 1036 // Use recursive function to avoid we add participant twice.
michael@0 1037 (function createUpdateThreadAndParticipant(ix) {
michael@0 1038 let threadId = wrongThreads[ix];
michael@0 1039 let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
michael@0 1040 messageStore.index("threadId").openCursor(range).onsuccess = function(event) {
michael@0 1041 let messageCursor = event.target.result;
michael@0 1042 if (!messageCursor) {
michael@0 1043 ix++;
michael@0 1044 if (ix === wrongThreads.length) {
michael@0 1045 next();
michael@0 1046 return;
michael@0 1047 }
michael@0 1048 createUpdateThreadAndParticipant(ix);
michael@0 1049 return;
michael@0 1050 }
michael@0 1051
michael@0 1052 let messageRecord = messageCursor.value;
michael@0 1053 let timestamp = messageRecord.timestamp;
michael@0 1054 let threadParticipants = [];
michael@0 1055 // Recaculate the thread participants of received message.
michael@0 1056 if (messageRecord.delivery === DELIVERY_RECEIVED ||
michael@0 1057 messageRecord.delivery === DELIVERY_NOT_DOWNLOADED) {
michael@0 1058 threadParticipants.push(messageRecord.sender);
michael@0 1059 if (messageRecord.type == "mms") {
michael@0 1060 this.fillReceivedMmsThreadParticipants(messageRecord, threadParticipants);
michael@0 1061 }
michael@0 1062 }
michael@0 1063 // Recaculate the thread participants of sent messages and error
michael@0 1064 // messages. In error sms messages, we don't have error received sms.
michael@0 1065 // In received MMS, we don't update the error to deliver field but
michael@0 1066 // deliverStatus. So we only consider sent message in DELIVERY_ERROR.
michael@0 1067 else if (messageRecord.delivery === DELIVERY_SENT ||
michael@0 1068 messageRecord.delivery === DELIVERY_ERROR) {
michael@0 1069 if (messageRecord.type == "sms") {
michael@0 1070 threadParticipants = [messageRecord.receiver];
michael@0 1071 } else if (messageRecord.type == "mms") {
michael@0 1072 threadParticipants = messageRecord.receivers;
michael@0 1073 }
michael@0 1074 }
michael@0 1075 self.findThreadRecordByParticipants(threadStore, participantStore,
michael@0 1076 threadParticipants, true,
michael@0 1077 function(threadRecord,
michael@0 1078 participantIds) {
michael@0 1079 if (!participantIds) {
michael@0 1080 debug("participantIds is empty!");
michael@0 1081 return;
michael@0 1082 }
michael@0 1083
michael@0 1084 let timestamp = messageRecord.timestamp;
michael@0 1085 // Setup participantIdsIndex.
michael@0 1086 messageRecord.participantIdsIndex = [];
michael@0 1087 for each (let id in participantIds) {
michael@0 1088 messageRecord.participantIdsIndex.push([id, timestamp]);
michael@0 1089 }
michael@0 1090 if (threadRecord) {
michael@0 1091 let needsUpdate = false;
michael@0 1092
michael@0 1093 if (threadRecord.lastTimestamp <= timestamp) {
michael@0 1094 threadRecord.lastTimestamp = timestamp;
michael@0 1095 threadRecord.subject = messageRecord.body;
michael@0 1096 threadRecord.lastMessageId = messageRecord.id;
michael@0 1097 threadRecord.lastMessageType = messageRecord.type;
michael@0 1098 needsUpdate = true;
michael@0 1099 }
michael@0 1100
michael@0 1101 if (!messageRecord.read) {
michael@0 1102 threadRecord.unreadCount++;
michael@0 1103 needsUpdate = true;
michael@0 1104 }
michael@0 1105
michael@0 1106 if (needsUpdate) {
michael@0 1107 threadStore.put(threadRecord);
michael@0 1108 }
michael@0 1109 messageRecord.threadId = threadRecord.id;
michael@0 1110 messageRecord.threadIdIndex = [threadRecord.id, timestamp];
michael@0 1111 messageCursor.update(messageRecord);
michael@0 1112 messageCursor.continue();
michael@0 1113 return;
michael@0 1114 }
michael@0 1115
michael@0 1116 let threadRecord = {
michael@0 1117 participantIds: participantIds,
michael@0 1118 participantAddresses: threadParticipants,
michael@0 1119 lastMessageId: messageRecord.id,
michael@0 1120 lastTimestamp: timestamp,
michael@0 1121 subject: messageRecord.body,
michael@0 1122 unreadCount: messageRecord.read ? 0 : 1,
michael@0 1123 lastMessageType: messageRecord.type
michael@0 1124 };
michael@0 1125 threadStore.add(threadRecord).onsuccess = function(event) {
michael@0 1126 let threadId = event.target.result;
michael@0 1127 // Setup threadId & threadIdIndex.
michael@0 1128 messageRecord.threadId = threadId;
michael@0 1129 messageRecord.threadIdIndex = [threadId, timestamp];
michael@0 1130 messageCursor.update(messageRecord);
michael@0 1131 messageCursor.continue();
michael@0 1132 };
michael@0 1133 });
michael@0 1134 };
michael@0 1135 })(0);
michael@0 1136 };
michael@0 1137 };
michael@0 1138 },
michael@0 1139
michael@0 1140 /**
michael@0 1141 * Add deliveryTimestamp.
michael@0 1142 */
michael@0 1143 upgradeSchema14: function(transaction, next) {
michael@0 1144 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 1145
michael@0 1146 messageStore.openCursor().onsuccess = function(event) {
michael@0 1147 let cursor = event.target.result;
michael@0 1148 if (!cursor) {
michael@0 1149 next();
michael@0 1150 return;
michael@0 1151 }
michael@0 1152
michael@0 1153 let messageRecord = cursor.value;
michael@0 1154 if (messageRecord.type == "sms") {
michael@0 1155 messageRecord.deliveryTimestamp = 0;
michael@0 1156 } else if (messageRecord.type == "mms") {
michael@0 1157 let deliveryInfo = messageRecord.deliveryInfo;
michael@0 1158 for (let i = 0; i < deliveryInfo.length; i++) {
michael@0 1159 deliveryInfo[i].deliveryTimestamp = 0;
michael@0 1160 }
michael@0 1161 }
michael@0 1162 cursor.update(messageRecord);
michael@0 1163 cursor.continue();
michael@0 1164 };
michael@0 1165 },
michael@0 1166
michael@0 1167 /**
michael@0 1168 * Add ICC ID.
michael@0 1169 */
michael@0 1170 upgradeSchema15: function(transaction, next) {
michael@0 1171 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 1172 messageStore.openCursor().onsuccess = function(event) {
michael@0 1173 let cursor = event.target.result;
michael@0 1174 if (!cursor) {
michael@0 1175 next();
michael@0 1176 return;
michael@0 1177 }
michael@0 1178
michael@0 1179 let messageRecord = cursor.value;
michael@0 1180 messageRecord.iccId = null;
michael@0 1181 cursor.update(messageRecord);
michael@0 1182 cursor.continue();
michael@0 1183 };
michael@0 1184 },
michael@0 1185
michael@0 1186 /**
michael@0 1187 * Add isReadReportSent for incoming MMS.
michael@0 1188 */
michael@0 1189 upgradeSchema16: function(transaction, next) {
michael@0 1190 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 1191
michael@0 1192 // Update type attributes.
michael@0 1193 messageStore.openCursor().onsuccess = function(event) {
michael@0 1194 let cursor = event.target.result;
michael@0 1195 if (!cursor) {
michael@0 1196 next();
michael@0 1197 return;
michael@0 1198 }
michael@0 1199
michael@0 1200 let messageRecord = cursor.value;
michael@0 1201 if (messageRecord.type == "mms") {
michael@0 1202 messageRecord.isReadReportSent = false;
michael@0 1203 cursor.update(messageRecord);
michael@0 1204 }
michael@0 1205 cursor.continue();
michael@0 1206 };
michael@0 1207 },
michael@0 1208
michael@0 1209 upgradeSchema17: function(transaction, next) {
michael@0 1210 let threadStore = transaction.objectStore(THREAD_STORE_NAME);
michael@0 1211 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 1212
michael@0 1213 // Add 'lastMessageSubject' to each thread record.
michael@0 1214 threadStore.openCursor().onsuccess = function(event) {
michael@0 1215 let cursor = event.target.result;
michael@0 1216 if (!cursor) {
michael@0 1217 next();
michael@0 1218 return;
michael@0 1219 }
michael@0 1220
michael@0 1221 let threadRecord = cursor.value;
michael@0 1222 // We have defined 'threadRecord.subject' in upgradeSchema7(), but it
michael@0 1223 // actually means 'threadRecord.body'. Swap the two values first.
michael@0 1224 threadRecord.body = threadRecord.subject;
michael@0 1225 delete threadRecord.subject;
michael@0 1226
michael@0 1227 // Only MMS supports subject so assign null for non-MMS one.
michael@0 1228 if (threadRecord.lastMessageType != "mms") {
michael@0 1229 threadRecord.lastMessageSubject = null;
michael@0 1230 cursor.update(threadRecord);
michael@0 1231
michael@0 1232 cursor.continue();
michael@0 1233 return;
michael@0 1234 }
michael@0 1235
michael@0 1236 messageStore.get(threadRecord.lastMessageId).onsuccess = function(event) {
michael@0 1237 let messageRecord = event.target.result;
michael@0 1238 let subject = messageRecord.headers.subject;
michael@0 1239 threadRecord.lastMessageSubject = subject || null;
michael@0 1240 cursor.update(threadRecord);
michael@0 1241
michael@0 1242 cursor.continue();
michael@0 1243 };
michael@0 1244 };
michael@0 1245 },
michael@0 1246
michael@0 1247 /**
michael@0 1248 * Add pid for incoming SMS.
michael@0 1249 */
michael@0 1250 upgradeSchema18: function(transaction, next) {
michael@0 1251 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 1252
michael@0 1253 messageStore.openCursor().onsuccess = function(event) {
michael@0 1254 let cursor = event.target.result;
michael@0 1255 if (!cursor) {
michael@0 1256 next();
michael@0 1257 return;
michael@0 1258 }
michael@0 1259
michael@0 1260 let messageRecord = cursor.value;
michael@0 1261 if (messageRecord.type == "sms") {
michael@0 1262 messageRecord.pid = RIL.PDU_PID_DEFAULT;
michael@0 1263 cursor.update(messageRecord);
michael@0 1264 }
michael@0 1265 cursor.continue();
michael@0 1266 };
michael@0 1267 },
michael@0 1268
michael@0 1269 /**
michael@0 1270 * Add readStatus and readTimestamp.
michael@0 1271 */
michael@0 1272 upgradeSchema19: function(transaction, next) {
michael@0 1273 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 1274 messageStore.openCursor().onsuccess = function(event) {
michael@0 1275 let cursor = event.target.result;
michael@0 1276 if (!cursor) {
michael@0 1277 next();
michael@0 1278 return;
michael@0 1279 }
michael@0 1280
michael@0 1281 let messageRecord = cursor.value;
michael@0 1282 if (messageRecord.type == "sms") {
michael@0 1283 cursor.continue();
michael@0 1284 return;
michael@0 1285 }
michael@0 1286
michael@0 1287 // We can always retrieve transaction id from
michael@0 1288 // |messageRecord.headers["x-mms-transaction-id"]|.
michael@0 1289 if (messageRecord.hasOwnProperty("transactionId")) {
michael@0 1290 delete messageRecord.transactionId;
michael@0 1291 }
michael@0 1292
michael@0 1293 // xpconnect gives "undefined" for an unassigned argument of an interface
michael@0 1294 // method.
michael@0 1295 if (messageRecord.envelopeIdIndex === "undefined") {
michael@0 1296 delete messageRecord.envelopeIdIndex;
michael@0 1297 }
michael@0 1298
michael@0 1299 // Convert some header fields that were originally decoded as BooleanValue
michael@0 1300 // to numeric enums.
michael@0 1301 for (let field of ["x-mms-cancel-status",
michael@0 1302 "x-mms-sender-visibility",
michael@0 1303 "x-mms-read-status"]) {
michael@0 1304 let value = messageRecord.headers[field];
michael@0 1305 if (value !== undefined) {
michael@0 1306 messageRecord.headers[field] = value ? 128 : 129;
michael@0 1307 }
michael@0 1308 }
michael@0 1309
michael@0 1310 // For all sent and received MMS messages, we have to add their
michael@0 1311 // |readStatus| and |readTimestamp| attributes in |deliveryInfo| array.
michael@0 1312 let readReportRequested =
michael@0 1313 messageRecord.headers["x-mms-read-report"] || false;
michael@0 1314 for (let element of messageRecord.deliveryInfo) {
michael@0 1315 element.readStatus = readReportRequested
michael@0 1316 ? MMS.DOM_READ_STATUS_PENDING
michael@0 1317 : MMS.DOM_READ_STATUS_NOT_APPLICABLE;
michael@0 1318 element.readTimestamp = 0;
michael@0 1319 }
michael@0 1320
michael@0 1321 cursor.update(messageRecord);
michael@0 1322 cursor.continue();
michael@0 1323 };
michael@0 1324 },
michael@0 1325
michael@0 1326 /**
michael@0 1327 * Add sentTimestamp.
michael@0 1328 */
michael@0 1329 upgradeSchema20: function(transaction, next) {
michael@0 1330 let messageStore = transaction.objectStore(MESSAGE_STORE_NAME);
michael@0 1331 messageStore.openCursor().onsuccess = function(event) {
michael@0 1332 let cursor = event.target.result;
michael@0 1333 if (!cursor) {
michael@0 1334 next();
michael@0 1335 return;
michael@0 1336 }
michael@0 1337
michael@0 1338 let messageRecord = cursor.value;
michael@0 1339 messageRecord.sentTimestamp = 0;
michael@0 1340
michael@0 1341 // We can still have changes to assign |sentTimestamp| for the existing
michael@0 1342 // MMS message records.
michael@0 1343 if (messageRecord.type == "mms" && messageRecord.headers["date"]) {
michael@0 1344 messageRecord.sentTimestamp = messageRecord.headers["date"].getTime();
michael@0 1345 }
michael@0 1346
michael@0 1347 cursor.update(messageRecord);
michael@0 1348 cursor.continue();
michael@0 1349 };
michael@0 1350 },
michael@0 1351
michael@0 1352 /**
michael@0 1353 * Add smsSegmentStore to store uncomplete SMS segments.
michael@0 1354 */
michael@0 1355 upgradeSchema21: function(db, transaction, next) {
michael@0 1356 /**
michael@0 1357 * This smsSegmentStore is used to store uncomplete SMS segments.
michael@0 1358 * Each entry looks like this:
michael@0 1359 *
michael@0 1360 * {
michael@0 1361 * [Common fields in SMS segment]
michael@0 1362 * messageType: <Number>,
michael@0 1363 * teleservice: <Number>,
michael@0 1364 * SMSC: <String>,
michael@0 1365 * sentTimestamp: <Number>,
michael@0 1366 * timestamp: <Number>,
michael@0 1367 * sender: <String>,
michael@0 1368 * pid: <Number>,
michael@0 1369 * encoding: <Number>,
michael@0 1370 * messageClass: <String>,
michael@0 1371 * iccId: <String>,
michael@0 1372 *
michael@0 1373 * [Concatenation Info]
michael@0 1374 * segmentRef: <Number>,
michael@0 1375 * segmentSeq: <Number>,
michael@0 1376 * segmentMaxSeq: <Number>,
michael@0 1377 *
michael@0 1378 * [Application Port Info]
michael@0 1379 * originatorPort: <Number>,
michael@0 1380 * destinationPort: <Number>,
michael@0 1381 *
michael@0 1382 * [MWI status]
michael@0 1383 * mwiPresent: <Boolean>,
michael@0 1384 * mwiDiscard: <Boolean>,
michael@0 1385 * mwiMsgCount: <Number>,
michael@0 1386 * mwiActive: <Boolean>,
michael@0 1387 *
michael@0 1388 * [CDMA Cellbroadcast related fields]
michael@0 1389 * serviceCategory: <Number>,
michael@0 1390 * language: <String>,
michael@0 1391 *
michael@0 1392 * [Message Body]
michael@0 1393 * data: <Uint8Array>, (available if it's 8bit encoding)
michael@0 1394 * body: <String>, (normal text body)
michael@0 1395 *
michael@0 1396 * [Handy fields created by DB for concatenation]
michael@0 1397 * id: <Number>, keypath of this objectStore.
michael@0 1398 * hash: <String>, Use to identify the segments to the same SMS.
michael@0 1399 * receivedSegments: <Number>,
michael@0 1400 * segments: []
michael@0 1401 * }
michael@0 1402 *
michael@0 1403 */
michael@0 1404 let smsSegmentStore = db.createObjectStore(SMS_SEGMENT_STORE_NAME,
michael@0 1405 { keyPath: "id",
michael@0 1406 autoIncrement: true });
michael@0 1407 smsSegmentStore.createIndex("hash", "hash", { unique: true });
michael@0 1408 next();
michael@0 1409 },
michael@0 1410
michael@0 1411 matchParsedPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
michael@0 1412 if ((parsedAddr1.internationalNumber &&
michael@0 1413 parsedAddr1.internationalNumber === parsedAddr2.internationalNumber) ||
michael@0 1414 (parsedAddr1.nationalNumber &&
michael@0 1415 parsedAddr1.nationalNumber === parsedAddr2.nationalNumber)) {
michael@0 1416 return true;
michael@0 1417 }
michael@0 1418
michael@0 1419 if (parsedAddr1.countryName != parsedAddr2.countryName) {
michael@0 1420 return false;
michael@0 1421 }
michael@0 1422
michael@0 1423 let ssPref = "dom.phonenumber.substringmatching." + parsedAddr1.countryName;
michael@0 1424 if (Services.prefs.getPrefType(ssPref) != Ci.nsIPrefBranch.PREF_INT) {
michael@0 1425 return false;
michael@0 1426 }
michael@0 1427
michael@0 1428 let val = Services.prefs.getIntPref(ssPref);
michael@0 1429 return addr1.length > val &&
michael@0 1430 addr2.length > val &&
michael@0 1431 addr1.slice(-val) === addr2.slice(-val);
michael@0 1432 },
michael@0 1433
michael@0 1434 matchPhoneNumbers: function(addr1, parsedAddr1, addr2, parsedAddr2) {
michael@0 1435 if (parsedAddr1 && parsedAddr2) {
michael@0 1436 return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
michael@0 1437 }
michael@0 1438
michael@0 1439 if (parsedAddr1) {
michael@0 1440 parsedAddr2 = PhoneNumberUtils.parseWithCountryName(addr2, parsedAddr1.countryName);
michael@0 1441 if (parsedAddr2) {
michael@0 1442 return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
michael@0 1443 }
michael@0 1444
michael@0 1445 return false;
michael@0 1446 }
michael@0 1447
michael@0 1448 if (parsedAddr2) {
michael@0 1449 parsedAddr1 = PhoneNumberUtils.parseWithCountryName(addr1, parsedAddr2.countryName);
michael@0 1450 if (parsedAddr1) {
michael@0 1451 return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
michael@0 1452 }
michael@0 1453 }
michael@0 1454
michael@0 1455 return false;
michael@0 1456 },
michael@0 1457
michael@0 1458 createDomMessageFromRecord: function(aMessageRecord) {
michael@0 1459 if (DEBUG) {
michael@0 1460 debug("createDomMessageFromRecord: " + JSON.stringify(aMessageRecord));
michael@0 1461 }
michael@0 1462 if (aMessageRecord.type == "sms") {
michael@0 1463 return gMobileMessageService.createSmsMessage(aMessageRecord.id,
michael@0 1464 aMessageRecord.threadId,
michael@0 1465 aMessageRecord.iccId,
michael@0 1466 aMessageRecord.delivery,
michael@0 1467 aMessageRecord.deliveryStatus,
michael@0 1468 aMessageRecord.sender,
michael@0 1469 aMessageRecord.receiver,
michael@0 1470 aMessageRecord.body,
michael@0 1471 aMessageRecord.messageClass,
michael@0 1472 aMessageRecord.timestamp,
michael@0 1473 aMessageRecord.sentTimestamp,
michael@0 1474 aMessageRecord.deliveryTimestamp,
michael@0 1475 aMessageRecord.read);
michael@0 1476 } else if (aMessageRecord.type == "mms") {
michael@0 1477 let headers = aMessageRecord["headers"];
michael@0 1478 if (DEBUG) {
michael@0 1479 debug("MMS: headers: " + JSON.stringify(headers));
michael@0 1480 }
michael@0 1481
michael@0 1482 let subject = headers["subject"];
michael@0 1483 if (subject == undefined) {
michael@0 1484 subject = "";
michael@0 1485 }
michael@0 1486
michael@0 1487 let smil = "";
michael@0 1488 let attachments = [];
michael@0 1489 let parts = aMessageRecord.parts;
michael@0 1490 if (parts) {
michael@0 1491 for (let i = 0; i < parts.length; i++) {
michael@0 1492 let part = parts[i];
michael@0 1493 if (DEBUG) {
michael@0 1494 debug("MMS: part[" + i + "]: " + JSON.stringify(part));
michael@0 1495 }
michael@0 1496 // Sometimes the part is incomplete because the device reboots when
michael@0 1497 // downloading MMS. Don't need to expose this part to the content.
michael@0 1498 if (!part) {
michael@0 1499 continue;
michael@0 1500 }
michael@0 1501
michael@0 1502 let partHeaders = part["headers"];
michael@0 1503 let partContent = part["content"];
michael@0 1504 // Don't need to make the SMIL part if it's present.
michael@0 1505 if (partHeaders["content-type"]["media"] == "application/smil") {
michael@0 1506 smil = partContent;
michael@0 1507 continue;
michael@0 1508 }
michael@0 1509 attachments.push({
michael@0 1510 "id": partHeaders["content-id"],
michael@0 1511 "location": partHeaders["content-location"],
michael@0 1512 "content": partContent
michael@0 1513 });
michael@0 1514 }
michael@0 1515 }
michael@0 1516 let expiryDate = 0;
michael@0 1517 if (headers["x-mms-expiry"] != undefined) {
michael@0 1518 expiryDate = aMessageRecord.timestamp + headers["x-mms-expiry"] * 1000;
michael@0 1519 }
michael@0 1520 let readReportRequested = headers["x-mms-read-report"] || false;
michael@0 1521 return gMobileMessageService.createMmsMessage(aMessageRecord.id,
michael@0 1522 aMessageRecord.threadId,
michael@0 1523 aMessageRecord.iccId,
michael@0 1524 aMessageRecord.delivery,
michael@0 1525 aMessageRecord.deliveryInfo,
michael@0 1526 aMessageRecord.sender,
michael@0 1527 aMessageRecord.receivers,
michael@0 1528 aMessageRecord.timestamp,
michael@0 1529 aMessageRecord.sentTimestamp,
michael@0 1530 aMessageRecord.read,
michael@0 1531 subject,
michael@0 1532 smil,
michael@0 1533 attachments,
michael@0 1534 expiryDate,
michael@0 1535 readReportRequested);
michael@0 1536 }
michael@0 1537 },
michael@0 1538
michael@0 1539 findParticipantRecordByAddress: function(aParticipantStore, aAddress,
michael@0 1540 aCreate, aCallback) {
michael@0 1541 if (DEBUG) {
michael@0 1542 debug("findParticipantRecordByAddress("
michael@0 1543 + JSON.stringify(aAddress) + ", " + aCreate + ")");
michael@0 1544 }
michael@0 1545
michael@0 1546 // Two types of input number to match here, international(+886987654321),
michael@0 1547 // and local(0987654321) types. The "nationalNumber" parsed from
michael@0 1548 // phonenumberutils will be "987654321" in this case.
michael@0 1549
michael@0 1550 // Normalize address before searching for participant record.
michael@0 1551 let normalizedAddress = PhoneNumberUtils.normalize(aAddress, false);
michael@0 1552 let allPossibleAddresses = [normalizedAddress];
michael@0 1553 let parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
michael@0 1554 if (parsedAddress && parsedAddress.internationalNumber &&
michael@0 1555 allPossibleAddresses.indexOf(parsedAddress.internationalNumber) < 0) {
michael@0 1556 // We only stores international numbers into participant store because
michael@0 1557 // the parsed national number doesn't contain country info and may
michael@0 1558 // duplicate in different country.
michael@0 1559 allPossibleAddresses.push(parsedAddress.internationalNumber);
michael@0 1560 }
michael@0 1561 if (DEBUG) {
michael@0 1562 debug("findParticipantRecordByAddress: allPossibleAddresses = " +
michael@0 1563 JSON.stringify(allPossibleAddresses));
michael@0 1564 }
michael@0 1565
michael@0 1566 // Make a copy here because we may need allPossibleAddresses again.
michael@0 1567 let needles = allPossibleAddresses.slice(0);
michael@0 1568 let request = aParticipantStore.index("addresses").get(needles.pop());
michael@0 1569 request.onsuccess = (function onsuccess(event) {
michael@0 1570 let participantRecord = event.target.result;
michael@0 1571 // 1) First try matching through "addresses" index of participant store.
michael@0 1572 // If we're lucky, return the fetched participant record.
michael@0 1573 if (participantRecord) {
michael@0 1574 if (DEBUG) {
michael@0 1575 debug("findParticipantRecordByAddress: got "
michael@0 1576 + JSON.stringify(participantRecord));
michael@0 1577 }
michael@0 1578 aCallback(participantRecord);
michael@0 1579 return;
michael@0 1580 }
michael@0 1581
michael@0 1582 // Try next possible address again.
michael@0 1583 if (needles.length) {
michael@0 1584 let request = aParticipantStore.index("addresses").get(needles.pop());
michael@0 1585 request.onsuccess = onsuccess.bind(this);
michael@0 1586 return;
michael@0 1587 }
michael@0 1588
michael@0 1589 // 2) Traverse throught all participants and check all alias addresses.
michael@0 1590 aParticipantStore.openCursor().onsuccess = (function(event) {
michael@0 1591 let cursor = event.target.result;
michael@0 1592 if (!cursor) {
michael@0 1593 // Have traversed whole object store but still in vain.
michael@0 1594 if (!aCreate) {
michael@0 1595 aCallback(null);
michael@0 1596 return;
michael@0 1597 }
michael@0 1598
michael@0 1599 let participantRecord = { addresses: [normalizedAddress] };
michael@0 1600 let addRequest = aParticipantStore.add(participantRecord);
michael@0 1601 addRequest.onsuccess = function(event) {
michael@0 1602 participantRecord.id = event.target.result;
michael@0 1603 if (DEBUG) {
michael@0 1604 debug("findParticipantRecordByAddress: created "
michael@0 1605 + JSON.stringify(participantRecord));
michael@0 1606 }
michael@0 1607 aCallback(participantRecord);
michael@0 1608 };
michael@0 1609 return;
michael@0 1610 }
michael@0 1611
michael@0 1612 let participantRecord = cursor.value;
michael@0 1613 for (let storedAddress of participantRecord.addresses) {
michael@0 1614 let parsedStoredAddress = PhoneNumberUtils.parseWithMCC(storedAddress, null);
michael@0 1615 let match = this.matchPhoneNumbers(normalizedAddress, parsedAddress,
michael@0 1616 storedAddress, parsedStoredAddress);
michael@0 1617 if (!match) {
michael@0 1618 // 3) Else we fail to match current stored participant record.
michael@0 1619 continue;
michael@0 1620 }
michael@0 1621 // Match!
michael@0 1622 if (aCreate) {
michael@0 1623 // In a READ-WRITE transaction, append one more possible address for
michael@0 1624 // this participant record.
michael@0 1625 participantRecord.addresses =
michael@0 1626 participantRecord.addresses.concat(allPossibleAddresses);
michael@0 1627 cursor.update(participantRecord);
michael@0 1628 }
michael@0 1629
michael@0 1630 if (DEBUG) {
michael@0 1631 debug("findParticipantRecordByAddress: match "
michael@0 1632 + JSON.stringify(cursor.value));
michael@0 1633 }
michael@0 1634 aCallback(participantRecord);
michael@0 1635 return;
michael@0 1636 }
michael@0 1637
michael@0 1638 // Check next participant record if available.
michael@0 1639 cursor.continue();
michael@0 1640 }).bind(this);
michael@0 1641 }).bind(this);
michael@0 1642 },
michael@0 1643
michael@0 1644 findParticipantIdsByAddresses: function(aParticipantStore, aAddresses,
michael@0 1645 aCreate, aSkipNonexistent, aCallback) {
michael@0 1646 if (DEBUG) {
michael@0 1647 debug("findParticipantIdsByAddresses("
michael@0 1648 + JSON.stringify(aAddresses) + ", "
michael@0 1649 + aCreate + ", " + aSkipNonexistent + ")");
michael@0 1650 }
michael@0 1651
michael@0 1652 if (!aAddresses || !aAddresses.length) {
michael@0 1653 if (DEBUG) debug("findParticipantIdsByAddresses: returning null");
michael@0 1654 aCallback(null);
michael@0 1655 return;
michael@0 1656 }
michael@0 1657
michael@0 1658 let self = this;
michael@0 1659 (function findParticipantId(index, result) {
michael@0 1660 if (index >= aAddresses.length) {
michael@0 1661 // Sort numerically.
michael@0 1662 result.sort(function(a, b) {
michael@0 1663 return a - b;
michael@0 1664 });
michael@0 1665 if (DEBUG) debug("findParticipantIdsByAddresses: returning " + result);
michael@0 1666 aCallback(result);
michael@0 1667 return;
michael@0 1668 }
michael@0 1669
michael@0 1670 self.findParticipantRecordByAddress(aParticipantStore,
michael@0 1671 aAddresses[index++], aCreate,
michael@0 1672 function(participantRecord) {
michael@0 1673 if (!participantRecord) {
michael@0 1674 if (!aSkipNonexistent) {
michael@0 1675 if (DEBUG) debug("findParticipantIdsByAddresses: returning null");
michael@0 1676 aCallback(null);
michael@0 1677 return;
michael@0 1678 }
michael@0 1679 } else if (result.indexOf(participantRecord.id) < 0) {
michael@0 1680 result.push(participantRecord.id);
michael@0 1681 }
michael@0 1682 findParticipantId(index, result);
michael@0 1683 });
michael@0 1684 }) (0, []);
michael@0 1685 },
michael@0 1686
michael@0 1687 findThreadRecordByParticipants: function(aThreadStore, aParticipantStore,
michael@0 1688 aAddresses, aCreateParticipants,
michael@0 1689 aCallback) {
michael@0 1690 if (DEBUG) {
michael@0 1691 debug("findThreadRecordByParticipants(" + JSON.stringify(aAddresses)
michael@0 1692 + ", " + aCreateParticipants + ")");
michael@0 1693 }
michael@0 1694 this.findParticipantIdsByAddresses(aParticipantStore, aAddresses,
michael@0 1695 aCreateParticipants, false,
michael@0 1696 function(participantIds) {
michael@0 1697 if (!participantIds) {
michael@0 1698 if (DEBUG) debug("findThreadRecordByParticipants: returning null");
michael@0 1699 aCallback(null, null);
michael@0 1700 return;
michael@0 1701 }
michael@0 1702 // Find record from thread store.
michael@0 1703 let request = aThreadStore.index("participantIds").get(participantIds);
michael@0 1704 request.onsuccess = function(event) {
michael@0 1705 let threadRecord = event.target.result;
michael@0 1706 if (DEBUG) {
michael@0 1707 debug("findThreadRecordByParticipants: return "
michael@0 1708 + JSON.stringify(threadRecord));
michael@0 1709 }
michael@0 1710 aCallback(threadRecord, participantIds);
michael@0 1711 };
michael@0 1712 });
michael@0 1713 },
michael@0 1714
michael@0 1715 newTxnWithCallback: function(aCallback, aFunc, aStoreNames) {
michael@0 1716 let self = this;
michael@0 1717 this.newTxn(READ_WRITE, function(aError, aTransaction, aStores) {
michael@0 1718 let notifyResult = function(aRv, aMessageRecord) {
michael@0 1719 if (!aCallback) {
michael@0 1720 return;
michael@0 1721 }
michael@0 1722 let domMessage =
michael@0 1723 aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
michael@0 1724 aCallback.notify(aRv, domMessage);
michael@0 1725 };
michael@0 1726
michael@0 1727 if (aError) {
michael@0 1728 notifyResult(aError, null);
michael@0 1729 return;
michael@0 1730 }
michael@0 1731
michael@0 1732 let capture = {};
michael@0 1733 aTransaction.oncomplete = function(event) {
michael@0 1734 notifyResult(Cr.NS_OK, capture.messageRecord);
michael@0 1735 };
michael@0 1736 aTransaction.onabort = function(event) {
michael@0 1737 // TODO bug 832140 check event.target.errorCode
michael@0 1738 notifyResult(Cr.NS_ERROR_FAILURE, null);
michael@0 1739 };
michael@0 1740
michael@0 1741 aFunc(capture, aStores);
michael@0 1742 }, aStoreNames);
michael@0 1743 },
michael@0 1744
michael@0 1745 saveRecord: function(aMessageRecord, aAddresses, aCallback) {
michael@0 1746 if (DEBUG) debug("Going to store " + JSON.stringify(aMessageRecord));
michael@0 1747
michael@0 1748 let self = this;
michael@0 1749 this.newTxn(READ_WRITE, function(error, txn, stores) {
michael@0 1750 let notifyResult = function(aRv, aMessageRecord) {
michael@0 1751 if (!aCallback) {
michael@0 1752 return;
michael@0 1753 }
michael@0 1754 let domMessage =
michael@0 1755 aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
michael@0 1756 aCallback.notify(aRv, domMessage);
michael@0 1757 };
michael@0 1758
michael@0 1759 if (error) {
michael@0 1760 notifyResult(error, null);
michael@0 1761 return;
michael@0 1762 }
michael@0 1763
michael@0 1764 txn.oncomplete = function oncomplete(event) {
michael@0 1765 if (aMessageRecord.id > self.lastMessageId) {
michael@0 1766 self.lastMessageId = aMessageRecord.id;
michael@0 1767 }
michael@0 1768 notifyResult(Cr.NS_OK, aMessageRecord);
michael@0 1769 };
michael@0 1770 txn.onabort = function onabort(event) {
michael@0 1771 // TODO bug 832140 check event.target.errorCode
michael@0 1772 notifyResult(Cr.NS_ERROR_FAILURE, null);
michael@0 1773 };
michael@0 1774
michael@0 1775 let messageStore = stores[0];
michael@0 1776 let participantStore = stores[1];
michael@0 1777 let threadStore = stores[2];
michael@0 1778 self.replaceShortMessageOnSave(txn, messageStore, participantStore,
michael@0 1779 threadStore, aMessageRecord, aAddresses);
michael@0 1780 }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME, THREAD_STORE_NAME]);
michael@0 1781 },
michael@0 1782
michael@0 1783 replaceShortMessageOnSave: function(aTransaction, aMessageStore,
michael@0 1784 aParticipantStore, aThreadStore,
michael@0 1785 aMessageRecord, aAddresses) {
michael@0 1786 let isReplaceTypePid = (aMessageRecord.pid) &&
michael@0 1787 ((aMessageRecord.pid >= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_1 &&
michael@0 1788 aMessageRecord.pid <= RIL.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_7) ||
michael@0 1789 aMessageRecord.pid == RIL.PDU_PID_RETURN_CALL_MESSAGE);
michael@0 1790
michael@0 1791 if (aMessageRecord.type != "sms" ||
michael@0 1792 aMessageRecord.delivery != DELIVERY_RECEIVED ||
michael@0 1793 !isReplaceTypePid) {
michael@0 1794 this.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
michael@0 1795 aThreadStore, aMessageRecord, aAddresses);
michael@0 1796 return;
michael@0 1797 }
michael@0 1798
michael@0 1799 // 3GPP TS 23.040 subclause 9.2.3.9 "TP-Protocol-Identifier (TP-PID)":
michael@0 1800 //
michael@0 1801 // ... the MS shall check the originating address and replace any
michael@0 1802 // existing stored message having the same Protocol Identifier code
michael@0 1803 // and originating address with the new short message and other
michael@0 1804 // parameter values. If there is no message to be replaced, the MS
michael@0 1805 // shall store the message in the normal way. ... it is recommended
michael@0 1806 // that the SC address should not be checked by the MS."
michael@0 1807 let self = this;
michael@0 1808 this.findParticipantRecordByAddress(aParticipantStore,
michael@0 1809 aMessageRecord.sender, false,
michael@0 1810 function(participantRecord) {
michael@0 1811 if (!participantRecord) {
michael@0 1812 self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
michael@0 1813 aThreadStore, aMessageRecord, aAddresses);
michael@0 1814 return;
michael@0 1815 }
michael@0 1816
michael@0 1817 let participantId = participantRecord.id;
michael@0 1818 let range = IDBKeyRange.bound([participantId, 0], [participantId, ""]);
michael@0 1819 let request = aMessageStore.index("participantIds").openCursor(range);
michael@0 1820 request.onsuccess = function onsuccess(event) {
michael@0 1821 let cursor = event.target.result;
michael@0 1822 if (!cursor) {
michael@0 1823 self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
michael@0 1824 aThreadStore, aMessageRecord, aAddresses);
michael@0 1825 return;
michael@0 1826 }
michael@0 1827
michael@0 1828 // A message record with same participantId found.
michael@0 1829 // Verify matching criteria.
michael@0 1830 let foundMessageRecord = cursor.value;
michael@0 1831 if (foundMessageRecord.type != "sms" ||
michael@0 1832 foundMessageRecord.sender != aMessageRecord.sender ||
michael@0 1833 foundMessageRecord.pid != aMessageRecord.pid) {
michael@0 1834 cursor.continue();
michael@0 1835 return;
michael@0 1836 }
michael@0 1837
michael@0 1838 // Match! Now replace that found message record with current one.
michael@0 1839 aMessageRecord.id = foundMessageRecord.id;
michael@0 1840 self.realSaveRecord(aTransaction, aMessageStore, aParticipantStore,
michael@0 1841 aThreadStore, aMessageRecord, aAddresses);
michael@0 1842 };
michael@0 1843 });
michael@0 1844 },
michael@0 1845
michael@0 1846 realSaveRecord: function(aTransaction, aMessageStore, aParticipantStore,
michael@0 1847 aThreadStore, aMessageRecord, aAddresses) {
michael@0 1848 let self = this;
michael@0 1849 this.findThreadRecordByParticipants(aThreadStore, aParticipantStore,
michael@0 1850 aAddresses, true,
michael@0 1851 function(threadRecord, participantIds) {
michael@0 1852 if (!participantIds) {
michael@0 1853 aTransaction.abort();
michael@0 1854 return;
michael@0 1855 }
michael@0 1856
michael@0 1857 let isOverriding = (aMessageRecord.id !== undefined);
michael@0 1858 if (!isOverriding) {
michael@0 1859 // |self.lastMessageId| is only updated in |txn.oncomplete|.
michael@0 1860 aMessageRecord.id = self.lastMessageId + 1;
michael@0 1861 }
michael@0 1862
michael@0 1863 let timestamp = aMessageRecord.timestamp;
michael@0 1864 let insertMessageRecord = function(threadId) {
michael@0 1865 // Setup threadId & threadIdIndex.
michael@0 1866 aMessageRecord.threadId = threadId;
michael@0 1867 aMessageRecord.threadIdIndex = [threadId, timestamp];
michael@0 1868 // Setup participantIdsIndex.
michael@0 1869 aMessageRecord.participantIdsIndex = [];
michael@0 1870 for each (let id in participantIds) {
michael@0 1871 aMessageRecord.participantIdsIndex.push([id, timestamp]);
michael@0 1872 }
michael@0 1873
michael@0 1874 if (!isOverriding) {
michael@0 1875 // Really add to message store.
michael@0 1876 aMessageStore.put(aMessageRecord);
michael@0 1877 return;
michael@0 1878 }
michael@0 1879
michael@0 1880 // If we're going to override an old message, we need to update the
michael@0 1881 // info of the original thread containing the overridden message.
michael@0 1882 // To get the original thread ID and read status of the overridden
michael@0 1883 // message record, we need to retrieve it before overriding it.
michael@0 1884 aMessageStore.get(aMessageRecord.id).onsuccess = function(event) {
michael@0 1885 let oldMessageRecord = event.target.result;
michael@0 1886 aMessageStore.put(aMessageRecord);
michael@0 1887 if (oldMessageRecord) {
michael@0 1888 self.updateThreadByMessageChange(aMessageStore,
michael@0 1889 aThreadStore,
michael@0 1890 oldMessageRecord.threadId,
michael@0 1891 aMessageRecord.id,
michael@0 1892 oldMessageRecord.read);
michael@0 1893 }
michael@0 1894 };
michael@0 1895 };
michael@0 1896
michael@0 1897 if (threadRecord) {
michael@0 1898 let needsUpdate = false;
michael@0 1899
michael@0 1900 if (threadRecord.lastTimestamp <= timestamp) {
michael@0 1901 let lastMessageSubject;
michael@0 1902 if (aMessageRecord.type == "mms") {
michael@0 1903 lastMessageSubject = aMessageRecord.headers.subject;
michael@0 1904 }
michael@0 1905 threadRecord.lastMessageSubject = lastMessageSubject || null;
michael@0 1906 threadRecord.lastTimestamp = timestamp;
michael@0 1907 threadRecord.body = aMessageRecord.body;
michael@0 1908 threadRecord.lastMessageId = aMessageRecord.id;
michael@0 1909 threadRecord.lastMessageType = aMessageRecord.type;
michael@0 1910 needsUpdate = true;
michael@0 1911 }
michael@0 1912
michael@0 1913 if (!aMessageRecord.read) {
michael@0 1914 threadRecord.unreadCount++;
michael@0 1915 needsUpdate = true;
michael@0 1916 }
michael@0 1917
michael@0 1918 if (needsUpdate) {
michael@0 1919 aThreadStore.put(threadRecord);
michael@0 1920 }
michael@0 1921
michael@0 1922 insertMessageRecord(threadRecord.id);
michael@0 1923 return;
michael@0 1924 }
michael@0 1925
michael@0 1926 let lastMessageSubject;
michael@0 1927 if (aMessageRecord.type == "mms") {
michael@0 1928 lastMessageSubject = aMessageRecord.headers.subject;
michael@0 1929 }
michael@0 1930
michael@0 1931 threadRecord = {
michael@0 1932 participantIds: participantIds,
michael@0 1933 participantAddresses: aAddresses,
michael@0 1934 lastMessageId: aMessageRecord.id,
michael@0 1935 lastTimestamp: timestamp,
michael@0 1936 lastMessageSubject: lastMessageSubject || null,
michael@0 1937 body: aMessageRecord.body,
michael@0 1938 unreadCount: aMessageRecord.read ? 0 : 1,
michael@0 1939 lastMessageType: aMessageRecord.type,
michael@0 1940 };
michael@0 1941 aThreadStore.add(threadRecord).onsuccess = function(event) {
michael@0 1942 let threadId = event.target.result;
michael@0 1943 insertMessageRecord(threadId);
michael@0 1944 };
michael@0 1945 });
michael@0 1946 },
michael@0 1947
michael@0 1948 forEachMatchedMmsDeliveryInfo: function(aDeliveryInfo, aNeedle, aCallback) {
michael@0 1949
michael@0 1950 let typedAddress = {
michael@0 1951 type: MMS.Address.resolveType(aNeedle),
michael@0 1952 address: aNeedle
michael@0 1953 };
michael@0 1954 let normalizedAddress, parsedAddress;
michael@0 1955 if (typedAddress.type === "PLMN") {
michael@0 1956 normalizedAddress = PhoneNumberUtils.normalize(aNeedle, false);
michael@0 1957 parsedAddress = PhoneNumberUtils.parse(normalizedAddress);
michael@0 1958 }
michael@0 1959
michael@0 1960 for (let element of aDeliveryInfo) {
michael@0 1961 let typedStoredAddress = {
michael@0 1962 type: MMS.Address.resolveType(element.receiver),
michael@0 1963 address: element.receiver
michael@0 1964 };
michael@0 1965 if (typedAddress.type !== typedStoredAddress.type) {
michael@0 1966 // Not even my type. Skip.
michael@0 1967 continue;
michael@0 1968 }
michael@0 1969
michael@0 1970 if (typedAddress.address == typedStoredAddress.address) {
michael@0 1971 // Have a direct match.
michael@0 1972 aCallback(element);
michael@0 1973 continue;
michael@0 1974 }
michael@0 1975
michael@0 1976 if (typedAddress.type !== "PLMN") {
michael@0 1977 // Address type other than "PLMN" must have direct match. Or, skip.
michael@0 1978 continue;
michael@0 1979 }
michael@0 1980
michael@0 1981 // Both are of "PLMN" type.
michael@0 1982 let normalizedStoredAddress =
michael@0 1983 PhoneNumberUtils.normalize(element.receiver, false);
michael@0 1984 let parsedStoredAddress =
michael@0 1985 PhoneNumberUtils.parseWithMCC(normalizedStoredAddress, null);
michael@0 1986 if (this.matchPhoneNumbers(normalizedAddress, parsedAddress,
michael@0 1987 normalizedStoredAddress, parsedStoredAddress)) {
michael@0 1988 aCallback(element);
michael@0 1989 }
michael@0 1990 }
michael@0 1991 },
michael@0 1992
michael@0 1993 updateMessageDeliveryById: function(id, type, receiver, delivery,
michael@0 1994 deliveryStatus, envelopeId, callback) {
michael@0 1995 if (DEBUG) {
michael@0 1996 debug("Setting message's delivery by " + type + " = "+ id
michael@0 1997 + " receiver: " + receiver
michael@0 1998 + " delivery: " + delivery
michael@0 1999 + " deliveryStatus: " + deliveryStatus
michael@0 2000 + " envelopeId: " + envelopeId);
michael@0 2001 }
michael@0 2002
michael@0 2003 let self = this;
michael@0 2004 this.newTxnWithCallback(callback, function(aCapture, aMessageStore) {
michael@0 2005 let getRequest;
michael@0 2006 if (type === "messageId") {
michael@0 2007 getRequest = aMessageStore.get(id);
michael@0 2008 } else if (type === "envelopeId") {
michael@0 2009 getRequest = aMessageStore.index("envelopeId").get(id);
michael@0 2010 }
michael@0 2011
michael@0 2012 getRequest.onsuccess = function onsuccess(event) {
michael@0 2013 let messageRecord = event.target.result;
michael@0 2014 if (!messageRecord) {
michael@0 2015 if (DEBUG) debug("type = " + id + " is not found");
michael@0 2016 throw Cr.NS_ERROR_FAILURE;
michael@0 2017 }
michael@0 2018
michael@0 2019 let isRecordUpdated = false;
michael@0 2020
michael@0 2021 // Update |messageRecord.delivery| if needed.
michael@0 2022 if (delivery && messageRecord.delivery != delivery) {
michael@0 2023 messageRecord.delivery = delivery;
michael@0 2024 messageRecord.deliveryIndex = [delivery, messageRecord.timestamp];
michael@0 2025 isRecordUpdated = true;
michael@0 2026
michael@0 2027 // When updating an message's delivey state to 'sent', we also update
michael@0 2028 // its |sentTimestamp| by the current device timestamp to represent
michael@0 2029 // when the message is successfully sent.
michael@0 2030 if (delivery == DELIVERY_SENT) {
michael@0 2031 messageRecord.sentTimestamp = Date.now();
michael@0 2032 }
michael@0 2033 }
michael@0 2034
michael@0 2035 // Attempt to update |deliveryStatus| and |deliveryTimestamp| of:
michael@0 2036 // - the |messageRecord| for SMS.
michael@0 2037 // - the element(s) in |messageRecord.deliveryInfo| for MMS.
michael@0 2038 if (deliveryStatus) {
michael@0 2039 // A callback for updating the deliveyStatus/deliveryTimestamp of
michael@0 2040 // each target.
michael@0 2041 let updateFunc = function(aTarget) {
michael@0 2042 if (aTarget.deliveryStatus == deliveryStatus) {
michael@0 2043 return;
michael@0 2044 }
michael@0 2045
michael@0 2046 aTarget.deliveryStatus = deliveryStatus;
michael@0 2047
michael@0 2048 // Update |deliveryTimestamp| if it's successfully delivered.
michael@0 2049 if (deliveryStatus == DELIVERY_STATUS_SUCCESS) {
michael@0 2050 aTarget.deliveryTimestamp = Date.now();
michael@0 2051 }
michael@0 2052
michael@0 2053 isRecordUpdated = true;
michael@0 2054 };
michael@0 2055
michael@0 2056 if (messageRecord.type == "sms") {
michael@0 2057 updateFunc(messageRecord);
michael@0 2058 } else if (messageRecord.type == "mms") {
michael@0 2059 if (!receiver) {
michael@0 2060 // If the receiver is specified, we only need to update the
michael@0 2061 // element(s) in deliveryInfo that match the same receiver.
michael@0 2062 messageRecord.deliveryInfo.forEach(updateFunc);
michael@0 2063 } else {
michael@0 2064 self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
michael@0 2065 receiver, updateFunc);
michael@0 2066 }
michael@0 2067 }
michael@0 2068 }
michael@0 2069
michael@0 2070 // Update |messageRecord.envelopeIdIndex| if needed.
michael@0 2071 if (envelopeId) {
michael@0 2072 if (messageRecord.envelopeIdIndex != envelopeId) {
michael@0 2073 messageRecord.envelopeIdIndex = envelopeId;
michael@0 2074 isRecordUpdated = true;
michael@0 2075 }
michael@0 2076 }
michael@0 2077
michael@0 2078 aCapture.messageRecord = messageRecord;
michael@0 2079 if (!isRecordUpdated) {
michael@0 2080 if (DEBUG) {
michael@0 2081 debug("The values of delivery, deliveryStatus and envelopeId " +
michael@0 2082 "don't need to be updated.");
michael@0 2083 }
michael@0 2084 return;
michael@0 2085 }
michael@0 2086
michael@0 2087 if (DEBUG) {
michael@0 2088 debug("The delivery, deliveryStatus or envelopeId are updated.");
michael@0 2089 }
michael@0 2090 aMessageStore.put(messageRecord);
michael@0 2091 };
michael@0 2092 });
michael@0 2093 },
michael@0 2094
michael@0 2095 fillReceivedMmsThreadParticipants: function(aMessage, threadParticipants) {
michael@0 2096 let receivers = aMessage.receivers;
michael@0 2097 // If we don't want to disable the MMS grouping for receiving, we need to
michael@0 2098 // add the receivers (excluding the user's own number) to the participants
michael@0 2099 // for creating the thread. Some cases might be investigated as below:
michael@0 2100 //
michael@0 2101 // 1. receivers.length == 0
michael@0 2102 // This usually happens when receiving an MMS notification indication
michael@0 2103 // which doesn't carry any receivers.
michael@0 2104 // 2. receivers.length == 1
michael@0 2105 // If the receivers contain single phone number, we don't need to
michael@0 2106 // add it into participants because we know that number is our own.
michael@0 2107 // 3. receivers.length >= 2
michael@0 2108 // If the receivers contain multiple phone numbers, we need to add all
michael@0 2109 // of them but not the user's own number into participants.
michael@0 2110 if (DISABLE_MMS_GROUPING_FOR_RECEIVING || receivers.length < 2) {
michael@0 2111 return;
michael@0 2112 }
michael@0 2113 let isSuccess = false;
michael@0 2114 let slicedReceivers = receivers.slice();
michael@0 2115 if (aMessage.msisdn) {
michael@0 2116 let found = slicedReceivers.indexOf(aMessage.msisdn);
michael@0 2117 if (found !== -1) {
michael@0 2118 isSuccess = true;
michael@0 2119 slicedReceivers.splice(found, 1);
michael@0 2120 }
michael@0 2121 }
michael@0 2122
michael@0 2123 if (!isSuccess) {
michael@0 2124 // For some SIMs we cannot retrieve the vaild MSISDN (i.e. the user's
michael@0 2125 // own phone number), so we cannot correcly exclude the user's own
michael@0 2126 // number from the receivers, thus wrongly building the thread index.
michael@0 2127 if (DEBUG) debug("Error! Cannot strip out user's own phone number!");
michael@0 2128 }
michael@0 2129
michael@0 2130 threadParticipants = threadParticipants.concat(slicedReceivers);
michael@0 2131 },
michael@0 2132
michael@0 2133 updateThreadByMessageChange: function(messageStore, threadStore, threadId,
michael@0 2134 messageId, messageRead) {
michael@0 2135 threadStore.get(threadId).onsuccess = function(event) {
michael@0 2136 // This must exist.
michael@0 2137 let threadRecord = event.target.result;
michael@0 2138 if (DEBUG) debug("Updating thread record " + JSON.stringify(threadRecord));
michael@0 2139
michael@0 2140 if (!messageRead) {
michael@0 2141 threadRecord.unreadCount--;
michael@0 2142 }
michael@0 2143
michael@0 2144 if (threadRecord.lastMessageId == messageId) {
michael@0 2145 // Check most recent sender/receiver.
michael@0 2146 let range = IDBKeyRange.bound([threadId, 0], [threadId, ""]);
michael@0 2147 let request = messageStore.index("threadId")
michael@0 2148 .openCursor(range, PREV);
michael@0 2149 request.onsuccess = function(event) {
michael@0 2150 let cursor = event.target.result;
michael@0 2151 if (!cursor) {
michael@0 2152 if (DEBUG) {
michael@0 2153 debug("Deleting mru entry for thread id " + threadId);
michael@0 2154 }
michael@0 2155 threadStore.delete(threadId);
michael@0 2156 return;
michael@0 2157 }
michael@0 2158
michael@0 2159 let nextMsg = cursor.value;
michael@0 2160 let lastMessageSubject;
michael@0 2161 if (nextMsg.type == "mms") {
michael@0 2162 lastMessageSubject = nextMsg.headers.subject;
michael@0 2163 }
michael@0 2164 threadRecord.lastMessageSubject = lastMessageSubject || null;
michael@0 2165 threadRecord.lastMessageId = nextMsg.id;
michael@0 2166 threadRecord.lastTimestamp = nextMsg.timestamp;
michael@0 2167 threadRecord.body = nextMsg.body;
michael@0 2168 threadRecord.lastMessageType = nextMsg.type;
michael@0 2169 if (DEBUG) {
michael@0 2170 debug("Updating mru entry: " +
michael@0 2171 JSON.stringify(threadRecord));
michael@0 2172 }
michael@0 2173 threadStore.put(threadRecord);
michael@0 2174 };
michael@0 2175 } else if (!messageRead) {
michael@0 2176 // Shortcut, just update the unread count.
michael@0 2177 if (DEBUG) {
michael@0 2178 debug("Updating unread count for thread id " + threadId + ": " +
michael@0 2179 (threadRecord.unreadCount + 1) + " -> " +
michael@0 2180 threadRecord.unreadCount);
michael@0 2181 }
michael@0 2182 threadStore.put(threadRecord);
michael@0 2183 }
michael@0 2184 };
michael@0 2185 },
michael@0 2186
michael@0 2187 /**
michael@0 2188 * nsIRilMobileMessageDatabaseService API
michael@0 2189 */
michael@0 2190
michael@0 2191 saveReceivedMessage: function(aMessage, aCallback) {
michael@0 2192 if ((aMessage.type != "sms" && aMessage.type != "mms") ||
michael@0 2193 (aMessage.type == "sms" && (aMessage.messageClass == undefined ||
michael@0 2194 aMessage.sender == undefined)) ||
michael@0 2195 (aMessage.type == "mms" && (aMessage.delivery == undefined ||
michael@0 2196 aMessage.deliveryStatus == undefined ||
michael@0 2197 !Array.isArray(aMessage.receivers))) ||
michael@0 2198 aMessage.timestamp == undefined) {
michael@0 2199 if (aCallback) {
michael@0 2200 aCallback.notify(Cr.NS_ERROR_FAILURE, null);
michael@0 2201 }
michael@0 2202 return;
michael@0 2203 }
michael@0 2204
michael@0 2205 let threadParticipants;
michael@0 2206 if (aMessage.type == "mms") {
michael@0 2207 if (aMessage.headers.from) {
michael@0 2208 aMessage.sender = aMessage.headers.from.address;
michael@0 2209 } else {
michael@0 2210 aMessage.sender = "anonymous";
michael@0 2211 }
michael@0 2212
michael@0 2213 threadParticipants = [aMessage.sender];
michael@0 2214 this.fillReceivedMmsThreadParticipants(aMessage, threadParticipants);
michael@0 2215 } else { // SMS
michael@0 2216 threadParticipants = [aMessage.sender];
michael@0 2217 }
michael@0 2218
michael@0 2219 let timestamp = aMessage.timestamp;
michael@0 2220
michael@0 2221 // Adding needed indexes and extra attributes for internal use.
michael@0 2222 // threadIdIndex & participantIdsIndex are filled in saveRecord().
michael@0 2223 aMessage.readIndex = [FILTER_READ_UNREAD, timestamp];
michael@0 2224 aMessage.read = FILTER_READ_UNREAD;
michael@0 2225
michael@0 2226 // If |sentTimestamp| is not specified, use 0 as default.
michael@0 2227 if (aMessage.sentTimestamp == undefined) {
michael@0 2228 aMessage.sentTimestamp = 0;
michael@0 2229 }
michael@0 2230
michael@0 2231 if (aMessage.type == "mms") {
michael@0 2232 aMessage.transactionIdIndex = aMessage.headers["x-mms-transaction-id"];
michael@0 2233 aMessage.isReadReportSent = false;
michael@0 2234
michael@0 2235 // As a receiver, we don't need to care about the delivery status of
michael@0 2236 // others, so we put a single element with self's phone number in the
michael@0 2237 // |deliveryInfo| array.
michael@0 2238 aMessage.deliveryInfo = [{
michael@0 2239 receiver: aMessage.phoneNumber,
michael@0 2240 deliveryStatus: aMessage.deliveryStatus,
michael@0 2241 deliveryTimestamp: 0,
michael@0 2242 readStatus: MMS.DOM_READ_STATUS_NOT_APPLICABLE,
michael@0 2243 readTimestamp: 0,
michael@0 2244 }];
michael@0 2245
michael@0 2246 delete aMessage.deliveryStatus;
michael@0 2247 }
michael@0 2248
michael@0 2249 if (aMessage.type == "sms") {
michael@0 2250 aMessage.delivery = DELIVERY_RECEIVED;
michael@0 2251 aMessage.deliveryStatus = DELIVERY_STATUS_SUCCESS;
michael@0 2252 aMessage.deliveryTimestamp = 0;
michael@0 2253
michael@0 2254 if (aMessage.pid == undefined) {
michael@0 2255 aMessage.pid = RIL.PDU_PID_DEFAULT;
michael@0 2256 }
michael@0 2257 }
michael@0 2258 aMessage.deliveryIndex = [aMessage.delivery, timestamp];
michael@0 2259
michael@0 2260 this.saveRecord(aMessage, threadParticipants, aCallback);
michael@0 2261 },
michael@0 2262
michael@0 2263 saveSendingMessage: function(aMessage, aCallback) {
michael@0 2264 if ((aMessage.type != "sms" && aMessage.type != "mms") ||
michael@0 2265 (aMessage.type == "sms" && aMessage.receiver == undefined) ||
michael@0 2266 (aMessage.type == "mms" && !Array.isArray(aMessage.receivers)) ||
michael@0 2267 aMessage.deliveryStatusRequested == undefined ||
michael@0 2268 aMessage.timestamp == undefined) {
michael@0 2269 if (aCallback) {
michael@0 2270 aCallback.notify(Cr.NS_ERROR_FAILURE, null);
michael@0 2271 }
michael@0 2272 return;
michael@0 2273 }
michael@0 2274
michael@0 2275 // Set |aMessage.deliveryStatus|. Note that for MMS record
michael@0 2276 // it must be an array of strings; For SMS, it's a string.
michael@0 2277 let deliveryStatus = aMessage.deliveryStatusRequested
michael@0 2278 ? DELIVERY_STATUS_PENDING
michael@0 2279 : DELIVERY_STATUS_NOT_APPLICABLE;
michael@0 2280 if (aMessage.type == "sms") {
michael@0 2281 aMessage.deliveryStatus = deliveryStatus;
michael@0 2282 // If |deliveryTimestamp| is not specified, use 0 as default.
michael@0 2283 if (aMessage.deliveryTimestamp == undefined) {
michael@0 2284 aMessage.deliveryTimestamp = 0;
michael@0 2285 }
michael@0 2286 } else if (aMessage.type == "mms") {
michael@0 2287 let receivers = aMessage.receivers
michael@0 2288 if (!Array.isArray(receivers)) {
michael@0 2289 if (DEBUG) {
michael@0 2290 debug("Need receivers for MMS. Fail to save the sending message.");
michael@0 2291 }
michael@0 2292 if (aCallback) {
michael@0 2293 aCallback.notify(Cr.NS_ERROR_FAILURE, null);
michael@0 2294 }
michael@0 2295 return;
michael@0 2296 }
michael@0 2297 let readStatus = aMessage.headers["x-mms-read-report"]
michael@0 2298 ? MMS.DOM_READ_STATUS_PENDING
michael@0 2299 : MMS.DOM_READ_STATUS_NOT_APPLICABLE;
michael@0 2300 aMessage.deliveryInfo = [];
michael@0 2301 for (let i = 0; i < receivers.length; i++) {
michael@0 2302 aMessage.deliveryInfo.push({
michael@0 2303 receiver: receivers[i],
michael@0 2304 deliveryStatus: deliveryStatus,
michael@0 2305 deliveryTimestamp: 0,
michael@0 2306 readStatus: readStatus,
michael@0 2307 readTimestamp: 0,
michael@0 2308 });
michael@0 2309 }
michael@0 2310 }
michael@0 2311
michael@0 2312 let timestamp = aMessage.timestamp;
michael@0 2313
michael@0 2314 // Adding needed indexes and extra attributes for internal use.
michael@0 2315 // threadIdIndex & participantIdsIndex are filled in saveRecord().
michael@0 2316 aMessage.deliveryIndex = [DELIVERY_SENDING, timestamp];
michael@0 2317 aMessage.readIndex = [FILTER_READ_READ, timestamp];
michael@0 2318 aMessage.delivery = DELIVERY_SENDING;
michael@0 2319 aMessage.messageClass = MESSAGE_CLASS_NORMAL;
michael@0 2320 aMessage.read = FILTER_READ_READ;
michael@0 2321
michael@0 2322 // |sentTimestamp| is not available when the message is still sedning.
michael@0 2323 aMessage.sentTimestamp = 0;
michael@0 2324
michael@0 2325 let addresses;
michael@0 2326 if (aMessage.type == "sms") {
michael@0 2327 addresses = [aMessage.receiver];
michael@0 2328 } else if (aMessage.type == "mms") {
michael@0 2329 addresses = aMessage.receivers;
michael@0 2330 }
michael@0 2331 this.saveRecord(aMessage, addresses, aCallback);
michael@0 2332 },
michael@0 2333
michael@0 2334 setMessageDeliveryByMessageId: function(messageId, receiver, delivery,
michael@0 2335 deliveryStatus, envelopeId, callback) {
michael@0 2336 this.updateMessageDeliveryById(messageId, "messageId",
michael@0 2337 receiver, delivery, deliveryStatus,
michael@0 2338 envelopeId, callback);
michael@0 2339
michael@0 2340 },
michael@0 2341
michael@0 2342 setMessageDeliveryStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
michael@0 2343 aDeliveryStatus, aCallback) {
michael@0 2344 this.updateMessageDeliveryById(aEnvelopeId, "envelopeId", aReceiver, null,
michael@0 2345 aDeliveryStatus, null, aCallback);
michael@0 2346 },
michael@0 2347
michael@0 2348 setMessageReadStatusByEnvelopeId: function(aEnvelopeId, aReceiver,
michael@0 2349 aReadStatus, aCallback) {
michael@0 2350 if (DEBUG) {
michael@0 2351 debug("Setting message's read status by envelopeId = " + aEnvelopeId +
michael@0 2352 ", receiver: " + aReceiver + ", readStatus: " + aReadStatus);
michael@0 2353 }
michael@0 2354
michael@0 2355 let self = this;
michael@0 2356 this.newTxnWithCallback(aCallback, function(aCapture, aMessageStore) {
michael@0 2357 let getRequest = aMessageStore.index("envelopeId").get(aEnvelopeId);
michael@0 2358 getRequest.onsuccess = function onsuccess(event) {
michael@0 2359 let messageRecord = event.target.result;
michael@0 2360 if (!messageRecord) {
michael@0 2361 if (DEBUG) debug("envelopeId '" + aEnvelopeId + "' not found");
michael@0 2362 throw Cr.NS_ERROR_FAILURE;
michael@0 2363 }
michael@0 2364
michael@0 2365 aCapture.messageRecord = messageRecord;
michael@0 2366
michael@0 2367 let isRecordUpdated = false;
michael@0 2368 self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
michael@0 2369 aReceiver, function(aEntry) {
michael@0 2370 if (aEntry.readStatus == aReadStatus) {
michael@0 2371 return;
michael@0 2372 }
michael@0 2373
michael@0 2374 aEntry.readStatus = aReadStatus;
michael@0 2375 if (aReadStatus == MMS.DOM_READ_STATUS_SUCCESS) {
michael@0 2376 aEntry.readTimestamp = Date.now();
michael@0 2377 } else {
michael@0 2378 aEntry.readTimestamp = 0;
michael@0 2379 }
michael@0 2380 isRecordUpdated = true;
michael@0 2381 });
michael@0 2382
michael@0 2383 if (!isRecordUpdated) {
michael@0 2384 if (DEBUG) {
michael@0 2385 debug("The values of readStatus don't need to be updated.");
michael@0 2386 }
michael@0 2387 return;
michael@0 2388 }
michael@0 2389
michael@0 2390 if (DEBUG) {
michael@0 2391 debug("The readStatus is updated.");
michael@0 2392 }
michael@0 2393 aMessageStore.put(messageRecord);
michael@0 2394 };
michael@0 2395 });
michael@0 2396 },
michael@0 2397
michael@0 2398 getMessageRecordByTransactionId: function(aTransactionId, aCallback) {
michael@0 2399 if (DEBUG) debug("Retrieving message with transaction ID " + aTransactionId);
michael@0 2400 let self = this;
michael@0 2401 this.newTxn(READ_ONLY, function(error, txn, messageStore) {
michael@0 2402 if (error) {
michael@0 2403 if (DEBUG) debug(error);
michael@0 2404 aCallback.notify(error, null, null);
michael@0 2405 return;
michael@0 2406 }
michael@0 2407 let request = messageStore.index("transactionId").get(aTransactionId);
michael@0 2408
michael@0 2409 txn.oncomplete = function oncomplete(event) {
michael@0 2410 if (DEBUG) debug("Transaction " + txn + " completed.");
michael@0 2411 let messageRecord = request.result;
michael@0 2412 if (!messageRecord) {
michael@0 2413 if (DEBUG) debug("Transaction ID " + aTransactionId + " not found");
michael@0 2414 aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
michael@0 2415 return;
michael@0 2416 }
michael@0 2417 // In this case, we don't need a dom message. Just pass null to the
michael@0 2418 // third argument.
michael@0 2419 aCallback.notify(Cr.NS_OK, messageRecord, null);
michael@0 2420 };
michael@0 2421
michael@0 2422 txn.onerror = function onerror(event) {
michael@0 2423 if (DEBUG) {
michael@0 2424 if (event.target) {
michael@0 2425 debug("Caught error on transaction", event.target.errorCode);
michael@0 2426 }
michael@0 2427 }
michael@0 2428 aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
michael@0 2429 };
michael@0 2430 });
michael@0 2431 },
michael@0 2432
michael@0 2433 getMessageRecordById: function(aMessageId, aCallback) {
michael@0 2434 if (DEBUG) debug("Retrieving message with ID " + aMessageId);
michael@0 2435 let self = this;
michael@0 2436 this.newTxn(READ_ONLY, function(error, txn, messageStore) {
michael@0 2437 if (error) {
michael@0 2438 if (DEBUG) debug(error);
michael@0 2439 aCallback.notify(error, null, null);
michael@0 2440 return;
michael@0 2441 }
michael@0 2442 let request = messageStore.mozGetAll(aMessageId);
michael@0 2443
michael@0 2444 txn.oncomplete = function oncomplete() {
michael@0 2445 if (DEBUG) debug("Transaction " + txn + " completed.");
michael@0 2446 if (request.result.length > 1) {
michael@0 2447 if (DEBUG) debug("Got too many results for id " + aMessageId);
michael@0 2448 aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
michael@0 2449 return;
michael@0 2450 }
michael@0 2451 let messageRecord = request.result[0];
michael@0 2452 if (!messageRecord) {
michael@0 2453 if (DEBUG) debug("Message ID " + aMessageId + " not found");
michael@0 2454 aCallback.notify(Cr.NS_ERROR_FILE_NOT_FOUND, null, null);
michael@0 2455 return;
michael@0 2456 }
michael@0 2457 if (messageRecord.id != aMessageId) {
michael@0 2458 if (DEBUG) {
michael@0 2459 debug("Requested message ID (" + aMessageId + ") is " +
michael@0 2460 "different from the one we got");
michael@0 2461 }
michael@0 2462 aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
michael@0 2463 return;
michael@0 2464 }
michael@0 2465 let domMessage = self.createDomMessageFromRecord(messageRecord);
michael@0 2466 aCallback.notify(Cr.NS_OK, messageRecord, domMessage);
michael@0 2467 };
michael@0 2468
michael@0 2469 txn.onerror = function onerror(event) {
michael@0 2470 if (DEBUG) {
michael@0 2471 if (event.target) {
michael@0 2472 debug("Caught error on transaction", event.target.errorCode);
michael@0 2473 }
michael@0 2474 }
michael@0 2475 aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
michael@0 2476 };
michael@0 2477 });
michael@0 2478 },
michael@0 2479
michael@0 2480 translateCrErrorToMessageCallbackError: function(aCrError) {
michael@0 2481 switch(aCrError) {
michael@0 2482 case Cr.NS_OK:
michael@0 2483 return Ci.nsIMobileMessageCallback.SUCCESS_NO_ERROR;
michael@0 2484 case Cr.NS_ERROR_UNEXPECTED:
michael@0 2485 return Ci.nsIMobileMessageCallback.UNKNOWN_ERROR;
michael@0 2486 case Cr.NS_ERROR_FILE_NOT_FOUND:
michael@0 2487 return Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR;
michael@0 2488 case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE:
michael@0 2489 return Ci.nsIMobileMessageCallback.STORAGE_FULL_ERROR;
michael@0 2490 default:
michael@0 2491 return Ci.nsIMobileMessageCallback.INTERNAL_ERROR;
michael@0 2492 }
michael@0 2493 },
michael@0 2494
michael@0 2495 saveSmsSegment: function(aSmsSegment, aCallback) {
michael@0 2496 let completeMessage = null;
michael@0 2497 this.newTxn(READ_WRITE, function(error, txn, segmentStore) {
michael@0 2498 if (error) {
michael@0 2499 if (DEBUG) debug(error);
michael@0 2500 aCallback.notify(error, null);
michael@0 2501 return;
michael@0 2502 }
michael@0 2503
michael@0 2504 txn.oncomplete = function oncomplete(event) {
michael@0 2505 if (DEBUG) debug("Transaction " + txn + " completed.");
michael@0 2506 if (completeMessage) {
michael@0 2507 // Rebuild full body
michael@0 2508 if (completeMessage.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
michael@0 2509 // Uint8Array doesn't have `concat`, so
michael@0 2510 // we have to merge all segements by hand.
michael@0 2511 let fullDataLen = 0;
michael@0 2512 for (let i = 1; i <= completeMessage.segmentMaxSeq; i++) {
michael@0 2513 fullDataLen += completeMessage.segments[i].length;
michael@0 2514 }
michael@0 2515
michael@0 2516 completeMessage.fullData = new Uint8Array(fullDataLen);
michael@0 2517 for (let d = 0, i = 1; i <= completeMessage.segmentMaxSeq; i++) {
michael@0 2518 let data = completeMessage.segments[i];
michael@0 2519 for (let j = 0; j < data.length; j++) {
michael@0 2520 completeMessage.fullData[d++] = data[j];
michael@0 2521 }
michael@0 2522 }
michael@0 2523 } else {
michael@0 2524 completeMessage.fullBody = completeMessage.segments.join("");
michael@0 2525 }
michael@0 2526
michael@0 2527 // Remove handy fields after completing the concatenation.
michael@0 2528 delete completeMessage.id;
michael@0 2529 delete completeMessage.hash;
michael@0 2530 delete completeMessage.receivedSegments;
michael@0 2531 delete completeMessage.segments;
michael@0 2532 }
michael@0 2533 aCallback.notify(Cr.NS_OK, completeMessage);
michael@0 2534 };
michael@0 2535
michael@0 2536 txn.onabort = function onerror(event) {
michael@0 2537 if (DEBUG) debug("Caught error on transaction", event.target.errorCode);
michael@0 2538 aCallback.notify(Cr.NS_ERROR_FAILURE, null, null);
michael@0 2539 };
michael@0 2540
michael@0 2541 aSmsSegment.hash = aSmsSegment.sender + ":" +
michael@0 2542 aSmsSegment.segmentRef + ":" +
michael@0 2543 aSmsSegment.segmentMaxSeq + ":" +
michael@0 2544 aSmsSegment.iccId;
michael@0 2545 let seq = aSmsSegment.segmentSeq;
michael@0 2546 if (DEBUG) {
michael@0 2547 debug("Saving SMS Segment: " + aSmsSegment.hash + ", seq: " + seq);
michael@0 2548 }
michael@0 2549 let getRequest = segmentStore.index("hash").get(aSmsSegment.hash);
michael@0 2550 getRequest.onsuccess = function(event) {
michael@0 2551 let segmentRecord = event.target.result;
michael@0 2552 if (!segmentRecord) {
michael@0 2553 if (DEBUG) {
michael@0 2554 debug("Not found! Create a new record to store the segments.");
michael@0 2555 }
michael@0 2556 aSmsSegment.receivedSegments = 1;
michael@0 2557 aSmsSegment.segments = [];
michael@0 2558 if (aSmsSegment.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
michael@0 2559 aSmsSegment.segments[seq] = aSmsSegment.data;
michael@0 2560 } else {
michael@0 2561 aSmsSegment.segments[seq] = aSmsSegment.body;
michael@0 2562 }
michael@0 2563
michael@0 2564 segmentStore.add(aSmsSegment);
michael@0 2565
michael@0 2566 return;
michael@0 2567 }
michael@0 2568
michael@0 2569 if (DEBUG) {
michael@0 2570 debug("Append SMS Segment into existed message object: " + segmentRecord.id);
michael@0 2571 }
michael@0 2572
michael@0 2573 if (segmentRecord.segments[seq]) {
michael@0 2574 if (DEBUG) debug("Got duplicated segment no. " + seq);
michael@0 2575 return;
michael@0 2576 }
michael@0 2577
michael@0 2578 segmentRecord.timestamp = aSmsSegment.timestamp;
michael@0 2579
michael@0 2580 if (segmentRecord.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) {
michael@0 2581 segmentRecord.segments[seq] = aSmsSegment.data;
michael@0 2582 } else {
michael@0 2583 segmentRecord.segments[seq] = aSmsSegment.body;
michael@0 2584 }
michael@0 2585 segmentRecord.receivedSegments++;
michael@0 2586
michael@0 2587 // The port information is only available in 1st segment for CDMA WAP Push.
michael@0 2588 // If the segments of a WAP Push are not received in sequence
michael@0 2589 // (e.g., SMS with seq == 1 is not the 1st segment received by the device),
michael@0 2590 // we have to retrieve the port information from 1st segment and
michael@0 2591 // save it into the segmentRecord.
michael@0 2592 if (aSmsSegment.teleservice === RIL.PDU_CDMA_MSG_TELESERIVCIE_ID_WAP
michael@0 2593 && seq === 1) {
michael@0 2594 if (aSmsSegment.originatorPort) {
michael@0 2595 segmentRecord.originatorPort = aSmsSegment.originatorPort;
michael@0 2596 }
michael@0 2597
michael@0 2598 if (aSmsSegment.destinationPort) {
michael@0 2599 segmentRecord.destinationPort = aSmsSegment.destinationPort;
michael@0 2600 }
michael@0 2601 }
michael@0 2602
michael@0 2603 if (segmentRecord.receivedSegments < segmentRecord.segmentMaxSeq) {
michael@0 2604 if (DEBUG) debug("Message is incomplete.");
michael@0 2605 segmentStore.put(segmentRecord);
michael@0 2606 return;
michael@0 2607 }
michael@0 2608
michael@0 2609 completeMessage = segmentRecord;
michael@0 2610
michael@0 2611 // Delete Record in DB
michael@0 2612 segmentStore.delete(segmentRecord.id);
michael@0 2613 };
michael@0 2614 }, [SMS_SEGMENT_STORE_NAME]);
michael@0 2615 },
michael@0 2616
michael@0 2617 /**
michael@0 2618 * nsIMobileMessageDatabaseService API
michael@0 2619 */
michael@0 2620
michael@0 2621 getMessage: function(aMessageId, aRequest) {
michael@0 2622 if (DEBUG) debug("Retrieving message with ID " + aMessageId);
michael@0 2623 let self = this;
michael@0 2624 let notifyCallback = {
michael@0 2625 notify: function(aRv, aMessageRecord, aDomMessage) {
michael@0 2626 if (Cr.NS_OK == aRv) {
michael@0 2627 aRequest.notifyMessageGot(aDomMessage);
michael@0 2628 return;
michael@0 2629 }
michael@0 2630 aRequest.notifyGetMessageFailed(
michael@0 2631 self.translateCrErrorToMessageCallbackError(aRv), null);
michael@0 2632 }
michael@0 2633 };
michael@0 2634 this.getMessageRecordById(aMessageId, notifyCallback);
michael@0 2635 },
michael@0 2636
michael@0 2637 deleteMessage: function(messageIds, length, aRequest) {
michael@0 2638 if (DEBUG) debug("deleteMessage: message ids " + JSON.stringify(messageIds));
michael@0 2639 let deleted = [];
michael@0 2640 let self = this;
michael@0 2641 this.newTxn(READ_WRITE, function(error, txn, stores) {
michael@0 2642 if (error) {
michael@0 2643 if (DEBUG) debug("deleteMessage: failed to open transaction");
michael@0 2644 aRequest.notifyDeleteMessageFailed(
michael@0 2645 self.translateCrErrorToMessageCallbackError(error));
michael@0 2646 return;
michael@0 2647 }
michael@0 2648 txn.onerror = function onerror(event) {
michael@0 2649 if (DEBUG) debug("Caught error on transaction", event.target.errorCode);
michael@0 2650 //TODO look at event.target.errorCode, pick appropriate error constant
michael@0 2651 aRequest.notifyDeleteMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 2652 };
michael@0 2653
michael@0 2654 const messageStore = stores[0];
michael@0 2655 const threadStore = stores[1];
michael@0 2656
michael@0 2657 txn.oncomplete = function oncomplete(event) {
michael@0 2658 if (DEBUG) debug("Transaction " + txn + " completed.");
michael@0 2659 aRequest.notifyMessageDeleted(deleted, length);
michael@0 2660 };
michael@0 2661
michael@0 2662 for (let i = 0; i < length; i++) {
michael@0 2663 let messageId = messageIds[i];
michael@0 2664 deleted[i] = false;
michael@0 2665 messageStore.get(messageId).onsuccess = function(messageIndex, event) {
michael@0 2666 let messageRecord = event.target.result;
michael@0 2667 let messageId = messageIds[messageIndex];
michael@0 2668 if (messageRecord) {
michael@0 2669 if (DEBUG) debug("Deleting message id " + messageId);
michael@0 2670
michael@0 2671 // First actually delete the message.
michael@0 2672 messageStore.delete(messageId).onsuccess = function(event) {
michael@0 2673 if (DEBUG) debug("Message id " + messageId + " deleted");
michael@0 2674 deleted[messageIndex] = true;
michael@0 2675
michael@0 2676 // Then update unread count and most recent message.
michael@0 2677 self.updateThreadByMessageChange(messageStore,
michael@0 2678 threadStore,
michael@0 2679 messageRecord.threadId,
michael@0 2680 messageId,
michael@0 2681 messageRecord.read);
michael@0 2682
michael@0 2683 Services.obs.notifyObservers(null,
michael@0 2684 "mobile-message-deleted",
michael@0 2685 JSON.stringify({ id: messageId }));
michael@0 2686 };
michael@0 2687 } else if (DEBUG) {
michael@0 2688 debug("Message id " + messageId + " does not exist");
michael@0 2689 }
michael@0 2690 }.bind(null, i);
michael@0 2691 }
michael@0 2692 }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
michael@0 2693 },
michael@0 2694
michael@0 2695 createMessageCursor: function(filter, reverse, callback) {
michael@0 2696 if (DEBUG) {
michael@0 2697 debug("Creating a message cursor. Filters:" +
michael@0 2698 " startDate: " + filter.startDate +
michael@0 2699 " endDate: " + filter.endDate +
michael@0 2700 " delivery: " + filter.delivery +
michael@0 2701 " numbers: " + filter.numbers +
michael@0 2702 " read: " + filter.read +
michael@0 2703 " threadId: " + filter.threadId +
michael@0 2704 " reverse: " + reverse);
michael@0 2705 }
michael@0 2706
michael@0 2707 let cursor = new GetMessagesCursor(this, callback);
michael@0 2708
michael@0 2709 let self = this;
michael@0 2710 self.newTxn(READ_ONLY, function(error, txn, stores) {
michael@0 2711 let collector = cursor.collector;
michael@0 2712 let collect = collector.collect.bind(collector);
michael@0 2713 FilterSearcherHelper.transact(self, txn, error, filter, reverse, collect);
michael@0 2714 }, [MESSAGE_STORE_NAME, PARTICIPANT_STORE_NAME]);
michael@0 2715
michael@0 2716 return cursor;
michael@0 2717 },
michael@0 2718
michael@0 2719 markMessageRead: function(messageId, value, aSendReadReport, aRequest) {
michael@0 2720 if (DEBUG) debug("Setting message " + messageId + " read to " + value);
michael@0 2721 let self = this;
michael@0 2722 this.newTxn(READ_WRITE, function(error, txn, stores) {
michael@0 2723 if (error) {
michael@0 2724 if (DEBUG) debug(error);
michael@0 2725 aRequest.notifyMarkMessageReadFailed(
michael@0 2726 self.translateCrErrorToMessageCallbackError(error));
michael@0 2727 return;
michael@0 2728 }
michael@0 2729
michael@0 2730 txn.onerror = function onerror(event) {
michael@0 2731 if (DEBUG) debug("Caught error on transaction ", event.target.errorCode);
michael@0 2732 aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 2733 };
michael@0 2734
michael@0 2735 let messageStore = stores[0];
michael@0 2736 let threadStore = stores[1];
michael@0 2737 messageStore.get(messageId).onsuccess = function onsuccess(event) {
michael@0 2738 let messageRecord = event.target.result;
michael@0 2739 if (!messageRecord) {
michael@0 2740 if (DEBUG) debug("Message ID " + messageId + " not found");
michael@0 2741 aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.NOT_FOUND_ERROR);
michael@0 2742 return;
michael@0 2743 }
michael@0 2744
michael@0 2745 if (messageRecord.id != messageId) {
michael@0 2746 if (DEBUG) {
michael@0 2747 debug("Retrieve message ID (" + messageId + ") is " +
michael@0 2748 "different from the one we got");
michael@0 2749 }
michael@0 2750 aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.UNKNOWN_ERROR);
michael@0 2751 return;
michael@0 2752 }
michael@0 2753
michael@0 2754 // If the value to be set is the same as the current message `read`
michael@0 2755 // value, we just notify successfully.
michael@0 2756 if (messageRecord.read == value) {
michael@0 2757 if (DEBUG) debug("The value of messageRecord.read is already " + value);
michael@0 2758 aRequest.notifyMessageMarkedRead(messageRecord.read);
michael@0 2759 return;
michael@0 2760 }
michael@0 2761
michael@0 2762 messageRecord.read = value ? FILTER_READ_READ : FILTER_READ_UNREAD;
michael@0 2763 messageRecord.readIndex = [messageRecord.read, messageRecord.timestamp];
michael@0 2764 let readReportMessageId, readReportTo;
michael@0 2765 if (aSendReadReport &&
michael@0 2766 messageRecord.type == "mms" &&
michael@0 2767 messageRecord.delivery == DELIVERY_RECEIVED &&
michael@0 2768 messageRecord.read == FILTER_READ_READ &&
michael@0 2769 !messageRecord.isReadReportSent) {
michael@0 2770 messageRecord.isReadReportSent = true;
michael@0 2771
michael@0 2772 let from = messageRecord.headers["from"];
michael@0 2773 readReportTo = from && from.address;
michael@0 2774 readReportMessageId = messageRecord.headers["message-id"];
michael@0 2775 }
michael@0 2776
michael@0 2777 if (DEBUG) debug("Message.read set to: " + value);
michael@0 2778 messageStore.put(messageRecord).onsuccess = function onsuccess(event) {
michael@0 2779 if (DEBUG) {
michael@0 2780 debug("Update successfully completed. Message: " +
michael@0 2781 JSON.stringify(event.target.result));
michael@0 2782 }
michael@0 2783
michael@0 2784 // Now update the unread count.
michael@0 2785 let threadId = messageRecord.threadId;
michael@0 2786
michael@0 2787 threadStore.get(threadId).onsuccess = function(event) {
michael@0 2788 let threadRecord = event.target.result;
michael@0 2789 threadRecord.unreadCount += value ? -1 : 1;
michael@0 2790 if (DEBUG) {
michael@0 2791 debug("Updating unreadCount for thread id " + threadId + ": " +
michael@0 2792 (value ?
michael@0 2793 threadRecord.unreadCount + 1 :
michael@0 2794 threadRecord.unreadCount - 1) +
michael@0 2795 " -> " + threadRecord.unreadCount);
michael@0 2796 }
michael@0 2797 threadStore.put(threadRecord).onsuccess = function(event) {
michael@0 2798 if(readReportMessageId && readReportTo) {
michael@0 2799 gMMSService.sendReadReport(readReportMessageId,
michael@0 2800 readReportTo,
michael@0 2801 messageRecord.iccId);
michael@0 2802 }
michael@0 2803 aRequest.notifyMessageMarkedRead(messageRecord.read);
michael@0 2804 };
michael@0 2805 };
michael@0 2806 };
michael@0 2807 };
michael@0 2808 }, [MESSAGE_STORE_NAME, THREAD_STORE_NAME]);
michael@0 2809 },
michael@0 2810
michael@0 2811 createThreadCursor: function(callback) {
michael@0 2812 if (DEBUG) debug("Getting thread list");
michael@0 2813
michael@0 2814 let cursor = new GetThreadsCursor(this, callback);
michael@0 2815 this.newTxn(READ_ONLY, function(error, txn, threadStore) {
michael@0 2816 let collector = cursor.collector;
michael@0 2817 if (error) {
michael@0 2818 collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
michael@0 2819 return;
michael@0 2820 }
michael@0 2821 txn.onerror = function onerror(event) {
michael@0 2822 if (DEBUG) debug("Caught error on transaction ", event.target.errorCode);
michael@0 2823 collector.collect(null, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
michael@0 2824 };
michael@0 2825 let request = threadStore.index("lastTimestamp").openKeyCursor(null, PREV);
michael@0 2826 request.onsuccess = function(event) {
michael@0 2827 let cursor = event.target.result;
michael@0 2828 if (cursor) {
michael@0 2829 if (collector.collect(txn, cursor.primaryKey, cursor.key)) {
michael@0 2830 cursor.continue();
michael@0 2831 }
michael@0 2832 } else {
michael@0 2833 collector.collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
michael@0 2834 }
michael@0 2835 };
michael@0 2836 }, [THREAD_STORE_NAME]);
michael@0 2837
michael@0 2838 return cursor;
michael@0 2839 }
michael@0 2840 };
michael@0 2841
michael@0 2842 let FilterSearcherHelper = {
michael@0 2843
michael@0 2844 /**
michael@0 2845 * @param index
michael@0 2846 * The name of a message store index to filter on.
michael@0 2847 * @param range
michael@0 2848 * A IDBKeyRange.
michael@0 2849 * @param direction
michael@0 2850 * NEXT or PREV.
michael@0 2851 * @param txn
michael@0 2852 * Ongoing IDBTransaction context object.
michael@0 2853 * @param collect
michael@0 2854 * Result colletor function. It takes three parameters -- txn, message
michael@0 2855 * id, and message timestamp.
michael@0 2856 */
michael@0 2857 filterIndex: function(index, range, direction, txn, collect) {
michael@0 2858 let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
michael@0 2859 let request = messageStore.index(index).openKeyCursor(range, direction);
michael@0 2860 request.onsuccess = function onsuccess(event) {
michael@0 2861 let cursor = event.target.result;
michael@0 2862 // Once the cursor has retrieved all keys that matches its key range,
michael@0 2863 // the filter search is done.
michael@0 2864 if (cursor) {
michael@0 2865 let timestamp = Array.isArray(cursor.key) ? cursor.key[1] : cursor.key;
michael@0 2866 if (collect(txn, cursor.primaryKey, timestamp)) {
michael@0 2867 cursor.continue();
michael@0 2868 }
michael@0 2869 } else {
michael@0 2870 collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
michael@0 2871 }
michael@0 2872 };
michael@0 2873 request.onerror = function onerror(event) {
michael@0 2874 if (DEBUG && event) debug("IDBRequest error " + event.target.errorCode);
michael@0 2875 collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
michael@0 2876 };
michael@0 2877 },
michael@0 2878
michael@0 2879 /**
michael@0 2880 * Explicitly fiter message on the timestamp index.
michael@0 2881 *
michael@0 2882 * @param startDate
michael@0 2883 * Timestamp of the starting date.
michael@0 2884 * @param endDate
michael@0 2885 * Timestamp of the ending date.
michael@0 2886 * @param direction
michael@0 2887 * NEXT or PREV.
michael@0 2888 * @param txn
michael@0 2889 * Ongoing IDBTransaction context object.
michael@0 2890 * @param collect
michael@0 2891 * Result colletor function. It takes three parameters -- txn, message
michael@0 2892 * id, and message timestamp.
michael@0 2893 */
michael@0 2894 filterTimestamp: function(startDate, endDate, direction, txn, collect) {
michael@0 2895 let range = null;
michael@0 2896 if (startDate != null && endDate != null) {
michael@0 2897 range = IDBKeyRange.bound(startDate.getTime(), endDate.getTime());
michael@0 2898 } else if (startDate != null) {
michael@0 2899 range = IDBKeyRange.lowerBound(startDate.getTime());
michael@0 2900 } else if (endDate != null) {
michael@0 2901 range = IDBKeyRange.upperBound(endDate.getTime());
michael@0 2902 }
michael@0 2903 this.filterIndex("timestamp", range, direction, txn, collect);
michael@0 2904 },
michael@0 2905
michael@0 2906 /**
michael@0 2907 * Instanciate a filtering transaction.
michael@0 2908 *
michael@0 2909 * @param mmdb
michael@0 2910 * A MobileMessageDB.
michael@0 2911 * @param txn
michael@0 2912 * Ongoing IDBTransaction context object.
michael@0 2913 * @param error
michael@0 2914 * Previous error while creating the transaction.
michael@0 2915 * @param filter
michael@0 2916 * A SmsFilter object.
michael@0 2917 * @param reverse
michael@0 2918 * A boolean value indicating whether we should filter message in
michael@0 2919 * reversed order.
michael@0 2920 * @param collect
michael@0 2921 * Result colletor function. It takes three parameters -- txn, message
michael@0 2922 * id, and message timestamp.
michael@0 2923 */
michael@0 2924 transact: function(mmdb, txn, error, filter, reverse, collect) {
michael@0 2925 if (error) {
michael@0 2926 //TODO look at event.target.errorCode, pick appropriate error constant.
michael@0 2927 if (DEBUG) debug("IDBRequest error " + error.target.errorCode);
michael@0 2928 collect(txn, COLLECT_ID_ERROR, COLLECT_TIMESTAMP_UNUSED);
michael@0 2929 return;
michael@0 2930 }
michael@0 2931
michael@0 2932 let direction = reverse ? PREV : NEXT;
michael@0 2933
michael@0 2934 // We support filtering by date range only (see `else` block below) or by
michael@0 2935 // number/delivery status/read status with an optional date range.
michael@0 2936 if (filter.delivery == null &&
michael@0 2937 filter.numbers == null &&
michael@0 2938 filter.read == null &&
michael@0 2939 filter.threadId == null) {
michael@0 2940 // Filtering by date range only.
michael@0 2941 if (DEBUG) {
michael@0 2942 debug("filter.timestamp " + filter.startDate + ", " + filter.endDate);
michael@0 2943 }
michael@0 2944
michael@0 2945 this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
michael@0 2946 collect);
michael@0 2947 return;
michael@0 2948 }
michael@0 2949
michael@0 2950 // Numeric 0 is smaller than any time stamp, and empty string is larger
michael@0 2951 // than all numeric values.
michael@0 2952 let startDate = 0, endDate = "";
michael@0 2953 if (filter.startDate != null) {
michael@0 2954 startDate = filter.startDate.getTime();
michael@0 2955 }
michael@0 2956 if (filter.endDate != null) {
michael@0 2957 endDate = filter.endDate.getTime();
michael@0 2958 }
michael@0 2959
michael@0 2960 let single, intersectionCollector;
michael@0 2961 {
michael@0 2962 let num = 0;
michael@0 2963 if (filter.delivery) num++;
michael@0 2964 if (filter.numbers) num++;
michael@0 2965 if (filter.read != undefined) num++;
michael@0 2966 if (filter.threadId != undefined) num++;
michael@0 2967 single = (num == 1);
michael@0 2968 }
michael@0 2969
michael@0 2970 if (!single) {
michael@0 2971 intersectionCollector = new IntersectionResultsCollector(collect, reverse);
michael@0 2972 }
michael@0 2973
michael@0 2974 // Retrieve the keys from the 'delivery' index that matches the value of
michael@0 2975 // filter.delivery.
michael@0 2976 if (filter.delivery) {
michael@0 2977 if (DEBUG) debug("filter.delivery " + filter.delivery);
michael@0 2978 let delivery = filter.delivery;
michael@0 2979 let range = IDBKeyRange.bound([delivery, startDate], [delivery, endDate]);
michael@0 2980 this.filterIndex("delivery", range, direction, txn,
michael@0 2981 single ? collect : intersectionCollector.newContext());
michael@0 2982 }
michael@0 2983
michael@0 2984 // Retrieve the keys from the 'read' index that matches the value of
michael@0 2985 // filter.read.
michael@0 2986 if (filter.read != undefined) {
michael@0 2987 if (DEBUG) debug("filter.read " + filter.read);
michael@0 2988 let read = filter.read ? FILTER_READ_READ : FILTER_READ_UNREAD;
michael@0 2989 let range = IDBKeyRange.bound([read, startDate], [read, endDate]);
michael@0 2990 this.filterIndex("read", range, direction, txn,
michael@0 2991 single ? collect : intersectionCollector.newContext());
michael@0 2992 }
michael@0 2993
michael@0 2994 // Retrieve the keys from the 'threadId' index that matches the value of
michael@0 2995 // filter.threadId.
michael@0 2996 if (filter.threadId != undefined) {
michael@0 2997 if (DEBUG) debug("filter.threadId " + filter.threadId);
michael@0 2998 let threadId = filter.threadId;
michael@0 2999 let range = IDBKeyRange.bound([threadId, startDate], [threadId, endDate]);
michael@0 3000 this.filterIndex("threadId", range, direction, txn,
michael@0 3001 single ? collect : intersectionCollector.newContext());
michael@0 3002 }
michael@0 3003
michael@0 3004 // Retrieve the keys from the 'sender' and 'receiver' indexes that
michael@0 3005 // match the values of filter.numbers
michael@0 3006 if (filter.numbers) {
michael@0 3007 if (DEBUG) debug("filter.numbers " + filter.numbers.join(", "));
michael@0 3008
michael@0 3009 if (!single) {
michael@0 3010 collect = intersectionCollector.newContext();
michael@0 3011 }
michael@0 3012
michael@0 3013 let participantStore = txn.objectStore(PARTICIPANT_STORE_NAME);
michael@0 3014 mmdb.findParticipantIdsByAddresses(participantStore, filter.numbers,
michael@0 3015 false, true,
michael@0 3016 (function(participantIds) {
michael@0 3017 if (!participantIds || !participantIds.length) {
michael@0 3018 // Oops! No such participant at all.
michael@0 3019
michael@0 3020 collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
michael@0 3021 return;
michael@0 3022 }
michael@0 3023
michael@0 3024 if (participantIds.length == 1) {
michael@0 3025 let id = participantIds[0];
michael@0 3026 let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
michael@0 3027 this.filterIndex("participantIds", range, direction, txn, collect);
michael@0 3028 return;
michael@0 3029 }
michael@0 3030
michael@0 3031 let unionCollector = new UnionResultsCollector(collect);
michael@0 3032
michael@0 3033 this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
michael@0 3034 unionCollector.newTimestampContext());
michael@0 3035
michael@0 3036 for (let i = 0; i < participantIds.length; i++) {
michael@0 3037 let id = participantIds[i];
michael@0 3038 let range = IDBKeyRange.bound([id, startDate], [id, endDate]);
michael@0 3039 this.filterIndex("participantIds", range, direction, txn,
michael@0 3040 unionCollector.newContext());
michael@0 3041 }
michael@0 3042 }).bind(this));
michael@0 3043 }
michael@0 3044 }
michael@0 3045 };
michael@0 3046
michael@0 3047 function ResultsCollector() {
michael@0 3048 this.results = [];
michael@0 3049 this.done = false;
michael@0 3050 }
michael@0 3051 ResultsCollector.prototype = {
michael@0 3052 results: null,
michael@0 3053 requestWaiting: null,
michael@0 3054 done: null,
michael@0 3055
michael@0 3056 /**
michael@0 3057 * Queue up passed id, reply if necessary.
michael@0 3058 *
michael@0 3059 * @param txn
michael@0 3060 * Ongoing IDBTransaction context object.
michael@0 3061 * @param id
michael@0 3062 * COLLECT_ID_END(0) for no more results, COLLECT_ID_ERROR(-1) for
michael@0 3063 * errors and valid otherwise.
michael@0 3064 * @param timestamp
michael@0 3065 * We assume this function is always called in timestamp order. So
michael@0 3066 * this parameter is actually unused.
michael@0 3067 *
michael@0 3068 * @return true if expects more. false otherwise.
michael@0 3069 */
michael@0 3070 collect: function(txn, id, timestamp) {
michael@0 3071 if (this.done) {
michael@0 3072 return false;
michael@0 3073 }
michael@0 3074
michael@0 3075 if (DEBUG) {
michael@0 3076 debug("collect: message ID = " + id);
michael@0 3077 }
michael@0 3078 if (id) {
michael@0 3079 // Queue up any id but '0' and replies later accordingly.
michael@0 3080 this.results.push(id);
michael@0 3081 }
michael@0 3082 if (id <= 0) {
michael@0 3083 // No more processing on '0' or negative values passed.
michael@0 3084 this.done = true;
michael@0 3085 }
michael@0 3086
michael@0 3087 if (!this.requestWaiting) {
michael@0 3088 if (DEBUG) debug("Cursor.continue() not called yet");
michael@0 3089 return !this.done;
michael@0 3090 }
michael@0 3091
michael@0 3092 // We assume there is only one request waiting throughout the message list
michael@0 3093 // retrieving process. So we don't bother continuing to process further
michael@0 3094 // waiting requests here. This assumption comes from DOMCursor::Continue()
michael@0 3095 // implementation.
michael@0 3096 let callback = this.requestWaiting;
michael@0 3097 this.requestWaiting = null;
michael@0 3098
michael@0 3099 this.drip(txn, callback);
michael@0 3100
michael@0 3101 return !this.done;
michael@0 3102 },
michael@0 3103
michael@0 3104 /**
michael@0 3105 * Callback right away with the first queued result entry if the filtering is
michael@0 3106 * done. Or queue up the request and callback when a new entry is available.
michael@0 3107 *
michael@0 3108 * @param callback
michael@0 3109 * A callback function that accepts a numeric id.
michael@0 3110 */
michael@0 3111 squeeze: function(callback) {
michael@0 3112 if (this.requestWaiting) {
michael@0 3113 throw new Error("Already waiting for another request!");
michael@0 3114 }
michael@0 3115
michael@0 3116 if (!this.done) {
michael@0 3117 // Database transaction ongoing, let it reply for us so that we won't get
michael@0 3118 // blocked by the existing transaction.
michael@0 3119 this.requestWaiting = callback;
michael@0 3120 return;
michael@0 3121 }
michael@0 3122
michael@0 3123 this.drip(null, callback);
michael@0 3124 },
michael@0 3125
michael@0 3126 /**
michael@0 3127 * @param txn
michael@0 3128 * Ongoing IDBTransaction context object or null.
michael@0 3129 * @param callback
michael@0 3130 * A callback function that accepts a numeric id.
michael@0 3131 */
michael@0 3132 drip: function(txn, callback) {
michael@0 3133 if (!this.results.length) {
michael@0 3134 if (DEBUG) debug("No messages matching the filter criteria");
michael@0 3135 callback(txn, COLLECT_ID_END);
michael@0 3136 return;
michael@0 3137 }
michael@0 3138
michael@0 3139 if (this.results[0] < 0) {
michael@0 3140 // An previous error found. Keep the answer in results so that we can
michael@0 3141 // reply INTERNAL_ERROR for further requests.
michael@0 3142 if (DEBUG) debug("An previous error found");
michael@0 3143 callback(txn, COLLECT_ID_ERROR);
michael@0 3144 return;
michael@0 3145 }
michael@0 3146
michael@0 3147 let firstMessageId = this.results.shift();
michael@0 3148 callback(txn, firstMessageId);
michael@0 3149 }
michael@0 3150 };
michael@0 3151
michael@0 3152 function IntersectionResultsCollector(collect, reverse) {
michael@0 3153 this.cascadedCollect = collect;
michael@0 3154 this.reverse = reverse;
michael@0 3155 this.contexts = [];
michael@0 3156 }
michael@0 3157 IntersectionResultsCollector.prototype = {
michael@0 3158 cascadedCollect: null,
michael@0 3159 reverse: false,
michael@0 3160 contexts: null,
michael@0 3161
michael@0 3162 /**
michael@0 3163 * Queue up {id, timestamp} pairs, find out intersections and report to
michael@0 3164 * |cascadedCollect|. Return true if it is still possible to have another match.
michael@0 3165 */
michael@0 3166 collect: function(contextIndex, txn, id, timestamp) {
michael@0 3167 if (DEBUG) {
michael@0 3168 debug("IntersectionResultsCollector: "
michael@0 3169 + contextIndex + ", " + id + ", " + timestamp);
michael@0 3170 }
michael@0 3171
michael@0 3172 let contexts = this.contexts;
michael@0 3173 let context = contexts[contextIndex];
michael@0 3174
michael@0 3175 if (id < 0) {
michael@0 3176 // Act as no more matched records.
michael@0 3177 id = 0;
michael@0 3178 }
michael@0 3179 if (!id) {
michael@0 3180 context.done = true;
michael@0 3181
michael@0 3182 if (!context.results.length) {
michael@0 3183 // Already empty, can't have further intersection results.
michael@0 3184 return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
michael@0 3185 }
michael@0 3186
michael@0 3187 for (let i = 0; i < contexts.length; i++) {
michael@0 3188 if (!contexts[i].done) {
michael@0 3189 // Don't call |this.cascadedCollect| because |context.results| might not
michael@0 3190 // be empty, so other contexts might still have a chance here.
michael@0 3191 return false;
michael@0 3192 }
michael@0 3193 }
michael@0 3194
michael@0 3195 // It was the last processing context and is no more processing.
michael@0 3196 return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
michael@0 3197 }
michael@0 3198
michael@0 3199 // Search id in other existing results. If no other results has it,
michael@0 3200 // and A) the last timestamp is smaller-equal to current timestamp,
michael@0 3201 // we wait for further results; either B) record timestamp is larger
michael@0 3202 // then current timestamp or C) no more processing for a filter, then we
michael@0 3203 // drop this id because there can't be a match anymore.
michael@0 3204 for (let i = 0; i < contexts.length; i++) {
michael@0 3205 if (i == contextIndex) {
michael@0 3206 continue;
michael@0 3207 }
michael@0 3208
michael@0 3209 let ctx = contexts[i];
michael@0 3210 let results = ctx.results;
michael@0 3211 let found = false;
michael@0 3212 for (let j = 0; j < results.length; j++) {
michael@0 3213 let result = results[j];
michael@0 3214 if (result.id == id) {
michael@0 3215 found = true;
michael@0 3216 break;
michael@0 3217 }
michael@0 3218 if ((!this.reverse && (result.timestamp > timestamp)) ||
michael@0 3219 (this.reverse && (result.timestamp < timestamp))) {
michael@0 3220 // B) Cannot find a match anymore. Drop.
michael@0 3221 return true;
michael@0 3222 }
michael@0 3223 }
michael@0 3224
michael@0 3225 if (!found) {
michael@0 3226 if (ctx.done) {
michael@0 3227 // C) Cannot find a match anymore. Drop.
michael@0 3228 if (results.length) {
michael@0 3229 let lastResult = results[results.length - 1];
michael@0 3230 if ((!this.reverse && (lastResult.timestamp >= timestamp)) ||
michael@0 3231 (this.reverse && (lastResult.timestamp <= timestamp))) {
michael@0 3232 // Still have a chance to get another match. Return true.
michael@0 3233 return true;
michael@0 3234 }
michael@0 3235 }
michael@0 3236
michael@0 3237 // Impossible to find another match because all results in ctx have
michael@0 3238 // timestamps smaller than timestamp.
michael@0 3239 context.done = true;
michael@0 3240 return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
michael@0 3241 }
michael@0 3242
michael@0 3243 // A) Pending.
michael@0 3244 context.results.push({
michael@0 3245 id: id,
michael@0 3246 timestamp: timestamp
michael@0 3247 });
michael@0 3248 return true;
michael@0 3249 }
michael@0 3250 }
michael@0 3251
michael@0 3252 // Now id is found in all other results. Report it.
michael@0 3253 return this.cascadedCollect(txn, id, timestamp);
michael@0 3254 },
michael@0 3255
michael@0 3256 newContext: function() {
michael@0 3257 let contextIndex = this.contexts.length;
michael@0 3258 this.contexts.push({
michael@0 3259 results: [],
michael@0 3260 done: false
michael@0 3261 });
michael@0 3262 return this.collect.bind(this, contextIndex);
michael@0 3263 }
michael@0 3264 };
michael@0 3265
michael@0 3266 function UnionResultsCollector(collect) {
michael@0 3267 this.cascadedCollect = collect;
michael@0 3268 this.contexts = [{
michael@0 3269 // Timestamp.
michael@0 3270 processing: 1,
michael@0 3271 results: []
michael@0 3272 }, {
michael@0 3273 processing: 0,
michael@0 3274 results: []
michael@0 3275 }];
michael@0 3276 }
michael@0 3277 UnionResultsCollector.prototype = {
michael@0 3278 cascadedCollect: null,
michael@0 3279 contexts: null,
michael@0 3280
michael@0 3281 collect: function(contextIndex, txn, id, timestamp) {
michael@0 3282 if (DEBUG) {
michael@0 3283 debug("UnionResultsCollector: "
michael@0 3284 + contextIndex + ", " + id + ", " + timestamp);
michael@0 3285 }
michael@0 3286
michael@0 3287 let contexts = this.contexts;
michael@0 3288 let context = contexts[contextIndex];
michael@0 3289
michael@0 3290 if (id < 0) {
michael@0 3291 // Act as no more matched records.
michael@0 3292 id = 0;
michael@0 3293 }
michael@0 3294 if (id) {
michael@0 3295 if (!contextIndex) {
michael@0 3296 // Timestamp.
michael@0 3297 context.results.push({
michael@0 3298 id: id,
michael@0 3299 timestamp: timestamp
michael@0 3300 });
michael@0 3301 } else {
michael@0 3302 context.results.push(id);
michael@0 3303 }
michael@0 3304 return true;
michael@0 3305 }
michael@0 3306
michael@0 3307 context.processing -= 1;
michael@0 3308 if (contexts[0].processing || contexts[1].processing) {
michael@0 3309 // At least one queue is still processing, but we got here because
michael@0 3310 // current cursor gives 0 as id meaning no more messages are
michael@0 3311 // available. Return false here to stop further cursor.continue() calls.
michael@0 3312 return false;
michael@0 3313 }
michael@0 3314
michael@0 3315 let tres = contexts[0].results;
michael@0 3316 let qres = contexts[1].results;
michael@0 3317 tres = tres.filter(function(element) {
michael@0 3318 return qres.indexOf(element.id) != -1;
michael@0 3319 });
michael@0 3320
michael@0 3321 for (let i = 0; i < tres.length; i++) {
michael@0 3322 this.cascadedCollect(txn, tres[i].id, tres[i].timestamp);
michael@0 3323 }
michael@0 3324 this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
michael@0 3325
michael@0 3326 return false;
michael@0 3327 },
michael@0 3328
michael@0 3329 newTimestampContext: function() {
michael@0 3330 return this.collect.bind(this, 0);
michael@0 3331 },
michael@0 3332
michael@0 3333 newContext: function() {
michael@0 3334 this.contexts[1].processing++;
michael@0 3335 return this.collect.bind(this, 1);
michael@0 3336 }
michael@0 3337 };
michael@0 3338
michael@0 3339 function GetMessagesCursor(mmdb, callback) {
michael@0 3340 this.mmdb = mmdb;
michael@0 3341 this.callback = callback;
michael@0 3342 this.collector = new ResultsCollector();
michael@0 3343
michael@0 3344 this.handleContinue(); // Trigger first run.
michael@0 3345 }
michael@0 3346 GetMessagesCursor.prototype = {
michael@0 3347 classID: RIL_GETMESSAGESCURSOR_CID,
michael@0 3348 QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),
michael@0 3349
michael@0 3350 mmdb: null,
michael@0 3351 callback: null,
michael@0 3352 collector: null,
michael@0 3353
michael@0 3354 getMessageTxn: function(messageStore, messageId) {
michael@0 3355 if (DEBUG) debug ("Fetching message " + messageId);
michael@0 3356
michael@0 3357 let getRequest = messageStore.get(messageId);
michael@0 3358 let self = this;
michael@0 3359 getRequest.onsuccess = function onsuccess(event) {
michael@0 3360 if (DEBUG) {
michael@0 3361 debug("notifyNextMessageInListGot - messageId: " + messageId);
michael@0 3362 }
michael@0 3363 let domMessage =
michael@0 3364 self.mmdb.createDomMessageFromRecord(event.target.result);
michael@0 3365 self.callback.notifyCursorResult(domMessage);
michael@0 3366 };
michael@0 3367 getRequest.onerror = function onerror(event) {
michael@0 3368 if (DEBUG) {
michael@0 3369 debug("notifyCursorError - messageId: " + messageId);
michael@0 3370 }
michael@0 3371 self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 3372 };
michael@0 3373 },
michael@0 3374
michael@0 3375 notify: function(txn, messageId) {
michael@0 3376 if (!messageId) {
michael@0 3377 this.callback.notifyCursorDone();
michael@0 3378 return;
michael@0 3379 }
michael@0 3380
michael@0 3381 if (messageId < 0) {
michael@0 3382 this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 3383 return;
michael@0 3384 }
michael@0 3385
michael@0 3386 // When filter transaction is not yet completed, we're called with current
michael@0 3387 // ongoing transaction object.
michael@0 3388 if (txn) {
michael@0 3389 let messageStore = txn.objectStore(MESSAGE_STORE_NAME);
michael@0 3390 this.getMessageTxn(messageStore, messageId);
michael@0 3391 return;
michael@0 3392 }
michael@0 3393
michael@0 3394 // Or, we have to open another transaction ourselves.
michael@0 3395 let self = this;
michael@0 3396 this.mmdb.newTxn(READ_ONLY, function(error, txn, messageStore) {
michael@0 3397 if (error) {
michael@0 3398 self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 3399 return;
michael@0 3400 }
michael@0 3401 self.getMessageTxn(messageStore, messageId);
michael@0 3402 }, [MESSAGE_STORE_NAME]);
michael@0 3403 },
michael@0 3404
michael@0 3405 // nsICursorContinueCallback
michael@0 3406
michael@0 3407 handleContinue: function() {
michael@0 3408 if (DEBUG) debug("Getting next message in list");
michael@0 3409 this.collector.squeeze(this.notify.bind(this));
michael@0 3410 }
michael@0 3411 };
michael@0 3412
michael@0 3413 function GetThreadsCursor(mmdb, callback) {
michael@0 3414 this.mmdb = mmdb;
michael@0 3415 this.callback = callback;
michael@0 3416 this.collector = new ResultsCollector();
michael@0 3417
michael@0 3418 this.handleContinue(); // Trigger first run.
michael@0 3419 }
michael@0 3420 GetThreadsCursor.prototype = {
michael@0 3421 classID: RIL_GETTHREADSCURSOR_CID,
michael@0 3422 QueryInterface: XPCOMUtils.generateQI([Ci.nsICursorContinueCallback]),
michael@0 3423
michael@0 3424 mmdb: null,
michael@0 3425 callback: null,
michael@0 3426 collector: null,
michael@0 3427
michael@0 3428 getThreadTxn: function(threadStore, threadId) {
michael@0 3429 if (DEBUG) debug ("Fetching thread " + threadId);
michael@0 3430
michael@0 3431 let getRequest = threadStore.get(threadId);
michael@0 3432 let self = this;
michael@0 3433 getRequest.onsuccess = function onsuccess(event) {
michael@0 3434 let threadRecord = event.target.result;
michael@0 3435 if (DEBUG) {
michael@0 3436 debug("notifyCursorResult: " + JSON.stringify(threadRecord));
michael@0 3437 }
michael@0 3438 let thread =
michael@0 3439 gMobileMessageService.createThread(threadRecord.id,
michael@0 3440 threadRecord.participantAddresses,
michael@0 3441 threadRecord.lastTimestamp,
michael@0 3442 threadRecord.lastMessageSubject || "",
michael@0 3443 threadRecord.body,
michael@0 3444 threadRecord.unreadCount,
michael@0 3445 threadRecord.lastMessageType);
michael@0 3446 self.callback.notifyCursorResult(thread);
michael@0 3447 };
michael@0 3448 getRequest.onerror = function onerror(event) {
michael@0 3449 if (DEBUG) {
michael@0 3450 debug("notifyCursorError - threadId: " + threadId);
michael@0 3451 }
michael@0 3452 self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 3453 };
michael@0 3454 },
michael@0 3455
michael@0 3456 notify: function(txn, threadId) {
michael@0 3457 if (!threadId) {
michael@0 3458 this.callback.notifyCursorDone();
michael@0 3459 return;
michael@0 3460 }
michael@0 3461
michael@0 3462 if (threadId < 0) {
michael@0 3463 this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 3464 return;
michael@0 3465 }
michael@0 3466
michael@0 3467 // When filter transaction is not yet completed, we're called with current
michael@0 3468 // ongoing transaction object.
michael@0 3469 if (txn) {
michael@0 3470 let threadStore = txn.objectStore(THREAD_STORE_NAME);
michael@0 3471 this.getThreadTxn(threadStore, threadId);
michael@0 3472 return;
michael@0 3473 }
michael@0 3474
michael@0 3475 // Or, we have to open another transaction ourselves.
michael@0 3476 let self = this;
michael@0 3477 this.mmdb.newTxn(READ_ONLY, function(error, txn, threadStore) {
michael@0 3478 if (error) {
michael@0 3479 self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
michael@0 3480 return;
michael@0 3481 }
michael@0 3482 self.getThreadTxn(threadStore, threadId);
michael@0 3483 }, [THREAD_STORE_NAME]);
michael@0 3484 },
michael@0 3485
michael@0 3486 // nsICursorContinueCallback
michael@0 3487
michael@0 3488 handleContinue: function() {
michael@0 3489 if (DEBUG) debug("Getting next thread in list");
michael@0 3490 this.collector.squeeze(this.notify.bind(this));
michael@0 3491 }
michael@0 3492 }
michael@0 3493
michael@0 3494 this.EXPORTED_SYMBOLS = [
michael@0 3495 'MobileMessageDB'
michael@0 3496 ];
michael@0 3497
michael@0 3498 function debug() {
michael@0 3499 dump("MobileMessageDB: " + Array.slice(arguments).join(" ") + "\n");
michael@0 3500 }

mercurial