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