dom/mobilemessage/src/gonk/MobileMessageDB.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

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;
  1004       // Participant store cursor iteration done.
  1005       if (!invalidParticipantIds.length) {
  1006         next();
  1007         return;
  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;
  1024           if (foundInvalid) {
  1025             wrongThreads.push(threadRecord.id);
  1026             threadCursor.delete();
  1028           threadCursor.continue();
  1029           return;
  1032         if (!wrongThreads.length) {
  1033           next();
  1034           return;
  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;
  1048               createUpdateThreadAndParticipant(ix);
  1049               return;
  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);
  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;
  1075             self.findThreadRecordByParticipants(threadStore, participantStore,
  1076                                                 threadParticipants, true,
  1077                                                 function(threadRecord,
  1078                                                           participantIds) {
  1079               if (!participantIds) {
  1080                 debug("participantIds is empty!");
  1081                 return;
  1084               let timestamp = messageRecord.timestamp;
  1085               // Setup participantIdsIndex.
  1086               messageRecord.participantIdsIndex = [];
  1087               for each (let id in participantIds) {
  1088                 messageRecord.participantIdsIndex.push([id, timestamp]);
  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;
  1101                 if (!messageRecord.read) {
  1102                   threadRecord.unreadCount++;
  1103                   needsUpdate = true;
  1106                 if (needsUpdate) {
  1107                   threadStore.put(threadRecord);
  1109                 messageRecord.threadId = threadRecord.id;
  1110                 messageRecord.threadIdIndex = [threadRecord.id, timestamp];
  1111                 messageCursor.update(messageRecord);
  1112                 messageCursor.continue();
  1113                 return;
  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;
  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;
  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;
  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;
  1200       let messageRecord = cursor.value;
  1201       if (messageRecord.type == "mms") {
  1202         messageRecord.isReadReportSent = false;
  1203         cursor.update(messageRecord);
  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;
  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;
  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;
  1260       let messageRecord = cursor.value;
  1261       if (messageRecord.type == "sms") {
  1262         messageRecord.pid = RIL.PDU_PID_DEFAULT;
  1263         cursor.update(messageRecord);
  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;
  1281       let messageRecord = cursor.value;
  1282       if (messageRecord.type == "sms") {
  1283         cursor.continue();
  1284         return;
  1287       // We can always retrieve transaction id from
  1288       // |messageRecord.headers["x-mms-transaction-id"]|.
  1289       if (messageRecord.hasOwnProperty("transactionId")) {
  1290         delete messageRecord.transactionId;
  1293       // xpconnect gives "undefined" for an unassigned argument of an interface
  1294       // method.
  1295       if (messageRecord.envelopeIdIndex === "undefined") {
  1296         delete messageRecord.envelopeIdIndex;
  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;
  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;
  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;
  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();
  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:
  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>,
  1373      *   [Concatenation Info]
  1374      *   segmentRef: <Number>,
  1375      *   segmentSeq: <Number>,
  1376      *   segmentMaxSeq: <Number>,
  1378      *   [Application Port Info]
  1379      *   originatorPort: <Number>,
  1380      *   destinationPort: <Number>,
  1382      *   [MWI status]
  1383      *   mwiPresent: <Boolean>,
  1384      *   mwiDiscard: <Boolean>,
  1385      *   mwiMsgCount: <Number>,
  1386      *   mwiActive: <Boolean>,
  1388      *   [CDMA Cellbroadcast related fields]
  1389      *   serviceCategory: <Number>,
  1390      *   language: <String>,
  1392      *   [Message Body]
  1393      *   data: <Uint8Array>, (available if it's 8bit encoding)
  1394      *   body: <String>, (normal text body)
  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      * }
  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;
  1419     if (parsedAddr1.countryName != parsedAddr2.countryName) {
  1420       return false;
  1423     let ssPref = "dom.phonenumber.substringmatching." + parsedAddr1.countryName;
  1424     if (Services.prefs.getPrefType(ssPref) != Ci.nsIPrefBranch.PREF_INT) {
  1425       return false;
  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);
  1439     if (parsedAddr1) {
  1440       parsedAddr2 = PhoneNumberUtils.parseWithCountryName(addr2, parsedAddr1.countryName);
  1441       if (parsedAddr2) {
  1442         return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
  1445       return false;
  1448     if (parsedAddr2) {
  1449       parsedAddr1 = PhoneNumberUtils.parseWithCountryName(addr1, parsedAddr2.countryName);
  1450       if (parsedAddr1) {
  1451         return this.matchParsedPhoneNumbers(addr1, parsedAddr1, addr2, parsedAddr2);
  1455     return false;
  1456   },
  1458   createDomMessageFromRecord: function(aMessageRecord) {
  1459     if (DEBUG) {
  1460       debug("createDomMessageFromRecord: " + JSON.stringify(aMessageRecord));
  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));
  1482       let subject = headers["subject"];
  1483       if (subject == undefined) {
  1484         subject = "";
  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));
  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;
  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;
  1509           attachments.push({
  1510             "id": partHeaders["content-id"],
  1511             "location": partHeaders["content-location"],
  1512             "content": partContent
  1513           });
  1516       let expiryDate = 0;
  1517       if (headers["x-mms-expiry"] != undefined) {
  1518         expiryDate = aMessageRecord.timestamp + headers["x-mms-expiry"] * 1000;
  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);
  1537   },
  1539   findParticipantRecordByAddress: function(aParticipantStore, aAddress,
  1540                                            aCreate, aCallback) {
  1541     if (DEBUG) {
  1542       debug("findParticipantRecordByAddress("
  1543             + JSON.stringify(aAddress) + ", " + aCreate + ")");
  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);
  1561     if (DEBUG) {
  1562       debug("findParticipantRecordByAddress: allPossibleAddresses = " +
  1563             JSON.stringify(allPossibleAddresses));
  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));
  1578         aCallback(participantRecord);
  1579         return;
  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;
  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;
  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));
  1607             aCallback(participantRecord);
  1608           };
  1609           return;
  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;
  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);
  1630           if (DEBUG) {
  1631             debug("findParticipantRecordByAddress: match "
  1632                   + JSON.stringify(cursor.value));
  1634           aCallback(participantRecord);
  1635           return;
  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 + ")");
  1652     if (!aAddresses || !aAddresses.length) {
  1653       if (DEBUG) debug("findParticipantIdsByAddresses: returning null");
  1654       aCallback(null);
  1655       return;
  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;
  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;
  1679         } else if (result.indexOf(participantRecord.id) < 0) {
  1680           result.push(participantRecord.id);
  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 + ")");
  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;
  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));
  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;
  1722         let domMessage =
  1723           aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
  1724         aCallback.notify(aRv, domMessage);
  1725       };
  1727       if (aError) {
  1728         notifyResult(aError, null);
  1729         return;
  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;
  1754         let domMessage =
  1755           aMessageRecord && self.createDomMessageFromRecord(aMessageRecord);
  1756         aCallback.notify(aRv, domMessage);
  1757       };
  1759       if (error) {
  1760         notifyResult(error, null);
  1761         return;
  1764       txn.oncomplete = function oncomplete(event) {
  1765         if (aMessageRecord.id > self.lastMessageId) {
  1766           self.lastMessageId = aMessageRecord.id;
  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;
  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;
  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;
  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;
  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;
  1857       let isOverriding = (aMessageRecord.id !== undefined);
  1858       if (!isOverriding) {
  1859         // |self.lastMessageId| is only updated in |txn.oncomplete|.
  1860         aMessageRecord.id = self.lastMessageId + 1;
  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]);
  1874         if (!isOverriding) {
  1875           // Really add to message store.
  1876           aMessageStore.put(aMessageRecord);
  1877           return;
  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);
  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;
  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;
  1913         if (!aMessageRecord.read) {
  1914           threadRecord.unreadCount++;
  1915           needsUpdate = true;
  1918         if (needsUpdate) {
  1919           aThreadStore.put(threadRecord);
  1922         insertMessageRecord(threadRecord.id);
  1923         return;
  1926       let lastMessageSubject;
  1927       if (aMessageRecord.type == "mms") {
  1928         lastMessageSubject = aMessageRecord.headers.subject;
  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);
  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;
  1970       if (typedAddress.address == typedStoredAddress.address) {
  1971         // Have a direct match.
  1972         aCallback(element);
  1973         continue;
  1976       if (typedAddress.type !== "PLMN") {
  1977         // Address type other than "PLMN" must have direct match.  Or, skip.
  1978         continue;
  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);
  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);
  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);
  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;
  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();
  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;
  2046             aTarget.deliveryStatus = deliveryStatus;
  2048             // Update |deliveryTimestamp| if it's successfully delivered.
  2049             if (deliveryStatus == DELIVERY_STATUS_SUCCESS) {
  2050               aTarget.deliveryTimestamp = Date.now();
  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);
  2070         // Update |messageRecord.envelopeIdIndex| if needed.
  2071         if (envelopeId) {
  2072           if (messageRecord.envelopeIdIndex != envelopeId) {
  2073             messageRecord.envelopeIdIndex = envelopeId;
  2074             isRecordUpdated = true;
  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.");
  2084           return;
  2087         if (DEBUG) {
  2088           debug("The delivery, deliveryStatus or envelopeId are updated.");
  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;
  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);
  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!");
  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--;
  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);
  2155             threadStore.delete(threadId);
  2156             return;
  2159           let nextMsg = cursor.value;
  2160           let lastMessageSubject;
  2161           if (nextMsg.type == "mms") {
  2162             lastMessageSubject = nextMsg.headers.subject;
  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));
  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);
  2182         threadStore.put(threadRecord);
  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);
  2202       return;
  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";
  2213       threadParticipants = [aMessage.sender];
  2214       this.fillReceivedMmsThreadParticipants(aMessage, threadParticipants);
  2215     } else { // SMS
  2216       threadParticipants = [aMessage.sender];
  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;
  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;
  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;
  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);
  2272       return;
  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;
  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.");
  2292         if (aCallback) {
  2293           aCallback.notify(Cr.NS_ERROR_FAILURE, null);
  2295         return;
  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         });
  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;
  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);
  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;
  2365         aCapture.messageRecord = messageRecord;
  2367         let isRecordUpdated = false;
  2368         self.forEachMatchedMmsDeliveryInfo(messageRecord.deliveryInfo,
  2369                                            aReceiver, function(aEntry) {
  2370           if (aEntry.readStatus == aReadStatus) {
  2371             return;
  2374           aEntry.readStatus = aReadStatus;
  2375           if (aReadStatus == MMS.DOM_READ_STATUS_SUCCESS) {
  2376             aEntry.readTimestamp = Date.now();
  2377           } else {
  2378             aEntry.readTimestamp = 0;
  2380           isRecordUpdated = true;
  2381         });
  2383         if (!isRecordUpdated) {
  2384           if (DEBUG) {
  2385             debug("The values of readStatus don't need to be updated.");
  2387           return;
  2390         if (DEBUG) {
  2391           debug("The readStatus is updated.");
  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;
  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;
  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);
  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;
  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;
  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;
  2457         if (messageRecord.id != aMessageId) {
  2458           if (DEBUG) {
  2459             debug("Requested message ID (" + aMessageId + ") is " +
  2460                   "different from the one we got");
  2462           aCallback.notify(Cr.NS_ERROR_UNEXPECTED, null, null);
  2463           return;
  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);
  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;
  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;
  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;
  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];
  2523           } else {
  2524             completeMessage.fullBody = completeMessage.segments.join("");
  2527           // Remove handy fields after completing the concatenation.
  2528           delete completeMessage.id;
  2529           delete completeMessage.hash;
  2530           delete completeMessage.receivedSegments;
  2531           delete completeMessage.segments;
  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);
  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.");
  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;
  2564           segmentStore.add(aSmsSegment);
  2566           return;
  2569         if (DEBUG) {
  2570           debug("Append SMS Segment into existed message object: " + segmentRecord.id);
  2573         if (segmentRecord.segments[seq]) {
  2574           if (DEBUG) debug("Got duplicated segment no. " + seq);
  2575           return;
  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;
  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;
  2598           if (aSmsSegment.destinationPort) {
  2599             segmentRecord.destinationPort = aSmsSegment.destinationPort;
  2603         if (segmentRecord.receivedSegments < segmentRecord.segmentMaxSeq) {
  2604           if (DEBUG) debug("Message is incomplete.");
  2605           segmentStore.put(segmentRecord);
  2606           return;
  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;
  2630         aRequest.notifyGetMessageFailed(
  2631           self.translateCrErrorToMessageCallbackError(aRv), null);
  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;
  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");
  2690         }.bind(null, i);
  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);
  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;
  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;
  2745         if (messageRecord.id != messageId) {
  2746           if (DEBUG) {
  2747             debug("Retrieve message ID (" + messageId + ") is " +
  2748                   "different from the one we got");
  2750           aRequest.notifyMarkMessageReadFailed(Ci.nsIMobileMessageCallback.UNKNOWN_ERROR);
  2751           return;
  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;
  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"];
  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));
  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);
  2797             threadStore.put(threadRecord).onsuccess = function(event) {
  2798               if(readReportMessageId && readReportTo) {
  2799                 gMMSService.sendReadReport(readReportMessageId,
  2800                                            readReportTo,
  2801                                            messageRecord.iccId);
  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;
  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();
  2832         } else {
  2833           collector.collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  2835       };
  2836     }, [THREAD_STORE_NAME]);
  2838     return cursor;
  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();
  2869       } else {
  2870         collect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  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.
  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());
  2903     this.filterIndex("timestamp", range, direction, txn, collect);
  2904   },
  2906   /**
  2907    * Instanciate a filtering transaction.
  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;
  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);
  2945       this.filterTimestamp(filter.startDate, filter.endDate, direction, txn,
  2946                            collect);
  2947       return;
  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();
  2956     if (filter.endDate != null) {
  2957       endDate = filter.endDate.getTime();
  2960     let single, intersectionCollector;
  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);
  2970     if (!single) {
  2971       intersectionCollector = new IntersectionResultsCollector(collect, reverse);
  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());
  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());
  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());
  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();
  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;
  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;
  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());
  3042       }).bind(this));
  3045 };
  3047 function ResultsCollector() {
  3048   this.results = [];
  3049   this.done = false;
  3051 ResultsCollector.prototype = {
  3052   results: null,
  3053   requestWaiting: null,
  3054   done: null,
  3056   /**
  3057    * Queue up passed id, reply if necessary.
  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.
  3068    * @return true if expects more. false otherwise.
  3069    */
  3070   collect: function(txn, id, timestamp) {
  3071     if (this.done) {
  3072       return false;
  3075     if (DEBUG) {
  3076       debug("collect: message ID = " + id);
  3078     if (id) {
  3079       // Queue up any id but '0' and replies later accordingly.
  3080       this.results.push(id);
  3082     if (id <= 0) {
  3083       // No more processing on '0' or negative values passed.
  3084       this.done = true;
  3087     if (!this.requestWaiting) {
  3088       if (DEBUG) debug("Cursor.continue() not called yet");
  3089       return !this.done;
  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.
  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!");
  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;
  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;
  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;
  3147     let firstMessageId = this.results.shift();
  3148     callback(txn, firstMessageId);
  3150 };
  3152 function IntersectionResultsCollector(collect, reverse) {
  3153   this.cascadedCollect = collect;
  3154   this.reverse = reverse;
  3155   this.contexts = [];
  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);
  3172     let contexts = this.contexts;
  3173     let context = contexts[contextIndex];
  3175     if (id < 0) {
  3176       // Act as no more matched records.
  3177       id = 0;
  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);
  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;
  3195       // It was the last processing context and is no more processing.
  3196       return this.cascadedCollect(txn, COLLECT_ID_END, COLLECT_TIMESTAMP_UNUSED);
  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;
  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;
  3218         if ((!this.reverse && (result.timestamp > timestamp)) ||
  3219             (this.reverse && (result.timestamp < timestamp))) {
  3220           // B) Cannot find a match anymore. Drop.
  3221           return true;
  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;
  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);
  3243         // A) Pending.
  3244         context.results.push({
  3245           id: id,
  3246           timestamp: timestamp
  3247         });
  3248         return true;
  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);
  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   }];
  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);
  3287     let contexts = this.contexts;
  3288     let context = contexts[contextIndex];
  3290     if (id < 0) {
  3291       // Act as no more matched records.
  3292       id = 0;
  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);
  3304       return true;
  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;
  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);
  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);
  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.
  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);
  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);
  3371       self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  3372     };
  3373   },
  3375   notify: function(txn, messageId) {
  3376     if (!messageId) {
  3377       this.callback.notifyCursorDone();
  3378       return;
  3381     if (messageId < 0) {
  3382       this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  3383       return;
  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;
  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;
  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));
  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.
  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));
  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);
  3452       self.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  3453     };
  3454   },
  3456   notify: function(txn, threadId) {
  3457     if (!threadId) {
  3458       this.callback.notifyCursorDone();
  3459       return;
  3462     if (threadId < 0) {
  3463       this.callback.notifyCursorError(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
  3464       return;
  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;
  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;
  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));
  3494 this.EXPORTED_SYMBOLS = [
  3495   'MobileMessageDB'
  3496 ];
  3498 function debug() {
  3499   dump("MobileMessageDB: " + Array.slice(arguments).join(" ") + "\n");

mercurial