Bug 836519 - Implement getAll API for contact manager using cursors. r=gwagner
authorReuben Morais <reuben.morais@gmail.com>
Sun, 03 Feb 2013 04:05:51 -0800
changeset 122310 00623df83261f40003c0ea738e6cbe2115cef55a
parent 122309 6bd304c9f9920c2ffe347befe2f4f8e93952c558
child 122311 8551e3fd968150bc57a29b2c720edac2bf754bdb
push id24332
push userryanvm@gmail.com
push dateTue, 19 Feb 2013 13:42:49 +0000
treeherdermozilla-central@cc37417e2c28 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgwagner
bugs836519
milestone21.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 836519 - Implement getAll API for contact manager using cursors. r=gwagner
dom/base/IndexedDBHelper.jsm
dom/contacts/ContactManager.js
dom/contacts/ContactManager.manifest
dom/contacts/fallback/ContactDB.jsm
dom/contacts/fallback/ContactService.jsm
dom/contacts/tests/Makefile.in
dom/contacts/tests/test_contacts_getall.html
dom/interfaces/contacts/nsIDOMContactManager.idl
dom/interfaces/contacts/nsIDOMContactProperties.idl
dom/tests/mochitest/general/test_interfaces.html
--- a/dom/base/IndexedDBHelper.jsm
+++ b/dom/base/IndexedDBHelper.jsm
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-"use strict"
+"use strict";
 
 let DEBUG = 0;
 let debug;
 if (DEBUG) {
   debug = function (s) { dump("-*- IndexedDBHelper: " + s + "\n"); }
 } else {
   debug = function (s) {}
 }
@@ -44,28 +44,28 @@ IndexedDBHelper.prototype = {
    * @param failureCb
    *        Error callback to call when an error is encountered.
    */
   open: function open(aSuccessCb, aFailureCb) {
     let self = this;
     if (DEBUG) debug("Try to open database:" + self.dbName + " " + self.dbVersion);
     let req = this.dbGlobal.indexedDB.open(this.dbName, this.dbVersion);
     req.onsuccess = function (event) {
-      if (DEBUG) debug("Opened database:" + self.dbName + " " + self.dbName);
+      if (DEBUG) debug("Opened database:" + self.dbName + " " + self.dbVersion);
       self._db = event.target.result;
       self._db.onversionchange = function(event) {
         if (DEBUG) debug("WARNING: DB modified from a different window.");
       }
       aSuccessCb();
     };
 
     req.onupgradeneeded = function (aEvent) {
       if (DEBUG) {
         debug("Database needs upgrade:" + self.dbName + aEvent.oldVersion + aEvent.newVersion);
-        debug("Correct new database version:" + aEvent.newVersion == this.dbVersion);
+        debug("Correct new database version:" + (aEvent.newVersion == this.dbVersion));
       }
 
       let _db = aEvent.target.result;
       self.upgradeSchema(req.transaction, _db, aEvent.oldVersion, aEvent.newVersion);
     };
     req.onerror = function (aEvent) {
       if (DEBUG) debug("Failed to open database:" + self.dbName);
       aFailureCb(aEvent.target.errorMessage);
@@ -112,29 +112,34 @@ IndexedDBHelper.prototype = {
     this.ensureDB(function () {
       if (DEBUG) debug("Starting new transaction" + txn_type);
       let txn = this._db.transaction(this.dbStoreNames, txn_type);
       if (DEBUG) debug("Retrieving object store", this.dbName);
       let store = txn.objectStore(store_name);
 
       txn.oncomplete = function (event) {
         if (DEBUG) debug("Transaction complete. Returning to callback.");
-        successCb(txn.result);
+        if (successCb) {
+          successCb(txn.result);
+        }
       };
 
       txn.onabort = function (event) {
         if (DEBUG) debug("Caught error on transaction");
         /*
          * event.target.error may be null
          * if txn was aborted by calling txn.abort()
          */
-        if (event.target.error)
+        if (failureCb) {
+          if (event.target.error) {
             failureCb(event.target.error.name);
-        else
+          } else {
             failureCb("UnknownError");
+          }
+        }
       };
       callback(txn, store);
     }.bind(this), failureCb);
   },
 
   /**
    * Initialize the DB. Does not call open.
    *
--- a/dom/contacts/ContactManager.js
+++ b/dom/contacts/ContactManager.js
@@ -129,34 +129,54 @@ ContactTelField.prototype = {
                                      contractID: CONTACTTELFIELD_CONTRACTID,
                                      classDescription: "ContactTelField",
                                      interfaces: [nsIDOMContactTelField],
                                      flags: nsIClassInfo.DOM_OBJECT}),
 
   QueryInterface : XPCOMUtils.generateQI([nsIDOMContactTelField])
 }
 
+//ContactFindSortOptions
+
+const CONTACTFINDSORTOPTIONS_CONTRACTID = "@mozilla.org/contactFindSortOptions;1"
+const CONTACTFINDSORTOPTIONS_CID        = Components.ID("{cb008c06-3bf8-495c-8865-f9ca1673a1e1}");
+const nsIDOMContactFindSortOptions      = Ci.nsIDOMContactFindSortOptions;
+
+function ContactFindSortOptions () { }
+
+ContactFindSortOptions.prototype = {
+  classID: CONTACTFINDSORTOPTIONS_CID,
+  classInfo: XPCOMUtils.generateCI({classID: CONTACTFINDSORTOPTIONS_CID,
+                                    contractID: CONTACTFINDSORTOPTIONS_CONTRACTID,
+                                    classDescription: "ContactFindSortOptions",
+                                    interfaces: [nsIDOMContactFindSortOptions],
+                                    flags: nsIClassInfo.DOM_OBJECT}),
+  QueryInterface: XPCOMUtils.generateQI([nsIDOMContactFindSortOptions])
+};
+
 //ContactFindOptions
 
 const CONTACTFINDOPTIONS_CONTRACTID = "@mozilla.org/contactFindOptions;1";
 const CONTACTFINDOPTIONS_CID        = Components.ID("{e31daea0-0cb6-11e1-be50-0800200c9a66}");
 const nsIDOMContactFindOptions      = Components.interfaces.nsIDOMContactFindOptions;
 
 function ContactFindOptions() { };
 
 ContactFindOptions.prototype = {
 
   classID : CONTACTFINDOPTIONS_CID,
   classInfo : XPCOMUtils.generateCI({classID: CONTACTFINDOPTIONS_CID,
                                      contractID: CONTACTFINDOPTIONS_CONTRACTID,
                                      classDescription: "ContactFindOptions",
-                                     interfaces: [nsIDOMContactFindOptions],
+                                     interfaces: [nsIDOMContactFindSortOptions,
+                                                  nsIDOMContactFindOptions],
                                      flags: nsIClassInfo.DOM_OBJECT}),
-              
-  QueryInterface : XPCOMUtils.generateQI([nsIDOMContactFindOptions])
+
+  QueryInterface : XPCOMUtils.generateQI([nsIDOMContactFindSortOptions,
+                                          nsIDOMContactFindOptions])
 }
 
 //Contact
 
 const CONTACT_CONTRACTID = "@mozilla.org/contact;1";
 const CONTACT_CID        = Components.ID("{da0f7040-388b-11e1-b86c-0800200c9a66}");
 const nsIDOMContact      = Components.interfaces.nsIDOMContact;
 
@@ -187,20 +207,20 @@ Contact.prototype = {
                       impp: 'rw',
                       anniversary: 'rw',
                       sex: 'rw',
                       genderIdentity: 'rw'
                      },
 
   init: function init(aProp) {
     // Accept non-array strings for DOMString[] properties and convert them.
-    function _create(aField) {   
+    function _create(aField) {
       if (Array.isArray(aField)) {
         for (let i = 0; i < aField.length; i++) {
-          if (typeof aField[i] !== "string")
+          if (typeof aField[i] != "string")
             aField[i] = String(aField[i]);
         }
         return aField;
       } else if (aField != null) {
         return [String(aField)];
       }
     };
 
@@ -312,28 +332,30 @@ Contact.prototype = {
                                      flags: nsIClassInfo.DOM_OBJECT}),
 
   QueryInterface : XPCOMUtils.generateQI([nsIDOMContact, nsIDOMContactProperties])
 }
 
 // ContactManager
 
 const CONTACTMANAGER_CONTRACTID = "@mozilla.org/contactManager;1";
-const CONTACTMANAGER_CID        = Components.ID("{d88af7e0-a45f-11e1-b3dd-0800200c9a66}");
+const CONTACTMANAGER_CID        = Components.ID("{1d70322b-f11b-4f19-9586-7bf291f212aa}");
 const nsIDOMContactManager      = Components.interfaces.nsIDOMContactManager;
 
 function ContactManager()
 {
   if (DEBUG) debug("Constructor");
 }
 
 ContactManager.prototype = {
   __proto__: DOMRequestIpcHelper.prototype,
   _oncontactchange: null,
 
+  _cursorData: {},
+
   set oncontactchange(aCallback) {
     if (DEBUG) debug("set oncontactchange");
     let allowCallback = function() {
       if (!this._oncontactchange) {
         cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
       }
       this._oncontactchange = aCallback;
     }.bind(this);
@@ -348,43 +370,56 @@ ContactManager.prototype = {
   },
 
   _setMetaData: function(aNewContact, aRecord) {
     aNewContact.id = aRecord.id;
     aNewContact.published = aRecord.published;
     aNewContact.updated = aRecord.updated;
   },
 
-  _convertContactsArray: function(aContacts) {
-    let contacts = new Array();
+  _convertContact: function CM_convertContact(aContact) {
+    let newContact = new Contact();
+    newContact.init(aContact.properties);
+    this._setMetaData(newContact, aContact);
+    return newContact;
+  },
+
+  _convertContacts: function(aContacts) {
+    let contacts = [];
     for (let i in aContacts) {
-      let newContact = new Contact();
-      newContact.init(aContacts[i].properties);
-      this._setMetaData(newContact, aContacts[i]);
-      contacts.push(newContact);
+      contacts.push(this._convertContact(aContacts[i]));
     }
     return contacts;
   },
 
   receiveMessage: function(aMessage) {
-    if (DEBUG) debug("Contactmanager::receiveMessage: " + aMessage.name);
+    if (DEBUG) debug("receiveMessage: " + aMessage.name);
     let msg = aMessage.json;
     let contacts = msg.contacts;
 
     let req;
     switch (aMessage.name) {
       case "Contacts:Find:Return:OK":
         req = this.getRequest(msg.requestID);
         if (req) {
-          let result = this._convertContactsArray(contacts);
+          let result = this._convertContacts(contacts);
           Services.DOMRequest.fireSuccess(req.request, result);
         } else {
           if (DEBUG) debug("no request stored!" + msg.requestID);
         }
         break;
+      case "Contacts:GetAll:Next":
+        let cursor = this._cursorData[msg.cursorId];
+        let contact = msg.contact ? this._convertContact(msg.contact) : null;
+        if (contact == null) {
+          Services.DOMRequest.fireDone(cursor);
+        } else {
+          Services.DOMRequest.fireSuccess(cursor, contact);
+        }
+        break;
       case "Contacts:GetSimContacts:Return:OK":
         req = this.getRequest(msg.requestID);
         if (req) {
           let result = contacts.map(function(c) {
             let contact = new Contact();
             let prop = {name: [c.alphaId], tel: [ { value: c.number } ]};
 
             if (c.email) {
@@ -460,17 +495,17 @@ ContactManager.prototype = {
       case "find":
       case "getSimContacts":
       case "listen":
         access = "read";
         break;
       default:
         access = "unknown";
       }
-      
+
     let requestID = this.getRequestId({
       request: aRequest,
       allow: function() {
         aAllowCallback();
       }.bind(this),
       cancel : function() {
         if (aCancelCallback) {
           aCancelCallback()
@@ -540,26 +575,53 @@ ContactManager.prototype = {
       cpmm.sendAsyncMessage("Contact:Save", {requestID: this.getRequestId({request: request, reason: reason}), options: options});
     }.bind(this)
     this.askPermission(reason, request, allowCallback);
     return request;
   },
 
   find: function(aOptions) {
     if (DEBUG) debug("find! " + JSON.stringify(aOptions));
-    let request;
-    request = this.createRequest();
+    let request = this.createRequest();
     let options = { findOptions: aOptions };
     let allowCallback = function() {
       cpmm.sendAsyncMessage("Contacts:Find", {requestID: this.getRequestId({request: request, reason: "find"}), options: options});
     }.bind(this)
     this.askPermission("find", request, allowCallback);
     return request;
   },
 
+  createCursor: function CM_createCursor(aRequest) {
+    let id = this._getRandomId();
+    let cursor = Services.DOMRequest.createCursor(this._window, function() {
+      this.handleContinue(id);
+    }.bind(this));
+    if (DEBUG) debug("saved cursor id: " + id);
+    this._cursorData[id] = cursor;
+    return [id, cursor];
+  },
+
+  getAll: function CM_getAll(aOptions) {
+    if (DEBUG) debug("getAll: " + JSON.stringify(aOptions));
+    let [cursorId, cursor] = this.createCursor();
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:GetAll", {
+        cursorId: cursorId, findOptions: aOptions});
+    }.bind(this);
+    this.askPermission("find", cursor, allowCallback);
+    return cursor;
+  },
+
+  handleContinue: function CM_handleContinue(aCursorId) {
+    if (DEBUG) debug("handleContinue: " + aCursorId);
+    cpmm.sendAsyncMessage("Contacts:GetAll:Continue", {
+      cursorId: aCursorId
+    });
+  },
+
   remove: function removeContact(aRecord) {
     let request;
     request = this.createRequest();
     let options = { id: aRecord.id };
     let allowCallback = function() {
       cpmm.sendAsyncMessage("Contact:Remove", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options});
     }.bind(this)
     this.askPermission("remove", request, allowCallback);
@@ -604,17 +666,18 @@ ContactManager.prototype = {
 
     this.initHelper(aWindow, ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO",
                               "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO",
                               "Contact:Save:Return:OK", "Contact:Save:Return:KO",
                               "Contact:Remove:Return:OK", "Contact:Remove:Return:KO",
                               "Contacts:GetSimContacts:Return:OK",
                               "Contacts:GetSimContacts:Return:KO",
                               "Contact:Changed",
-                              "PermissionPromptHelper:AskPermission:OK"]);
+                              "PermissionPromptHelper:AskPermission:OK",
+                              "Contacts:GetAll:Next"]);
   },
 
   // Called from DOMRequestIpcHelper
   uninit: function uninit() {
     if (DEBUG) debug("uninit call");
     if (this._oncontactchange)
       this._oncontactchange = null;
   },
@@ -625,9 +688,9 @@ ContactManager.prototype = {
   classInfo : XPCOMUtils.generateCI({classID: CONTACTMANAGER_CID,
                                      contractID: CONTACTMANAGER_CONTRACTID,
                                      classDescription: "ContactManager",
                                      interfaces: [nsIDOMContactManager],
                                      flags: nsIClassInfo.DOM_OBJECT})
 }
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
-                       [Contact, ContactManager, ContactProperties, ContactAddress, ContactField, ContactTelField, ContactFindOptions])
+                       [Contact, ContactManager, ContactProperties, ContactAddress, ContactField, ContactTelField, ContactFindSortOptions, ContactFindOptions])
--- a/dom/contacts/ContactManager.manifest
+++ b/dom/contacts/ContactManager.manifest
@@ -5,18 +5,21 @@ component {eba48030-89e8-11e1-b0c4-08002
 contract @mozilla.org/contactAddress;1 {eba48030-89e8-11e1-b0c4-0800200c9a66}
 
 component {e2cb19c0-e4aa-11e1-9b23-0800200c9a66} ContactManager.js
 contract @mozilla.org/contactField;1 {e2cb19c0-e4aa-11e1-9b23-0800200c9a66}
 
 component {ed0ab260-e4aa-11e1-9b23-0800200c9a66} ContactManager.js
 contract @mozilla.org/contactTelField;1 {ed0ab260-e4aa-11e1-9b23-0800200c9a66}
 
-component {e31daea0-0cb6-11e1-be50-0800200c9a66} ContactManager.js
-contract @mozilla.org/contactFindOptions;1 {e31daea0-0cb6-11e1-be50-0800200c9a66}
+component {cb008c06-3bf8-495c-8865-f9ca1673a1e1} ContactManager.js
+contract @mozilla.org/contactFindSortOptions;1 {cb008c06-3bf8-495c-8865-f9ca1673a1e1}
+
+component {e13ca4c5-c9cd-40bb-95e9-b636d42f5edf} ContactManager.js
+contract @mozilla.org/contactFindOptions;1 {e13ca4c5-c9cd-40bb-95e9-b636d42f5edf}
 
 component {da0f7040-388b-11e1-b86c-0800200c9a66} ContactManager.js
 contract @mozilla.org/contact;1 {da0f7040-388b-11e1-b86c-0800200c9a66}
 category JavaScript-global-constructor mozContact @mozilla.org/contact;1
 
-component {d88af7e0-a45f-11e1-b3dd-0800200c9a66} ContactManager.js
-contract @mozilla.org/contactManager;1 {d88af7e0-a45f-11e1-b3dd-0800200c9a66}
+component {1d70322b-f11b-4f19-9586-7bf291f212aa} ContactManager.js
+contract @mozilla.org/contactManager;1 {1d70322b-f11b-4f19-9586-7bf291f212aa}
 category JavaScript-navigator-property mozContacts @mozilla.org/contactManager;1
--- a/dom/contacts/fallback/ContactDB.jsm
+++ b/dom/contacts/fallback/ContactDB.jsm
@@ -13,27 +13,30 @@ const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
 Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
 
 const DB_NAME = "contacts";
-const DB_VERSION = 7;
+const DB_VERSION = 8;
 const STORE_NAME = "contacts";
+const SAVED_GETALL_STORE_NAME = "getallcache";
 
 this.ContactDB = function ContactDB(aGlobal) {
   if (DEBUG) debug("Constructor");
   this._global = aGlobal;
 }
 
 ContactDB.prototype = {
   __proto__: IndexedDBHelper.prototype,
 
+  cursorData: {},
+
   upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
     if (DEBUG) debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
     let db = aDb;
     let objectStore;
     for (let currVersion = aOldVersion; currVersion < aNewVersion; currVersion++) {
       if (currVersion == 0) {
         /**
          * Create the initial database schema.
@@ -233,16 +236,19 @@ ContactDB.prototype = {
         let blackList = ["tel", "familyName", "givenName",  "familyNameLowerCase",
                          "givenNameLowerCase", "telLowerCase", "category", "email",
                          "emailLowerCase"];
         for (var i = 0; i < names.length; i++) {
           if (blackList.indexOf(names[i]) < 0) {
             objectStore.deleteIndex(names[i]);
           }
         }
+      } else if (currVersion == 7) {
+        if (DEBUG) debug("Adding object store for cached searches");
+        db.createObjectStore(SAVED_GETALL_STORE_NAME);
       }
     }
   },
 
   makeImport: function makeImport(aContact) {
     let contact = {};
     contact.properties = {
       name:            [],
@@ -377,16 +383,51 @@ ContactDB.prototype = {
       Cu.reportError("Contact without ID");
     }
     if (!record.published) {
       record.published = new Date();
     }
     record.updated = new Date();
   },
 
+  removeObjectFromCache: function CDB_removeObjectFromCache(aObjectId, aCallback) {
+    if (DEBUG) debug("removeObjectFromCache: " + aObjectId);
+    if (!aObjectId) {
+      if (DEBUG) debug("No object ID passed");
+      return;
+    }
+    this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) {
+      store.openCursor().onsuccess = function(e) {
+        let cursor = e.target.result;
+        if (cursor) {
+          for (let i = 0; i < cursor.value.length; ++i) {
+            if (cursor.value[i] == aObjectId) {
+              if (DEBUG) debug("id matches cache");
+              cursor.value.splice(i, 1);
+              cursor.update(cursor.value);
+              break;
+            }
+          }
+          cursor.continue();
+        } else {
+          aCallback();
+        }
+      }.bind(this);
+    }.bind(this));
+  },
+
+  // Invalidate the entire cache. It will be incrementally regenerated on demand
+  // See getCacheForQuery
+  invalidateCache: function CDB_invalidateCache() {
+    if (DEBUG) debug("invalidate cache");
+    this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) {
+      store.clear();
+    });
+  },
+
   saveContact: function saveContact(aContact, successCb, errorCb) {
     let contact = this.makeImport(aContact);
     this.newTxn("readwrite", STORE_NAME, function (txn, store) {
       if (DEBUG) debug("Going to update" + JSON.stringify(contact));
 
       // Look up the existing record and compare the update timestamp.
       // If no record exists, just add the new entry.
       let newRequest = store.get(contact.id);
@@ -403,64 +444,227 @@ ContactDB.prototype = {
             return;
           } else {
             if (DEBUG) debug("rev check OK");
             contact.published = event.target.result.published;
             contact.updated = new Date();
             store.put(contact);
           }
         }
+        this.invalidateCache();
       }.bind(this);
     }.bind(this), successCb, errorCb);
   },
 
   removeContact: function removeContact(aId, aSuccessCb, aErrorCb) {
-    this.newTxn("readwrite", STORE_NAME, function (txn, store) {
-      if (DEBUG) debug("Going to delete" + aId);
-      store.delete(aId);
-    }, aSuccessCb, aErrorCb);
+    if (DEBUG) debug("removeContact: " + aId);
+    this.removeObjectFromCache(aId, function() {
+      this.newTxn("readwrite", STORE_NAME, function(txn, store) {
+        store.delete(aId).onsuccess = function() {
+          aSuccessCb();
+        };
+      }, null, aErrorCb);
+    }.bind(this));
   },
 
   clear: function clear(aSuccessCb, aErrorCb) {
     this.newTxn("readwrite", STORE_NAME, function (txn, store) {
       if (DEBUG) debug("Going to clear all!");
       store.clear();
     }, aSuccessCb, aErrorCb);
   },
 
+  getObjectById: function CDB_getObjectById(aStore, aObjectId, aCallback) {
+    if (DEBUG) debug("getObjectById: " + aStore + ":" + aObjectId);
+    this.newTxn("readonly", aStore, function (txn, store) {
+      let req = store.get(aObjectId);
+      req.onsuccess = function (event) {
+        aCallback(event.target.result);
+      };
+      req.onerror = function (event) {
+        aCallback(null);
+      };
+    });
+  },
+
+  getCacheForQuery: function CDB_getCacheForQuery(aQuery, aCursorId, aSuccessCb) {
+    if (DEBUG) debug("getCacheForQuery");
+    // Here we try to get the cached results for query `aQuery'. If they don't
+    // exist, it means the cache was invalidated and needs to be recreated, so
+    // we do that. Otherwise, we just return the existing cache.
+    this.getObjectById(SAVED_GETALL_STORE_NAME, aQuery, function (aCache) {
+      if (!aCache) {
+        if (DEBUG) debug("creating cache for query " + aQuery);
+        this.createCacheForQuery(aQuery, aCursorId, aSuccessCb);
+      } else {
+        if (DEBUG) debug("cache exists");
+        if (!this.cursorData[aCursorId]) {
+          this.cursorData[aCursorId] = aCache;
+        }
+        aSuccessCb(aCache);
+      }
+    }.bind(this));
+  },
+
+  setCacheForQuery: function CDB_setCacheForQuery(aQuery, aCache, aCallback) {
+    this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) {
+      let req = store.put(aCache, aQuery);
+      if (!aCallback) {
+        return;
+      }
+      req.onsuccess = function () {
+        aCallback(true);
+      };
+      req.onerror = function () {
+        aCallback(false);
+      };
+    });
+  },
+
+  createCacheForQuery: function CDB_createCacheForQuery(aQuery, aCursorId, aSuccessCb, aFailureCb) {
+    this.find(function (aContacts) {
+      if (aContacts) {
+        let contactsArray = [];
+        for (let i in aContacts) {
+          contactsArray.push(aContacts[i].id);
+        }
+
+        this.setCacheForQuery(aQuery, contactsArray);
+        this.cursorData[aCursorId] = contactsArray;
+        aSuccessCb(contactsArray);
+      } else {
+        aSuccessCb(null);
+      }
+    }.bind(this),
+    function (aErrorMsg) { aFailureCb(aErrorMsg); },
+    JSON.parse(aQuery));
+  },
+
+  getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) {
+    // Recreate the cache for this query if needed
+    let optionStr = JSON.stringify(aOptions);
+    this.getCacheForQuery(optionStr, aCursorId, function (aCachedResults) {
+      if (aCachedResults && aCachedResults.length > 0) {
+        if (DEBUG) debug("query returned at least one contact");
+        this.getObjectById(STORE_NAME, aCachedResults[0], function (aContact) {
+          this.cursorData[aCursorId].shift();
+          aSuccessCb(aContact);
+        }.bind(this));
+      } else { // no contacts
+        if (DEBUG) debug("query returned no contacts");
+        aSuccessCb(null);
+      }
+    }.bind(this));
+  },
+
+  getNext: function CDB_getNext(aSuccessCb, aFailureCb, aCursorId) {
+    if (DEBUG) debug("ContactDB:getNext: " + aCursorId);
+    let aCachedResults = this.cursorData[aCursorId];
+    if (DEBUG) debug("got transient cache");
+    if (aCachedResults.length > 0) {
+      this.getObjectById(STORE_NAME, aCachedResults[0], function(aContact) {
+        this.cursorData[aCursorId].shift();
+        if (aContact) {
+          aSuccessCb(aContact);
+        } else {
+          // If the contact ID in cache is invalid, it was removed recently and
+          // the cache hasn't been updated to reflect the change, so we skip it.
+          if (DEBUG) debug("invalid contact in cache: " + aCachedResults[0]);
+          return this.getNext(aSuccessCb, aFailureCb, aCursorId);
+        }
+      }.bind(this));
+    } else { // last contact
+      delete this.cursorData[aCursorId];
+      aSuccessCb(null);
+    }
+  },
+
+  releaseCursors: function CDB_releaseCursors(aCursors) {
+    for (let i of aCursors) {
+      delete this.cursorData[i];
+    }
+  },
+
+  /*
+   * Sorting the contacts by sortBy field. aSortBy can either be familyName or givenName.
+   * If 2 entries have the same sortyBy field or no sortBy field is present, we continue
+   * sorting with the other sortyBy field.
+   */
+  sortResults: function CDB_sortResults(aResults, aFindOptions) {
+    if (!aFindOptions)
+      return;
+    if (aFindOptions.sortBy != "undefined") {
+      aResults.sort(function (a, b) {
+        let x, y;
+        let result = 0;
+        let sortOrder = aFindOptions.sortOrder;
+        let sortBy = aFindOptions.sortBy == "familyName" ? [ "familyName", "givenName" ] : [ "givenName" , "familyName" ];
+        let xIndex = 0;
+        let yIndex = 0;
+
+        do {
+          while (xIndex < sortBy.length && !x) {
+            x = a.properties[sortBy[xIndex]] ? a.properties[sortBy[xIndex]][0].toLowerCase() : null;
+            xIndex++;
+          }
+          if (!x) {
+            return sortOrder == 'ascending' ? 1 : -1;
+          }
+          while (yIndex < sortBy.length && !y) {
+            y = b.properties[sortBy[yIndex]] ? b.properties[sortBy[yIndex]][0].toLowerCase() : null;
+            yIndex++;
+          }
+          if (!y) {
+            return sortOrder == 'ascending' ? 1 : -1;
+          }
+
+          result = x.localeCompare(y);
+          x = null;
+          y = null;
+        } while (result == 0);
+
+        return sortOrder == 'ascending' ? result : -result;
+      });
+    }
+    if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) {
+      if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit);
+      aResults.splice(aFindOptions.filterLimit, aResults.length);
+    }
+  },
+
   /**
    * @param successCb
    *        Callback function to invoke with result array.
    * @param failureCb [optional]
    *        Callback function to invoke when there was an error.
    * @param options [optional]
    *        Object specifying search options. Possible attributes:
    *        - filterBy
    *        - filterOp
    *        - filterValue
    *        - count
    */
   find: function find(aSuccessCb, aFailureCb, aOptions) {
-    if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp + "\n");
+    if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp);
     let self = this;
     this.newTxn("readonly", STORE_NAME, function (txn, store) {
       if (aOptions && (aOptions.filterOp == "equals" || aOptions.filterOp == "contains")) {
         self._findWithIndex(txn, store, aOptions);
       } else {
         self._findAll(txn, store, aOptions);
       }
     }, aSuccessCb, aFailureCb);
   },
 
   _findWithIndex: function _findWithIndex(txn, store, options) {
     if (DEBUG) debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " ");
     let fields = options.filterBy;
     for (let key in fields) {
       if (DEBUG) debug("key: " + fields[key]);
-      if (!store.indexNames.contains(fields[key]) && !fields[key] == "id") {
+      if (!store.indexNames.contains(fields[key]) && fields[key] != "id") {
         if (DEBUG) debug("Key not valid!" + fields[key] + ", " + store.indexNames);
         txn.abort();
         return;
       }
     }
 
     // lookup for all keys
     if (options.filterBy.length == 0) {
@@ -501,32 +705,35 @@ ContactDB.prototype = {
         let range = this._global.IDBKeyRange.bound(tmp, tmp + "\uFFFF");
         let index = store.index(key + "LowerCase");
         request = index.mozGetAll(range, limit);
       }
       if (!txn.result)
         txn.result = {};
 
       request.onsuccess = function (event) {
-        if (DEBUG) debug("Request successful. Record count:" + event.target.result.length);
+        if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
+        this.sortResults(event.target.result, options);
         for (let i in event.target.result)
           txn.result[event.target.result[i].id] = this.makeExport(event.target.result[i]);
       }.bind(this);
     }
   },
 
   _findAll: function _findAll(txn, store, options) {
     if (DEBUG) debug("ContactDB:_findAll:  " + JSON.stringify(options));
     if (!txn.result)
       txn.result = {};
     // Sorting functions takes care of limit if set.
     let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
     store.mozGetAll(null, limit).onsuccess = function (event) {
-      if (DEBUG) debug("Request successful. Record count:", event.target.result.length);
-      for (let i in event.target.result)
+      if (DEBUG) debug("Request successful. Record count:" + event.target.result.length);
+      this.sortResults(event.target.result, options);
+      for (let i in event.target.result) {
         txn.result[event.target.result[i].id] = this.makeExport(event.target.result[i]);
+      }
     }.bind(this);
   },
 
   init: function init(aGlobal) {
-      this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME], aGlobal);
+      this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME], aGlobal);
   }
 };
--- a/dom/contacts/fallback/ContactService.jsm
+++ b/dom/contacts/fallback/ContactService.jsm
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const DEBUG = false;
 function debug(s) { dump("-*- Fallback ContactService component: " + s + "\n"); }
 
-const Cu = Components.utils; 
+const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 this.EXPORTED_SYMBOLS = ["DOMContactManager"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/ContactDB.jsm");
@@ -32,131 +32,116 @@ XPCOMUtils.defineLazyGetter(this, "mRIL"
     };
   }
   return telephony.getService(Ci.nsIRadioInterfaceLayer);
 });
 
 let myGlobal = this;
 
 this.DOMContactManager = {
+  // maps children to their live cursors so we can cleanup on shutdown/crash
+  _liveCursors: {},
+
   init: function() {
     if (DEBUG) debug("Init");
-    this._messages = ["Contacts:Find", "Contacts:Clear", "Contact:Save",
+    this._messages = ["Contacts:Find", "Contacts:GetAll", "Contacts:GetAll:Continue", "Contacts:Clear", "Contact:Save",
                       "Contact:Remove", "Contacts:GetSimContacts",
                       "Contacts:RegisterForMessages", "child-process-shutdown"];
     this._children = [];
-    this._messages.forEach((function(msgName) {
+    this._messages.forEach(function(msgName) {
       ppmm.addMessageListener(msgName, this);
-    }).bind(this));
+    }.bind(this));
 
     var idbManager = Components.classes["@mozilla.org/dom/indexeddb/manager;1"].getService(Ci.nsIIndexedDatabaseManager);
     idbManager.initWindowless(myGlobal);
     this._db = new ContactDB(myGlobal);
     this._db.init(myGlobal);
 
     Services.obs.addObserver(this, "profile-before-change", false);
   },
 
   observe: function(aSubject, aTopic, aData) {
     myGlobal = null;
-    this._messages.forEach((function(msgName) {
+    this._messages.forEach(function(msgName) {
       ppmm.removeMessageListener(msgName, this);
-    }).bind(this));
+    }.bind(this));
     Services.obs.removeObserver(this, "profile-before-change");
     ppmm = null;
     this._messages = null;
     if (this._db)
       this._db.close();
     this._db = null;
   },
 
   assertPermission: function(aMessage, aPerm) {
     if (!aMessage.target.assertPermission(aPerm)) {
-      Cu.reportError("Contacts message " + msg.name +
+      Cu.reportError("Contacts message " + aMessage.name +
                      " from a content process with no" + aPerm + " privileges.");
       return false;
     }
     return true;
   },
 
   broadcastMessage: function broadcastMessage(aMsgName, aContent) {
     this._children.forEach(function(msgMgr) {
       msgMgr.sendAsyncMessage(aMsgName, aContent);
     });
   },
 
   receiveMessage: function(aMessage) {
-    if (DEBUG) debug("Fallback DOMContactManager::receiveMessage " + aMessage.name);
+    if (DEBUG) debug("receiveMessage " + aMessage.name);
     let mm = aMessage.target;
     let msg = aMessage.data;
 
-    /*
-     * Sorting the contacts by sortBy field. sortBy can either be familyName or givenName.
-     * If 2 entries have the same sortyBy field or no sortBy field is present, we continue 
-     * sorting with the other sortyBy field.
-     */
-    function sortfunction(a, b){
-      let x, y;
-      let result = 0;
-      let findOptions = msg.options.findOptions;
-      let sortOrder = findOptions.sortOrder;
-      let sortBy = findOptions.sortBy === "familyName" ? [ "familyName", "givenName" ] : [ "givenName" , "familyName" ];
-      let xIndex = 0;
-      let yIndex = 0;
-
-      do {
-        while (xIndex < sortBy.length && !x) {
-          x = a.properties[sortBy[xIndex]] ? a.properties[sortBy[xIndex]][0].toLowerCase() : null;
-          xIndex++;
-        }
-        if (!x) {
-          return sortOrder == 'ascending' ? 1 : -1;
-        }
-        while (yIndex < sortBy.length && !y) {
-          y = b.properties[sortBy[yIndex]] ? b.properties[sortBy[yIndex]][0].toLowerCase() : null;
-          yIndex++;
-        }
-        if (!y) {
-          return sortOrder == 'ascending' ? 1 : -1;
-        }
-
-        result = x.localeCompare(y);
-        x = null;
-        y = null;
-      } while (result === 0);
-
-      return sortOrder == 'ascending' ? result : -result;
-    }
-
     switch (aMessage.name) {
       case "Contacts:Find":
         if (!this.assertPermission(aMessage, "contacts-read")) {
           return null;
         }
-        let result = new Array();
+        let result = [];
         this._db.find(
           function(contacts) {
-            for (let i in contacts)
+            for (let i in contacts) {
               result.push(contacts[i]);
-            if (msg.options && msg.options.findOptions) {
-              let findOptions = msg.options.findOptions;
-              if (findOptions.sortOrder !== 'undefined' && findOptions.sortBy !== 'undefined') {
-                if (DEBUG) debug('sortBy: ' + findOptions.sortBy + ', sortOrder: ' + findOptions.sortOrder );
-                result.sort(sortfunction);
-                if (findOptions.filterLimit)
-                  result = result.slice(0, findOptions.filterLimit);
-              }
             }
 
             if (DEBUG) debug("result:" + JSON.stringify(result));
             mm.sendAsyncMessage("Contacts:Find:Return:OK", {requestID: msg.requestID, contacts: result});
           }.bind(this),
-          function(aErrorMsg) { mm.sendAsyncMessage("Contacts:Find:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }) }.bind(this),
+          function(aErrorMsg) { mm.sendAsyncMessage("Contacts:Find:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }); }.bind(this),
           msg.options.findOptions);
         break;
+      case "Contacts:GetAll":
+        if (!this.assertPermission(aMessage, "contacts-read")) {
+          return null;
+        }
+        this._db.getAll(
+          function(aContact) {
+            mm.sendAsyncMessage("Contacts:GetAll:Next", {cursorId: msg.cursorId, contact: aContact});
+          },
+          function(aErrorMsg) { mm.sendAsyncMessage("Contacts:Find:Return:KO", { errorMsg: aErrorMsg }); },
+          msg.findOptions, msg.cursorId);
+        if (Array.isArray(this._liveCursors[mm])) {
+          this._liveCursors[mm].push(msg.cursorId);
+        } else {
+          this._liveCursors[mm] = [msg.cursorId];
+        }
+        break;
+      case "Contacts:GetAll:Continue":
+        this._db.getNext(
+          function(aContact) {
+            if (aContact == null) { // last contact, release the cursor
+              let cursors = this._liveCursors[mm];
+              cursors.splice(cursors.indexOf(msg.cursorId), 1);
+            }
+            mm.sendAsyncMessage("Contacts:GetAll:Next", {cursorId: msg.cursorId, contact: aContact});
+          }.bind(this),
+          function(aErrorMsg) { mm.sendAsyncMessage("Contacts:Find:Return:KO", { errorMsg: aErrorMsg }); },
+          msg.cursorId);
+        break;
       case "Contact:Save":
         if (msg.options.reason === "create") {
           if (!this.assertPermission(aMessage, "contacts-create")) {
             return null;
           }
         } else {
           if (!this.assertPermission(aMessage, "contacts-write")) {
             return null;
@@ -220,16 +205,18 @@ this.DOMContactManager = {
         }
         if (DEBUG) debug("Register!");
         if (this._children.indexOf(mm) == -1) {
           this._children.push(mm);
         }
         break;
       case "child-process-shutdown":
         if (DEBUG) debug("Unregister");
+        this._db.releaseCursors(this._liveCursors[mm]);
+        delete this._liveCursors[mm];
         let index = this._children.indexOf(mm);
         if (index != -1) {
           if (DEBUG) debug("Unregister index: " + index);
           this._children.splice(index, 1);
         }
         break;
       default:
         if (DEBUG) debug("WRONG MESSAGE NAME: " + aMessage.name);
--- a/dom/contacts/tests/Makefile.in
+++ b/dom/contacts/tests/Makefile.in
@@ -14,12 +14,13 @@ include $(DEPTH)/config/autoconf.mk
 DIRS = \
   $(NULL)
 
 MOCHITEST_FILES = \
   test_contacts_basics.html \
   test_contacts_events.html \
   test_contacts_blobs.html \
   test_contacts_international.html \
+  test_contacts_getall.html \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_getall.html
@@ -0,0 +1,425 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=836519
+-->
+<head>
+  <title>Mozilla Bug 836519</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=836519">Mozilla Bug 836519</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.8">
+
+"use strict";
+
+// this shouldn't be necessary when bug 792594 is fixed
+if (!SpecialPowers.getBoolPref("dom.mozContacts.enabled")) {
+  let comp = SpecialPowers.wrap(SpecialPowers.Components);
+  comp.utils.import("resource://gre/modules/ContactService.jsm");
+  comp.utils.import("resource://gre/modules/PermissionPromptHelper.jsm");
+  SpecialPowers.setBoolPref("dom.mozContacts.enabled", true);
+}
+
+SpecialPowers.addPermission("contacts-write", true, document);
+SpecialPowers.addPermission("contacts-read", true, document);
+SpecialPowers.addPermission("contacts-create", true, document);
+
+let adr1 = {
+  type: "work",
+  streetAddress: "street 1",
+  locality: "locality 1",
+  region: "region 1",
+  postalCode: "postal code 1",
+  countryName: "country 1"
+};
+
+let properties1 = {
+  name: "Testname1",
+  familyName: ["TestFamilyName","Wagner"],
+  givenName: ["Test1","Test2"],
+  nickname: "nicktest",
+  tel: [{type: ["work"], value: "123456", carrier: "testCarrier"} , {type: ["home", "fax"], value: "+9-876-5432"}],
+  adr: adr1,
+  email: [{type: ["work"], value: "x@y.com"}]
+};
+
+function onUnwantedSuccess() {
+  ok(false, "onUnwantedSuccess: shouldn't get here");
+}
+
+function onFailure() {
+  ok(false, "in on Failure!");
+}
+
+function checkStr(str1, str2, msg) {
+  if (str1)
+    ok(typeof str1 == "string" ? [str1] : str1, (typeof str2 == "string") ? [str2] : str2, msg);
+}
+
+function checkAddress(adr1, adr2) {
+  checkStr(adr1.type, adr2.type, "Same type");
+  checkStr(adr1.streetAddress, adr2.streetAddress, "Same streetAddress");
+  checkStr(adr1.locality, adr2.locality, "Same locality");
+  checkStr(adr1.region, adr2.region, "Same region");
+  checkStr(adr1.postalCode, adr2.postalCode, "Same postalCode");
+  checkStr(adr1.countryName, adr2.countryName, "Same countryName");
+}
+
+function checkTel(tel1, tel2) {
+  checkStr(tel1.type, tel2.type, "Same type");
+  checkStr(tel1.value, tel2.value, "Same value");
+  checkStr(tel1.carrier, tel2.carrier, "Same carrier");
+}
+
+function checkField(field1, field2) {
+  checkStr(field1.type, field2.type, "Same type");
+  checkStr(field1.value, field2.value, "Same value");
+}
+
+function checkContacts(contact1, contact2) {
+  checkStr(contact1.name, contact2.name, "Same name");
+  checkStr(contact1.honorificPrefix, contact2.honorificPrefix, "Same honorificPrefix");
+  checkStr(contact1.givenName, contact2.givenName, "Same givenName");
+  checkStr(contact1.additionalName, contact2.additionalName, "Same additionalName");
+  checkStr(contact1.familyName, contact2.familyName, "Same familyName");
+  checkStr(contact1.honorificSuffix, contact2.honorificSuffix, "Same honorificSuffix");
+  checkStr(contact1.nickname, contact2.nickname, "Same nickname");
+  checkStr(contact1.category, contact2.category, "Same category");
+  checkStr(contact1.org, contact2.org, "Same org");
+  checkStr(contact1.jobTitle, contact2.jobTitle, "Same jobTitle");
+  is(contact1.bday ? contact1.bday.valueOf() : null, contact2.bday ? contact2.bday.valueOf() : null, "Same birthday");
+  checkStr(contact1.note, contact2.note, "Same note");
+  is(contact1.anniversary ? contact1.anniversary.valueOf() : null , contact2.anniversary ? contact2.anniversary.valueOf() : null, "Same anniversary");
+  is(contact1.sex, contact2.sex, "Same sex");
+  is(contact1.genderIdentity, contact2.genderIdentity, "Same genderIdentity");
+
+  for (let i in contact1.email) {
+    if (contact1.email) {
+      ok(contact2.email != null, "conatct2.email exists");
+    }
+    if (contact2.email) {
+      ok(contact1.email != null, "conatct1.email exists");
+    }
+    checkField(contact1.email[i], contact2.email[i]);
+  }
+  for (let i in contact1.adr) {
+    if (contact1.adr) {
+      ok(contact2.adr != null, "conatct2.adr exists");
+    }
+    if (contact2.adr) {
+      ok(contact1.adr != null, "conatct1.adr exists");
+    }
+    checkAddress(contact1.adr[i], contact2.adr[i]);
+  }
+  for (let i in contact1.tel) {
+    if (contact1.tel) {
+      ok(contact2.tel != null, "conatct2.tel exists");
+    }
+    if (contact2.tel) {
+      ok(contact1.tel != null, "conatct1.tel exists");
+    }
+    checkTel(contact1.tel[i], contact2.tel[i]);
+  }
+  for (let i in contact1.url) {
+    if (contact1.url) {
+      ok(contact2.url != null, "conatct2.url exists");
+    }
+    if (contact2.url) {
+      ok(contact1.url != null, "conatct1.url exists");
+    }
+    checkField(contact1.url[i], contact2.url[i]);
+  }
+  for (let i in contact1.impp) {
+    if (contact1.impp) {
+      ok(contact2.impp != null, "conatct2.impp exists");
+    }
+    if (contact2.impp) {
+      ok(contact1.impp != null, "conatct1.impp exists");
+    }
+    checkField(contact1.impp[i], contact2.impp[i]);
+  }
+}
+
+function clearDatabase() {
+  ok(true, "Clearing database");
+  req = mozContacts.clear();
+  req.onsuccess = function() {
+    ok(true, "Cleared the database");
+    next();
+  };
+  req.onerror = onFailure;
+}
+
+function add20Contacts() {
+  ok(true, "Adding 20 contacts");
+  for (let i=0; i<19; i++) {
+    createResult1 = new mozContact();
+    createResult1.init(properties1);
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function() {
+      ok(createResult1.id, "The contact now has an ID.");
+    };
+    req.onerror = onFailure;
+  };
+  createResult1 = new mozContact();
+  createResult1.init(properties1);
+  req = mozContacts.save(createResult1);
+  req.onsuccess = function() {
+    ok(createResult1.id, "The contact now has an ID.");
+    ok(createResult1.name == properties1.name, "Same Name");
+    next();
+  };
+  req.onerror = onFailure;
+}
+
+let createResult1;
+
+let index = 0;
+let req;
+let mozContacts = window.navigator.mozContacts;
+
+let steps = [
+  clearDatabase,
+  function() {
+    // add a contact
+    createResult1 = new mozContact();
+    createResult1.init({});
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function() {
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Retrieving one contact with getAll");
+    req = mozContacts.getAll({});
+    let count = 0;
+    req.onsuccess = function(event) {
+      ok(true, "on success");
+      if (req.result) {
+        ok(true, "result is valid");
+        count++;
+        req.continue();
+      } else {
+        is(count, 1, "last contact - only one contact returned");
+        next();
+      }
+    };
+    req.onerror = onFailure;
+  },
+
+  clearDatabase,
+  add20Contacts,
+
+  function() {
+    ok(true, "Retrieving 20 contacts with getAll");
+    req = mozContacts.getAll({});
+    let count = 0;
+    req.onsuccess = function(event) {
+      if (req.result) {
+        ok(true, "result is valid");
+        count++;
+        req.continue();
+      } else {
+        is(count, 20, "last contact - 20 contacts returned");
+        next();
+      }
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Deleting one contact");
+    req = mozContacts.remove(createResult1);
+    req.onsuccess = function() {
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Test cache invalidation");
+    req = mozContacts.getAll({});
+    let count = 0;
+    req.onsuccess = function(event) {
+      ok(true, "on success");
+      if (req.result) {
+        ok(true, "result is valid");
+        count++;
+        req.continue();
+      } else {
+        is(count, 19, "last contact - 19 contacts returned");
+        next();
+      }
+    };
+    req.onerror = onFailure;
+  },
+
+  clearDatabase,
+  add20Contacts,
+
+  function() {
+    ok(true, "Test cache invalidation between getAll and getNext");
+    req = mozContacts.find({});
+    req.onsuccess = function(e) {
+      let lastContact = e.target.result[e.target.result.length-1];
+      req = mozContacts.getAll({});
+      let count = 0;
+      let firstResult = true;
+      req.onsuccess = function(event) {
+        ok(true, "on success");
+        if (firstResult) {
+          if (req.result) {
+            count++;
+          }
+          let delReq = mozContacts.remove(lastContact);
+          delReq.onsuccess = function() {
+            firstResult = false;
+            req.continue();
+          };
+        } else {
+          if (req.result) {
+            ok(true, "result is valid");
+            count++;
+            req.continue();
+          } else {
+            is(count, 19, "19 contacts returned");
+            ok(true, "last contact");
+            next();
+          }
+        }
+      };
+    };
+  },
+
+  clearDatabase,
+  add20Contacts,
+
+  function() {
+    ok(true, "Delete the currect contact while iterating");
+    req = mozContacts.getAll({});
+    let count = 0;
+    let previousId = null;
+    req.onsuccess = function() {
+      if (req.result) {
+        ok(true, "on success");
+        if (previousId) {
+          isnot(previousId, req.result.id, "different contacts returned");
+        }
+        previousId = req.result.id;
+        count++;
+        let delReq = mozContacts.remove(req.result);
+        delReq.onsuccess = function() {
+          ok(true, "deleted current contact");
+          req.continue();
+        };
+      } else {
+        is(count, 20, "returned 20 contacts");
+        next();
+      }
+    };
+  },
+
+  clearDatabase,
+  add20Contacts,
+
+  function() {
+    ok(true, "Iterating through the contact list inside a cursor callback");
+    let count1 = 0, count2 = 0;
+    let req1 = mozContacts.getAll({});
+    let req2;
+    req1.onsuccess = function() {
+      if (count1 == 0) {
+        count1++;
+        req2 = mozContacts.getAll({});
+        req2.onsuccess = function() {
+          if (req2.result) {
+            count2++;
+            req2.continue();
+          } else {
+            is(count2, 20, "inner cursor returned 20 contacts");
+            req1.continue();
+          }
+        };
+      } else {
+        if (req1.result) {
+          count1++;
+          req1.continue();
+        } else {
+          is(count1, 20, "outer cursor returned 20 contacts");
+          next();
+        }
+      }
+    };
+  },
+
+  clearDatabase,
+  add20Contacts,
+
+  function() {
+    ok(true, "20 concurrent cursors");
+    const NUM_CURSORS = 20;
+    let completed = 0;
+    for (let i = 0; i < NUM_CURSORS; ++i) {
+      mozContacts.getAll({}).onsuccess = (function(i) {
+        let count = 0;
+        return function(event) {
+          let req = event.target;
+          if (req.result) {
+            count++;
+            req.continue();
+          } else {
+            is(count, 20, "cursor " + i + " returned 20 contacts");
+            if (++completed == NUM_CURSORS) {
+              next();
+            }
+          }
+        };
+      })(i);
+    }
+  },
+
+  clearDatabase,
+
+  function() {
+    ok(true, "all done!\n");
+    SimpleTest.finish();
+  }
+];
+
+function next() {
+  ok(true, "Begin!");
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index++]();
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+function permissionTest() {
+  if (gContactsEnabled) {
+    next();
+  } else {
+    is(mozContacts, null, "mozContacts is null when not enabled.");
+    SimpleTest.finish();
+  }
+}
+
+let gContactsEnabled = SpecialPowers.getBoolPref("dom.mozContacts.enabled");
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(permissionTest);
+</script>
+</pre>
+</body>
+</html>
--- a/dom/interfaces/contacts/nsIDOMContactManager.idl
+++ b/dom/interfaces/contacts/nsIDOMContactManager.idl
@@ -2,37 +2,41 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "domstubs.idl"
 #include "nsIDOMContactProperties.idl"
 #include "nsIDOMEventTarget.idl"
 
 interface nsIArray;
+interface nsIDOMContactFindSortOptions;
 interface nsIDOMContactFindOptions;
 interface nsIDOMContactProperties;
 interface nsIDOMDOMRequest;
+interface nsIDOMDOMCursor;
 
 [scriptable, uuid(da0f7040-388b-11e1-b86c-0800200c9a66)]
 interface nsIDOMContact : nsIDOMContactProperties
 {
   attribute DOMString id;
   readonly attribute jsval     published;
   readonly attribute jsval     updated;
 
   void init(in nsIDOMContactProperties properties);  // Workaround BUG 723206
 };
 
-[scriptable, uuid(d88af7e0-a45f-11e1-b3dd-0800200c9a66)]
+[scriptable, uuid(1d70322b-f11b-4f19-9586-7bf291f212aa)]
 interface nsIDOMContactManager : nsISupports
 {
   nsIDOMDOMRequest find(in nsIDOMContactFindOptions options);
 
+  nsIDOMDOMCursor getAll(in nsIDOMContactFindSortOptions options);
+
   nsIDOMDOMRequest clear();
 
   nsIDOMDOMRequest save(in nsIDOMContact contact);
-  
+
   nsIDOMDOMRequest remove(in nsIDOMContact contact);
 
   nsIDOMDOMRequest getSimContacts(in DOMString type);
 
   attribute nsIDOMEventListener oncontactchange;
 };
--- a/dom/interfaces/contacts/nsIDOMContactProperties.idl
+++ b/dom/interfaces/contacts/nsIDOMContactProperties.idl
@@ -26,24 +26,29 @@ interface nsIDOMContactField : nsISuppor
 };
 
 [scriptable, uuid(ed0ab260-e4aa-11e1-9b23-0800200c9a66)]
 interface nsIDOMContactTelField : nsIDOMContactField
 {
   attribute DOMString carrier;
 };
 
-[scriptable, uuid(e31daea0-0cb6-11e1-be50-0800200c9a66)]
-interface nsIDOMContactFindOptions : nsISupports
+[scriptable, uuid(cb008c06-3bf8-495c-8865-f9ca1673a1e1)]
+interface nsIDOMContactFindSortOptions : nsISupports
+{
+  attribute DOMString sortBy;       // "givenName" or "familyName"
+  attribute DOMString sortOrder;    // e.g. "descending"
+};
+
+[scriptable, uuid(e13ca4c5-c9cd-40bb-95e9-b636d42f5edf)]
+interface nsIDOMContactFindOptions : nsIDOMContactFindSortOptions
 {
   attribute DOMString filterValue;  // e.g. "Tom"
   attribute DOMString filterOp;     // e.g. "contains"
   attribute jsval filterBy;         // DOMString[], e.g. ["givenName", "nickname"]
-  attribute DOMString sortBy;       // "givenName" or "familyName"
-  attribute DOMString sortOrder;    // e.g. "descending"
   attribute unsigned long filterLimit;
 };
 
 [scriptable, uuid(f0ddb360-e4aa-11e1-9b23-0800200c9a66)]
 interface nsIDOMContactProperties : nsISupports
 {
   attribute jsval         name;               // DOMString[]
   attribute jsval         honorificPrefix;    // DOMString[]
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -401,16 +401,17 @@ var interfaceNamesInGlobalScope =
     "SVGMetadataElement",
     "EventListenerInfo",
     "CSSStyleRule",
     "IDBRequest",
     "Performance",
     "XULMenuListElement",
     "SVGTransform",
     "SVGTextPositioningElement",
+    "ContactFindSortOptions",
     "ContactFindOptions",
     "SVGFEMergeElement",
     "FileRequest",
     "SVGDefsElement",
     "Element",
     "HTMLBaseElement",
     "GeoPositionErrorCallback",
     "HTMLVideoElement",