Bug 850430 - Convert the Contacts API to WebIDL. r=bz r=gwagner
authorReuben Morais <reuben.morais@gmail.com>
Thu, 17 Oct 2013 18:29:56 -0300
changeset 165140 533d30be7e6e34e27198fd6f03b2a1e15192d245
parent 165139 9ba162545a939b738d909e31ae3afab10fb5cb76
child 165141 dad5d17328b2eecbc28e4c10b9fc6cdc30526beb
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, gwagner
bugs850430
milestone27.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 850430 - Convert the Contacts API to WebIDL. r=bz r=gwagner
browser/installer/package-manifest.in
dom/contacts/ContactManager.js
dom/contacts/ContactManager.manifest
dom/contacts/fallback/ContactDB.jsm
dom/contacts/tests/test_contacts_basics.html
dom/contacts/tests/test_contacts_blobs.html
dom/contacts/tests/test_contacts_getall.html
dom/contacts/tests/test_contacts_international.html
dom/contacts/tests/test_contacts_substringmatching.html
dom/contacts/tests/test_contacts_substringmatchingVE.html
dom/icc/interfaces/nsIDOMIccManager.idl
dom/icc/interfaces/nsIIccProvider.idl
dom/icc/src/IccManager.cpp
dom/icc/tests/marionette/test_icc_contact.js
dom/interfaces/contacts/moz.build
dom/interfaces/contacts/nsIContactProperties.idl
dom/interfaces/contacts/nsIDOMContactManager.idl
dom/moz.build
dom/system/gonk/RILContentHelper.js
dom/webidl/Contacts.webidl
dom/webidl/moz.build
mobile/android/installer/package-manifest.in
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -182,17 +182,16 @@
 @BINPATH@/components/dom_icc.xpt
 @BINPATH@/components/dom_wappush.xpt
 #endif
 #ifdef MOZ_B2G_BT
 @BINPATH@/components/dom_bluetooth.xpt
 #endif
 @BINPATH@/components/dom_camera.xpt
 @BINPATH@/components/dom_canvas.xpt
-@BINPATH@/components/dom_contacts.xpt
 @BINPATH@/components/dom_alarm.xpt
 @BINPATH@/components/dom_core.xpt
 @BINPATH@/components/dom_css.xpt
 @BINPATH@/components/dom_devicestorage.xpt
 @BINPATH@/components/dom_events.xpt
 @BINPATH@/components/dom_file.xpt
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_media.xpt
--- a/dom/contacts/ContactManager.js
+++ b/dom/contacts/ContactManager.js
@@ -10,470 +10,281 @@ function debug(s) { dump("-*- ContactMan
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest",
+                                   "@mozilla.org/dom/dom-request-service;1",
+                                   "nsIDOMRequestService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "pm",
+                                   "@mozilla.org/permissionmanager;1",
+                                   "nsIPermissionManager");
+
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
 const CONTACTS_SENDMORE_MINIMUM = 5;
 
-function stringOrBust(aObj) {
-  if (typeof aObj != "string") {
-    if (DEBUG) debug("Field is not a string and was ignored.");
-    return undefined;
-  } else {
-    return aObj;
-  }
-}
+function ContactAddress() { }
 
-function sanitizeStringArray(aArray) {
-  if (!Array.isArray(aArray)) {
-    aArray = [aArray];
-  }
-  return aArray.map(stringOrBust).filter(function(el) { return el != undefined; });
-}
-
-const nsIClassInfo            = Ci.nsIClassInfo;
-const CONTACTPROPERTIES_CID   = Components.ID("{35ad8a4e-9486-44b6-883d-550f14635e49}");
-const nsIContactProperties    = Ci.nsIContactProperties;
-
-// ContactProperties is not directly instantiated. It is used as interface.
-
-function ContactProperties(aProp) { if (DEBUG) debug("ContactProperties Constructor"); }
+ContactAddress.prototype = {
+  // This function is meant to be called via bindings code for type checking,
+  // don't call it directly. Instead, create a content object and call initialize
+  // on that.
+  initialize: function(aType, aStreetAddress, aLocality, aRegion, aPostalCode, aCountryName, aPref) {
+    this.type = aType;
+    this.streetAddress = aStreetAddress;
+    this.locality = aLocality;
+    this.region = aRegion;
+    this.postalCode = aPostalCode;
+    this.countryName = aCountryName;
+    this.pref = aPref;
+  },
 
-ContactProperties.prototype = {
-
-  classID : CONTACTPROPERTIES_CID,
-  classInfo : XPCOMUtils.generateCI({classID: CONTACTPROPERTIES_CID,
-                                     contractID:"@mozilla.org/contactProperties;1",
-                                     classDescription: "ContactProperties",
-                                     interfaces: [nsIContactProperties],
-                                     flags: nsIClassInfo.DOM_OBJECT}),
-
-  QueryInterface : XPCOMUtils.generateQI([nsIContactProperties])
-}
+  toJSON: function(excludeExposedProps) {
+    let json = {
+      type: this.type,
+      streetAddress: this.streetAddress,
+      locality: this.locality,
+      region: this.region,
+      postalCode: this.postalCode,
+      countryName: this.countryName,
+      pref: this.pref,
+    };
+    if (!excludeExposedProps) {
+      json.__exposedProps__ = {
+        type: "rw",
+        streetAddress: "rw",
+        locality: "rw",
+        region: "rw",
+        postalCode: "rw",
+        countryName: "rw",
+        pref: "rw",
+      };
+    }
+    return json;
+  },
 
-//ContactAddress
-
-const CONTACTADDRESS_CONTRACTID = "@mozilla.org/contactAddress;1";
-const CONTACTADDRESS_CID        = Components.ID("{9cbfa81c-bcab-4ca9-b0d2-f4318f295e33}");
-const nsIContactAddress         = Components.interfaces.nsIContactAddress;
-
-function ContactAddress(aType, aStreetAddress, aLocality, aRegion, aPostalCode, aCountryName, aPref) {
-  this.type = sanitizeStringArray(aType);
-  this.streetAddress = stringOrBust(aStreetAddress);
-  this.locality = stringOrBust(aLocality);
-  this.region = stringOrBust(aRegion);
-  this.postalCode = stringOrBust(aPostalCode);
-  this.countryName = stringOrBust(aCountryName);
-  this.pref = aPref;
+  classID: Components.ID("{9cbfa81c-bcab-4ca9-b0d2-f4318f295e33}"),
+  contractID: "@mozilla.org/contactAddress;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 };
 
-ContactAddress.prototype = {
-  __exposedProps__: {
-                      type: 'rw',
-                      streetAddress: 'rw',
-                      locality: 'rw',
-                      region: 'rw',
-                      postalCode: 'rw',
-                      countryName: 'rw',
-                      pref: 'rw'
-                     },
-
-  classID : CONTACTADDRESS_CID,
-  classInfo : XPCOMUtils.generateCI({classID: CONTACTADDRESS_CID,
-                                     contractID: CONTACTADDRESS_CONTRACTID,
-                                     classDescription: "ContactAddress",
-                                     interfaces: [nsIContactAddress],
-                                     flags: nsIClassInfo.DOM_OBJECT}),
-
-  QueryInterface : XPCOMUtils.generateQI([nsIContactAddress])
-}
-
-//ContactField
-
-const CONTACTFIELD_CONTRACTID = "@mozilla.org/contactField;1";
-const CONTACTFIELD_CID        = Components.ID("{ad19a543-69e4-44f0-adfa-37c011556bc1}");
-const nsIContactField         = Components.interfaces.nsIContactField;
-
-function ContactField(aType, aValue, aPref) {
-  this.type = sanitizeStringArray(aType);
-  this.value = stringOrBust(aValue);
-  this.pref = aPref;
-};
+function ContactField() { }
 
 ContactField.prototype = {
-  __exposedProps__: {
-                      type: 'rw',
-                      value: 'rw',
-                      pref: 'rw'
-                     },
-
-  classID : CONTACTFIELD_CID,
-  classInfo : XPCOMUtils.generateCI({classID: CONTACTFIELD_CID,
-                                     contractID: CONTACTFIELD_CONTRACTID,
-                                     classDescription: "ContactField",
-                                     interfaces: [nsIContactField],
-                                     flags: nsIClassInfo.DOM_OBJECT}),
+  // This function is meant to be called via bindings code for type checking,
+  // don't call it directly. Instead, create a content object and call initialize
+  // on that.
+  initialize: function(aType, aValue, aPref) {
+    this.type = aType;
+    this.value = aValue;
+    this.pref = aPref;
+  },
 
-  QueryInterface : XPCOMUtils.generateQI([nsIContactField])
-}
-
-//ContactTelField
+  toJSON: function(excludeExposedProps) {
+    let json = {
+      type: this.type,
+      value: this.value,
+      pref: this.pref,
+    };
+    if (!excludeExposedProps) {
+      json.__exposedProps__ = {
+        type: "rw",
+        value: "rw",
+        pref: "rw",
+      };
+    }
+    return json;
+  },
 
-const CONTACTTELFIELD_CONTRACTID = "@mozilla.org/contactTelField;1";
-const CONTACTTELFIELD_CID        = Components.ID("{4d42c5a9-ea5d-4102-80c3-40cc986367ca}");
-const nsIContactTelField         = Components.interfaces.nsIContactTelField;
+  classID: Components.ID("{ad19a543-69e4-44f0-adfa-37c011556bc1}"),
+  contractID: "@mozilla.org/contactField;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
+};
 
-function ContactTelField(aType, aValue, aCarrier, aPref) {
-  this.type = sanitizeStringArray(aType);
-  this.value = stringOrBust(aValue);
-  this.carrier = stringOrBust(aCarrier);
-  this.pref = aPref;
-};
+function ContactTelField() { }
 
 ContactTelField.prototype = {
-  __exposedProps__: {
-                      type: 'rw',
-                      value: 'rw',
-                      carrier: 'rw',
-                      pref: 'rw'
-                     },
-
-  classID : CONTACTTELFIELD_CID,
-  classInfo : XPCOMUtils.generateCI({classID: CONTACTTELFIELD_CID,
-                                     contractID: CONTACTTELFIELD_CONTRACTID,
-                                     classDescription: "ContactTelField",
-                                     interfaces: [nsIContactTelField],
-                                     flags: nsIClassInfo.DOM_OBJECT}),
-
-  QueryInterface : XPCOMUtils.generateQI([nsIContactTelField])
-}
+  // This function is meant to be called via bindings code for type checking,
+  // don't call it directly. Instead, create a content object and call initialize
+  // on that.
+  initialize: function(aType, aValue, aCarrier, aPref) {
+    this.type = aType;
+    this.value = aValue;
+    this.carrier = aCarrier;
+    this.pref = aPref;
+  },
 
-//ContactFindSortOptions
-
-const CONTACTFINDSORTOPTIONS_CONTRACTID = "@mozilla.org/contactFindSortOptions;1"
-const CONTACTFINDSORTOPTIONS_CID        = Components.ID("{0a5b1fab-70da-46dd-b902-619904d920c2}");
-const nsIContactFindSortOptions         = Ci.nsIContactFindSortOptions;
-
-function ContactFindSortOptions () { }
+  toJSON: function(excludeExposedProps) {
+    let json = {
+      type: this.type,
+      value: this.value,
+      carrier: this.carrier,
+      pref: this.pref,
+    };
+    if (!excludeExposedProps) {
+      json.__exposedProps__ = {
+        type: "rw",
+        value: "rw",
+        carrier: "rw",
+        pref: "rw",
+      };
+    }
+    return json;
+  },
 
-ContactFindSortOptions.prototype = {
-  classID: CONTACTFINDSORTOPTIONS_CID,
-  classInfo: XPCOMUtils.generateCI({classID: CONTACTFINDSORTOPTIONS_CID,
-                                    contractID: CONTACTFINDSORTOPTIONS_CONTRACTID,
-                                    classDescription: "ContactFindSortOptions",
-                                    interfaces: [nsIContactFindSortOptions],
-                                    flags: nsIClassInfo.DOM_OBJECT}),
-  QueryInterface: XPCOMUtils.generateQI([nsIContactFindSortOptions])
+  classID: Components.ID("{4d42c5a9-ea5d-4102-80c3-40cc986367ca}"),
+  contractID: "@mozilla.org/contactTelField;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 };
 
-//ContactFindOptions
-
-const CONTACTFINDOPTIONS_CONTRACTID = "@mozilla.org/contactFindOptions;1";
-const CONTACTFINDOPTIONS_CID        = Components.ID("{28ce07d0-45d9-4b7a-8843-521df4edd8bc}");
-const nsIContactFindOptions         = Components.interfaces.nsIContactFindOptions;
-
-function ContactFindOptions() { };
+function validateArrayField(data, createCb) {
+  function isVanillaObj(aObj) {
+    return Object.prototype.toString.call(aObj) == "[object Object]";
+  }
 
-ContactFindOptions.prototype = {
-
-  classID : CONTACTFINDOPTIONS_CID,
-  classInfo : XPCOMUtils.generateCI({classID: CONTACTFINDOPTIONS_CID,
-                                     contractID: CONTACTFINDOPTIONS_CONTRACTID,
-                                     classDescription: "ContactFindOptions",
-                                     interfaces: [nsIContactFindSortOptions,
-                                                  nsIContactFindOptions],
-                                     flags: nsIClassInfo.DOM_OBJECT}),
+  // We use an array-like Proxy to validate data set by content, since we don't
+  // have WebIDL arrays yet. See bug 851726.
 
-  QueryInterface : XPCOMUtils.generateQI([nsIContactFindSortOptions,
-                                          nsIContactFindOptions])
-}
-
-//Contact
-
-const CONTACT_CONTRACTID = "@mozilla.org/contact;1";
-const CONTACT_CID        = Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}");
-const nsIDOMContact      = Components.interfaces.nsIDOMContact;
-
-function checkBlobArray(aBlob) {
-  if (Array.isArray(aBlob)) {
-    for (let i = 0; i < aBlob.length; i++) {
-      if (typeof aBlob != 'object') {
-        return null;
+  // ArrayPropertyExposedPropsProxy is used to return "rw" for any valid index
+  // and "length" in __exposedProps__.
+  const ArrayPropertyExposedPropsProxy = new Proxy({}, {
+    get: function(target, name) {
+      // Test for index access
+      if (String(name >>> 0) === name) {
+        return "rw";
       }
-      if (!(aBlob[i] instanceof Components.interfaces.nsIDOMBlob)) {
-        return null;
+      if (name === "length") {
+        return "r";
       }
     }
-    return aBlob;
-  }
-  return null;
-}
+  });
 
-function isVanillaObj(aObj) {
-  return Object.prototype.toString.call(aObj) == "[object Object]";
-}
+  const ArrayPropertyHandler = {
+    set: function(target, name, val, receiver) {
+      // Test for index access
+      if (String(name >>> 0) === name) {
+        target[name] = createCb(val);
+      }
+    },
+    get: function(target, name) {
+      if (name === "__exposedProps__") {
+        return ArrayPropertyExposedPropsProxy;
+      }
+      return target[name];
+    }
+  };
 
-function validateArrayField(data, createCb) {
   if (data) {
     data = Array.isArray(data) ? data : [data];
     let filtered = [];
     for (let obj of data) {
-      if (obj && isVanillaObj(obj)) {
+      if (isVanillaObj(obj)) {
         filtered.push(createCb(obj));
       }
     }
-    return filtered;
+    if (filtered.length === 0) {
+      return undefined;
+    }
+    return new Proxy(filtered, ArrayPropertyHandler);
   }
   return undefined;
 }
 
-function Contact() { };
+// We need this to create a copy of the mozContact object in ContactManager.save
+// Keep in sync with the interfaces.
+const PROPERTIES = [
+  "name", "honorificPrefix", "givenName", "additionalName", "familyName",
+  "honorificSuffix", "nickname", "photo", "category", "org", "jobTitle",
+  "bday", "note", "anniversary", "sex", "genderIdentity", "key"
+];
+const ADDRESS_PROPERTIES = ["adr"];
+const FIELD_PROPERTIES = ["email", "url", "impp"];
+const TELFIELD_PROPERTIES = ["tel"];
+
+function Contact() { }
 
 Contact.prototype = {
-  __exposedProps__: {
-                      id: 'rw',
-                      updated: 'rw',
-                      published:  'rw',
-                      name: 'rw',
-                      honorificPrefix: 'rw',
-                      givenName: 'rw',
-                      additionalName: 'rw',
-                      familyName: 'rw',
-                      honorificSuffix: 'rw',
-                      nickname: 'rw',
-                      email: 'rw',
-                      photo: 'rw',
-                      url: 'rw',
-                      category: 'rw',
-                      adr: 'rw',
-                      tel: 'rw',
-                      org: 'rw',
-                      jobTitle: 'rw',
-                      bday: 'rw',
-                      note: 'rw',
-                      impp: 'rw',
-                      anniversary: 'rw',
-                      sex: 'rw',
-                      genderIdentity: 'rw',
-                      key: 'rw',
-                     },
-
-  set name(aName) {
-    this._name = sanitizeStringArray(aName);
-  },
-
-  get name() {
-    return this._name;
-  },
-
-  set honorificPrefix(aHonorificPrefix) {
-    this._honorificPrefix = sanitizeStringArray(aHonorificPrefix);
-  },
-
-  get honorificPrefix() {
-    return this._honorificPrefix;
-  },
-
-  set givenName(aGivenName) {
-    this._givenName = sanitizeStringArray(aGivenName);
-  },
-
-  get givenName() {
-    return this._givenName;
-  },
-
-  set additionalName(aAdditionalName) {
-    this._additionalName = sanitizeStringArray(aAdditionalName);
-  },
-
-  get additionalName() {
-    return this._additionalName;
-  },
-
-  set familyName(aFamilyName) {
-    this._familyName = sanitizeStringArray(aFamilyName);
-  },
-
-  get familyName() {
-    return this._familyName;
-  },
-
-  set honorificSuffix(aHonorificSuffix) {
-    this._honorificSuffix = sanitizeStringArray(aHonorificSuffix);
-  },
-
-  get honorificSuffix() {
-    return this._honorificSuffix;
-  },
-
-  set nickname(aNickname) {
-    this._nickname = sanitizeStringArray(aNickname);
-  },
-
-  get nickname() {
-    return this._nickname;
-  },
-
-  set photo(aPhoto) {
-    this._photo = checkBlobArray(aPhoto);
-  },
-
-  get photo() {
-    return this._photo;
-  },
-
-  set category(aCategory) {
-    this._category = sanitizeStringArray(aCategory);
-  },
-
-  get category() {
-    return this._category;
-  },
-
+  // We need to create the content interfaces in these setters, otherwise when
+  // we return these objects (e.g. from a find call), the values in the array
+  // will be COW's, and content cannot see the properties.
   set email(aEmail) {
     this._email = validateArrayField(aEmail, function(email) {
-      return new ContactField(email.type, email.value, email.pref);
-    });
+      let obj = new this._window.ContactField();
+      obj.initialize(email.type, email.value, email.pref);
+      return obj;
+    }.bind(this));
   },
 
   get email() {
     return this._email;
   },
 
   set adr(aAdr) {
     this._adr = validateArrayField(aAdr, function(adr) {
-      return new ContactAddress(adr.type, adr.streetAddress, adr.locality,
-                                adr.region, adr.postalCode, adr.countryName,
-                                adr.pref);
-    });
+      let obj = new this._window.ContactAddress();
+      obj.initialize(adr.type, adr.streetAddress, adr.locality,
+                     adr.region, adr.postalCode, adr.countryName,
+                     adr.pref);
+      return obj;
+    }.bind(this));
   },
 
   get adr() {
     return this._adr;
   },
 
   set tel(aTel) {
     this._tel = validateArrayField(aTel, function(tel) {
-      return new ContactTelField(tel.type, tel.value, tel.carrier, tel.pref);
-    });
+      let obj = new this._window.ContactTelField();
+      obj.initialize(tel.type, tel.value, tel.carrier, tel.pref);
+      return obj;
+    }.bind(this));
   },
 
   get tel() {
     return this._tel;
   },
 
   set impp(aImpp) {
     this._impp = validateArrayField(aImpp, function(impp) {
-      return new ContactField(impp.type, impp.value, impp.pref);
-    });
+      let obj = new this._window.ContactField();
+      obj.initialize(impp.type, impp.value, impp.pref);
+      return obj;
+    }.bind(this));
   },
 
   get impp() {
     return this._impp;
   },
 
   set url(aUrl) {
     this._url = validateArrayField(aUrl, function(url) {
-      return new ContactField(url.type, url.value, url.pref);
-    });
+      let obj = new this._window.ContactField();
+      obj.initialize(url.type, url.value, url.pref);
+      return obj;
+    }.bind(this));
   },
 
   get url() {
     return this._url;
   },
 
-  set org(aOrg) {
-    this._org = sanitizeStringArray(aOrg);
-  },
-
-  get org() {
-    return this._org;
-  },
-
-  set jobTitle(aJobTitle) {
-    this._jobTitle = sanitizeStringArray(aJobTitle);
-  },
-
-  get jobTitle() {
-    return this._jobTitle;
-  },
-
-  set note(aNote) {
-    this._note = sanitizeStringArray(aNote);
-  },
-
-  get note() {
-    return this._note;
-  },
-
-  set bday(aBday) {
-    if (aBday && aBday.constructor.name === "Date") {
-      this._bday = aBday;
-    } else if (typeof aBday === "string" || typeof aBday === "number") {
-      this._bday = new Date(aBday);
-    }
-  },
-
-  get bday() {
-    return this._bday;
+  init: function(aWindow) {
+    this._window = aWindow;
   },
 
-  set anniversary(aAnniversary) {
-    if (aAnniversary && aAnniversary.constructor.name === "Date") {
-      this._anniversary = aAnniversary;
-    } else if (typeof aAnniversary === "string" || typeof aAnniversary === "number") {
-      this._anniversary = new Date(aAnniversary);
-    }
-  },
-
-  get anniversary() {
-    return this._anniversary;
-  },
-
-  set sex(aSex) {
-    if (aSex !== "undefined") {
-      this._sex = aSex;
-    } else {
-      this._sex = null;
-    }
-  },
-
-  get sex() {
-    return this._sex;
-  },
-
-  set genderIdentity(aGenderIdentity) {
-    if (aGenderIdentity !== "undefined") {
-      this._genderIdentity = aGenderIdentity;
-    } else {
-      this._genderIdentity = null;
-    }
-  },
-
-  get genderIdentity() {
-    return this._genderIdentity;
-  },
-
-  set key(aKey) {
-    this._key = sanitizeStringArray(aKey);
-  },
-
-  get key() {
-    return this._key;
-  },
-
-  init: function init(aProp) {
+  __init: function(aProp) {
     this.name =            aProp.name;
     this.honorificPrefix = aProp.honorificPrefix;
     this.givenName =       aProp.givenName;
     this.additionalName =  aProp.additionalName;
     this.familyName =      aProp.familyName;
     this.honorificSuffix = aProp.honorificSuffix;
     this.nickname =        aProp.nickname;
     this.email =           aProp.email;
@@ -488,86 +299,115 @@ Contact.prototype = {
     this.note =            aProp.note;
     this.impp =            aProp.impp;
     this.anniversary =     aProp.anniversary;
     this.sex =             aProp.sex;
     this.genderIdentity =  aProp.genderIdentity;
     this.key =             aProp.key;
   },
 
-  get published () {
-    return this._published;
-  },
-
-  set published(aPublished) {
-    this._published = aPublished;
-  },
-
-  get updated () {
-    return this._updated;
-  },
-
-  set updated(aUpdated) {
-    this._updated = aUpdated;
+  setMetadata: function(aId, aPublished, aUpdated) {
+    this.id = aId;
+    if (aPublished) {
+      this.published = aPublished;
+    }
+    if (aUpdated) {
+      this.updated = aUpdated;
+    }
   },
 
-  classID : CONTACT_CID,
-  classInfo : XPCOMUtils.generateCI({classID: CONTACT_CID,
-                                     contractID: CONTACT_CONTRACTID,
-                                     classDescription: "Contact",
-                                     interfaces: [nsIDOMContact, nsIContactProperties],
-                                     flags: nsIClassInfo.DOM_OBJECT}),
+  toJSON: function() {
+    return {
+      id:              this.id,
+      published:       this.published,
+      updated:         this.updated,
 
-  QueryInterface : XPCOMUtils.generateQI([nsIDOMContact, nsIContactProperties])
-}
+      name:            this.name,
+      honorificPrefix: this.honorificPrefix,
+      givenName:       this.givenName,
+      additionalName:  this.additionalName,
+      familyName:      this.familyName,
+      honorificSuffix: this.honorificSuffix,
+      nickname:        this.nickname,
+      category:        this.category,
+      org:             this.org,
+      jobTitle:        this.jobTitle,
+      note:            this.note,
+      sex:             this.sex,
+      genderIdentity:  this.genderIdentity,
+      email:           this.email,
+      photo:           this.photo,
+      adr:             this.adr,
+      url:             this.url,
+      tel:             this.tel,
+      bday:            this.bday,
+      impp:            this.impp,
+      anniversary:     this.anniversary,
+      key:             this.key,
 
-// ContactManager
+      __exposedProps__: {
+        id:              "rw",
+        published:       "rw",
+        updated:         "rw",
+        name:            "rw",
+        honorificPrefix: "rw",
+        givenName:       "rw",
+        additionalName:  "rw",
+        familyName:      "rw",
+        honorificSuffix: "rw",
+        nickname:        "rw",
+        category:        "rw",
+        org:             "rw",
+        jobTitle:        "rw",
+        note:            "rw",
+        sex:             "rw",
+        genderIdentity:  "rw",
+        email:           "rw",
+        photo:           "rw",
+        adr:             "rw",
+        url:             "rw",
+        tel:             "rw",
+        bday:            "rw",
+        impp:            "rw",
+        anniversary:     "rw",
+        key:             "rw",
+      }
+    };
+  },
 
-const CONTACTMANAGER_CONTRACTID = "@mozilla.org/contactManager;1";
-const CONTACTMANAGER_CID        = Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}");
-const nsIDOMContactManager      = Components.interfaces.nsIDOMContactManager;
+  classID: Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}"),
+  contractID: "@mozilla.org/contact;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+};
 
-function ContactManager()
-{
-  if (DEBUG) debug("Constructor");
-}
+function ContactManager() { }
 
 ContactManager.prototype = {
   __proto__: DOMRequestIpcHelper.prototype,
-  _oncontactchange: null,
+  hasListenPermission: false,
   _cachedContacts: [] ,
 
-  set oncontactchange(aCallback) {
-    if (DEBUG) debug("set oncontactchange");
-    let allowCallback = function() {
-      if (!this._oncontactchange) {
-        cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
-      }
-      this._oncontactchange = aCallback;
-    }.bind(this);
-    let cancelCallback = function() {
-      throw Components.results.NS_ERROR_FAILURE;
-    }
-    this.askPermission("listen", null, allowCallback, cancelCallback);
+  set oncontactchange(aHandler) {
+    this.__DOM_IMPL__.setEventHandler("oncontactchange", aHandler);
   },
 
   get oncontactchange() {
-    return this._oncontactchange;
+    return this.__DOM_IMPL__.getEventHandler("oncontactchange");
   },
 
-  _setMetaData: function(aNewContact, aRecord) {
-    aNewContact.id = aRecord.id;
-    aNewContact.published = aRecord.published;
-    aNewContact.updated = aRecord.updated;
-  },
-
-  _convertContact: function CM_convertContact(aContact) {
-    let newContact = new Contact();
-    newContact.init(aContact.properties);
-    this._setMetaData(newContact, aContact);
+  _convertContact: function(aContact) {
+    if (aContact.properties.bday) {
+      aContact.properties.bday = new Date(aContact.properties.bday);
+    }
+    if (aContact.properties.anniversary) {
+      aContact.properties.anniversary = new Date(aContact.properties.anniversary);
+    }
+    let newContact = new this._window.mozContact(aContact.properties);
+    newContact.setMetadata(aContact.id, aContact.published, aContact.updated);
     return newContact;
   },
 
   _convertContacts: function(aContacts) {
     let contacts = [];
     for (let i in aContacts) {
       contacts.push(this._convertContact(aContacts[i]));
     }
@@ -667,23 +507,21 @@ ContactManager.prototype = {
           req.allow();
         } else {
           req.cancel();
         }
         break;
       case "Contact:Changed":
         // Fire oncontactchange event
         if (DEBUG) debug("Contacts:ContactChanged: " + msg.contactID + ", " + msg.reason);
-        if (this._oncontactchange) {
-          let event = new this._window.MozContactChangeEvent("contactchanged", {
-            contactID: msg.contactID,
-            reason: msg.reason
-          });
-          this._oncontactchange.handleEvent(event);
-        }
+        let event = new this._window.MozContactChangeEvent("contactchange", {
+          contactID: msg.contactID,
+          reason: msg.reason
+        });
+        this.dispatchEvent(event);
         break;
       case "Contacts:Revision":
         if (DEBUG) debug("new revision: " + msg.revision);
         req = this.getRequest(msg.requestID);
         if (req) {
           Services.DOMRequest.fireSuccess(req.request, msg.revision);
         }
         break;
@@ -695,16 +533,22 @@ ContactManager.prototype = {
         }
         break;
       default:
         if (DEBUG) debug("Wrong message: " + aMessage.name);
     }
     this.removeRequest(msg.requestID);
   },
 
+  dispatchEvent: function(event) {
+    if (this.hasListenPermission) {
+      this.__DOM_IMPL__.dispatchEvent(event);
+    }
+  },
+
   askPermission: function (aAccess, aRequest, aAllowCallback, aCancelCallback) {
     if (DEBUG) debug("askPermission for contacts");
     let access;
     switch(aAccess) {
       case "create":
         access = "create";
         break;
       case "update":
@@ -752,62 +596,84 @@ ContactManager.prototype = {
       origin: principal.origin,
       appID: principal.appId,
       browserFlag: principal.isInBrowserElement,
       windowID: this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID
     });
   },
 
   save: function save(aContact) {
-    if (DEBUG) debug("save: " + JSON.stringify(aContact) + " :" + aContact.id);
-    let newContact = {};
-    newContact.properties = {
-      name:            [],
-      honorificPrefix: [],
-      givenName:       [],
-      additionalName:  [],
-      familyName:      [],
-      honorificSuffix: [],
-      nickname:        [],
-      email:           [],
-      photo:           [],
-      url:             [],
-      category:        [],
-      adr:             [],
-      tel:             [],
-      org:             [],
-      jobTitle:        [],
-      bday:            null,
-      note:            [],
-      impp:            [],
-      anniversary:     null,
-      sex:             null,
-      genderIdentity:  null,
-      key:             [],
-    };
-    for (let field in newContact.properties) {
-      newContact.properties[field] = aContact[field];
+    // We have to do a deep copy of the contact manually here because
+    // nsFrameMessageManager doesn't know how to create a structured clone of a
+    // mozContact object.
+    let newContact = {properties: {}};
+
+    for (let field of PROPERTIES) {
+      if (aContact[field]) {
+        newContact.properties[field] = aContact[field];
+      }
+    }
+
+    for (let prop of ADDRESS_PROPERTIES) {
+      if (aContact[prop]) {
+        newContact.properties[prop] = [];
+        for (let i of aContact[prop]) {
+          if (i) {
+            let json = ContactAddress.prototype.toJSON.apply(i, [true]);
+            newContact.properties[prop].push(json);
+          }
+        }
+      }
     }
+
+    for (let prop of FIELD_PROPERTIES) {
+      if (aContact[prop]) {
+        newContact.properties[prop] = [];
+        for (let i of aContact[prop]) {
+          if (i) {
+            let json = ContactField.prototype.toJSON.apply(i, [true]);
+            newContact.properties[prop].push(json);
+          }
+        }
+      }
+    }
+
+    for (let prop of TELFIELD_PROPERTIES) {
+      if (aContact[prop]) {
+        newContact.properties[prop] = [];
+        for (let i of aContact[prop]) {
+          if (i) {
+            let json = ContactTelField.prototype.toJSON.apply(i, [true]);
+            newContact.properties[prop].push(json);
+          }
+        }
+      }
+    }
+
     let request = this.createRequest();
     let requestID = this.getRequestId({request: request, reason: reason});
 
     let reason;
     if (aContact.id == "undefined") {
       // for example {25c00f01-90e5-c545-b4d4-21E2ddbab9e0} becomes
       // 25c00f0190e5c545b4d421E2ddbab9e0
-      aContact.id = this._getRandomId().replace('-', '', 'g').replace('{', '').replace('}', '');
+      aContact.id = this._getRandomId().replace(/[{}-]/g, "");
       // Cache the contact so that its ID may be updated later if necessary
       this._cachedContacts[requestID] = aContact;
       reason = "create";
     } else {
       reason = "update";
     }
 
-    this._setMetaData(newContact, aContact);
+    newContact.id = aContact.id;
+    newContact.published = aContact.published;
+    newContact.updated = aContact.updated;
+
     if (DEBUG) debug("send: " + JSON.stringify(newContact));
+
     let options = { contact: newContact, reason: reason };
     let allowCallback = function() {
       cpmm.sendAsyncMessage("Contact:Save", {requestID: requestID, options: options});
     }.bind(this)
     this.askPermission(reason, request, allowCallback);
     return request;
   },
 
@@ -873,29 +739,28 @@ ContactManager.prototype = {
     if (!aRecord || !aRecord.id) {
       Services.DOMRequest.fireErrorAsync(request, true);
       return request;
     }
 
     let options = { id: aRecord.id };
     let allowCallback = function() {
       cpmm.sendAsyncMessage("Contact:Remove", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options});
-    }.bind(this)
+    }.bind(this);
     this.askPermission("remove", request, allowCallback);
     return request;
   },
 
   clear: function() {
     if (DEBUG) debug("clear");
-    let request;
-    request = this.createRequest();
+    let request = this.createRequest();
     let options = {};
     let allowCallback = function() {
       cpmm.sendAsyncMessage("Contacts:Clear", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options});
-    }.bind(this)
+    }.bind(this);
     this.askPermission("remove", request, allowCallback);
     return request;
   },
 
   getRevision: function() {
     let request = this.createRequest();
 
     let allowCallback = function() {
@@ -925,40 +790,37 @@ ContactManager.prototype = {
       Services.DOMRequest.fireError(request);
     };
 
     this.askPermission("count", request, allowCallback, cancelCallback);
     return request;
   },
 
   init: function(aWindow) {
+    // DOMRequestIpcHelper.initHelper sets this._window
     this.initDOMRequestHelper(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",
                               "Contact:Changed",
                               "PermissionPromptHelper:AskPermission:OK",
                               "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO",
                               "Contacts:Count",
                               "Contacts:Revision", "Contacts:GetRevision:Return:KO",]);
-  },
+
 
-  // Called from DOMRequestIpcHelper
-  uninit: function uninit() {
-    if (DEBUG) debug("uninit call");
-    if (this._oncontactchange)
-      this._oncontactchange = null;
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
+      this.hasListenPermission = true;
+    }.bind(this);
+
+    this.askPermission("listen", null, allowCallback);
   },
 
-  classID : CONTACTMANAGER_CID,
-  QueryInterface : XPCOMUtils.generateQI([nsIDOMContactManager,
-                                          Ci.nsIDOMGlobalPropertyInitializer,
-                                          Ci.nsISupportsWeakReference]),
+  classID: Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}"),
+  contractID: "@mozilla.org/contactManager;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+};
 
-  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, ContactFindSortOptions, ContactFindOptions])
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+  Contact, ContactManager, ContactAddress, ContactField, ContactTelField
+]);
--- a/dom/contacts/ContactManager.manifest
+++ b/dom/contacts/ContactManager.manifest
@@ -1,25 +1,14 @@
-component {35ad8a4e-9486-44b6-883d-550f14635e49} ContactManager.js
-contract @mozilla.org/contactProperties;1 {35ad8a4e-9486-44b6-883d-550f14635e49}
-
 component {9cbfa81c-bcab-4ca9-b0d2-f4318f295e33} ContactManager.js
 contract @mozilla.org/contactAddress;1 {9cbfa81c-bcab-4ca9-b0d2-f4318f295e33}
 
 component {ad19a543-69e4-44f0-adfa-37c011556bc1} ContactManager.js
 contract @mozilla.org/contactField;1 {ad19a543-69e4-44f0-adfa-37c011556bc1}
 
 component {4d42c5a9-ea5d-4102-80c3-40cc986367ca} ContactManager.js
 contract @mozilla.org/contactTelField;1 {4d42c5a9-ea5d-4102-80c3-40cc986367ca}
 
-component {0a5b1fab-70da-46dd-b902-619904d920c2} ContactManager.js
-contract @mozilla.org/contactFindSortOptions;1 {0a5b1fab-70da-46dd-b902-619904d920c2}
-
-component {28ce07d0-45d9-4b7a-8843-521df4edd8bc} ContactManager.js
-contract @mozilla.org/contactFindOptions;1 {28ce07d0-45d9-4b7a-8843-521df4edd8bc}
-
 component {72a5ee28-81d8-4af8-90b3-ae935396cc66} ContactManager.js
 contract @mozilla.org/contact;1 {72a5ee28-81d8-4af8-90b3-ae935396cc66}
-category JavaScript-global-constructor mozContact @mozilla.org/contact;1
 
 component {8beb3a66-d70a-4111-b216-b8e995ad3aff} ContactManager.js
 contract @mozilla.org/contactManager;1 {8beb3a66-d70a-4111-b216-b8e995ad3aff}
-category JavaScript-navigator-property mozContacts @mozilla.org/contactManager;1
--- a/dom/contacts/fallback/ContactDB.jsm
+++ b/dom/contacts/fallback/ContactDB.jsm
@@ -15,34 +15,28 @@ 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 = 14;
+const DB_VERSION = 15;
 const STORE_NAME = "contacts";
 const SAVED_GETALL_STORE_NAME = "getallcache";
 const CHUNK_SIZE = 20;
 const REVISION_STORE = "revision";
 const REVISION_KEY = "revision";
 
 function exportContact(aRecord) {
-  let contact = {};
-  contact.properties = aRecord.properties;
-
-  for (let field in aRecord.properties)
-    contact.properties[field] = aRecord.properties[field];
-
-  contact.updated = aRecord.updated;
-  contact.published = aRecord.published;
-  contact.id = aRecord.id;
-  return contact;
+  if (aRecord) {
+    delete aRecord.search;
+  }
+  return aRecord;
 }
 
 function ContactDispatcher(aContacts, aFullContacts, aCallback, aNewTxn, aClearDispatcher, aFailureCb) {
   let nextIndex = 0;
 
   let sendChunk;
   let count = 0;
   if (aFullContacts) {
@@ -145,19 +139,17 @@ ContactDB.prototype = {
       }
 
       let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
       objectStore = aTransaction.objectStore(STORE_NAME);
 
       for (let i = 0; i < contacts.length; i++) {
         let contact = {};
         contact.properties = contacts[i];
-        contact.id = idService.generateUUID().toString().replace('-', '', 'g')
-                                                        .replace('{', '')
-                                                        .replace('}', '');
+        contact.id = idService.generateUUID().toString().replace(/[{}-]/g, "");
         contact = this.makeImport(contact);
         this.updateRecordMetadata(contact);
         if (DEBUG) debug("import: " + JSON.stringify(contact));
         objectStore.put(contact);
       }
     }.bind(this);
 
     function createFinalSchema() {
@@ -539,21 +531,64 @@ ContactDB.prototype = {
 
           let cursor = event.target.result;
           if (cursor) {
             let modified = removeEmptyStrings(cursor.value.search.parsedTel);
             let modified2 = removeEmptyStrings(cursor.value.search.tel);
             if (modified || modified2) {
               cursor.update(cursor.value);
             }
+            cursor.continue();
           } else {
             next();
           }
         };
       },
+      function upgrade14to15() {
+        if (DEBUG) debug("Fix array properties saved as scalars");
+        if (!objectStore) {
+         objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+        const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel",
+                                 "name", "honorificPrefix", "givenName",
+                                 "additionalName", "familyName", "honorificSuffix",
+                                 "nickname", "category", "org", "jobTitle",
+                                 "note", "key"];
+        const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"];
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          let changed = false;
+          if (cursor) {
+            let props = cursor.value.properties;
+            for (let prop of ARRAY_PROPERTIES) {
+              if (props[prop]) {
+                if (!Array.isArray(props[prop])) {
+                  cursor.value.properties[prop] = [props[prop]];
+                  changed = true;
+                }
+                if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
+                  let subprop = cursor.value.properties[prop];
+                  for (let i = 0; i < subprop.length; ++i) {
+                    if (!Array.isArray(subprop[i].type)) {
+                      cursor.value.properties[prop][i].type = [subprop[i].type];
+                      changed = true;
+                    }
+                  }
+                }
+              }
+            }
+            if (changed) {
+              cursor.update(cursor.value);
+            }
+            cursor.continue();
+          } else {
+           next();
+          }
+        };
+      },
     ];
 
     let index = aOldVersion;
     let outer = this;
     function next() {
       if (index == aNewVersion) {
         outer.incrementRevision(aTransaction);
         return;
@@ -571,41 +606,17 @@ ContactDB.prototype = {
     if (aNewVersion > steps.length) {
       dump("Contacts DB upgrade error!");
       aTransaction.abort();
     }
     next();
   },
 
   makeImport: function makeImport(aContact) {
-    let contact = {};
-    contact.properties = {
-      name:            [],
-      honorificPrefix: [],
-      givenName:       [],
-      additionalName:  [],
-      familyName:      [],
-      honorificSuffix: [],
-      nickname:        [],
-      email:           [],
-      photo:           [],
-      url:             [],
-      category:        [],
-      adr:             [],
-      tel:             [],
-      org:             [],
-      jobTitle:        [],
-      bday:            null,
-      note:            [],
-      impp:            [],
-      anniversary:     null,
-      sex:             null,
-      genderIdentity:  null,
-      key:             [],
-    };
+    let contact = {properties: {}};
 
     contact.search = {
       givenName:       [],
       familyName:      [],
       email:           [],
       category:        [],
       tel:             [],
       exactTel:        [],
@@ -682,17 +693,16 @@ ContactDB.prototype = {
               if (typeof val == "string") {
                 contact.search[field].push(val.toLowerCase());
               }
             }
           }
         }
       }
     }
-    if (DEBUG) debug("contact:" + JSON.stringify(contact));
 
     contact.updated = aContact.updated;
     contact.published = aContact.published;
     contact.id = aContact.id;
 
     return contact;
   },
 
--- a/dom/contacts/tests/test_contacts_basics.html
+++ b/dom/contacts/tests/test_contacts_basics.html
@@ -56,87 +56,88 @@ var c3 = {
 
 var c4 = {
   name: ["c c", "a a", "c c"],
   familyName: ["c","a","c"],
   givenName: ["c","a","c"],
 };
 
 var c5 = {
-  nickname: "empty"
+  familyName: [],
+  givenName: [],
 };
 
 var c6 = {
-  name: "e",
+  name: ["e"],
   familyName: ["e","e","e"],
   givenName: ["e","e","e"],
 };
 
 var c7 = {
-  name: "e",
+  name: ["e"],
   familyName: ["e","e","e"],
   givenName: ["e","e","e"],
 };
 
 var c8 = {
-  name: "e",
+  name: ["e"],
   familyName: ["e","e","e"],
   givenName: ["e","e","e"],
 };
 
 var adr1 = {
-  type: "work",
+  type: ["work"],
   streetAddress: "street 1",
   locality: "locality 1",
   region: "region 1",
   postalCode: "postal code 1",
   countryName: "country 1"
 };
 
 var adr2 = {
-  type: "home, fax",
+  type: ["home, fax"],
   streetAddress: "street2",
   locality: "locality2",
   region: "region2",
   postalCode: "postal code2",
   countryName: "country2"
 };
 
 var properties1 = {
   name: ["Test1 TestFamilyName", "Test2 Wagner"],
   familyName: ["TestFamilyName","Wagner"],
   givenName: ["Test1","Test2"],
-  nickname: "nicktest",
+  nickname: ["nicktest"],
   tel: [{type: ["work"], value: "123456", carrier: "testCarrier"} , {type: ["home", "fax"], value: "+55 (31) 9876-3456"}, {type: ["home"], value: "+49 451 491934"}],
-  adr: adr1,
+  adr: [adr1],
   email: [{type: ["work"], value: "x@y.com"}],
 };
 
 var properties2 = {
   name: ["dummyHonorificPrefix dummyGivenName dummyFamilyName dummyHonorificSuffix", "dummyHonorificPrefix2"],
-  familyName: "dummyFamilyName",
-  givenName: "dummyGivenName",
+  familyName: ["dummyFamilyName"],
+  givenName: ["dummyGivenName"],
   honorificPrefix: ["dummyHonorificPrefix","dummyHonorificPrefix2"],
-  honorificSuffix: "dummyHonorificSuffix",
-  additionalName: "dummyadditionalName",
-  nickname: "dummyNickname",
+  honorificSuffix: ["dummyHonorificSuffix"],
+  additionalName: ["dummyadditionalName"],
+  nickname: ["dummyNickname"],
   tel: [{type: ["test"], value: "7932012345", carrier: "myCarrier", pref: 1},{type: ["home", "custom"], value: "7932012346", pref: 0}],
   email: [{type: ["test"], value: "a@b.c"}, {value: "b@c.d", pref: 1}],
   adr: [adr1, adr2],
   impp: [{type: ["aim"], value:"im1", pref: 1}, {value: "im2"}],
   org: ["org1", "org2"],
   jobTitle: ["boss", "superboss"],
-  note: "test note",
+  note: ["test note"],
   category: ["cat1", "cat2"],
   url: [{type: ["work", "work2"], value: "www.1.com", pref: 1}, {value:"www2.com"}],
   bday: new Date("1980, 12, 01"),
   anniversary: new Date("2000, 12, 01"),
   sex: "male",
   genderIdentity: "test",
-  key: "ERPJ394GJJWEVJ0349GJ09W3H4FG0WFW80VHW3408GH30WGH348G3H"
+  key: ["ERPJ394GJJWEVJ0349GJ09W3H4FG0WFW80VHW3408GH30WGH348G3H"]
 };
 
 var sample_id1;
 var sample_id2;
 
 var createResult1;
 var createResult2;
 
@@ -161,104 +162,131 @@ function onFailure() {
   next();
 }
 
 function checkStr(str1, str2, msg) {
   if (str1 ^ str2) {
     ok(false, "Expected both strings to be either present or absent");
     return;
   }
+  if (!str1 || str1 == "null") {
+    str1 = null;
+  }
+  if (!str2 || str2 == "null") {
+    str2 = null;
+  }
   is(str1, str2, msg);
 }
 
 function checkStrArray(str1, str2, msg) {
-  // comparing /[null(,null)+]/ and undefined should pass
-  function nonNull(e) {
-    return e != null;
+  function normalize_falsy(v) {
+    if (!v || v == "null" || v == "undefined") {
+      return "";
+    }
+    return v;
+  }
+  function optArray(val) {
+    return Array.isArray(val) ? val : [val];
   }
-  if ((Array.isArray(str1) && str1.filter(nonNull).length == 0 && str2 == undefined)
-     ||(Array.isArray(str2) && str2.filter(nonNull).length == 0 && str1 == undefined)) {
-    ok(true, msg);
-  } else if (str1) {
-    is(JSON.stringify(typeof str1 == "string" ? [str1] : str1), JSON.stringify(typeof str2 == "string" ? [str2] : str2), msg);
+  str1 = optArray(str1).map(normalize_falsy).filter(v => v != "");
+  str2 = optArray(str2).map(normalize_falsy).filter(v => v != "");
+  ise(JSON.stringify(str1), JSON.stringify(str2), msg);
+}
+
+function checkPref(pref1, pref2) {
+  // If on Android treat one preference as 0 and the other as undefined as matching
+  if (isAndroid) {
+    if ((!pref1 && pref2 == undefined) || (pref1 == undefined && !pref2)) {
+      pref1 = false;
+      pref2 = false;
+    }
   }
+  ise(!!pref1, !!pref2, "Same pref");
 }
 
 function checkAddress(adr1, adr2) {
   if (adr1 ^ adr2) {
     ok(false, "Expected both adrs to be either present or absent");
     return;
   }
   checkStrArray(adr1.type, adr2.type, "Same type");
-  checkStrArray(adr1.streetAddress, adr2.streetAddress, "Same streetAddress");
-  checkStrArray(adr1.locality, adr2.locality, "Same locality");
-  checkStrArray(adr1.region, adr2.region, "Same region");
-  checkStrArray(adr1.postalCode, adr2.postalCode, "Same postalCode");
-  checkStrArray(adr1.countryName, adr2.countryName, "Same countryName");
+  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");
   checkPref(adr1.pref, adr2.pref);
 }
 
-function checkTel(tel1, tel2) {
-  if (tel1 ^ tel2) {
-    ok(false, "Expected both tels to be either present or absent");
-    return;
-  }
-  checkStrArray(tel1.type, tel2.type, "Same type");
-  checkStrArray(tel1.value, tel2.value, "Same value");
-  checkStrArray(tel1.carrier, tel2.carrier, "Same carrier");
-  checkPref(tel1.pref, tel2.pref);
-}
-
 function checkField(field1, field2) {
   if (field1 ^ field2) {
     ok(false, "Expected both fields to be either present or absent");
     return;
   }
   checkStrArray(field1.type, field2.type, "Same type");
-  checkStrArray(field1.value, field2.value, "Same value");
+  checkStr(field1.value, field2.value, "Same value");
   checkPref(field1.pref, field2.pref);
 }
 
-function checkPref(pref1, pref2) {
-  // If on Android treat one preference as 0 and the other as undefined as matching
-  if (isAndroid) {
-    if ((pref1 == 0 && pref2 == undefined) || (pref1 == undefined && pref2 == 0)) {
-      pref1 = 0;
-      pref2 = 0;
-    }
+function checkTel(tel1, tel2) {
+  if (tel1 ^ tel2) {
+    ok(false, "Expected both tels to be either present or absent");
+    return;
   }
-  is(pref1, pref2, "Same pref");
+  checkField(tel1, tel2);
+  checkStr(tel1.carrier, tel2.carrier, "Same carrier");
 }
 
 function checkCategory(category1, category2) {
   // Android adds contacts to the a default category. This should be removed from the
   // results before comparing them
   if (isAndroid) {
     category1 = removeAndroidDefaultCategory(category1);
     category2 = removeAndroidDefaultCategory(category2);
   }
   checkStrArray(category1, category2, "Same Category")
 }
 
 function removeAndroidDefaultCategory(category) {
-  if (category == undefined) {
-    return;
+  if (!category) {
+    return category;
   }
 
-  for (var i = 0; i < category.length; i++) {
+  var result = [];
+
+  for (var i of category) {
     // Some devices may return the full group name (prefixed with "System Group: ")
-    if (category[i] == "My Contacts" || category[i] == "System Group: My Contacts") {
-      category.splice(i, 1);
+    if (i != "My Contacts" && i != "System Group: My Contacts") {
+      result.push(i);
     }
   }
 
-  return category;
+  return result;
+}
+
+function checkArrayField(array1, array2, func, msg) {
+  if (!!array1 ^ !!array2) {
+    ok(false, "Expected both arrays to be either present or absent");
+    return;
+  }
+  if (!array1 && !array2)  {
+    ok(true, msg);
+    return;
+  }
+  ise(array1.length, array2.length, "Same length");
+  for (var i = 0; i < array1.length; ++i) {
+    func(array1[i], array2[i], msg);
+  }
 }
 
 function checkContacts(contact1, contact2) {
+  if (!!contact1 ^ !!contact2) {
+    ok(false, "Expected both contacts to be either present or absent");
+    return;
+  }
   checkStrArray(contact1.name, contact2.name, "Same name");
   checkStrArray(contact1.honorificPrefix, contact2.honorificPrefix, "Same honorificPrefix");
   checkStrArray(contact1.givenName, contact2.givenName, "Same givenName");
   checkStrArray(contact1.additionalName, contact2.additionalName, "Same additionalName");
   checkStrArray(contact1.familyName, contact2.familyName, "Same familyName");
   checkStrArray(contact1.honorificSuffix, contact2.honorificSuffix, "Same honorificSuffix");
   checkStrArray(contact1.nickname, contact2.nickname, "Same nickname");
   checkCategory(contact1.category, contact2.category);
@@ -266,38 +294,32 @@ function checkContacts(contact1, contact
   checkStrArray(contact1.jobTitle, contact2.jobTitle, "Same jobTitle");
   is(contact1.bday ? contact1.bday.valueOf() : null, contact2.bday ? contact2.bday.valueOf() : null, "Same birthday");
   checkStrArray(contact1.note, contact2.note, "Same note");
   is(contact1.anniversary ? contact1.anniversary.valueOf() : null , contact2.anniversary ? contact2.anniversary.valueOf() : null, "Same anniversary");
   checkStr(contact1.sex, contact2.sex, "Same sex");
   checkStr(contact1.genderIdentity, contact2.genderIdentity, "Same genderIdentity");
   checkStrArray(contact1.key, contact2.key, "Same key");
 
-  for (var i in contact1.email) {
-    checkField(contact1.email[i], contact2.email[i]);
-  }
-  for (var i in contact1.adr) {
-    checkAddress(contact1.adr[i], contact2.adr[i]);
-  }
-  for (var i in contact1.tel) {
-    checkTel(contact1.tel[i], contact2.tel[i]);
-  }
-  for (var i in contact1.url) {
-    checkField(contact1.url[i], contact2.url[i]);
-  }
-  for (var i in contact1.impp) {
-    checkField(contact1.impp[i], contact2.impp[i]);
-  }
+  checkArrayField(contact1.adr, contact2.adr, checkAddress, "Same adr");
+  checkArrayField(contact1.tel, contact2.tel, checkTel, "Same tel");
+  checkArrayField(contact1.email, contact2.email, checkField, "Same email");
+  checkArrayField(contact1.url, contact2.url, checkField, "Same url");
+  checkArrayField(contact1.impp, contact2.impp, checkField, "Same impp");
 }
 
 var req;
 var index = 0;
 
 var initialRev;
 
+var defaultOptions = {
+  sortBy: "givenName",
+};
+
 function checkRevision(revision, msg, then) {
   var revReq = mozContacts.getRevision();
   revReq.onsuccess = function(e) {
     is(e.target.result, initialRev+revision, msg);
     then();
   };
   // The revision function isn't supported on Android so treat on failure as success
   if (isAndroid) {
@@ -314,18 +336,16 @@ function checkCount(count, msg, then) {
   request.onsuccess = function(e) {
     is(e.target.result, count, msg);
     then();
   };
   request.onerror = onFailure;
 }
 
 var mozContacts = window.navigator.mozContacts;
-ok(mozContacts, "mozContacts exists");
-ok("mozContact" in window, "mozContact exists");
 var steps = [
   function() {
     req = mozContacts.getRevision();
     req.onsuccess = function(e) {
       initialRev = e.target.result;
       next();
     };
 
@@ -349,65 +369,63 @@ var steps = [
           checkRevision(1, "Revision was incremented on clear", next);
         });
       };
       req.onerror = onFailure;
     });
   },
   function () {
     ok(true, "Retrieving all contacts");
-    req = mozContacts.find({});
+    req = mozContacts.find(defaultOptions);
     req.onsuccess = function () {
       is(req.result.length, 0, "Empty database.");
       checkRevision(1, "Revision was not incremented on find", next);
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding empty contact");
-    createResult1 = new mozContact();
-    createResult1.init({});
+    createResult1 = new mozContact({});
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       checkCount(1, "1 contact after adding empty contact", function() {
         checkRevision(2, "Revision was incremented on save", next);
       });
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving all contacts");
-    req = mozContacts.find({});
+    req = mozContacts.find(defaultOptions);
     req.onsuccess = function () {
       is(req.result.length, 1, "One contact.");
       findResult1 = req.result[0];
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Deleting empty contact");
     req = navigator.mozContacts.remove(findResult1);
     req.onsuccess = function () {
-      var req2 = mozContacts.find({});
+      var req2 = mozContacts.find(defaultOptions);
       req2.onsuccess = function () {
         is(req2.result.length, 0, "Empty Database.");
         clearTemps();
         checkRevision(3, "Revision was incremented on remove", next);
       }
       req2.onerror = onFailure;
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact1");
-    createResult1 = new mozContact();
-    createResult1.init(properties1);
+    createResult1 = new mozContact(properties1);
 
     mozContacts.oncontactchange = function(event) {
       is(event.contactID, createResult1.id, "Same contactID");
       is(event.reason, "create", "Same reason");
       next();
     }
 
     req = navigator.mozContacts.save(createResult1);
@@ -482,18 +500,17 @@ var steps = [
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact");
     mozContacts.oncontactchange = function(event) {
        is(event.contactID, createResult2.id, "Same contactID");
        is(event.reason, "create", "Same reason");
      }
-    createResult2 = new mozContact();
-    createResult2.init({name: "newName"});
+    createResult2 = new mozContact({name: ["newName"]});
     req = navigator.mozContacts.save(createResult2);
     req.onsuccess = function () {
       ok(createResult2.id, "The contact now has an ID.");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
@@ -568,18 +585,17 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact with properties1");
-    createResult1 = new mozContact();
-    createResult1.init(properties1);
+    createResult1 = new mozContact(properties1);
     mozContacts.oncontactchange = null;
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       checkContacts(createResult1, properties1);
       next();
     };
@@ -783,37 +799,39 @@ var steps = [
       ok(findResult1.id == sample_id1, "Same ID");
       checkContacts(createResult1, properties1);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving all contacts");
-    req = mozContacts.find({});
+    req = mozContacts.find(defaultOptions);
     req.onsuccess = function() {
       is(req.result.length, 1, "Found exactly 1 contact.");
       findResult1 = req.result[0];
       ok(findResult1.id == sample_id1, "Same ID");
       checkContacts(createResult1, findResult1);
-      ok(findResult1.updated, "Has updated field");
-      ok(findResult1.published, "Has published field");
+      if (!isAndroid) {
+        ok(findResult1.updated, "Has updated field");
+        ok(findResult1.published, "Has published field");
+      }
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Modifying contact1");
     if (!findResult1) {
       SpecialPowers.executeSoon(next);
     } else {
       findResult1.impp = properties1.impp = [{value:"phil impp"}];
       req = navigator.mozContacts.save(findResult1);
       req.onsuccess = function () {
-        var req2 = mozContacts.find({});
+        var req2 = mozContacts.find(defaultOptions);
         req2.onsuccess = function() {
           is(req2.result.length, 1, "Found exactly 1 contact.");
           findResult2 = req2.result[0];
           ok(findResult2.id == sample_id1, "Same ID");
           checkContacts(findResult2, properties1);
           is(findResult2.impp.length, 1, "Found exactly 1 IMS info.");
           next();
         };
@@ -867,17 +885,17 @@ var steps = [
   function () {
     ok(true, "Modifying contact2");
     if (!findResult1) {
       SpecialPowers.executeSoon(next);
     } else {
       findResult1.impp = properties1.impp = [{value: "phil impp"}];
       req = mozContacts.save(findResult1);
       req.onsuccess = function () {
-        var req2 = mozContacts.find({});
+        var req2 = mozContacts.find(defaultOptions);
         req2.onsuccess = function () {
           is(req2.result.length, 1, "Found exactly 1 contact.");
           findResult1 = req2.result[0];
           ok(findResult1.id == sample_id1, "Same ID");
           checkContacts(findResult1, properties1);
           is(findResult1.impp.length, 1, "Found exactly 1 IMS info.");
           next();
         }
@@ -932,19 +950,18 @@ var steps = [
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Modifying contact3");
     if (!findResult1) {
       SpecialPowers.executeSoon(next);
     } else {
       findResult1.email = [{value: properties1.nickname}];
-      findResult1.nickname = "TEST";
-      var newContact = new mozContact();
-      newContact.init(findResult1);
+      findResult1.nickname = ["TEST"];
+      var newContact = new mozContact(findResult1);
       req = mozContacts.save(newContact);
       req.onsuccess = function () {
         var options = {filterBy: ["email", "givenName"],
                        filterOp: "startsWith",
                        filterValue: properties1.givenName[0]};
         // One contact has it in nickname and the other in email
         var req2 = mozContacts.find(options);
         req2.onsuccess = function () {
@@ -956,17 +973,17 @@ var steps = [
       };
       req.onerror = onFailure;
     }
   },
   function () {
     ok(true, "Deleting contact" + findResult1);
     req = mozContacts.remove(findResult1);
     req.onsuccess = function () {
-      var req2 = mozContacts.find({});
+      var req2 = mozContacts.find(defaultOptions);
       req2.onsuccess = function () {
         is(req2.result.length, 1, "One contact left.");
         findResult1 = req2.result[0];
         next();
       }
       req2.onerror = onFailure;
     }
     req.onerror = onFailure;
@@ -977,44 +994,42 @@ var steps = [
     req.onsuccess =  function () {
       clearTemps();
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact");
-    createResult1 = new mozContact();
-    createResult1.init(properties1);
+    createResult1 = new mozContact(properties1);
     req = mozContacts.save(createResult1)
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact2");
-    createResult2 = new mozContact();
-    createResult2.init(properties2);
+    createResult2 = new mozContact(properties2);
     req = mozContacts.save(createResult2);
     req.onsuccess = function () {
       ok(createResult2.id, "The contact now has an ID.");
       sample_id2 = createResult2.id;
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving all contacts");
-    req = mozContacts.find({sortBy: 'FamilyName',})
+    req = mozContacts.find({sortBy: "familyName"});
     req.onsuccess = function () {
       is(req.result.length, 2, "Found exactly 2 contact.");
-      checkContacts(properties2, req.result[1]);
+      checkContacts(req.result[1], properties1);
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     console.log("Searching contacts by query1");
     var options = {filterBy: ["givenName", "email"],
                    filterOp: "startsWith",
@@ -1081,37 +1096,35 @@ var steps = [
       clearTemps();
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding 20 contacts");
     for (var i=0; i<19; i++) {
-      createResult1 = new mozContact();
-      createResult1.init(properties1);
+      createResult1 = new mozContact(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);
+    createResult1 = new mozContact(properties1);
     req = mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkStrArray(createResult1.name, properties1.name, "Same Name");
       checkCount(20, "20 contacts in DB", next);
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving all contacts");
-    req = mozContacts.find({});
+    req = mozContacts.find(defaultOptions);
     req.onsuccess = function () {
       is(req.result.length, 20, "20 Entries.");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving all contacts with limit 10");
@@ -1147,82 +1160,83 @@ var steps = [
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving all contacts3");
     var options = {filterBy: ["givenName", "tel", "email"],
                    filterOp: "startsWith",
-                   filterValue: properties1.givenName[0].substring(0, 4),
-                   filterLimit: 15 };
+                   filterValue: properties1.givenName[0].substring(0, 4)};
     req = mozContacts.find(options);
     req.onsuccess = function () {
-      is(req.result.length, 15, "15 Entries.");
+      is(req.result.length, 20, "20 Entries.");
       checkContacts(createResult1, req.result[10]);
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Deleting database");
     req = mozContacts.clear();
     req.onsuccess = function () {
       clearTemps();
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Testing clone contact");
-    createResult1 = new mozContact();
-    createResult1.init(properties1);
+    createResult1 = new mozContact(properties1);
     req = mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkStrArray(createResult1.name, properties1.name, "Same Name");
       next();
     }
     req.onerror = onFailure;
   },
   function() {
     ok(true, "Testing clone contact2");
     var cloned = new mozContact(createResult1);
     ok(cloned.id != createResult1.id, "Cloned contact has new ID");
-    cloned.email = {value: "new email!"};
-    cloned.givenName = "Tom";
+    cloned.email = [{value: "new email!"}];
+    cloned.givenName = ["Tom"];
     req = mozContacts.save(cloned);
     req.onsuccess = function () {
       ok(cloned.id, "The contact now has an ID.");
-      ok(cloned.email[0].value == "new email!", "Same Email");
-      ok(createResult1.email != cloned.email, "Clone has different email");
-      ok(cloned.givenName == "Tom", "New Name");
+      is(cloned.email[0].value, "new email!", "Same Email");
+      isnot(createResult1.email[0].value, cloned.email[0].value, "Clone has different email");
+      is(cloned.givenName, "Tom", "New Name");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving all contacts");
     var options = {filterBy: ["givenName"],
                    filterOp: "startsWith",
                    filterValue: properties2.givenName[0].substring(0, 4)};
-    req = mozContacts.find({});
+    req = mozContacts.find(defaultOptions);
     req.onsuccess = function () {
       is(req.result.length, 2, "2 Entries.");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Search with redundant fields should only return 1 contact");
-    createResult1 = new mozContact();
-    createResult1.init({name: "XXX", givenName: "XXX", email: [{value: "XXX"}], tel: {value: "XXX"}});
+    createResult1 = new mozContact({name: ["XXX"],
+                                    givenName: ["XXX"],
+                                    email: [{value: "XXX"}],
+                                    tel: [{value: "XXX"}]
+                                   });
     req = mozContacts.save(createResult1);
     req.onsuccess = function() {
-      var options = {filterBy: [],
+      var options = {filterBy: ["givenName", "familyName"],
                      filterOp: "equals",
                      filterValue: "XXX"};
       var req2 = mozContacts.find(options);
       req2.onsuccess = function() {
         is(req2.result.length, 1, "1 Entry");
         next();
       }
       req2.onerror = onFailure;
@@ -1235,54 +1249,50 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c3);
+    createResult1 = new mozContact(c3);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c3, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c2);
+    createResult1 = new mozContact(c2);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c2, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c4);
+    createResult1 = new mozContact(c4);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c4, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c1);
+    createResult1 = new mozContact(c1);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c1, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
@@ -1313,18 +1323,17 @@ var steps = [
       checkContacts(req.result[2], c2);
       checkContacts(req.result[3], c1);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c5);
+    createResult1 = new mozContact(c5);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c5, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
@@ -1341,27 +1350,26 @@ var steps = [
       checkContacts(req.result[3], c3);
       checkContacts(req.result[4], c4);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Don't allow to add custom fields");
-    createResult1 = new mozContact();
-    createResult1.init({givenName: "customTest", yyy: "XXX"});
+    createResult1 = new mozContact({givenName: ["customTest"], yyy: "XXX"});
     req = mozContacts.save(createResult1);
     req.onsuccess = function() {
-      var options = {filterBy: [],
+      var options = {filterBy: ["givenName"],
                      filterOp: "equals",
                      filterValue: "customTest"};
       var req2 = mozContacts.find(options);
       req2.onsuccess = function() {
         is(req2.result.length, 1, "1 Entry");
-        checkStrArray(req2.result.givenName, "customTest", "same name");
+        checkStrArray(req2.result[0].givenName, ["customTest"], "same name");
         ok(req2.result.yyy === undefined, "custom property undefined");
         next();
       }
       req2.onerror = onFailure;
     }
     req.onerror = onFailure;
   },
   function () {
@@ -1370,42 +1378,39 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c7);
+    createResult1 = new mozContact(c7);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c7, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c6);
+    createResult1 = new mozContact(c6);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c6, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Test sorting");
-    createResult1 = new mozContact();
-    createResult1.init(c8);
+    createResult1 = new mozContact(c8);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       checkContacts(c8, createResult1);
       next();
     };
     req.onerror = onFailure;
   },
@@ -1425,27 +1430,26 @@ var steps = [
       ok(req.result[0].published < req.result[1].published, "Right sorting order");
       ok(req.result[1].published < req.result[2].published, "Right sorting order");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Deleting database");
-    req = mozContacts.clear()
+    req = mozContacts.clear();
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact with properties2");
-    createResult2 = new mozContact();
-    createResult2.init(properties2);
+    createResult2 = new mozContact(properties2);
     req = mozContacts.save(createResult2);
     req.onsuccess = function () {
       ok(createResult2.id, "The contact now has an ID.");
       sample_id2 = createResult2.id;
       next();
     };
     req.onerror = onFailure;
   },
@@ -1480,19 +1484,18 @@ var steps = [
     req = mozContacts.clear()
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
-    ok(true, "Adding empty contact");
-    createResult1 = new mozContact();
-    createResult1.init({name: "5", givenName: "5"});
+    ok(true, "Adding contact for category search");
+    createResult1 = new mozContact({name: ["5"], givenName: ["5"]});
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
@@ -1517,30 +1520,20 @@ var steps = [
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact with invalid data");
     var input = document.createElement("input");
     var obj = {
-        name: [1, 2],
-        familyName: 3,
-        givenName: 4,
         honorificPrefix: [],
-        honorificSuffix: {foo: "bar"},
-        additionalName: 7,
-        nickname: [8, 9],
-        org: [10, 11],
-        jobTitle: [12, 13],
-        note: 14,
-        category: [15, 16],
+        honorificSuffix: [{foo: "bar"}],
         sex: 17,
         genderIdentity: 18,
-        key: 4,
         email: input,
         adr: input,
         tel: input,
         impp: input,
         url: input
     };
     obj.honorificPrefix.__defineGetter__('0',(function() {
       var c = 0;
@@ -1548,54 +1541,51 @@ var steps = [
         if (c == 0) {
           c++;
           return "string";
         } else {
           return {foo:"bar"};
         }
       }
     })());
-    createResult1 = new mozContact();
-    createResult1.init(obj);
+    createResult1 = new mozContact(obj);
     req = mozContacts.save(createResult1);
     req.onsuccess = function () {
       checkContacts(createResult1, {
-        honorificPrefix: "string",
+        honorificPrefix: ["string"],
+        honorificSuffix: ["[object Object]"],
         sex: "17",
         genderIdentity: "18"
       });
       next();
     };
   },
   function () {
     ok(true, "Adding contact with no number but carrier");
-    createResult1 = new mozContact();
-    createResult1.init({ tel: [{type: ["home"], carrier: "myCarrier"} ] });
+    createResult1 = new mozContact({ tel: [{type: ["home"], carrier: "myCarrier"} ] });
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact with email but no value");
-    createResult1 = new mozContact();
-    createResult1.init({ email: [{type: ["home"]}] });
+    createResult1 = new mozContact({ email: [{type: ["home"]}] });
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Testing numbersOnly search 1");
-    createResult1 = new mozContact();
-    createResult1.init({ name: ["aaaaaaaaa"], givenName: ["aaaaaaaaa"], tel: [{ value: "1234567890"}]});
+    createResult1 = new mozContact({ name: ["aaaaaaaaa"], givenName: ["aaaaaaaaa"], tel: [{ value: "1234567890"}]});
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
@@ -1665,27 +1655,20 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function() {
     ok(true, "Test setting array properties to scalar values")
-    const DOMStrings = ["name","honorificPrefix","givenName","additionalName",
-                        "familyName", "honorificSuffix","nickname","category",
-                        "org","jobTitle","note"];
     const FIELDS = ["email","url","adr","tel","impp"];
     createResult1 = new mozContact();
-    for (var prop of DOMStrings) {
-      createResult1[prop] = "foo";
-      ok(Array.isArray(createResult1[prop]), prop + " is array");
-    }
     for (var prop of FIELDS) {
-      createResult1[prop] = {type: "foo"};
+      createResult1[prop] = {type: ["foo"]};
       ok(Array.isArray(createResult1[prop]), prop + " is array");
     }
     next();
   },
   function () {
     ok(true, "all done!\n");
     clearTemps();
 
--- a/dom/contacts/tests/test_contacts_blobs.html
+++ b/dom/contacts/tests/test_contacts_blobs.html
@@ -85,24 +85,24 @@ function verifyBuffers(buffer1, buffer2,
   if (isLast)
     next();
 }
 
 var randomBlob = getRandomBlob(1024);
 var randomBlob2 = getRandomBlob(1024);
 
 var properties1 = {
-  name: "xTestname1",
-  givenName: "xTestname1",
+  name: ["xTestname1"],
+  givenName: ["xTestname1"],
   photo: [randomBlob]
 };
 
 var properties2 = {
-  name: "yTestname2",
-  givenName: "yTestname2",
+  name: ["yTestname2"],
+  givenName: ["yTestname2"],
   photo: [randomBlob, randomBlob2]
 };
 
 var sample_id1;
 var createResult1;
 var findResult1;
 
 function onUnwantedSuccess() {
@@ -172,115 +172,61 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact with photo");
-    createResult1 = new mozContact();
-    createResult1.init(properties1);
-    req = navigator.mozContacts.save(createResult1);
-    req.onsuccess = function () {
-      ok(createResult1.id, "The contact now has an ID.");
-      sample_id1 = createResult1.id;
-      next();
-    };
-    req.onerror = onFailure;
-  },
-  function () {
-    ok(true, "Retrieving by substring");
-    var options = {filterBy: ["givenName"],
-                   filterOp: "startsWith",
-                   filterValue: properties1.givenName.substring(0,3)};
-    req = mozContacts.find(options);
-    req.onsuccess = function () {
-      ok(req.result.length == 1, "Found exactly 1 contact.");
-      findResult1 = req.result[0];
-      ok(findResult1.id == sample_id1, "Same ID");
-      verifyBlobArray(createResult1.photo, properties1.photo);
-    };
-    req.onerror = onFailure;
-  },
-  function () {
-    ok(true, "Adding contact with 2 photos");
-    createResult1 = new mozContact();
-    createResult1.init(properties2);
+    createResult1 = new mozContact(properties1);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving by substring");
     var options = {filterBy: ["givenName"],
                    filterOp: "startsWith",
-                   filterValue: properties2.givenName.substring(0,3)};
+                   filterValue: properties1.givenName[0].substring(0,3)};
     req = mozContacts.find(options);
     req.onsuccess = function () {
       ok(req.result.length == 1, "Found exactly 1 contact.");
       findResult1 = req.result[0];
       ok(findResult1.id == sample_id1, "Same ID");
-      verifyBlobArray(createResult1.photo, properties2.photo);
+      verifyBlobArray(createResult1.photo, properties1.photo);
     };
     req.onerror = onFailure;
   },
   function () {
-    ok(true, "Adding photo as String");
-    createResult1 = new mozContact();
-    createResult1.init({givenName: "asdf", photo: ["xyz"]});
+    ok(true, "Adding contact with 2 photos");
+    createResult1 = new mozContact(properties2);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
-      is(createResult1.photo, null, "No photo")
-      next();
-    };
-    req.onerror = onFailure;
-  },
-  function () {
-    ok(true, "Adding photo as String");
-    createResult1 = new mozContact();
-    createResult1.init({givenName: "jkl", photo: "xyz"});
-    req = navigator.mozContacts.save(createResult1);
-    req.onsuccess = function () {
-      ok(createResult1.id, "The contact now has an ID.");
-      is(createResult1.photo, null, "No photo")
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Retrieving by substring");
     var options = {filterBy: ["givenName"],
                    filterOp: "startsWith",
-                   filterValue: "asdf"};
+                   filterValue: properties2.givenName[0].substring(0,3)};
     req = mozContacts.find(options);
     req.onsuccess = function () {
       ok(req.result.length == 1, "Found exactly 1 contact.");
       findResult1 = req.result[0];
       ok(findResult1.id == sample_id1, "Same ID");
-      is(findResult1.photo, null, "No photo");
-      next();
-    };
-    req.onerror = onFailure;
-  },
-  function () {
-    ok(true, "Adding photo as Object");
-    createResult1 = new mozContact();
-    createResult1.init({photo: [{}]});
-    req = navigator.mozContacts.save(createResult1);
-    req.onsuccess = function () {
-      ok(createResult1.id, "The contact now has an ID.");
-      is(createResult1.photo, null, "No photo")
-      next();
+      verifyBlobArray(createResult1.photo, properties2.photo);
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Deleting database");
     req = mozContacts.clear()
     req.onsuccess = function () {
       ok(true, "Deleted the database");
--- a/dom/contacts/tests/test_contacts_getall.html
+++ b/dom/contacts/tests/test_contacts_getall.html
@@ -31,182 +31,216 @@ SpecialPowers.addPermission("contacts-re
 SpecialPowers.addPermission("contacts-create", true, document);
 
 var isAndroid = (navigator.userAgent.indexOf("Android") !== -1);
 var androidVersion = SpecialPowers.Cc['@mozilla.org/system-info;1']
                                   .getService(SpecialPowers.Ci.nsIPropertyBag2)
                                   .getProperty('version');
 
 let adr1 = {
-  type: "work",
+  type: ["work"],
   streetAddress: "street 1",
   locality: "locality 1",
   region: "region 1",
   postalCode: "postal code 1",
   countryName: "country 1"
 };
 
 let properties1 = {
   name: ["Testname1 TestFamilyName"],
   familyName: ["TestFamilyName","Wagner"],
   givenName: ["Test1","Test2"],
-  nickname: "nicktest",
+  nickname: ["nicktest"],
   tel: [{type: ["work"], value: "123456", carrier: "testCarrier"} , {type: ["home", "fax"], value: "+9-876-5432"}],
-  adr: adr1,
+  adr: [adr1],
   email: [{type: ["work"], value: "x@y.com"}]
 };
 
 function onFailure() {
   ok(false, "in on Failure!");
   next();
 }
-
 function checkStr(str1, str2, msg) {
-  // comparing /[null(,null)+]/ and undefined should pass
-  function nonNull(e) {
-    return e != null;
+  if (str1 ^ str2) {
+    ok(false, "Expected both strings to be either present or absent");
+    return;
   }
-  if ((Array.isArray(str1) && str1.filter(nonNull).length == 0 && str2 == undefined)
-     ||(Array.isArray(str2) && str2.filter(nonNull).length == 0 && str1 == undefined)) {
-    ok(true, msg);
-  } else if (str1) {
-    is(JSON.stringify(typeof str1 == "string" ? [str1] : str1), JSON.stringify(typeof str2 == "string" ? [str2] : str2), msg);
+  is(str1, str2, msg);
+}
+
+function checkStrArray(str1, str2, msg) {
+  function normalize_falsy(k, v) {
+    if (!v || v == "null" || v == "undefined") {
+      return "";
+    }
+    return v;
   }
+  ise(JSON.stringify(str1, normalize_falsy), JSON.stringify(str2, normalize_falsy), msg);
+}
+
+function checkPref(pref1, pref2) {
+  // If on Android treat one preference as 0 and the other as undefined as matching
+  if (isAndroid) {
+    if ((!pref1 && pref2 == undefined) || (pref1 == undefined && !pref2)) {
+      pref1 = false;
+      pref2 = false;
+    }
+  }
+  ise(!!pref1, !!pref2, "Same pref");
 }
 
 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");
+  if (adr1 ^ adr2) {
+    ok(false, "Expected both adrs to be either present or absent");
+    return;
+  }
+  checkStrArray(adr1.type, adr2.type, "Same type");
+  checkStrArray(adr1.streetAddress, adr2.streetAddress, "Same streetAddress");
+  checkStrArray(adr1.locality, adr2.locality, "Same locality");
+  checkStrArray(adr1.region, adr2.region, "Same region");
+  checkStrArray(adr1.postalCode, adr2.postalCode, "Same postalCode");
+  checkStrArray(adr1.countryName, adr2.countryName, "Same countryName");
+  checkPref(adr1.pref, adr2.pref);
+}
+
+function checkField(field1, field2) {
+  if (field1 ^ field2) {
+    ok(false, "Expected both fields to be either present or absent");
+    return;
+  }
+  checkStrArray(field1.type, field2.type, "Same type");
+  checkStrArray(field1.value, field2.value, "Same value");
+  checkPref(field1.pref, field2.pref);
 }
 
 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");
+  if (tel1 ^ tel2) {
+    ok(false, "Expected both tels to be either present or absent");
+    return;
+  }
+  checkField(tel1, tel2);
+  checkStrArray(tel1.carrier, tel2.carrier, "Same carrier");
+}
+
+function checkCategory(category1, category2) {
+  // Android adds contacts to the a default category. This should be removed from the
+  // results before comparing them
+  if (isAndroid) {
+    category1 = removeAndroidDefaultCategory(category1);
+    category2 = removeAndroidDefaultCategory(category2);
+  }
+  checkStrArray(category1, category2, "Same Category")
 }
 
-function checkField(field1, field2) {
-  checkStr(field1.type, field2.type, "Same type");
-  checkStr(field1.value, field2.value, "Same value");
+function removeAndroidDefaultCategory(category) {
+  if (!category) {
+    return category;
+  }
+
+  var result = [];
+
+  for (var i of category) {
+    // Some devices may return the full group name (prefixed with "System Group: ")
+    if (i != "My Contacts" && i != "System Group: My Contacts") {
+      result.push(i);
+    }
+  }
+
+  return result;
+}
+
+function checkArrayField(array1, array2, func, msg) {
+  if (!!array1 ^ !!array2) {
+    ok(false, "Expected both arrays to be either present or absent");
+    return;
+  }
+  if (!array1 && !array2)  {
+    ok(true, msg);
+    return;
+  }
+  ise(array1.length, array2.length, "Same length");
+  for (var i = 0; i < array1.length; ++i) {
+    func(array1[i], array2[i], msg);
+  }
 }
 
 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]);
+  if (!!contact1 ^ !!contact2) {
+    ok(false, "Expected both contacts to be either present or absent");
+    return;
   }
-  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]);
-  }
+  checkStrArray(contact1.name, contact2.name, "Same name");
+  checkStrArray(contact1.honorificPrefix, contact2.honorificPrefix, "Same honorificPrefix");
+  checkStrArray(contact1.givenName, contact2.givenName, "Same givenName");
+  checkStrArray(contact1.additionalName, contact2.additionalName, "Same additionalName");
+  checkStrArray(contact1.familyName, contact2.familyName, "Same familyName");
+  checkStrArray(contact1.honorificSuffix, contact2.honorificSuffix, "Same honorificSuffix");
+  checkStrArray(contact1.nickname, contact2.nickname, "Same nickname");
+  checkCategory(contact1.category, contact2.category);
+  checkStrArray(contact1.org, contact2.org, "Same org");
+  checkStrArray(contact1.jobTitle, contact2.jobTitle, "Same jobTitle");
+  is(contact1.bday ? contact1.bday.valueOf() : null, contact2.bday ? contact2.bday.valueOf() : null, "Same birthday");
+  checkStrArray(contact1.note, contact2.note, "Same note");
+  is(contact1.anniversary ? contact1.anniversary.valueOf() : null , contact2.anniversary ? contact2.anniversary.valueOf() : null, "Same anniversary");
+  checkStr(contact1.sex, contact2.sex, "Same sex");
+  checkStr(contact1.genderIdentity, contact2.genderIdentity, "Same genderIdentity");
+  checkStrArray(contact1.key, contact2.key, "Same key");
+
+  checkArrayField(contact1.adr, contact2.adr, checkAddress, "Same adr");
+  checkArrayField(contact1.tel, contact2.tel, checkTel, "Same tel");
+  checkArrayField(contact1.email, contact2.email, checkField, "Same email");
+  checkArrayField(contact1.url, contact2.url, checkField, "Same url");
+  checkArrayField(contact1.impp, contact2.impp, checkField, "Same impp");
 }
 
 function clearDatabase() {
   ok(true, "Clearing database");
   req = mozContacts.clear();
   req.onsuccess = function() {
     ok(true, "Cleared the database");
     next();
   };
   req.onerror = onFailure;
 }
 
 function addContacts() {
   ok(true, "Adding 40 contacts");
   for (let i = 0; i < 39; ++i) {
-    createResult1 = new mozContact();
     properties1.familyName[0] = "Testname" + (i < 10 ? "0" + i : i);
-    properties1.name = properties1.givenName[0] + " " + properties1.familyName[0];
-    createResult1.init(properties1);
+    properties1.name = [properties1.givenName[0] + " " + properties1.familyName[0]];
+    createResult1 = new mozContact(properties1);
     req = mozContacts.save(createResult1);
     req.onsuccess = function() {
       ok(createResult1.id, "The contact now has an ID.");
     };
     req.onerror = onFailure;
   };
-  createResult1 = new mozContact();
   properties1.familyName[0] = "Testname39";
-  properties1.name = properties1.givenName[0] + " Testname39";
-  createResult1.init(properties1);
+  properties1.name = [properties1.givenName[0] + " Testname39"];
+  createResult1 = new mozContact(properties1);
   req = mozContacts.save(createResult1);
   req.onsuccess = function() {
     ok(createResult1.id, "The contact now has an ID.");
-    ok(createResult1.name == properties1.name, "Same Name");
+    checkStrArray(createResult1.name, properties1.name, "Same Name");
     next();
   };
   req.onerror = onFailure;
 }
 
 let createResult1;
 
 let index = 0;
 let req;
 let mozContacts = window.navigator.mozContacts;
 
 function getOne(msg) {
   return function() {
     ok(true, msg || "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 {
@@ -241,21 +275,24 @@ function getAll(msg) {
         next();
       }
     };
     req.onerror = onFailure;
   }
 }
 
 let steps = [
+  function start() {
+    SpecialPowers.Cc["@mozilla.org/tools/profiler;1"].getService(SpecialPowers.Ci.nsIProfiler).AddMarker("GETALL_START");
+    next();
+  },
   clearDatabase,
   function() {
     // add a contact
-    createResult1 = new mozContact();
-    createResult1.init({});
+    createResult1 = new mozContact({});
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function() {
       next();
     };
     req.onerror = onFailure;
   },
 
   getOne(),
@@ -436,16 +473,17 @@ let steps = [
       next();
     }
   },
 
   clearDatabase,
 
   function() {
     ok(true, "all done!\n");
+    SpecialPowers.Cc["@mozilla.org/tools/profiler;1"].getService(SpecialPowers.Ci.nsIProfiler).AddMarker("GETALL_END");
     SimpleTest.finish();
   }
 ];
 
 function next() {
   ok(true, "Begin!");
   if (index >= steps.length) {
     ok(false, "Shouldn't get here!");
--- a/dom/contacts/tests/test_contacts_international.html
+++ b/dom/contacts/tests/test_contacts_international.html
@@ -46,33 +46,33 @@ var number1 = {
 };
 
 var number2 = {
   local: "7932012346",
   international: "+557932012346"
 };
 
 var properties1 = {
-  name: "Testname1",
+  name: ["Testname1"],
   tel: [{type: ["work"], value: number1.local, carrier: "testCarrier"} , {type: ["home", "fax"], value: number2.local}],
 };
 
 var shortNumber = "888";
 var properties2 = {
-  name: "Testname2",
+  name: ["Testname2"],
   tel: [{type: ["work"], value: shortNumber, carrier: "testCarrier"}]
 };
 
 var number3 = {
   international1: "0041557932012345",
   international2: "+557932012345"
 };
 
 var properties3 = {
-  name: "Testname2",
+  name: ["Testname2"],
   tel: [{value: number3.international2}]
 };
 
 var req;
 var index = 0;
 var createResult1;
 var findResult1;
 var sample_id1;
@@ -86,30 +86,28 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact1");
-    createResult1 = new mozContact();
-    createResult1.init(properties1);
+    createResult1 = new mozContact(properties1);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact2");
-    var createResult2 = new mozContact();
-    createResult2.init(properties2);
+    var createResult2 = new mozContact(properties2);
     req = navigator.mozContacts.save(createResult2);
     req.onsuccess = function () {
       ok(createResult2.id, "The contact now has an ID.");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
@@ -244,18 +242,17 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding a new contact with country code");
-    createResult1 = new mozContact();
-    createResult1.init(properties3);
+    createResult1 = new mozContact(properties3);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
--- a/dom/contacts/tests/test_contacts_substringmatching.html
+++ b/dom/contacts/tests/test_contacts_substringmatching.html
@@ -77,18 +77,17 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact");
-    createResult1 = new mozContact();
-    createResult1.init(prop);
+    createResult1 = new mozContact(prop);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
@@ -161,18 +160,17 @@ var steps = [
     req.onsuccess = function () {
       is(req.result.length, 1, "Found exactly 1 contacts.");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact");
-    createResult1 = new mozContact();
-    createResult1.init(prop2);
+    createResult1 = new mozContact(prop2);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
@@ -254,18 +252,17 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact");
-    createResult1 = new mozContact();
-    createResult1.init(prop3);
+    createResult1 = new mozContact(prop3);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
@@ -284,18 +281,17 @@ var steps = [
       };
       req.onerror = onFailure;
     } else {
       SpecialPowers.executeSoon(next);
     }
   },
   function () {
     ok(true, "Adding contact");
-    createResult1 = new mozContact();
-    createResult1.init(prop4);
+    createResult1 = new mozContact(prop4);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
--- a/dom/contacts/tests/test_contacts_substringmatchingVE.html
+++ b/dom/contacts/tests/test_contacts_substringmatchingVE.html
@@ -68,18 +68,17 @@ var steps = [
     req.onsuccess = function () {
       ok(true, "Deleted the database");
       next();
     }
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact");
-    createResult1 = new mozContact();
-    createResult1.init(prop);
+    createResult1 = new mozContact(prop);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
@@ -107,18 +106,17 @@ var steps = [
       ok(findResult1.id == sample_id1, "Same ID");
       is(findResult1.tel[1].value, "7704143727591", "Same Value");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
     ok(true, "Adding contact");
-    createResult1 = new mozContact();
-    createResult1.init(prop2);
+    createResult1 = new mozContact(prop2);
     req = navigator.mozContacts.save(createResult1);
     req.onsuccess = function () {
       ok(createResult1.id, "The contact now has an ID.");
       sample_id1 = createResult1.id;
       next();
     };
     req.onerror = onFailure;
   },
--- a/dom/icc/interfaces/nsIDOMIccManager.idl
+++ b/dom/icc/interfaces/nsIDOMIccManager.idl
@@ -492,17 +492,17 @@ interface nsIDOMMozIccManager : nsIDOMEv
    *        - 'adn': Abbreviated Dialling Number
    *        - 'fdn': Fixed Dialling Number
    * @param contact
    *        The contact will be updated in ICC
    * @param [optional] pin2
    *        PIN2 is only required for 'fdn'.
    */
   nsIDOMDOMRequest updateContact(in DOMString contactType,
-                                 in nsIDOMContact contact,
+                                 in nsISupports contact,
                                  [optional] in DOMString pin2);
 
   // End of UICC Phonebook Interfaces.
 
   // UICC Secure Element Interfaces
 
   /**
    * A secure element is a smart card chip that can hold
--- a/dom/icc/interfaces/nsIIccProvider.idl
+++ b/dom/icc/interfaces/nsIIccProvider.idl
@@ -67,17 +67,17 @@ interface nsIIccProvider : nsISupports
   /**
    * Phonebook interfaces.
    */
   nsIDOMDOMRequest readContacts(in nsIDOMWindow window,
                                 in DOMString contactType);
 
   nsIDOMDOMRequest updateContact(in nsIDOMWindow window,
                                  in DOMString contactType,
-                                 in nsIDOMContact contact,
+                                 in nsISupports contact,
                                  in DOMString pin2);
 
   /**
    * Secure Card Icc communication channel
    */
   nsIDOMDOMRequest iccOpenChannel(in nsIDOMWindow window,
                                   in DOMString aid);
 
--- a/dom/icc/src/IccManager.cpp
+++ b/dom/icc/src/IccManager.cpp
@@ -229,17 +229,17 @@ IccManager::ReadContacts(const nsAString
     return NS_ERROR_FAILURE;
   }
 
   return mProvider->ReadContacts(GetOwner(), aContactType, aRequest);
 }
 
 NS_IMETHODIMP
 IccManager::UpdateContact(const nsAString& aContactType,
-                          nsIDOMContact* aContact,
+                          nsISupports* aContact,
                           const nsAString& aPin2,
                           nsIDOMDOMRequest** aRequest)
 {
   if (!mProvider) {
     return NS_ERROR_FAILURE;
   }
 
   return mProvider->UpdateContact(GetOwner(), aContactType, aContact, aPin2, aRequest);
--- a/dom/icc/tests/marionette/test_icc_contact.js
+++ b/dom/icc/tests/marionette/test_icc_contact.js
@@ -10,46 +10,44 @@ ok(icc instanceof MozIccManager, "icc is
 
 function testReadContacts(type) {
   let request = icc.readContacts(type);
   request.onsuccess = function onsuccess() {
     let contacts = request.result;
 
     is(Array.isArray(contacts), true);
 
-    is(contacts[0].name, "Mozilla");
+    is(contacts[0].name[0], "Mozilla");
     is(contacts[0].tel[0].value, "15555218201");
     is(contacts[0].id, "890141032111185107201");
 
-    is(contacts[1].name, "Saßê黃");
+    is(contacts[1].name[0], "Saßê黃");
     is(contacts[1].tel[0].value, "15555218202");
     is(contacts[1].id, "890141032111185107202");
 
-    is(contacts[2].name, "Fire 火");
+    is(contacts[2].name[0], "Fire 火");
     is(contacts[2].tel[0].value, "15555218203");
     is(contacts[2].id, "890141032111185107203");
 
-    is(contacts[3].name, "Huang 黃");
+    is(contacts[3].name[0], "Huang 黃");
     is(contacts[3].tel[0].value, "15555218204");
     is(contacts[3].id, "890141032111185107204");
 
     runNextTest();
   };
 
   request.onerror = function onerror() {
     ok(false, "Cannot get " + type + " contacts");
     runNextTest();
   };
 };
 
 function testAddContact(type, pin2) {
-  let contact = new mozContact();
-
-  contact.init({
-    name: "add",
+  let contact = new mozContact({
+    name: ["add"],
     tel: [{value: "0912345678"}],
     email:[]
   });
 
   let updateRequest = icc.updateContact(type, contact, pin2);
 
   updateRequest.onsuccess = function onsuccess() {
     // Get ICC contact for checking new contact
@@ -57,17 +55,17 @@ function testAddContact(type, pin2) {
     let getRequest = icc.readContacts(type);
 
     getRequest.onsuccess = function onsuccess() {
       let contacts = getRequest.result;
 
       // There are 4 SIM contacts which are harded in emulator
       is(contacts.length, 5);
 
-      is(contacts[4].name, "add");
+      is(contacts[4].name[0], "add");
       is(contacts[4].tel[0].value, "0912345678");
 
       runNextTest();
     };
 
     getRequest.onerror = function onerror() {
       ok(false, "Cannot get " + type + " contacts: " + getRequest.error.name);
       runNextTest();
deleted file mode 100644
--- a/dom/interfaces/contacts/moz.build
+++ /dev/null
@@ -1,15 +0,0 @@
-# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-XPIDL_SOURCES += [
-    'nsIContactProperties.idl',
-    'nsIDOMContactManager.idl',
-]
-
-XPIDL_MODULE = 'dom_contacts'
-
-MODULE = 'dom'
-
deleted file mode 100644
--- a/dom/interfaces/contacts/nsIContactProperties.idl
+++ /dev/null
@@ -1,74 +0,0 @@
-/* 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/. */
-
-#include "domstubs.idl"
-
-[scriptable, uuid(9cbfa81c-bcab-4ca9-b0d2-f4318f295e33)]
-interface nsIContactAddress : nsISupports
-{
-  attribute DOMString type;
-  attribute boolean   pref; // false = no pref, true = preferred (vCard3 TYPE:PREF; vCard4 PREF:1)
-  attribute DOMString streetAddress;
-  attribute DOMString locality;
-  attribute DOMString region;
-  attribute DOMString postalCode;
-  attribute DOMString countryName;
-};
-
-[scriptable, uuid(ad19a543-69e4-44f0-adfa-37c011556bc1)]
-interface nsIContactField : nsISupports
-{
-  attribute jsval     type; // DOMString[], "home", "work", etc.
-  attribute DOMString value;
-  attribute boolean   pref; // false = no pref, true = preferred (vCard3 TYPE:PREF; vCard4 PREF:1)
-};
-
-[scriptable, uuid(4d42c5a9-ea5d-4102-80c3-40cc986367ca)]
-interface nsIContactTelField : nsIContactField
-{
-  attribute DOMString carrier;
-};
-
-[scriptable, uuid(0a5b1fab-70da-46dd-b902-619904d920c2)]
-interface nsIContactFindSortOptions : nsISupports
-{
-  attribute DOMString sortBy;       // "givenName" or "familyName"
-  attribute DOMString sortOrder;    // e.g. "descending"
-};
-
-[scriptable, uuid(28ce07d0-45d9-4b7a-8843-521df4edd8bc)]
-interface nsIContactFindOptions : nsIContactFindSortOptions
-{
-  attribute DOMString filterValue;  // e.g. "Tom"
-  attribute DOMString filterOp;     // e.g. "startsWith"
-  attribute jsval filterBy;         // DOMString[], e.g. ["givenName", "nickname"]
-  attribute unsigned long filterLimit;
-};
-
-[scriptable, uuid(35ad8a4e-9486-44b6-883d-550f14635e49)]
-interface nsIContactProperties : nsISupports
-{
-  attribute jsval         name;               // DOMString[]
-  attribute jsval         honorificPrefix;    // DOMString[]
-  attribute jsval         givenName;          // DOMString[]
-  attribute jsval         additionalName;     // DOMString[]
-  attribute jsval         familyName;         // DOMString[]
-  attribute jsval         honorificSuffix;    // DOMString[]
-  attribute jsval         nickname;           // DOMString[]
-  attribute jsval         email;              // ContactField[]
-  attribute jsval         photo;              // nsIDOMBlob[]
-  attribute jsval         url;                // ContactField[]
-  attribute jsval         category;           // DOMString[]
-  attribute jsval         adr;                // ContactAddress[]
-  attribute jsval         tel;                // ContactTelField[]
-  attribute jsval         org;                // DOMString[]
-  attribute jsval         jobTitle;           // DOMString[]
-  attribute jsval         bday;               // Date
-  attribute jsval         note;               // DOMString[]
-  attribute jsval         impp;               // ContactField[]
-  attribute jsval         anniversary;        // Date
-  attribute DOMString     sex;                // DOMString
-  attribute DOMString     genderIdentity;     // DOMString
-  attribute jsval         key;                // DOMString[]
-};
deleted file mode 100644
--- a/dom/interfaces/contacts/nsIDOMContactManager.idl
+++ /dev/null
@@ -1,41 +0,0 @@
-/* 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/. */
-
-#include "domstubs.idl"
-#include "nsIContactProperties.idl"
-#include "nsIDOMEventTarget.idl"
-
-interface nsIArray;
-interface nsIDOMDOMRequest;
-interface nsIDOMDOMCursor;
-
-[scriptable, uuid(72a5ee28-81d8-4af8-90b3-ae935396cc66)]
-interface nsIDOMContact : nsIContactProperties
-{
-  attribute DOMString id;
-  readonly attribute jsval     published;
-  readonly attribute jsval     updated;
-
-  void init(in nsIContactProperties properties);  // Workaround BUG 723206
-};
-
-[scriptable, uuid(8beb3a66-d70a-4111-b216-b8e995ad3aff)]
-interface nsIDOMContactManager : nsISupports
-{
-  nsIDOMDOMRequest find(in nsIContactFindOptions options);
-
-  nsIDOMDOMCursor getAll(in nsIContactFindSortOptions options);
-
-  nsIDOMDOMRequest clear();
-
-  nsIDOMDOMRequest save(in nsIDOMContact contact);
-
-  nsIDOMDOMRequest remove(in nsIDOMContact contact);
-
-  attribute nsIDOMEventListener oncontactchange;
-
-  nsIDOMDOMRequest getRevision();
-
-  nsIDOMDOMRequest getCount();
-};
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -2,17 +2,16 @@
 # vim: set filetype=python:
 # 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/.
 
 interfaces = [
     'base',
     'canvas',
-    'contacts',
     'core',
     'html',
     'events',
     'devicestorage',
     'settings',
     'stylesheets',
     'sidebar',
     'css',
--- a/dom/system/gonk/RILContentHelper.js
+++ b/dom/system/gonk/RILContentHelper.js
@@ -1715,30 +1715,29 @@ RILContentHelper.prototype = {
       this.fireRequestError(message.requestId, message.errorMsg);
       return;
     }
 
     let window = this._windowsMap[message.requestId];
     delete this._windowsMap[message.requestId];
     let contacts = message.contacts;
     let result = contacts.map(function(c) {
-      let contact = Cc["@mozilla.org/contact;1"].createInstance(Ci.nsIDOMContact);
       let prop = {name: [c.alphaId], tel: [{value: c.number}]};
 
       if (c.email) {
         prop.email = [{value: c.email}];
       }
 
       // ANR - Additional Number
       let anrLen = c.anr ? c.anr.length : 0;
       for (let i = 0; i < anrLen; i++) {
         prop.tel.push({value: c.anr[i]});
       }
 
-      contact.init(prop);
+      let contact = new window.mozContact(prop);
       contact.id = message.iccid + c.recordId;
       return contact;
     });
 
     this.fireRequestSuccess(message.requestId,
                             ObjectWrapper.wrap(result, window));
   },
 
new file mode 100644
--- /dev/null
+++ b/dom/webidl/Contacts.webidl
@@ -0,0 +1,170 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[ChromeOnly, Constructor, JSImplementation="@mozilla.org/contactAddress;1"]
+interface ContactAddress {
+  attribute object?    type; // DOMString[]
+  attribute DOMString? streetAddress;
+  attribute DOMString? locality;
+  attribute DOMString? region;
+  attribute DOMString? postalCode;
+  attribute DOMString? countryName;
+  attribute boolean?   pref;
+
+  [ChromeOnly]
+  void initialize(optional sequence<DOMString>? type,
+                  optional DOMString streetAddress,
+                  optional DOMString locality,
+                  optional DOMString region,
+                  optional DOMString postalCode,
+                  optional DOMString countryName,
+                  optional boolean pref);
+};
+
+dictionary ContactAddressInit {
+  sequence<DOMString>? type;
+  DOMString?           streetAddress;
+  DOMString?           locality;
+  DOMString?           region;
+  DOMString?           postalCode;
+  DOMString?           countryName;
+  boolean?             pref;
+};
+
+
+[ChromeOnly, Constructor, JSImplementation="@mozilla.org/contactField;1"]
+interface ContactField {
+  attribute object?    type; // DOMString[]
+  attribute DOMString? value;
+  attribute boolean?   pref;
+
+  [ChromeOnly]
+  void initialize(optional sequence<DOMString>? type,
+                  optional DOMString value,
+                  optional boolean pref);
+};
+
+dictionary ContactFieldInit {
+  sequence<DOMString>? type;
+  DOMString?           value;
+  boolean?             pref;
+};
+
+
+[ChromeOnly, Constructor, JSImplementation="@mozilla.org/contactTelField;1"]
+interface ContactTelField : ContactField {
+  attribute DOMString? carrier;
+
+  [ChromeOnly]
+  void initialize(optional sequence<DOMString>? type,
+                  optional DOMString value,
+                  optional DOMString? carrier,
+                  optional boolean pref);
+};
+
+dictionary ContactTelFieldInit : ContactFieldInit {
+  DOMString? carrier;
+};
+
+
+dictionary ContactProperties {
+  Date?                          bday;
+  Date?                          anniversary;
+
+  DOMString?                     sex;
+  DOMString?                     genderIdentity;
+
+  sequence<Blob>?                photo;
+
+  sequence<ContactAddressInit>?  adr;
+
+  sequence<ContactFieldInit>?    email;
+  sequence<ContactFieldInit>?    url;
+  sequence<ContactFieldInit>?    impp;
+
+  sequence<ContactTelFieldInit>? tel;
+
+  sequence<DOMString>?           name;
+  sequence<DOMString>?           honorificPrefix;
+  sequence<DOMString>?           givenName;
+  sequence<DOMString>?           additionalName;
+  sequence<DOMString>?           familyName;
+  sequence<DOMString>?           honorificSuffix;
+  sequence<DOMString>?           nickname;
+  sequence<DOMString>?           category;
+  sequence<DOMString>?           org;
+  sequence<DOMString>?           jobTitle;
+  sequence<DOMString>?           note;
+  sequence<DOMString>?           key;
+};
+
+[Constructor(optional ContactProperties properties),
+ JSImplementation="@mozilla.org/contact;1"]
+interface mozContact {
+           attribute DOMString    id;
+  readonly attribute Date?        published;
+  readonly attribute Date?        updated;
+
+           attribute Date?        bday;
+           attribute Date?        anniversary;
+
+           attribute DOMString?   sex;
+           attribute DOMString?   genderIdentity;
+
+           attribute object?      photo;
+
+           attribute object?      adr;
+
+           attribute object?      email;
+           attribute object?      url;
+           attribute object?      impp;
+
+           attribute object?      tel;
+
+           attribute object?      name;
+           attribute object?      honorificPrefix;
+           attribute object?      givenName;
+           attribute object?      additionalName;
+           attribute object?      familyName;
+           attribute object?      honorificSuffix;
+           attribute object?      nickname;
+           attribute object?      category;
+           attribute object?      org;
+           attribute object?      jobTitle;
+           attribute object?      note;
+           attribute object?      key;
+
+  [ChromeOnly]
+  void setMetadata(DOMString id, optional Date published, optional Date updated);
+
+  jsonifier;
+};
+
+dictionary ContactFindSortOptions {
+  DOMString sortBy;                    // "givenName" or "familyName"
+  DOMString sortOrder = "ascending";   // e.g. "descending"
+};
+
+dictionary ContactFindOptions : ContactFindSortOptions {
+  DOMString      filterValue;  // e.g. "Tom"
+  DOMString      filterOp;     // e.g. "startsWith"
+  any            filterBy;     // e.g. ["givenName", "nickname"]
+  unsigned long  filterLimit;
+};
+
+[NoInterfaceObject, NavigatorProperty="mozContacts",
+ JSImplementation="@mozilla.org/contactManager;1"]
+interface ContactManager : EventTarget {
+  DOMRequest find(optional ContactFindOptions options);
+  DOMCursor  getAll(optional ContactFindSortOptions options);
+  DOMRequest clear();
+  DOMRequest save(mozContact contact);
+  DOMRequest remove(mozContact contact);
+  DOMRequest getRevision();
+  DOMRequest getCount();
+
+  attribute  EventHandler oncontactchange;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -54,16 +54,17 @@ WEBIDL_FILES = [
     'ChannelMergerNode.webidl',
     'ChannelSplitterNode.webidl',
     'CharacterData.webidl',
     'ChildNode.webidl',
     'ClipboardEvent.webidl',
     'CommandEvent.webidl',
     'Comment.webidl',
     'CompositionEvent.webidl',
+    'Contacts.webidl',
     'ConvolverNode.webidl',
     'Coordinates.webidl',
     'DOMCursor.webidl',
     'DOMError.webidl',
     'DOMException.webidl',
     'DOMImplementation.webidl',
     'DOMMMIError.webidl',
     'DOMParser.webidl',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -130,17 +130,16 @@
 @BINPATH@/components/directory.xpt
 @BINPATH@/components/docshell.xpt
 @BINPATH@/components/dom.xpt
 @BINPATH@/components/dom_apps.xpt
 @BINPATH@/components/dom_base.xpt
 @BINPATH@/components/dom_camera.xpt
 @BINPATH@/components/dom_canvas.xpt
 @BINPATH@/components/dom_core.xpt
-@BINPATH@/components/dom_contacts.xpt
 @BINPATH@/components/dom_css.xpt
 @BINPATH@/components/dom_devicestorage.xpt
 @BINPATH@/components/dom_events.xpt
 @BINPATH@/components/dom_file.xpt
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_media.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt