Backed out changeset 27695ca9f8cd (bug 1310864) for failures in test_navigator_resolve_identity.html, test_bug707564.html, and test_dom_xrays.html
authorPhil Ringnalda <philringnalda@gmail.com>
Mon, 31 Oct 2016 19:39:06 -0700
changeset 432873 6fcb0e886dcb934ddfe4392a0d7977fa4ca52a81
parent 432872 a0cc5e0ca6384f17f557733c04ff570153d9a15b
child 432874 909d5ee27ca7b9e0a9ab4622e59e4e57be9f0f8d
push id34456
push userbmo:rchien@mozilla.com
push dateThu, 03 Nov 2016 01:18:02 +0000
bugs1310864, 707564
milestone52.0a1
backs out27695ca9f8cd0f21aa142a2e4ff3a4891f509fa3
Backed out changeset 27695ca9f8cd (bug 1310864) for failures in test_navigator_resolve_identity.html, test_bug707564.html, and test_dom_xrays.html
b2g/app/b2g.js
b2g/chrome/content/shell.js
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/apps/PermissionsTable.jsm
dom/base/Navigator.cpp
dom/base/Navigator.h
dom/bindings/Bindings.conf
dom/contacts/ContactManager.js
dom/contacts/ContactManager.manifest
dom/contacts/fallback/ContactDB.jsm
dom/contacts/fallback/ContactService.jsm
dom/contacts/moz.build
dom/contacts/tests/chrome.ini
dom/contacts/tests/file_contacts_basics.html
dom/contacts/tests/file_contacts_basics2.html
dom/contacts/tests/file_contacts_blobs.html
dom/contacts/tests/file_contacts_events.html
dom/contacts/tests/file_contacts_getall.html
dom/contacts/tests/file_contacts_getall2.html
dom/contacts/tests/file_contacts_international.html
dom/contacts/tests/file_contacts_substringmatching.html
dom/contacts/tests/file_contacts_substringmatchingCL.html
dom/contacts/tests/file_contacts_substringmatchingVE.html
dom/contacts/tests/file_migration.html
dom/contacts/tests/file_permission_denied.html
dom/contacts/tests/mochitest.ini
dom/contacts/tests/shared.js
dom/contacts/tests/test_contacts_a_cache.xul
dom/contacts/tests/test_contacts_a_shutdown.xul
dom/contacts/tests/test_contacts_a_upgrade.xul
dom/contacts/tests/test_contacts_basics.html
dom/contacts/tests/test_contacts_basics2.html
dom/contacts/tests/test_contacts_blobs.html
dom/contacts/tests/test_contacts_events.html
dom/contacts/tests/test_contacts_getall.html
dom/contacts/tests/test_contacts_getall2.html
dom/contacts/tests/test_contacts_international.html
dom/contacts/tests/test_contacts_substringmatching.html
dom/contacts/tests/test_contacts_substringmatchingCL.html
dom/contacts/tests/test_contacts_substringmatchingVE.html
dom/contacts/tests/test_migration.html
dom/contacts/tests/test_migration_chrome.js
dom/contacts/tests/test_permission_denied.html
dom/events/test/test_all_synthetic_events.html
dom/gamepad/GamepadServiceTest.h
dom/icc/Assertions.cpp
dom/icc/Icc.cpp
dom/icc/Icc.h
dom/icc/IccCallback.cpp
dom/icc/IccCallback.h
dom/icc/IccCardLockError.cpp
dom/icc/IccCardLockError.h
dom/icc/IccContact.cpp
dom/icc/IccContact.h
dom/icc/IccInfo.cpp
dom/icc/IccInfo.h
dom/icc/IccListener.cpp
dom/icc/IccListener.h
dom/icc/IccManager.cpp
dom/icc/IccManager.h
dom/icc/gonk/IccService.js
dom/icc/gonk/IccService.manifest
dom/icc/gonk/StkCmdFactory.js
dom/icc/gonk/StkCmdFactory.manifest
dom/icc/interfaces/moz.build
dom/icc/interfaces/nsIGonkIccService.idl
dom/icc/interfaces/nsIIccContact.idl
dom/icc/interfaces/nsIIccInfo.idl
dom/icc/interfaces/nsIIccMessenger.idl
dom/icc/interfaces/nsIIccService.idl
dom/icc/interfaces/nsIStkCmdFactory.idl
dom/icc/interfaces/nsIStkProactiveCmd.idl
dom/icc/ipc/IccChild.cpp
dom/icc/ipc/IccChild.h
dom/icc/ipc/IccIPCService.cpp
dom/icc/ipc/IccIPCService.h
dom/icc/ipc/IccIPCUtils.cpp
dom/icc/ipc/IccIPCUtils.h
dom/icc/ipc/IccParent.cpp
dom/icc/ipc/IccParent.h
dom/icc/ipc/PIcc.ipdl
dom/icc/ipc/PIccRequest.ipdl
dom/icc/ipc/PIccTypes.ipdlh
dom/icc/moz.build
dom/icc/tests/marionette/head.js
dom/icc/tests/marionette/manifest.ini
dom/icc/tests/marionette/test_icc_access_invalid_object.js
dom/icc/tests/marionette/test_icc_card_lock_change_pin.js
dom/icc/tests/marionette/test_icc_card_lock_enable_pin.js
dom/icc/tests/marionette/test_icc_card_lock_get_retry_count.js
dom/icc/tests/marionette/test_icc_card_lock_unlock_pin.js
dom/icc/tests/marionette/test_icc_card_lock_unlock_puk.js
dom/icc/tests/marionette/test_icc_card_state.js
dom/icc/tests/marionette/test_icc_contact_add.js
dom/icc/tests/marionette/test_icc_contact_read.js
dom/icc/tests/marionette/test_icc_contact_update.js
dom/icc/tests/marionette/test_icc_detected_undetected_event.js
dom/icc/tests/marionette/test_icc_info.js
dom/icc/tests/marionette/test_icc_match_mvno.js
dom/icc/tests/marionette/test_icc_service_state.js
dom/icc/tests/marionette/test_stk_bip_command.js
dom/icc/tests/marionette/test_stk_display_text.js
dom/icc/tests/marionette/test_stk_event_download.js
dom/icc/tests/marionette/test_stk_get_inkey.js
dom/icc/tests/marionette/test_stk_get_input.js
dom/icc/tests/marionette/test_stk_launch_browser.js
dom/icc/tests/marionette/test_stk_local_info.js
dom/icc/tests/marionette/test_stk_menu_selection.js
dom/icc/tests/marionette/test_stk_play_tone.js
dom/icc/tests/marionette/test_stk_poll_interval.js
dom/icc/tests/marionette/test_stk_poll_off.js
dom/icc/tests/marionette/test_stk_refresh.js
dom/icc/tests/marionette/test_stk_response.js
dom/icc/tests/marionette/test_stk_select_item.js
dom/icc/tests/marionette/test_stk_send_dtmf.js
dom/icc/tests/marionette/test_stk_send_sms.js
dom/icc/tests/marionette/test_stk_send_ss.js
dom/icc/tests/marionette/test_stk_send_ussd.js
dom/icc/tests/marionette/test_stk_setup_call.js
dom/icc/tests/marionette/test_stk_setup_event_list.js
dom/icc/tests/marionette/test_stk_setup_idle_mode_text.js
dom/icc/tests/marionette/test_stk_setup_menu.js
dom/icc/tests/marionette/test_stk_timer_expiration.js
dom/icc/tests/marionette/test_stk_timer_management.js
dom/ipc/ContentChild.cpp
dom/ipc/ContentChild.h
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
dom/ipc/PContent.ipdl
dom/mobileconnection/Assertions.cpp
dom/mobileconnection/MobileCallForwardingOptions.cpp
dom/mobileconnection/MobileCallForwardingOptions.h
dom/mobileconnection/MobileCellInfo.cpp
dom/mobileconnection/MobileCellInfo.h
dom/mobileconnection/MobileConnection.cpp
dom/mobileconnection/MobileConnection.h
dom/mobileconnection/MobileConnectionArray.cpp
dom/mobileconnection/MobileConnectionArray.h
dom/mobileconnection/MobileConnectionCallback.cpp
dom/mobileconnection/MobileConnectionCallback.h
dom/mobileconnection/MobileConnectionInfo.cpp
dom/mobileconnection/MobileConnectionInfo.h
dom/mobileconnection/MobileNetworkInfo.cpp
dom/mobileconnection/MobileNetworkInfo.h
dom/mobileconnection/gonk/MobileConnectionService.js
dom/mobileconnection/gonk/MobileConnectionService.manifest
dom/mobileconnection/gonk/nsIGonkMobileConnectionService.idl
dom/mobileconnection/gonk/nsIMobileConnectionMessenger.idl
dom/mobileconnection/interfaces/nsICellInfo.idl
dom/mobileconnection/interfaces/nsIMobileCallForwardingOptions.idl
dom/mobileconnection/interfaces/nsIMobileCellInfo.idl
dom/mobileconnection/interfaces/nsIMobileConnectionInfo.idl
dom/mobileconnection/interfaces/nsIMobileConnectionService.idl
dom/mobileconnection/interfaces/nsIMobileDeviceIdentities.idl
dom/mobileconnection/interfaces/nsIMobileNetworkInfo.idl
dom/mobileconnection/interfaces/nsINeighboringCellInfo.idl
dom/mobileconnection/ipc/MobileConnectionChild.cpp
dom/mobileconnection/ipc/MobileConnectionChild.h
dom/mobileconnection/ipc/MobileConnectionIPCSerializer.h
dom/mobileconnection/ipc/MobileConnectionIPCService.cpp
dom/mobileconnection/ipc/MobileConnectionIPCService.h
dom/mobileconnection/ipc/MobileConnectionParent.cpp
dom/mobileconnection/ipc/MobileConnectionParent.h
dom/mobileconnection/ipc/PMobileConnection.ipdl
dom/mobileconnection/ipc/PMobileConnectionRequest.ipdl
dom/mobileconnection/ipc/PMobileConnectionTypes.ipdlh
dom/mobileconnection/moz.build
dom/mobileconnection/tests/marionette/head.js
dom/mobileconnection/tests/marionette/head_chrome.js
dom/mobileconnection/tests/marionette/manifest.ini
dom/mobileconnection/tests/marionette/test_call_barring_basic_operations.js
dom/mobileconnection/tests/marionette/test_call_barring_change_password.js
dom/mobileconnection/tests/marionette/test_call_barring_get_error.js
dom/mobileconnection/tests/marionette/test_call_barring_set_error.js
dom/mobileconnection/tests/marionette/test_call_waiting.js
dom/mobileconnection/tests/marionette/test_dsds_mobile_data_connection.js
dom/mobileconnection/tests/marionette/test_mobile_call_forwarding.js
dom/mobileconnection/tests/marionette/test_mobile_call_forwarding_get_error.js
dom/mobileconnection/tests/marionette/test_mobile_call_forwarding_set_error.js
dom/mobileconnection/tests/marionette/test_mobile_cell_Info_list.js
dom/mobileconnection/tests/marionette/test_mobile_clir.js
dom/mobileconnection/tests/marionette/test_mobile_clir_radio_off.js
dom/mobileconnection/tests/marionette/test_mobile_connections_array_uninitialized.js
dom/mobileconnection/tests/marionette/test_mobile_data_connection.js
dom/mobileconnection/tests/marionette/test_mobile_data_ipv6.js
dom/mobileconnection/tests/marionette/test_mobile_data_location.js
dom/mobileconnection/tests/marionette/test_mobile_data_state.js
dom/mobileconnection/tests/marionette/test_mobile_icc_change.js
dom/mobileconnection/tests/marionette/test_mobile_last_known_network.js
dom/mobileconnection/tests/marionette/test_mobile_neighboring_cell_ids.js
dom/mobileconnection/tests/marionette/test_mobile_networks.js
dom/mobileconnection/tests/marionette/test_mobile_operator_names.js
dom/mobileconnection/tests/marionette/test_mobile_operator_names_plmnlist.js
dom/mobileconnection/tests/marionette/test_mobile_operator_names_roaming.js
dom/mobileconnection/tests/marionette/test_mobile_preferred_network_type.js
dom/mobileconnection/tests/marionette/test_mobile_preferred_network_type_radio_off.js
dom/mobileconnection/tests/marionette/test_mobile_roaming_preference.js
dom/mobileconnection/tests/marionette/test_mobile_set_radio.js
dom/mobileconnection/tests/marionette/test_mobile_signal_strength.js
dom/mobileconnection/tests/marionette/test_mobile_supported_network_types.js
dom/mobileconnection/tests/marionette/test_mobile_voice_location.js
dom/mobileconnection/tests/marionette/test_mobile_voice_privacy.js
dom/mobileconnection/tests/marionette/test_mobile_voice_state.js
dom/mobileconnection/tests/mochitest/mochitest.ini
dom/mobileconnection/tests/mochitest/test_mobileconnection_permission.html
dom/mobileconnection/tests/mochitest/test_mobilenetwork_permission.html
dom/moz.build
dom/permission/tests/mochitest-ril.ini
dom/permission/tests/test_mobileconnection.html
dom/system/NetworkGeolocationProvider.js
dom/webidl/CFStateChangeEvent.webidl
dom/webidl/Contacts.webidl
dom/webidl/DataErrorEvent.webidl
dom/webidl/IccCardLockError.webidl
dom/webidl/IccChangeEvent.webidl
dom/webidl/MozClirModeEvent.webidl
dom/webidl/MozContactChangeEvent.webidl
dom/webidl/MozEmergencyCbModeEvent.webidl
dom/webidl/MozIcc.webidl
dom/webidl/MozIccInfo.webidl
dom/webidl/MozIccManager.webidl
dom/webidl/MozMobileCellInfo.webidl
dom/webidl/MozMobileConnection.webidl
dom/webidl/MozMobileConnectionArray.webidl
dom/webidl/MozMobileConnectionInfo.webidl
dom/webidl/MozMobileNetworkInfo.webidl
dom/webidl/MozOtaStatusEvent.webidl
dom/webidl/MozStkCommandEvent.webidl
dom/webidl/Navigator.webidl
dom/webidl/moz.build
layout/build/nsLayoutModule.cpp
modules/libpref/init/all.js
testing/marionette/harness/marionette/tests/webapi-tests.ini
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -195,16 +195,17 @@ pref("privacy.item.downloads", true);
 pref("privacy.item.passwords", true);
 pref("privacy.item.sessions", true);
 pref("privacy.item.geolocation", true);
 pref("privacy.item.siteSettings", true);
 pref("privacy.item.syncAccount", true);
 
 // base url for the wifi geolocation network provider
 pref("geo.provider.use_mls", false);
+pref("geo.cell.scan", true);
 pref("geo.wifi.uri", "https://location.services.mozilla.com/v1/geolocate?key=%MOZILLA_API_KEY%");
 
 // base url for the stumbler
 pref("geo.stumbler.url", "https://location.services.mozilla.com/v1/geosubmit?key=%MOZILLA_API_KEY%");
 
 // enable geo
 pref("geo.enabled", true);
 
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -1,16 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* 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/. */
 
 window.performance.mark('gecko-shell-loadstart');
 
+Cu.import('resource://gre/modules/ContactService.jsm');
 Cu.import('resource://gre/modules/NotificationDB.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import('resource://gre/modules/UserAgentOverrides.jsm');
 Cu.import('resource://gre/modules/Keyboard.jsm');
 Cu.import('resource://gre/modules/ErrorPage.jsm');
 Cu.import('resource://gre/modules/AlertsHelper.jsm');
 Cu.import('resource://gre/modules/SystemUpdateService.jsm');
 
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -152,17 +152,21 @@
 @RESPATH@/components/dom_audiochannel.xpt
 @RESPATH@/components/dom_base.xpt
 @RESPATH@/components/dom_system.xpt
 @RESPATH@/components/dom_workers.xpt
 #ifdef MOZ_WIDGET_GONK
 @RESPATH@/components/dom_wifi.xpt
 @RESPATH@/components/dom_system_gonk.xpt
 #endif
+#ifdef MOZ_B2G_RIL
+@RESPATH@/components/dom_mobileconnection.xpt
+#endif
 @RESPATH@/components/dom_canvas.xpt
+@RESPATH@/components/dom_contacts.xpt
 @RESPATH@/components/dom_core.xpt
 @RESPATH@/components/dom_css.xpt
 @RESPATH@/components/dom_events.xpt
 @RESPATH@/components/dom_geolocation.xpt
 @RESPATH@/components/dom_media.xpt
 @RESPATH@/components/dom_network.xpt
 #ifdef MOZ_SECUREELEMENT
 @RESPATH@/components/dom_secureelement.xpt
@@ -328,16 +332,18 @@
 
 ; JavaScript components
 @RESPATH@/components/ConsoleAPI.manifest
 @RESPATH@/components/ConsoleAPIStorage.js
 @RESPATH@/components/BrowserElementParent.manifest
 @RESPATH@/components/BrowserElementParent.js
 @RESPATH@/components/BrowserElementProxy.manifest
 @RESPATH@/components/BrowserElementProxy.js
+@RESPATH@/components/ContactManager.js
+@RESPATH@/components/ContactManager.manifest
 @RESPATH@/components/PhoneNumberService.js
 @RESPATH@/components/PhoneNumberService.manifest
 @RESPATH@/components/NotificationStorage.js
 @RESPATH@/components/NotificationStorage.manifest
 @RESPATH@/components/PermissionSettings.js
 @RESPATH@/components/PermissionSettings.manifest
 @RESPATH@/components/PermissionPromptService.js
 @RESPATH@/components/PermissionPromptService.manifest
@@ -439,16 +445,18 @@
 @RESPATH@/components/IccService.manifest
 @RESPATH@/components/MmsService.js
 @RESPATH@/components/MmsService.manifest
 @RESPATH@/components/MobileMessageDatabaseService.js
 @RESPATH@/components/MobileMessageDatabaseService.manifest
 #ifndef DISABLE_MOZ_RIL_GEOLOC
 @RESPATH@/components/DataCallInterfaceService.js
 @RESPATH@/components/DataCallInterfaceService.manifest
+@RESPATH@/components/MobileConnectionService.js
+@RESPATH@/components/MobileConnectionService.manifest
 @RESPATH@/components/RadioInterfaceLayer.js
 @RESPATH@/components/RadioInterfaceLayer.manifest
 @RESPATH@/components/SmsService.js
 @RESPATH@/components/SmsService.manifest
 #endif
 @RESPATH@/components/StkCmdFactory.js
 @RESPATH@/components/StkCmdFactory.manifest
 @RESPATH@/components/RILSystemMessengerHelper.js
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -188,16 +188,17 @@
 @RESPATH@/components/dom_core.xpt
 @RESPATH@/components/dom_css.xpt
 @RESPATH@/components/dom_events.xpt
 @RESPATH@/components/dom_geolocation.xpt
 @RESPATH@/components/dom_media.xpt
 @RESPATH@/components/dom_network.xpt
 @RESPATH@/components/dom_notification.xpt
 @RESPATH@/components/dom_html.xpt
+@RESPATH@/components/dom_icc.xpt
 @RESPATH@/components/dom_offline.xpt
 @RESPATH@/components/dom_json.xpt
 @RESPATH@/components/dom_power.xpt
 @RESPATH@/components/dom_push.xpt
 @RESPATH@/components/dom_quota.xpt
 @RESPATH@/components/dom_range.xpt
 @RESPATH@/components/dom_security.xpt
 @RESPATH@/components/dom_settings.xpt
@@ -503,16 +504,18 @@
 @RESPATH@/components/AppsService.manifest
 @RESPATH@/components/recording-cmdline.js
 @RESPATH@/components/recording-cmdline.manifest
 @RESPATH@/components/htmlMenuBuilder.js
 @RESPATH@/components/htmlMenuBuilder.manifest
 
 @RESPATH@/components/PermissionSettings.js
 @RESPATH@/components/PermissionSettings.manifest
+@RESPATH@/components/ContactManager.js
+@RESPATH@/components/ContactManager.manifest
 @RESPATH@/components/PhoneNumberService.js
 @RESPATH@/components/PhoneNumberService.manifest
 @RESPATH@/components/NotificationStorage.js
 @RESPATH@/components/NotificationStorage.manifest
 @RESPATH@/components/Push.js
 @RESPATH@/components/Push.manifest
 @RESPATH@/components/PushComponents.js
 
--- a/dom/apps/PermissionsTable.jsm
+++ b/dom/apps/PermissionsTable.jsm
@@ -131,16 +131,21 @@ this.PermissionsTable =  { geolocation: 
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "browser:embedded-system-app": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
+                           mobileconnection: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
                            mobilenetwork: {
                              app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
                            power: {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -35,32 +35,36 @@
 #ifdef MOZ_GAMEPAD
 #include "mozilla/dom/GamepadServiceTest.h"
 #endif
 #include "mozilla/dom/PowerManager.h"
 #include "mozilla/dom/WakeLock.h"
 #include "mozilla/dom/power/PowerManagerService.h"
 #include "mozilla/dom/FlyWebPublishedServer.h"
 #include "mozilla/dom/FlyWebService.h"
+#include "mozilla/dom/IccManager.h"
 #include "mozilla/dom/InputPortManager.h"
 #include "mozilla/dom/Permissions.h"
 #include "mozilla/dom/Presentation.h"
 #include "mozilla/dom/ServiceWorkerContainer.h"
 #include "mozilla/dom/StorageManager.h"
 #include "mozilla/dom/TCPSocket.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/dom/workers/RuntimeService.h"
 #include "mozilla/Hal.h"
 #include "nsISiteSpecificUserAgent.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/SSE.h"
 #include "mozilla/StaticPtr.h"
 #include "Connection.h"
 #include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
 #include "nsGlobalWindow.h"
+#ifdef MOZ_B2G_RIL
+#include "mozilla/dom/MobileConnectionArray.h"
+#endif
 #include "nsIIdleObserver.h"
 #include "nsIPermissionManager.h"
 #include "nsMimeTypes.h"
 #include "nsNetUtil.h"
 #include "nsStringStream.h"
 #include "nsComponentManagerUtils.h"
 #include "nsIStringStream.h"
 #include "nsIHttpChannel.h"
@@ -202,19 +206,23 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMimeTypes)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlugins)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPermissions)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGeolocation)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotification)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBatteryManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBatteryPromise)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPowerManager)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIccManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputPortManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConnection)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStorageManager)
+#ifdef MOZ_B2G_RIL
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMobileConnections)
+#endif
 #ifdef MOZ_AUDIO_CHANNEL_MANAGER
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelManager)
 #endif
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaDevices)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTimeManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorkerContainer)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
@@ -265,25 +273,36 @@ Navigator::Invalidate()
 
   mBatteryPromise = nullptr;
 
   if (mPowerManager) {
     mPowerManager->Shutdown();
     mPowerManager = nullptr;
   }
 
+  if (mIccManager) {
+    mIccManager->Shutdown();
+    mIccManager = nullptr;
+  }
+
   if (mInputPortManager) {
     mInputPortManager = nullptr;
   }
 
   if (mConnection) {
     mConnection->Shutdown();
     mConnection = nullptr;
   }
 
+#ifdef MOZ_B2G_RIL
+  if (mMobileConnections) {
+    mMobileConnections = nullptr;
+  }
+#endif
+
   mMediaDevices = nullptr;
 
 #ifdef MOZ_AUDIO_CHANNEL_MANAGER
   if (mAudioChannelManager) {
     mAudioChannelManager = nullptr;
   }
 #endif
 
@@ -1602,16 +1621,50 @@ Navigator::GetInputPortManager(ErrorResu
 
 already_AddRefed<LegacyMozTCPSocket>
 Navigator::MozTCPSocket()
 {
   RefPtr<LegacyMozTCPSocket> socket = new LegacyMozTCPSocket(GetWindow());
   return socket.forget();
 }
 
+#ifdef MOZ_B2G_RIL
+
+MobileConnectionArray*
+Navigator::GetMozMobileConnections(ErrorResult& aRv)
+{
+  if (!mMobileConnections) {
+    if (!mWindow) {
+      aRv.Throw(NS_ERROR_UNEXPECTED);
+      return nullptr;
+    }
+    mMobileConnections = new MobileConnectionArray(mWindow);
+  }
+
+  return mMobileConnections;
+}
+
+#endif // MOZ_B2G_RIL
+
+IccManager*
+Navigator::GetMozIccManager(ErrorResult& aRv)
+{
+  if (!mIccManager) {
+    if (!mWindow) {
+      aRv.Throw(NS_ERROR_UNEXPECTED);
+      return nullptr;
+    }
+    NS_ENSURE_TRUE(mWindow->GetDocShell(), nullptr);
+
+    mIccManager = new IccManager(mWindow);
+  }
+
+  return mIccManager;
+}
+
 #ifdef MOZ_GAMEPAD
 void
 Navigator::GetGamepads(nsTArray<RefPtr<Gamepad> >& aGamepads,
                        ErrorResult& aRv)
 {
   if (!mWindow) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return;
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -67,17 +67,22 @@ class GamepadServiceTest;
 class NavigatorUserMediaSuccessCallback;
 class NavigatorUserMediaErrorCallback;
 class MozGetUserMediaDevicesSuccessCallback;
 
 namespace network {
 class Connection;
 } // namespace network
 
+#ifdef MOZ_B2G_RIL
+class MobileConnectionArray;
+#endif
+
 class PowerManager;
+class IccManager;
 class InputPortManager;
 class DeviceStorageAreaListener;
 class Presentation;
 class LegacyMozTCPSocket;
 class VRDisplay;
 class StorageManager;
 
 namespace time {
@@ -204,21 +209,25 @@ public:
                          nsTArray<RefPtr<nsDOMDeviceStorage> >& aStores,
                          ErrorResult& aRv);
 
   already_AddRefed<nsDOMDeviceStorage>
   GetDeviceStorageByNameAndType(const nsAString& aName, const nsAString& aType,
                                 ErrorResult& aRv);
 
   DesktopNotificationCenter* GetMozNotification(ErrorResult& aRv);
+  IccManager* GetMozIccManager(ErrorResult& aRv);
   InputPortManager* GetInputPortManager(ErrorResult& aRv);
   already_AddRefed<LegacyMozTCPSocket> MozTCPSocket();
   network::Connection* GetConnection(ErrorResult& aRv);
   MediaDevices* GetMediaDevices(ErrorResult& aRv);
 
+#ifdef MOZ_B2G_RIL
+  MobileConnectionArray* GetMozMobileConnections(ErrorResult& aRv);
+#endif // MOZ_B2G_RIL
 #ifdef MOZ_GAMEPAD
   void GetGamepads(nsTArray<RefPtr<Gamepad> >& aGamepads, ErrorResult& aRv);
   GamepadServiceTest* RequestGamepadServiceTest();
 #endif // MOZ_GAMEPAD
   already_AddRefed<Promise> GetVRDisplays(ErrorResult& aRv);
   void GetActiveVRDisplays(nsTArray<RefPtr<VRDisplay>>& aDisplays) const;
 #ifdef MOZ_TIME_MANAGER
   time::TimeManager* GetMozTime(ErrorResult& aRv);
@@ -297,18 +306,22 @@ private:
   RefPtr<nsMimeTypeArray> mMimeTypes;
   RefPtr<nsPluginArray> mPlugins;
   RefPtr<Permissions> mPermissions;
   RefPtr<Geolocation> mGeolocation;
   RefPtr<DesktopNotificationCenter> mNotification;
   RefPtr<battery::BatteryManager> mBatteryManager;
   RefPtr<Promise> mBatteryPromise;
   RefPtr<PowerManager> mPowerManager;
+  RefPtr<IccManager> mIccManager;
   RefPtr<InputPortManager> mInputPortManager;
   RefPtr<network::Connection> mConnection;
+#ifdef MOZ_B2G_RIL
+  RefPtr<MobileConnectionArray> mMobileConnections;
+#endif
 #ifdef MOZ_AUDIO_CHANNEL_MANAGER
   RefPtr<system::AudioChannelManager> mAudioChannelManager;
 #endif
   RefPtr<MediaDevices> mMediaDevices;
   nsTArray<nsWeakPtr> mDeviceStorageStores;
   RefPtr<time::TimeManager> mTimeManager;
   RefPtr<ServiceWorkerContainer> mServiceWorkerContainer;
   nsCOMPtr<nsPIDOMWindowInner> mWindow;
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -583,16 +583,58 @@ DOMInterfaces = {
     'nativeType': 'mozilla::dom::HTMLCanvasPrintState',
 },
 
 'MozChannel': {
     'nativeType': 'nsIChannel',
     'notflattened': True
 },
 
+'MozCdmaIccInfo': {
+    'headerFile': 'mozilla/dom/IccInfo.h',
+    'nativeType': 'mozilla::dom::CdmaIccInfo',
+},
+
+'MozGsmIccInfo': {
+    'headerFile': 'mozilla/dom/IccInfo.h',
+    'nativeType': 'mozilla::dom::GsmIccInfo',
+},
+
+'MozIcc': {
+    'nativeType': 'mozilla::dom::Icc',
+},
+
+'MozIccInfo': {
+    'nativeType': 'mozilla::dom::IccInfo',
+},
+
+'MozIccManager': {
+    'nativeType': 'mozilla::dom::IccManager',
+},
+
+'MozMobileCellInfo': {
+    'nativeType': 'mozilla::dom::MobileCellInfo',
+},
+
+'MozMobileConnection': {
+    'nativeType': 'mozilla::dom::MobileConnection',
+},
+
+'MozMobileConnectionArray': {
+    'nativeType': 'mozilla::dom::MobileConnectionArray',
+},
+
+'MozMobileConnectionInfo': {
+    'nativeType': 'mozilla::dom::MobileConnectionInfo',
+},
+
+'MozMobileNetworkInfo': {
+    'nativeType': 'mozilla::dom::MobileNetworkInfo',
+},
+
 'MozSpeakerManager': {
     'nativeType': 'mozilla::dom::SpeakerManager',
     'headerFile': 'SpeakerManager.h'
 },
 
 'MozPowerManager': {
     'nativeType': 'mozilla::dom::PowerManager',
 },
new file mode 100644
--- /dev/null
+++ b/dom/contacts/ContactManager.js
@@ -0,0 +1,541 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const DEBUG = false;
+function debug(s) { dump("-*- ContactManager: " + s + "\n"); }
+
+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, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
+
+const CONTACTS_SENDMORE_MINIMUM = 5;
+
+// 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",
+  "phoneticGivenName", "phoneticFamilyName",
+  "honorificSuffix", "nickname", "photo", "category", "org", "jobTitle",
+  "bday", "note", "anniversary", "sex", "genderIdentity", "key", "adr", "email",
+  "url", "impp", "tel"
+];
+
+var mozContactInitWarned = false;
+
+function Contact() { }
+
+Contact.prototype = {
+  __init: function(aProp) {
+    for (let prop in aProp) {
+      this[prop] = aProp[prop];
+    }
+  },
+
+  init: function(aProp) {
+    // init is deprecated, warn once in the console if it's used
+    if (!mozContactInitWarned) {
+      mozContactInitWarned = true;
+      Cu.reportError("mozContact.init is DEPRECATED. Use the mozContact constructor instead. " +
+                     "See https://developer.mozilla.org/docs/WebAPI/Contacts for details.");
+    }
+
+    for (let prop of PROPERTIES) {
+      this[prop] = aProp[prop];
+    }
+  },
+
+  setMetadata: function(aId, aPublished, aUpdated) {
+    this.id = aId;
+    if (aPublished) {
+      this.published = aPublished;
+    }
+    if (aUpdated) {
+      this.updated = aUpdated;
+    }
+  },
+
+  classID: Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}"),
+  contractID: "@mozilla.org/contact;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
+};
+
+function ContactManager() { }
+
+ContactManager.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+  hasListenPermission: false,
+  _cachedContacts: [] ,
+
+  set oncontactchange(aHandler) {
+    this.__DOM_IMPL__.setEventHandler("oncontactchange", aHandler);
+  },
+
+  get oncontactchange() {
+    return this.__DOM_IMPL__.getEventHandler("oncontactchange");
+  },
+
+  _convertContact: function(aContact) {
+    let properties = aContact.properties;
+    if (properties.photo && properties.photo.length) {
+      properties.photo = Cu.cloneInto(properties.photo, this._window);
+    }
+    let newContact = new this._window.mozContact(aContact.properties);
+    newContact.setMetadata(aContact.id, aContact.published, aContact.updated);
+    return newContact;
+  },
+
+  _convertContacts: function(aContacts) {
+    let contacts = new this._window.Array();
+    for (let i in aContacts) {
+      contacts.push(this._convertContact(aContacts[i]));
+    }
+    return contacts;
+  },
+
+  _fireSuccessOrDone: function(aCursor, aResult) {
+    if (aResult == null) {
+      Services.DOMRequest.fireDone(aCursor);
+    } else {
+      Services.DOMRequest.fireSuccess(aCursor, aResult);
+    }
+  },
+
+  _pushArray: function(aArr1, aArr2) {
+    aArr1.push.apply(aArr1, aArr2);
+  },
+
+  receiveMessage: function(aMessage) {
+    if (DEBUG) debug("receiveMessage: " + aMessage.name);
+    let msg = aMessage.json;
+    let contacts = msg.contacts;
+
+    let req;
+    switch (aMessage.name) {
+      case "Contacts:Find:Return:OK":
+        req = this.getRequest(msg.requestID);
+        if (req) {
+          let result = this._convertContacts(contacts);
+          Services.DOMRequest.fireSuccess(req.request, result);
+        } else {
+          if (DEBUG) debug("no request stored!" + msg.requestID);
+        }
+        break;
+      case "Contacts:GetAll:Next":
+        let data = this.getRequest(msg.cursorId);
+        if (!data) {
+          break;
+        }
+        let result = contacts ? this._convertContacts(contacts) : [null];
+        if (data.waitingForNext) {
+          if (DEBUG) debug("cursor waiting for contact, sending");
+          data.waitingForNext = false;
+          let contact = result.shift();
+          this._pushArray(data.cachedContacts, result);
+          this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact));
+          if (!contact) {
+            this.removeRequest(msg.cursorId);
+          }
+        } else {
+          if (DEBUG) debug("cursor not waiting, saving");
+          this._pushArray(data.cachedContacts, result);
+        }
+        break;
+      case "Contact:Save:Return:OK":
+        // If a cached contact was saved and a new contact ID was returned, update the contact's ID
+        if (this._cachedContacts[msg.requestID]) {
+          if (msg.contactID) {
+            this._cachedContacts[msg.requestID].id = msg.contactID;
+          }
+          delete this._cachedContacts[msg.requestID];
+        }
+      case "Contacts:Clear:Return:OK":
+      case "Contact:Remove:Return:OK":
+        req = this.getRequest(msg.requestID);
+        if (req)
+          Services.DOMRequest.fireSuccess(req.request, null);
+        break;
+      case "Contacts:Find:Return:KO":
+      case "Contact:Save:Return:KO":
+      case "Contact:Remove:Return:KO":
+      case "Contacts:Clear:Return:KO":
+      case "Contacts:GetRevision:Return:KO":
+      case "Contacts:Count:Return:KO":
+        req = this.getRequest(msg.requestID);
+        if (req) {
+          if (req.request) {
+            req = req.request;
+          }
+          Services.DOMRequest.fireError(req, msg.errorMsg);
+        }
+        break;
+      case "Contacts:GetAll:Return:KO":
+        req = this.getRequest(msg.requestID);
+        if (req) {
+          Services.DOMRequest.fireError(req.cursor, msg.errorMsg);
+        }
+        break;
+      case "Contact:Changed":
+        // Fire oncontactchange event
+        if (DEBUG) debug("Contacts:ContactChanged: " + msg.contactID + ", " + msg.reason);
+        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;
+      case "Contacts:Count":
+        if (DEBUG) debug("count: " + msg.count);
+        req = this.getRequest(msg.requestID);
+        if (req) {
+          Services.DOMRequest.fireSuccess(req.request, msg.count);
+        }
+        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":
+      case "remove":
+        access = "write";
+        break;
+      case "find":
+      case "listen":
+      case "revision":
+      case "count":
+        access = "read";
+        break;
+      default:
+        access = "unknown";
+      }
+
+    // Shortcut for ALLOW_ACTION so we avoid a parent roundtrip
+    let principal = this._window.document.nodePrincipal;
+    let type = "contacts-" + access;
+    let permValue =
+      Services.perms.testExactPermissionFromPrincipal(principal, type);
+    DEBUG && debug("Existing permission " + permValue);
+    if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
+      if (aAllowCallback) {
+        aAllowCallback();
+      }
+      return;
+    } else if (permValue == Ci.nsIPermissionManager.DENY_ACTION ||
+               permValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
+      if (aCancelCallback) {
+        aCancelCallback("PERMISSION_DENIED");
+      }
+      return;
+    }
+
+    // Create an array with a single nsIContentPermissionType element.
+    type = {
+      type: "contacts",
+      access: access,
+      options: [],
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionType])
+    };
+    let typeArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+    typeArray.appendElement(type, false);
+
+    // create a nsIContentPermissionRequest
+    let request = {
+      types: typeArray,
+      principal: principal,
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionRequest]),
+      allow: function() {
+        aAllowCallback && aAllowCallback();
+        DEBUG && debug("Permission granted. Access " + access +"\n");
+      },
+      cancel: function() {
+        aCancelCallback && aCancelCallback("PERMISSION_DENIED");
+        DEBUG && debug("Permission denied. Access " + access +"\n");
+      },
+      window: this._window
+    };
+
+    // Using askPermission from nsIDOMWindowUtils that takes care of the
+    // remoting if needed.
+    let windowUtils = this._window.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIDOMWindowUtils);
+    windowUtils.askPermission(request);
+  },
+
+  save: function save(aContact) {
+    // 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: {}};
+
+    try {
+      for (let field of PROPERTIES) {
+        // This hack makes sure modifications to the sequence attributes get validated.
+        aContact[field] = aContact[field];
+        newContact.properties[field] = aContact[field];
+      }
+    } catch (e) {
+      // And then make sure we throw a proper error message (no internal file and line #)
+      throw new this._window.Error(e.message);
+    }
+
+    let request = this.createRequest();
+    let requestID = this.getRequestId({request: request});
+
+    let reason;
+    if (aContact.id == "undefined") {
+      // for example {25c00f01-90e5-c545-b4d4-21E2ddbab9e0} becomes
+      // 25c00f0190e5c545b4d421E2ddbab9e0
+      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";
+    }
+
+    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);
+
+    let cancelCallback = function(reason) {
+      Services.DOMRequest.fireErrorAsync(request, reason);
+    };
+
+    this.askPermission(reason, request, allowCallback, cancelCallback);
+    return request;
+  },
+
+  find: function(aOptions) {
+    if (DEBUG) debug("find! " + JSON.stringify(aOptions));
+    let request = this.createRequest();
+    let options = { findOptions: aOptions };
+
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:Find", {
+        requestID: this.getRequestId({request: request, reason: "find"}),
+        options: options
+      });
+    }.bind(this);
+
+    let cancelCallback = function(reason) {
+      Services.DOMRequest.fireErrorAsync(request, reason);
+    };
+
+    this.askPermission("find", request, allowCallback, cancelCallback);
+    return request;
+  },
+
+  createCursor: function CM_createCursor(aRequest) {
+    let data = {
+      cursor: Services.DOMRequest.createCursor(this._window, function() {
+        this.handleContinue(id);
+      }.bind(this)),
+      cachedContacts: [],
+      waitingForNext: true,
+    };
+    let id = this.getRequestId(data);
+    if (DEBUG) debug("saved cursor id: " + id);
+    return [id, data.cursor];
+  },
+
+  getAll: function CM_getAll(aOptions) {
+    if (DEBUG) debug("getAll: " + JSON.stringify(aOptions));
+    let [cursorId, cursor] = this.createCursor();
+
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:GetAll", {
+        cursorId: cursorId,
+        findOptions: aOptions
+      });
+    }.bind(this);
+
+    let cancelCallback = function(reason) {
+      Services.DOMRequest.fireErrorAsync(cursor, reason);
+    };
+
+    this.askPermission("find", cursor, allowCallback, cancelCallback);
+    return cursor;
+  },
+
+  nextTick: function nextTick(aCallback) {
+    Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  handleContinue: function CM_handleContinue(aCursorId) {
+    if (DEBUG) debug("handleContinue: " + aCursorId);
+    let data = this.getRequest(aCursorId);
+    if (data.cachedContacts.length > 0) {
+      if (DEBUG) debug("contact in cache");
+      let contact = data.cachedContacts.shift();
+      this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact));
+      if (!contact) {
+        this.removeRequest(aCursorId);
+      } else if (data.cachedContacts.length === CONTACTS_SENDMORE_MINIMUM) {
+        cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId });
+      }
+    } else {
+      if (DEBUG) debug("waiting for contact");
+      data.waitingForNext = true;
+    }
+  },
+
+  remove: function removeContact(aRecordOrId) {
+    let request = this.createRequest();
+    let id;
+    if (typeof aRecordOrId === "string") {
+      id = aRecordOrId;
+    } else if (!aRecordOrId || !aRecordOrId.id) {
+      Services.DOMRequest.fireErrorAsync(request, true);
+      return request;
+    } else {
+      id = aRecordOrId.id;
+    }
+
+    let options = { id: id };
+
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contact:Remove", {
+        requestID: this.getRequestId({request: request, reason: "remove"}),
+        options: options
+      });
+    }.bind(this);
+
+    let cancelCallback = function(reason) {
+      Services.DOMRequest.fireErrorAsync(request, reason);
+    };
+
+    this.askPermission("remove", request, allowCallback, cancelCallback);
+    return request;
+  },
+
+  clear: function() {
+    if (DEBUG) debug("clear");
+    let request = this.createRequest();
+    let options = {};
+
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:Clear", {
+        requestID: this.getRequestId({request: request, reason: "remove"}),
+        options: options
+      });
+    }.bind(this);
+
+    let cancelCallback = function(reason) {
+      Services.DOMRequest.fireErrorAsync(request, reason);
+    };
+
+    this.askPermission("remove", request, allowCallback, cancelCallback);
+    return request;
+  },
+
+  getRevision: function() {
+    let request = this.createRequest();
+
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:GetRevision", {
+        requestID: this.getRequestId({ request: request })
+      });
+    }.bind(this);
+
+    let cancelCallback = function(reason) {
+      Services.DOMRequest.fireErrorAsync(request, reason);
+    };
+
+    this.askPermission("revision", request, allowCallback, cancelCallback);
+    return request;
+  },
+
+  getCount: function() {
+    let request = this.createRequest();
+
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:GetCount", {
+        requestID: this.getRequestId({ request: request })
+      });
+    }.bind(this);
+
+    let cancelCallback = function(reason) {
+      Services.DOMRequest.fireErrorAsync(request, reason);
+    };
+
+    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",
+                              "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO",
+                              "Contacts:Count",
+                              "Contacts:Revision", "Contacts:GetRevision:Return:KO",]);
+
+
+    let allowCallback = function() {
+      cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
+      this.hasListenPermission = true;
+    }.bind(this);
+
+    this.askPermission("listen", null, allowCallback);
+  },
+
+  classID: Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}"),
+  contractID: "@mozilla.org/contactManager;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+  Contact, ContactManager
+]);
new file mode 100644
--- /dev/null
+++ b/dom/contacts/ContactManager.manifest
@@ -0,0 +1,5 @@
+component {72a5ee28-81d8-4af8-90b3-ae935396cc66} ContactManager.js
+contract @mozilla.org/contact;1 {72a5ee28-81d8-4af8-90b3-ae935396cc66}
+
+component {8beb3a66-d70a-4111-b216-b8e995ad3aff} ContactManager.js
+contract @mozilla.org/contactManager;1 {8beb3a66-d70a-4111-b216-b8e995ad3aff}
new file mode 100644
--- /dev/null
+++ b/dom/contacts/fallback/ContactDB.jsm
@@ -0,0 +1,1401 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Everything but "ContactDB" is only exported here for testing.
+this.EXPORTED_SYMBOLS = ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME",
+                         "REVISION_STORE", "DB_VERSION"];
+
+const DEBUG = false;
+function debug(s) { dump("-*- ContactDB component: " + s + "\n"); }
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberUtils",
+                                  "resource://gre/modules/PhoneNumberUtils.jsm");
+Cu.importGlobalProperties(["indexedDB"]);
+
+/* all exported symbols need to be bound to this on B2G - Bug 961777 */
+this.DB_NAME = "contacts";
+this.DB_VERSION = 20;
+this.STORE_NAME = "contacts";
+this.SAVED_GETALL_STORE_NAME = "getallcache";
+const CHUNK_SIZE = 20;
+this.REVISION_STORE = "revision";
+const REVISION_KEY = "revision";
+
+function exportContact(aRecord) {
+  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) {
+    sendChunk = function() {
+      try {
+        let chunk = aContacts.splice(0, CHUNK_SIZE);
+        if (chunk.length > 0) {
+          aCallback(chunk);
+        }
+        if (aContacts.length === 0) {
+          aCallback(null);
+          aClearDispatcher();
+        }
+      } catch (e) {
+        aClearDispatcher();
+      }
+    }
+  } else {
+    sendChunk = function() {
+      try {
+        let start = nextIndex;
+        nextIndex += CHUNK_SIZE;
+        let chunk = [];
+        aNewTxn("readonly", STORE_NAME, function(txn, store) {
+          for (let i = start; i < Math.min(start+CHUNK_SIZE, aContacts.length); ++i) {
+            store.get(aContacts[i]).onsuccess = function(e) {
+              chunk.push(exportContact(e.target.result));
+              count++;
+              if (count === aContacts.length) {
+                aCallback(chunk);
+                aCallback(null);
+                aClearDispatcher();
+              } else if (chunk.length === CHUNK_SIZE) {
+                aCallback(chunk);
+                chunk.length = 0;
+              }
+            }
+          }
+        }, null, function(errorMsg) {
+          aFailureCb(errorMsg);
+        });
+      } catch (e) {
+        aClearDispatcher();
+      }
+    }
+  }
+
+  return {
+    sendNow: function() {
+      sendChunk();
+    }
+  };
+}
+
+this.ContactDB = function ContactDB() {
+  if (DEBUG) debug("Constructor");
+};
+
+ContactDB.prototype = {
+  __proto__: IndexedDBHelper.prototype,
+
+  _dispatcher: {},
+
+  useFastUpgrade: true,
+
+  upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
+    let loadInitialContacts = function() {
+      // Add default contacts
+      let jsm = {};
+      Cu.import("resource://gre/modules/FileUtils.jsm", jsm);
+      Cu.import("resource://gre/modules/NetUtil.jsm", jsm);
+      // Loading resource://app/defaults/contacts.json doesn't work because
+      // contacts.json is not in the omnijar.
+      // So we look for the app dir instead and go from here...
+      let contactsFile = jsm.FileUtils.getFile("DefRt", ["contacts.json"], false);
+      if (!contactsFile || (contactsFile && !contactsFile.exists())) {
+        // For b2g desktop
+        contactsFile = jsm.FileUtils.getFile("ProfD", ["contacts.json"], false);
+        if (!contactsFile || (contactsFile && !contactsFile.exists())) {
+          return;
+        }
+      }
+
+      let chan = jsm.NetUtil.newChannel({
+        uri: NetUtil.newURI(contactsFile),
+        loadUsingSystemPrincipal: true});
+
+      let stream = chan.open2();
+      // Obtain a converter to read from a UTF-8 encoded input stream.
+      let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                      .createInstance(Ci.nsIScriptableUnicodeConverter);
+      converter.charset = "UTF-8";
+      let rawstr = converter.ConvertToUnicode(jsm.NetUtil.readInputStreamToString(
+                                              stream,
+                                              stream.available()) || "");
+      stream.close();
+      let contacts;
+      try {
+        contacts = JSON.parse(rawstr);
+      } catch(e) {
+        if (DEBUG) debug("Error parsing " + contactsFile.path + " : " + e);
+        return;
+      }
+
+      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, "");
+        contact = this.makeImport(contact);
+        this.updateRecordMetadata(contact);
+        if (DEBUG) debug("import: " + JSON.stringify(contact));
+        objectStore.put(contact);
+      }
+    }.bind(this);
+
+    function createFinalSchema() {
+      if (DEBUG) debug("creating final schema");
+      let objectStore = aDb.createObjectStore(STORE_NAME, {keyPath: "id"});
+      objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
+      objectStore.createIndex("givenName",  "properties.givenName",  { multiEntry: true });
+      objectStore.createIndex("name",      "properties.name",        { multiEntry: true });
+      objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
+      objectStore.createIndex("givenNameLowerCase",  "search.givenName",  { multiEntry: true });
+      objectStore.createIndex("nameLowerCase",       "search.name",       { multiEntry: true });
+      objectStore.createIndex("telLowerCase",        "search.tel",        { multiEntry: true });
+      objectStore.createIndex("emailLowerCase",      "search.email",      { multiEntry: true });
+      objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
+      objectStore.createIndex("category", "properties.category", { multiEntry: true });
+      objectStore.createIndex("email", "search.email", { multiEntry: true });
+      objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true});
+      objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true });
+      objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true });
+      objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true });
+      objectStore.createIndex("phoneticGivenNameLowerCase",  "search.phoneticGivenName",  { multiEntry: true });
+      aDb.createObjectStore(SAVED_GETALL_STORE_NAME);
+      aDb.createObjectStore(REVISION_STORE).put(0, REVISION_KEY);
+    }
+
+    let valueUpgradeSteps = [];
+
+    function scheduleValueUpgrade(upgradeFunc) {
+      var length = valueUpgradeSteps.push(upgradeFunc);
+      if (DEBUG) debug("Scheduled a value upgrade function, index " + (length - 1));
+    }
+
+    // We always output this debug line because it's useful and the noise ratio
+    // very low.
+    debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
+    let db = aDb;
+    let objectStore;
+
+    if (aOldVersion === 0 && this.useFastUpgrade) {
+      createFinalSchema();
+      loadInitialContacts();
+      return;
+    }
+
+    let steps = [
+      function upgrade0to1() {
+        /**
+         * Create the initial database schema.
+         *
+         * The schema of records stored is as follows:
+         *
+         * {id:            "...",       // UUID
+         *  published:     Date(...),   // First published date.
+         *  updated:       Date(...),   // Last updated date.
+         *  properties:    {...}        // Object holding the ContactProperties
+         * }
+         */
+        if (DEBUG) debug("create schema");
+        objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"});
+
+        // Properties indexes
+        objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
+        objectStore.createIndex("givenName",  "properties.givenName",  { multiEntry: true });
+
+        objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
+        objectStore.createIndex("givenNameLowerCase",  "search.givenName",  { multiEntry: true });
+        objectStore.createIndex("telLowerCase",        "search.tel",        { multiEntry: true });
+        objectStore.createIndex("emailLowerCase",      "search.email",      { multiEntry: true });
+        next();
+      },
+      function upgrade1to2() {
+        if (DEBUG) debug("upgrade 1");
+
+        // Create a new scheme for the tel field. We move from an array of tel-numbers to an array of
+        // ContactTelephone.
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+        // Delete old tel index.
+        if (objectStore.indexNames.contains("tel")) {
+          objectStore.deleteIndex("tel");
+        }
+
+        // Upgrade existing tel field in the DB.
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (DEBUG) debug("upgrade tel1: " + JSON.stringify(cursor.value));
+            for (let number in cursor.value.properties.tel) {
+              cursor.value.properties.tel[number] = {number: number};
+            }
+            cursor.update(cursor.value);
+            if (DEBUG) debug("upgrade tel2: " + JSON.stringify(cursor.value));
+            cursor.continue();
+          } else {
+            next();
+          }
+        };
+
+        // Create new searchable indexes.
+        objectStore.createIndex("tel", "search.tel", { multiEntry: true });
+        objectStore.createIndex("category", "properties.category", { multiEntry: true });
+      },
+      function upgrade2to3() {
+        if (DEBUG) debug("upgrade 2");
+        // Create a new scheme for the email field. We move from an array of emailaddresses to an array of
+        // ContactEmail.
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+
+        // Delete old email index.
+        if (objectStore.indexNames.contains("email")) {
+          objectStore.deleteIndex("email");
+        }
+
+        // Upgrade existing email field in the DB.
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (cursor.value.properties.email) {
+              if (DEBUG) debug("upgrade email1: " + JSON.stringify(cursor.value));
+              cursor.value.properties.email =
+                cursor.value.properties.email.map(function(address) { return { address: address }; });
+              cursor.update(cursor.value);
+              if (DEBUG) debug("upgrade email2: " + JSON.stringify(cursor.value));
+            }
+            cursor.continue();
+          } else {
+            next();
+          }
+        };
+
+        // Create new searchable indexes.
+        objectStore.createIndex("email", "search.email", { multiEntry: true });
+      },
+      function upgrade3to4() {
+        if (DEBUG) debug("upgrade 3");
+
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+
+        // Upgrade existing impp field in the DB.
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (cursor.value.properties.impp) {
+              if (DEBUG) debug("upgrade impp1: " + JSON.stringify(cursor.value));
+              cursor.value.properties.impp =
+                cursor.value.properties.impp.map(function(value) { return { value: value }; });
+              cursor.update(cursor.value);
+              if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
+            }
+            cursor.continue();
+          }
+        };
+        // Upgrade existing url field in the DB.
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (cursor.value.properties.url) {
+              if (DEBUG) debug("upgrade url1: " + JSON.stringify(cursor.value));
+              cursor.value.properties.url =
+                cursor.value.properties.url.map(function(value) { return { value: value }; });
+              cursor.update(cursor.value);
+              if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
+            }
+            cursor.continue();
+          } else {
+            next();
+          }
+        };
+      },
+      function upgrade4to5() {
+        if (DEBUG) debug("Add international phone numbers upgrade");
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (cursor.value.properties.tel) {
+              if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
+              cursor.value.properties.tel.forEach(
+                function(duple) {
+                  let parsedNumber = PhoneNumberUtils.parse(duple.value.toString());
+                  if (parsedNumber) {
+                    if (DEBUG) {
+                      debug("InternationalFormat: " + parsedNumber.internationalFormat);
+                      debug("InternationalNumber: " + parsedNumber.internationalNumber);
+                      debug("NationalNumber: " + parsedNumber.nationalNumber);
+                      debug("NationalFormat: " + parsedNumber.nationalFormat);
+                    }
+                    if (duple.value.toString() !== parsedNumber.internationalNumber) {
+                      cursor.value.search.tel.push(parsedNumber.internationalNumber);
+                    }
+                  } else {
+                    dump("Warning: No international number found for " + duple.value + "\n");
+                  }
+                }
+              )
+              cursor.update(cursor.value);
+            }
+            if (DEBUG) debug("upgrade2 : " + JSON.stringify(cursor.value));
+            cursor.continue();
+          } else {
+            next();
+          }
+        };
+      },
+      function upgrade5to6() {
+        if (DEBUG) debug("Add index for equals tel searches");
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+
+        // Delete old tel index (not on the right field).
+        if (objectStore.indexNames.contains("tel")) {
+          objectStore.deleteIndex("tel");
+        }
+
+        // Create new index for "equals" searches
+        objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
+
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (cursor.value.properties.tel) {
+              if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
+              cursor.value.properties.tel.forEach(
+                function(duple) {
+                  let number = duple.value.toString();
+                  let parsedNumber = PhoneNumberUtils.parse(number);
+
+                  cursor.value.search.exactTel = [number];
+                  if (parsedNumber &&
+                      parsedNumber.internationalNumber &&
+                      number !== parsedNumber.internationalNumber) {
+                    cursor.value.search.exactTel.push(parsedNumber.internationalNumber);
+                  }
+                }
+              )
+              cursor.update(cursor.value);
+            }
+            if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
+            cursor.continue();
+          } else {
+            next();
+          }
+        };
+      },
+      function upgrade6to7() {
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+        let names = objectStore.indexNames;
+        let whiteList = ["tel", "familyName", "givenName",  "familyNameLowerCase",
+                         "givenNameLowerCase", "telLowerCase", "category", "email",
+                         "emailLowerCase"];
+        for (var i = 0; i < names.length; i++) {
+          if (whiteList.indexOf(names[i]) < 0) {
+            objectStore.deleteIndex(names[i]);
+          }
+        }
+        next();
+      },
+      function upgrade7to8() {
+        if (DEBUG) debug("Adding object store for cached searches");
+        db.createObjectStore(SAVED_GETALL_STORE_NAME);
+        next();
+      },
+      function upgrade8to9() {
+        if (DEBUG) debug("Make exactTel only contain the value entered by the user");
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (cursor.value.properties.tel) {
+              cursor.value.search.exactTel = [];
+              cursor.value.properties.tel.forEach(
+                function(tel) {
+                  let normalized = PhoneNumberUtils.normalize(tel.value.toString());
+                  cursor.value.search.exactTel.push(normalized);
+                }
+              );
+              cursor.update(cursor.value);
+            }
+            cursor.continue();
+          } else {
+            next();
+          }
+        };
+      },
+      function upgrade9to10() {
+        // no-op, see https://bugzilla.mozilla.org/show_bug.cgi?id=883770#c16
+        next();
+      },
+      function upgrade10to11() {
+        if (DEBUG) debug("Adding object store for database revision");
+        db.createObjectStore(REVISION_STORE).put(0, REVISION_KEY);
+        next();
+      },
+      function upgrade11to12() {
+        if (DEBUG) debug("Add a telMatch index with national and international numbers");
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+        if (!objectStore.indexNames.contains("telMatch")) {
+          objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true});
+        }
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            if (cursor.value.properties.tel) {
+              cursor.value.search.parsedTel = [];
+              cursor.value.properties.tel.forEach(
+                function(tel) {
+                  let parsed = PhoneNumberUtils.parse(tel.value.toString());
+                  if (parsed) {
+                    cursor.value.search.parsedTel.push(parsed.nationalNumber);
+                    cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.nationalFormat));
+                    cursor.value.search.parsedTel.push(parsed.internationalNumber);
+                    cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.internationalFormat));
+                  }
+                  cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(tel.value.toString()));
+                }
+              );
+              cursor.update(cursor.value);
+            }
+            cursor.continue();
+          } else {
+            next();
+          }
+        };
+      },
+      function upgrade12to13() {
+        if (DEBUG) debug("Add phone substring to the search index if appropriate for country");
+        if (this.substringMatching) {
+          scheduleValueUpgrade(function upgradeValue12to13(value) {
+            if (value.properties.tel) {
+              value.search.parsedTel = value.search.parsedTel || [];
+              value.properties.tel.forEach(
+                function(tel) {
+                  let normalized = PhoneNumberUtils.normalize(tel.value.toString());
+                  if (normalized) {
+                    if (this.substringMatching && normalized.length > this.substringMatching) {
+                      let sub = normalized.slice(-this.substringMatching);
+                      if (value.search.parsedTel.indexOf(sub) === -1) {
+                        if (DEBUG) debug("Adding substring index: " + tel + ", " + sub);
+                        value.search.parsedTel.push(sub);
+                      }
+                    }
+                  }
+                }.bind(this)
+              );
+              return true;
+            } else {
+              return false;
+            }
+          }.bind(this));
+        }
+        next();
+      },
+      function upgrade13to14() {
+        if (DEBUG) debug("Cleaning up empty substring entries in telMatch index");
+        scheduleValueUpgrade(function upgradeValue13to14(value) {
+          function removeEmptyStrings(value) {
+            if (value) {
+              const oldLength = value.length;
+              for (let i = 0; i < value.length; ++i) {
+                if (!value[i] || value[i] == "null") {
+                  value.splice(i, 1);
+                }
+              }
+              return oldLength !== value.length;
+            }
+          }
+
+          let modified = removeEmptyStrings(value.search.parsedTel);
+          let modified2 = removeEmptyStrings(value.search.tel);
+          return (modified || modified2);
+        });
+
+        next();
+      },
+      function upgrade14to15() {
+        if (DEBUG) debug("Fix array properties saved as scalars");
+        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"];
+
+        scheduleValueUpgrade(function upgradeValue14to15(value) {
+          let changed = false;
+
+          let props = value.properties;
+          for (let prop of ARRAY_PROPERTIES) {
+            if (props[prop]) {
+              if (!Array.isArray(props[prop])) {
+                value.properties[prop] = [props[prop]];
+                changed = true;
+              }
+              if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
+                let subprop = value.properties[prop];
+                for (let i = 0; i < subprop.length; ++i) {
+                  if (!Array.isArray(subprop[i].type)) {
+                    value.properties[prop][i].type = [subprop[i].type];
+                    changed = true;
+                  }
+                }
+              }
+            }
+          }
+
+          return changed;
+        });
+
+        next();
+      },
+      function upgrade15to16() {
+        if (DEBUG) debug("Fix Date properties");
+        const DATE_PROPERTIES = ["bday", "anniversary"];
+
+        scheduleValueUpgrade(function upgradeValue15to16(value) {
+          let changed = false;
+          let props = value.properties;
+          for (let prop of DATE_PROPERTIES) {
+            if (props[prop] && !(props[prop] instanceof Date)) {
+              value.properties[prop] = new Date(props[prop]);
+              changed = true;
+            }
+          }
+
+          return changed;
+        });
+
+        next();
+      },
+      function upgrade16to17() {
+        if (DEBUG) debug("Fix array with null values");
+        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"];
+
+        const DATE_PROPERTIES = ["bday", "anniversary"];
+
+        scheduleValueUpgrade(function upgradeValue16to17(value) {
+          let changed;
+
+          function filterInvalidValues(val) {
+            let shouldKeep = val != null; // null or undefined
+            if (!shouldKeep) {
+              changed = true;
+            }
+            return shouldKeep;
+          }
+
+          function filteredArray(array) {
+            return array.filter(filterInvalidValues);
+          }
+
+          let props = value.properties;
+
+          for (let prop of ARRAY_PROPERTIES) {
+
+            // properties that were empty strings weren't converted to arrays
+            // in upgrade14to15
+            if (props[prop] != null && !Array.isArray(props[prop])) {
+              props[prop] = [props[prop]];
+              changed = true;
+            }
+
+            if (props[prop] && props[prop].length) {
+              props[prop] = filteredArray(props[prop]);
+
+              if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
+                let subprop = props[prop];
+
+                for (let i = 0; i < subprop.length; ++i) {
+                  let curSubprop = subprop[i];
+                  // upgrade14to15 transformed type props into an array
+                  // without checking invalid values
+                  if (curSubprop.type) {
+                    curSubprop.type = filteredArray(curSubprop.type);
+                  }
+                }
+              }
+            }
+          }
+
+          for (let prop of DATE_PROPERTIES) {
+            if (props[prop] != null && !(props[prop] instanceof Date)) {
+              // props[prop] is probably '' and wasn't converted
+              // in upgrade15to16
+              props[prop] = null;
+              changed = true;
+            }
+          }
+
+          if (changed) {
+            value.properties = props;
+            return true;
+          } else {
+            return false;
+          }
+        });
+
+        next();
+      },
+      function upgrade17to18() {
+        // this upgrade function has been moved to the next upgrade path because
+        // a previous version of it had a bug
+        next();
+      },
+      function upgrade18to19() {
+        if (DEBUG) {
+          debug("Adding the name index");
+        }
+
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+
+        // an earlier version of this code could have run, so checking whether
+        // the index exists
+        if (!objectStore.indexNames.contains("name")) {
+          objectStore.createIndex("name", "properties.name", { multiEntry: true });
+          objectStore.createIndex("nameLowerCase", "search.name", { multiEntry: true });
+        }
+
+        scheduleValueUpgrade(function upgradeValue18to19(value) {
+          value.search.name = [];
+          if (value.properties.name) {
+            value.properties.name.forEach(function addNameIndex(name) {
+              var lowerName = name.toLowerCase();
+              // an earlier version of this code could have added it already
+              if (value.search.name.indexOf(lowerName) === -1) {
+                value.search.name.push(lowerName);
+              }
+            });
+          }
+          return true;
+        });
+
+        next();
+      },
+      function upgrade19to20() {
+        if (DEBUG) debug("upgrade19to20 create schema(phonetic)");
+        if (!objectStore) {
+          objectStore = aTransaction.objectStore(STORE_NAME);
+        }
+        objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true });
+        objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true });
+        objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true });
+        objectStore.createIndex("phoneticGivenNameLowerCase",  "search.phoneticGivenName",  { multiEntry: true });
+        next();
+      },
+    ];
+
+    let index = aOldVersion;
+    let outer = this;
+
+    /* This function runs all upgrade functions that are in the
+     * valueUpgradeSteps array. These functions have the following properties:
+     * - they must be synchronous
+     * - they must take the value as parameter and modify it directly. They
+     *   must not create a new object.
+     * - they must return a boolean true/false; true if the value was actually
+     *   changed
+     */
+    function runValueUpgradeSteps(done) {
+      if (DEBUG) debug("Running the value upgrade functions.");
+      if (!objectStore) {
+        objectStore = aTransaction.objectStore(STORE_NAME);
+      }
+      objectStore.openCursor().onsuccess = function(event) {
+        let cursor = event.target.result;
+        if (cursor) {
+          let changed = false;
+          let oldValue;
+          let value = cursor.value;
+          if (DEBUG) {
+            oldValue = JSON.stringify(value);
+          }
+          valueUpgradeSteps.forEach(function(upgradeFunc, i) {
+            if (DEBUG) debug("Running upgrade function " + i);
+            changed = upgradeFunc(value) || changed;
+          });
+
+          if (changed) {
+            cursor.update(value);
+          } else if (DEBUG) {
+            let newValue = JSON.stringify(value);
+            if (newValue !== oldValue) {
+              // oops something went wrong
+              debug("upgrade: `changed` was false and still the value changed! Aborting.");
+              aTransaction.abort();
+              return;
+            }
+          }
+          cursor.continue();
+        } else {
+          done();
+        }
+      };
+    }
+
+    function finish() {
+      // We always output this debug line because it's useful and the noise ratio
+      // very low.
+      debug("Upgrade finished");
+
+      outer.incrementRevision(aTransaction);
+    }
+
+    function next() {
+      if (index == aNewVersion) {
+        runValueUpgradeSteps(finish);
+        return;
+      }
+
+      try {
+        var i = index++;
+        if (DEBUG) debug("Upgrade step: " + i + "\n");
+        steps[i].call(outer);
+      } catch(ex) {
+        dump("Caught exception" + ex);
+        aTransaction.abort();
+        return;
+      }
+    }
+
+    function fail(why) {
+      why = why || "";
+      if (this.error) {
+        why += " (root cause: " + this.error.name + ")";
+      }
+
+      debug("Contacts DB upgrade error: " + why);
+      aTransaction.abort();
+    }
+
+    if (aNewVersion > steps.length) {
+      fail("No migration steps for the new version!");
+    }
+
+    this.cpuLock = Cc["@mozilla.org/power/powermanagerservice;1"]
+                     .getService(Ci.nsIPowerManagerService)
+                     .newWakeLock("cpu");
+
+    function unlockCPU() {
+      if (outer.cpuLock) {
+        if (DEBUG) debug("unlocking cpu wakelock");
+        outer.cpuLock.unlock();
+        outer.cpuLock = null;
+      }
+    }
+
+    aTransaction.addEventListener("complete", unlockCPU);
+    aTransaction.addEventListener("abort", unlockCPU);
+
+    next();
+  },
+
+  makeImport: function makeImport(aContact) {
+    let contact = {properties: {}};
+
+    contact.search = {
+      name:            [],
+      givenName:       [],
+      familyName:      [],
+      email:           [],
+      category:        [],
+      tel:             [],
+      exactTel:        [],
+      parsedTel:       [],
+      phoneticFamilyName:   [],
+      phoneticGivenName:    [],
+    };
+
+    for (let field in aContact.properties) {
+      contact.properties[field] = aContact.properties[field];
+      // Add search fields
+      if (aContact.properties[field] && contact.search[field]) {
+        for (let i = 0; i <= aContact.properties[field].length; i++) {
+          if (aContact.properties[field][i]) {
+            if (field == "tel" && aContact.properties[field][i].value) {
+              let number = aContact.properties.tel[i].value.toString();
+              let normalized = PhoneNumberUtils.normalize(number);
+              // We use an object here to avoid duplicates
+              let containsSearch = {};
+              let matchSearch = {};
+
+              if (normalized) {
+                // exactTel holds normalized version of entered phone number.
+                // normalized: +1 (949) 123 - 4567 -> +19491234567
+                contact.search.exactTel.push(normalized);
+                // matchSearch holds normalized version of entered phone number,
+                // nationalNumber, nationalFormat, internationalNumber, internationalFormat
+                matchSearch[normalized] = 1;
+                let parsedNumber = PhoneNumberUtils.parse(number);
+                if (parsedNumber) {
+                  if (DEBUG) {
+                    debug("InternationalFormat: " + parsedNumber.internationalFormat);
+                    debug("InternationalNumber: " + parsedNumber.internationalNumber);
+                    debug("NationalNumber: " + parsedNumber.nationalNumber);
+                    debug("NationalFormat: " + parsedNumber.nationalFormat);
+                    debug("NationalMatchingFormat: " + parsedNumber.nationalMatchingFormat);
+                  }
+                  matchSearch[parsedNumber.nationalNumber] = 1;
+                  matchSearch[parsedNumber.internationalNumber] = 1;
+                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalFormat)] = 1;
+                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.internationalFormat)] = 1;
+                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalMatchingFormat)] = 1;
+                } else if (this.substringMatching && normalized.length > this.substringMatching) {
+                  matchSearch[normalized.slice(-this.substringMatching)] = 1;
+                }
+
+                // containsSearch holds incremental search values for:
+                // normalized number and national format
+                for (let i = 0; i < normalized.length; i++) {
+                  containsSearch[normalized.substring(i, normalized.length)] = 1;
+                }
+                if (parsedNumber && parsedNumber.nationalFormat) {
+                  let number = PhoneNumberUtils.normalize(parsedNumber.nationalFormat);
+                  for (let i = 0; i < number.length; i++) {
+                    containsSearch[number.substring(i, number.length)] = 1;
+                  }
+                }
+              }
+              for (let num in containsSearch) {
+                if (num && num != "null") {
+                  contact.search.tel.push(num);
+                }
+              }
+              for (let num in matchSearch) {
+                if (num && num != "null") {
+                  contact.search.parsedTel.push(num);
+                }
+              }
+            } else if ((field == "impp" || field == "email") && aContact.properties[field][i].value) {
+              let value = aContact.properties[field][i].value;
+              if (value && typeof value == "string") {
+                contact.search[field].push(value.toLowerCase());
+              }
+            } else {
+              let val = aContact.properties[field][i];
+              if (typeof val == "string") {
+                contact.search[field].push(val.toLowerCase());
+              }
+            }
+          }
+        }
+      }
+    }
+
+    contact.updated = aContact.updated;
+    contact.published = aContact.published;
+    contact.id = aContact.id;
+
+    return contact;
+  },
+
+  updateRecordMetadata: function updateRecordMetadata(record) {
+    if (!record.id) {
+      Cu.reportError("Contact without ID");
+    }
+    if (!record.published) {
+      record.published = new Date();
+    }
+    record.updated = new Date();
+  },
+
+  removeObjectFromCache: function CDB_removeObjectFromCache(aObjectId, aCallback, aFailureCb) {
+    if (DEBUG) debug("removeObjectFromCache: " + aObjectId);
+    if (!aObjectId) {
+      if (DEBUG) debug("No object ID passed");
+      return;
+    }
+    this.newTxn("readwrite", this.dbStoreNames, function(txn, stores) {
+      let store = txn.objectStore(SAVED_GETALL_STORE_NAME);
+      store.openCursor().onsuccess = function(e) {
+        let cursor = e.target.result;
+        if (cursor) {
+          for (let i = 0; i < cursor.value.length; ++i) {
+            if (cursor.value[i] == aObjectId) {
+              if (DEBUG) debug("id matches cache");
+              cursor.value.splice(i, 1);
+              cursor.update(cursor.value);
+              break;
+            }
+          }
+          cursor.continue();
+        } else {
+          aCallback(txn);
+        }
+      }.bind(this);
+    }.bind(this), null, aFailureCb);
+  },
+
+  incrementRevision: function CDB_incrementRevision(txn) {
+    let revStore = txn.objectStore(REVISION_STORE);
+    revStore.get(REVISION_KEY).onsuccess = function(e) {
+      revStore.put(parseInt(e.target.result, 10) + 1, REVISION_KEY);
+    };
+  },
+
+  saveContact: function CDB_saveContact(aContact, successCb, errorCb) {
+    let contact = this.makeImport(aContact);
+    this.newTxn("readwrite", this.dbStoreNames, function (txn, stores) {
+      if (DEBUG) debug("Going to update" + JSON.stringify(contact));
+      let store = txn.objectStore(STORE_NAME);
+
+      // Look up the existing record and compare the update timestamp.
+      // If no record exists, just add the new entry.
+      let newRequest = store.get(contact.id);
+      newRequest.onsuccess = function (event) {
+        if (!event.target.result) {
+          if (DEBUG) debug("new record!")
+          this.updateRecordMetadata(contact);
+          store.put(contact);
+        } else {
+          if (DEBUG) debug("old record!")
+          if (new Date(typeof contact.updated === "undefined" ? 0 : contact.updated) < new Date(event.target.result.updated)) {
+            if (DEBUG) debug("rev check fail!");
+            txn.abort();
+            return;
+          } else {
+            if (DEBUG) debug("rev check OK");
+            contact.published = event.target.result.published;
+            contact.updated = new Date();
+            store.put(contact);
+          }
+        }
+        // Invalidate the entire cache. It will be incrementally regenerated on demand
+        // See getCacheForQuery
+        let getAllStore = txn.objectStore(SAVED_GETALL_STORE_NAME);
+        getAllStore.clear().onerror = errorCb;
+      }.bind(this);
+
+      this.incrementRevision(txn);
+    }.bind(this), successCb, errorCb);
+  },
+
+  removeContact: function removeContact(aId, aSuccessCb, aErrorCb) {
+    if (DEBUG) debug("removeContact: " + aId);
+    this.removeObjectFromCache(aId, function(txn) {
+      let store = txn.objectStore(STORE_NAME)
+      store.delete(aId).onsuccess = function() {
+        aSuccessCb();
+      };
+      this.incrementRevision(txn);
+    }.bind(this), aErrorCb);
+  },
+
+  clear: function clear(aSuccessCb, aErrorCb) {
+    this.newTxn("readwrite", STORE_NAME, function (txn, store) {
+      if (DEBUG) debug("Going to clear all!");
+      store.clear();
+      this.incrementRevision(txn);
+    }.bind(this), aSuccessCb, aErrorCb);
+  },
+
+  createCacheForQuery: function CDB_createCacheForQuery(aQuery, aSuccessCb, aFailureCb) {
+    this.find(function (aContacts) {
+      if (aContacts) {
+        let contactsArray = [];
+        for (let i in aContacts) {
+          contactsArray.push(aContacts[i]);
+        }
+
+        let contactIdsArray = contactsArray.map(el => el.id);
+
+        // save contact ids in cache
+        this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) {
+          store.put(contactIdsArray, aQuery);
+        }, null, aFailureCb);
+
+        // send full contacts
+        aSuccessCb(contactsArray, true);
+      } else {
+        aSuccessCb([], true);
+      }
+    }.bind(this),
+    function (aErrorMsg) { aFailureCb(aErrorMsg); },
+    JSON.parse(aQuery));
+  },
+
+  getCacheForQuery: function CDB_getCacheForQuery(aQuery, aSuccessCb, aFailureCb) {
+    if (DEBUG) debug("getCacheForQuery");
+    // Here we try to get the cached results for query `aQuery'. If they don't
+    // exist, it means the cache was invalidated and needs to be recreated, so
+    // we do that. Otherwise, we just return the existing cache.
+    this.newTxn("readonly", SAVED_GETALL_STORE_NAME, function(txn, store) {
+      let req = store.get(aQuery);
+      req.onsuccess = function(e) {
+        if (e.target.result) {
+          if (DEBUG) debug("cache exists");
+          aSuccessCb(e.target.result, false);
+        } else {
+          if (DEBUG) debug("creating cache for query " + aQuery);
+          this.createCacheForQuery(aQuery, aSuccessCb);
+        }
+      }.bind(this);
+      req.onerror = function(e) {
+        aFailureCb(e.target.errorMessage);
+      };
+    }.bind(this), null, aFailureCb);
+  },
+
+  sendNow: function CDB_sendNow(aCursorId) {
+    if (aCursorId in this._dispatcher) {
+      this._dispatcher[aCursorId].sendNow();
+    }
+  },
+
+  clearDispatcher: function CDB_clearDispatcher(aCursorId) {
+    if (DEBUG) debug("clearDispatcher: " + aCursorId);
+    if (aCursorId in this._dispatcher) {
+      delete this._dispatcher[aCursorId];
+    }
+  },
+
+  getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) {
+    if (DEBUG) debug("getAll")
+    let optionStr = JSON.stringify(aOptions);
+    this.getCacheForQuery(optionStr, function(aCachedResults, aFullContacts) {
+      // aFullContacts is true if the cache didn't exist and had to be created.
+      // In that case, we receive the full contacts since we already have them
+      // in memory to create the cache. This allows us to avoid accessing the
+      // object store again.
+      if (aCachedResults && aCachedResults.length > 0) {
+        let newTxnFn = this.newTxn.bind(this);
+        let clearDispatcherFn = this.clearDispatcher.bind(this, aCursorId);
+        this._dispatcher[aCursorId] = new ContactDispatcher(aCachedResults, aFullContacts,
+                                                            aSuccessCb, newTxnFn,
+                                                            clearDispatcherFn, aFailureCb);
+        this._dispatcher[aCursorId].sendNow();
+      } else { // no contacts
+        if (DEBUG) debug("query returned no contacts");
+        aSuccessCb(null);
+      }
+    }.bind(this), aFailureCb);
+  },
+
+  getRevision: function CDB_getRevision(aSuccessCb, aErrorCb) {
+    if (DEBUG) debug("getRevision");
+    this.newTxn("readonly", REVISION_STORE, function (txn, store) {
+      store.get(REVISION_KEY).onsuccess = function (e) {
+        aSuccessCb(e.target.result);
+      };
+    },null, aErrorCb);
+  },
+
+  getCount: function CDB_getCount(aSuccessCb, aErrorCb) {
+    if (DEBUG) debug("getCount");
+    this.newTxn("readonly", STORE_NAME, function (txn, store) {
+      store.count().onsuccess = function (e) {
+        aSuccessCb(e.target.result);
+      };
+    }, null, aErrorCb);
+  },
+
+  getSortByParam: function CDB_getSortByParam(aFindOptions) {
+    switch (aFindOptions.sortBy) {
+      case "familyName":
+        return [ "familyName", "givenName" ];
+      case "givenName":
+        return [ "givenName" , "familyName" ];
+      case "phoneticFamilyName":
+        return [ "phoneticFamilyName" , "phoneticGivenName" ];
+      case "phoneticGivenName":
+        return [ "phoneticGivenName" , "phoneticFamilyName" ];
+      default:
+        return [ "givenName" , "familyName" ];
+    }
+  },
+
+  /*
+   * Sorting the contacts by sortBy field. aSortBy can either be familyName or givenName.
+   * If 2 entries have the same sortyBy field or no sortBy field is present, we continue
+   * sorting with the other sortyBy field.
+   */
+  sortResults: function CDB_sortResults(aResults, aFindOptions) {
+    if (!aFindOptions)
+      return;
+    if (aFindOptions.sortBy != "undefined") {
+      const sortOrder = aFindOptions.sortOrder;
+      const sortBy = this.getSortByParam(aFindOptions);
+
+      aResults.sort(function (a, b) {
+        let x, y;
+        let result = 0;
+        let xIndex = 0;
+        let yIndex = 0;
+
+        do {
+          while (xIndex < sortBy.length && !x) {
+            x = a.properties[sortBy[xIndex]];
+            if (x) {
+              x = x.join("").toLowerCase();
+            }
+            xIndex++;
+          }
+          while (yIndex < sortBy.length && !y) {
+            y = b.properties[sortBy[yIndex]];
+            if (y) {
+              y = y.join("").toLowerCase();
+            }
+            yIndex++;
+          }
+          if (!x) {
+            if (!y) {
+              let px, py;
+              px = JSON.stringify(a.published);
+              py = JSON.stringify(b.published);
+              if (px && py) {
+                return px.localeCompare(py);
+              }
+            } else {
+              return sortOrder == 'descending' ? 1 : -1;
+            }
+          }
+          if (!y) {
+            return sortOrder == "ascending" ? 1 : -1;
+          }
+
+          result = x.localeCompare(y);
+          x = null;
+          y = null;
+        } while (result == 0);
+
+        return sortOrder == "ascending" ? result : -result;
+      });
+    }
+    if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) {
+      if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit);
+      aResults.splice(aFindOptions.filterLimit, aResults.length);
+    }
+  },
+
+  /**
+   * @param successCb
+   *        Callback function to invoke with result array.
+   * @param failureCb [optional]
+   *        Callback function to invoke when there was an error.
+   * @param options [optional]
+   *        Object specifying search options. Possible attributes:
+   *        - filterBy
+   *        - filterOp
+   *        - filterValue
+   *        - count
+   */
+  find: function find(aSuccessCb, aFailureCb, aOptions) {
+    if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp);
+    let self = this;
+    this.newTxn("readonly", STORE_NAME, function (txn, store) {
+      let filterOps = ["equals", "contains", "match", "startsWith"];
+      if (aOptions && (filterOps.indexOf(aOptions.filterOp) >= 0)) {
+        self._findWithIndex(txn, store, aOptions);
+      } else {
+        self._findAll(txn, store, aOptions);
+      }
+    }, aSuccessCb, aFailureCb);
+  },
+
+  _findWithIndex: function _findWithIndex(txn, store, options) {
+    if (DEBUG) debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " ");
+    let fields = options.filterBy;
+    for (let key in fields) {
+      if (DEBUG) debug("key: " + fields[key]);
+      if (!store.indexNames.contains(fields[key]) && fields[key] != "id") {
+        if (DEBUG) debug("Key not valid!" + fields[key] + ", " + JSON.stringify(store.indexNames));
+        txn.abort();
+        return;
+      }
+    }
+
+    // lookup for all keys
+    if (options.filterBy.length == 0) {
+      if (DEBUG) debug("search in all fields!" + JSON.stringify(store.indexNames));
+      for(let myIndex = 0; myIndex < store.indexNames.length; myIndex++) {
+        fields = Array.concat(fields, store.indexNames[myIndex])
+      }
+    }
+
+    // Sorting functions takes care of limit if set.
+    let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
+
+    let filter_keys = fields.slice();
+    for (let key = filter_keys.shift(); key; key = filter_keys.shift()) {
+      let request;
+      let substringResult = {};
+      if (key == "id") {
+        // store.get would return an object and not an array
+        request = store.mozGetAll(options.filterValue);
+      } else if (key == "category") {
+        let index = store.index(key);
+        request = index.mozGetAll(options.filterValue, limit);
+      } else if (options.filterOp == "equals") {
+        if (DEBUG) debug("Getting index: " + key);
+        // case sensitive
+        let index = store.index(key);
+        let filterValue = options.filterValue;
+        if (key == "tel") {
+          filterValue = PhoneNumberUtils.normalize(filterValue,
+                                                   /*numbersOnly*/ true);
+        }
+        request = index.mozGetAll(filterValue, limit);
+      } else if (options.filterOp == "match") {
+        if (DEBUG) debug("match");
+        if (key != "tel") {
+          dump("ContactDB: 'match' filterOp only works on tel\n");
+          return txn.abort();
+        }
+
+        let index = store.index("telMatch");
+        let normalized = PhoneNumberUtils.normalize(options.filterValue,
+                                                    /*numbersOnly*/ true);
+
+        if (!normalized.length) {
+          dump("ContactDB: normalized filterValue is empty, can't perform match search.\n");
+          return txn.abort();
+        }
+
+        // Some countries need special handling for number matching. Bug 877302
+        if (this.substringMatching && normalized.length > this.substringMatching) {
+          let substring = normalized.slice(-this.substringMatching);
+          if (DEBUG) debug("Substring: " + substring);
+
+          let substringRequest = index.mozGetAll(substring, limit);
+
+          substringRequest.onsuccess = function (event) {
+            if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
+            for (let i in event.target.result) {
+              substringResult[event.target.result[i].id] = event.target.result[i];
+            }
+          }.bind(this);
+        } else if (normalized[0] !== "+") {
+          // We might have an international prefix like '00'
+          let parsed = PhoneNumberUtils.parse(normalized);
+          if (parsed && parsed.internationalNumber &&
+              parsed.nationalNumber  &&
+              parsed.nationalNumber !== normalized &&
+              parsed.internationalNumber !== normalized) {
+            if (DEBUG) debug("Search with " + parsed.internationalNumber);
+            let prefixRequest = index.mozGetAll(parsed.internationalNumber, limit);
+
+            prefixRequest.onsuccess = function (event) {
+              if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
+              for (let i in event.target.result) {
+                substringResult[event.target.result[i].id] = event.target.result[i];
+              }
+            }.bind(this);
+          }
+        }
+
+        request = index.mozGetAll(normalized, limit);
+      } else {
+        // XXX: "contains" should be handled separately, this is "startsWith"
+        if (options.filterOp === 'contains' && key !== 'tel') {
+          dump("ContactDB: 'contains' only works for 'tel'. Falling back " +
+               "to 'startsWith'.\n");
+        }
+        // not case sensitive
+        let lowerCase = options.filterValue.toString().toLowerCase();
+        if (key === "tel") {
+          let origLength = lowerCase.length;
+          let tmp = PhoneNumberUtils.normalize(lowerCase, /*numbersOnly*/ true);
+          if (tmp.length != origLength) {
+            let NON_SEARCHABLE_CHARS = /[^#+\*\d\s()-]/;
+            // e.g. number "123". find with "(123)" but not with "123a"
+            if (tmp === "" || NON_SEARCHABLE_CHARS.test(lowerCase)) {
+              if (DEBUG) debug("Call continue!");
+              continue;
+            }
+            lowerCase = tmp;
+          }
+        }
+        if (DEBUG) debug("lowerCase: " + lowerCase);
+        let range = IDBKeyRange.bound(lowerCase, lowerCase + "\uFFFF");
+        let index = store.index(key + "LowerCase");
+        request = index.mozGetAll(range, limit);
+      }
+      if (!txn.result)
+        txn.result = {};
+
+      request.onsuccess = function (event) {
+        if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
+        if (Object.keys(substringResult).length > 0) {
+          for (let attrname in substringResult) {
+            event.target.result[attrname] = substringResult[attrname];
+          }
+        }
+        this.sortResults(event.target.result, options);
+        for (let i in event.target.result)
+          txn.result[event.target.result[i].id] = exportContact(event.target.result[i]);
+      }.bind(this);
+    }
+  },
+
+  _findAll: function _findAll(txn, store, options) {
+    if (DEBUG) debug("ContactDB:_findAll:  " + JSON.stringify(options));
+    if (!txn.result)
+      txn.result = {};
+    // Sorting functions takes care of limit if set.
+    let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
+    store.mozGetAll(null, limit).onsuccess = function (event) {
+      if (DEBUG) debug("Request successful. Record count:" + event.target.result.length);
+      this.sortResults(event.target.result, options);
+      for (let i in event.target.result) {
+        txn.result[event.target.result[i].id] = exportContact(event.target.result[i]);
+      }
+    }.bind(this);
+  },
+
+  // Enable special phone number substring matching. Does not update existing DB entries.
+  enableSubstringMatching: function enableSubstringMatching(aDigits) {
+    if (DEBUG) debug("MCC enabling substring matching " + aDigits);
+    this.substringMatching = aDigits;
+  },
+
+  disableSubstringMatching: function disableSubstringMatching() {
+    if (DEBUG) debug("MCC disabling substring matching");
+    delete this.substringMatching;
+  },
+
+  init: function init() {
+    this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE]);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/dom/contacts/fallback/ContactService.jsm
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const DEBUG = false;
+function debug(s) { dump("-*- Fallback ContactService component: " + s + "\n"); }
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+this.EXPORTED_SYMBOLS = ["ContactService"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContactDB",
+                                  "resource://gre/modules/ContactDB.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberUtils",
+                                  "resource://gre/modules/PhoneNumberUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageListenerManager");
+
+
+/* all exported symbols need to be bound to this on B2G - Bug 961777 */
+var ContactService = this.ContactService = {
+  init: function() {
+    if (DEBUG) debug("Init");
+    this._messages = ["Contacts:Find", "Contacts:GetAll", "Contacts:GetAll:SendNow",
+                      "Contacts:Clear", "Contact:Save",
+                      "Contact:Remove", "Contacts:RegisterForMessages",
+                      "child-process-shutdown", "Contacts:GetRevision",
+                      "Contacts:GetCount"];
+    this._children = [];
+    this._cursors = new Map();
+    this._messages.forEach(function(msgName) {
+      ppmm.addMessageListener(msgName, this);
+    }.bind(this));
+
+    this._db = new ContactDB();
+    this._db.init();
+
+    this.configureSubstringMatching();
+
+    Services.obs.addObserver(this, "profile-before-change", false);
+    Services.prefs.addObserver("ril.lastKnownSimMcc", this, false);
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic === 'profile-before-change') {
+      this._messages.forEach(function(msgName) {
+        ppmm.removeMessageListener(msgName, this);
+      }.bind(this));
+      Services.obs.removeObserver(this, "profile-before-change");
+      Services.prefs.removeObserver("dom.phonenumber.substringmatching", this);
+      ppmm = null;
+      this._messages = null;
+      if (this._db)
+        this._db.close();
+      this._db = null;
+      this._children = null;
+      this._cursors = null;
+    } else if (aTopic === 'nsPref:changed' && aData === "ril.lastKnownSimMcc") {
+      this.configureSubstringMatching();
+    }
+  },
+
+  configureSubstringMatching: function() {
+    let countryName = PhoneNumberUtils.getCountryName();
+    if (Services.prefs.getPrefType("dom.phonenumber.substringmatching." + countryName) == Ci.nsIPrefBranch.PREF_INT) {
+      let val = Services.prefs.getIntPref("dom.phonenumber.substringmatching." + countryName);
+      if (val) {
+        this._db.enableSubstringMatching(val);
+        return;
+      }
+    }
+    // if we got here, we dont have a substring setting
+    // for this country, so disable substring matching
+    this._db.disableSubstringMatching();
+  },
+
+  assertPermission: function(aMessage, aPerm) {
+    if (!aMessage.target.assertPermission(aPerm)) {
+      Cu.reportError("Contacts message " + aMessage.name +
+                     " from a content process with no" + aPerm + " privileges.");
+      return false;
+    }
+    return true;
+  },
+
+  broadcastMessage: function broadcastMessage(aMsgName, aContent) {
+    this._children.forEach(function(msgMgr) {
+      msgMgr.sendAsyncMessage(aMsgName, aContent);
+    });
+  },
+
+  receiveMessage: function(aMessage) {
+    if (DEBUG) debug("receiveMessage " + aMessage.name);
+    let mm = aMessage.target;
+    let msg = aMessage.data;
+    let cursorList;
+
+    switch (aMessage.name) {
+      case "Contacts:Find":
+        if (!this.assertPermission(aMessage, "contacts-read")) {
+          return null;
+        }
+        let result = [];
+        this._db.find(
+          function(contacts) {
+            for (let i in contacts) {
+              result.push(contacts[i]);
+            }
+
+            if (DEBUG) debug("result:" + JSON.stringify(result));
+            mm.sendAsyncMessage("Contacts:Find:Return:OK", {requestID: msg.requestID, contacts: result});
+          }.bind(this),
+          function(aErrorMsg) { mm.sendAsyncMessage("Contacts:Find:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }); }.bind(this),
+          msg.options.findOptions);
+        break;
+      case "Contacts:GetAll":
+        if (!this.assertPermission(aMessage, "contacts-read")) {
+          return null;
+        }
+        cursorList = this._cursors.get(mm);
+        if (!cursorList) {
+          cursorList = [];
+          this._cursors.set(mm, cursorList);
+        }
+        cursorList.push(msg.cursorId);
+
+        this._db.getAll(
+          function(aContacts) {
+            try {
+              mm.sendAsyncMessage("Contacts:GetAll:Next", {cursorId: msg.cursorId, contacts: aContacts});
+              if (aContacts === null) {
+                let cursorList = this._cursors.get(mm);
+                let index = cursorList.indexOf(msg.cursorId);
+                cursorList.splice(index, 1);
+              }
+            } catch (e) {
+              if (DEBUG) debug("Child is dead, DB should stop sending contacts");
+              throw e;
+            }
+          }.bind(this),
+          function(aErrorMsg) { mm.sendAsyncMessage("Contacts:GetAll:Return:KO", { requestID: msg.cursorId, errorMsg: aErrorMsg }); },
+          msg.findOptions, msg.cursorId);
+        break;
+      case "Contacts:GetAll:SendNow":
+        // sendNow is a no op if there isn't an existing cursor in the DB, so we
+        // don't need to assert the permission again.
+        this._db.sendNow(msg.cursorId);
+        break;
+      case "Contact:Save":
+        if (msg.options.reason === "create") {
+          if (!this.assertPermission(aMessage, "contacts-create")) {
+            return null;
+          }
+        } else {
+          if (!this.assertPermission(aMessage, "contacts-write")) {
+            return null;
+          }
+        }
+        this._db.saveContact(
+          msg.options.contact,
+          function() {
+            mm.sendAsyncMessage("Contact:Save:Return:OK", { requestID: msg.requestID, contactID: msg.options.contact.id });
+            this.broadcastMessage("Contact:Changed", { contactID: msg.options.contact.id, reason: msg.options.reason });
+          }.bind(this),
+          function(aErrorMsg) { mm.sendAsyncMessage("Contact:Save:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }); }.bind(this)
+        );
+        break;
+      case "Contact:Remove":
+        if (!this.assertPermission(aMessage, "contacts-write")) {
+          return null;
+        }
+        this._db.removeContact(
+          msg.options.id,
+          function() {
+            mm.sendAsyncMessage("Contact:Remove:Return:OK", { requestID: msg.requestID, contactID: msg.options.id });
+            this.broadcastMessage("Contact:Changed", { contactID: msg.options.id, reason: "remove" });
+          }.bind(this),
+          function(aErrorMsg) { mm.sendAsyncMessage("Contact:Remove:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }); }.bind(this)
+        );
+        break;
+      case "Contacts:Clear":
+        if (!this.assertPermission(aMessage, "contacts-write")) {
+          return null;
+        }
+        this._db.clear(
+          function() {
+            mm.sendAsyncMessage("Contacts:Clear:Return:OK", { requestID: msg.requestID });
+            this.broadcastMessage("Contact:Changed", { reason: "remove" });
+          }.bind(this),
+          function(aErrorMsg) {
+            mm.sendAsyncMessage("Contacts:Clear:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg });
+          }.bind(this)
+        );
+        break;
+      case "Contacts:GetRevision":
+        if (!this.assertPermission(aMessage, "contacts-read")) {
+          return null;
+        }
+        this._db.getRevision(
+          function(revision) {
+            mm.sendAsyncMessage("Contacts:Revision", {
+              requestID: msg.requestID,
+              revision: revision
+            });
+          },
+          function(aErrorMsg) {
+            mm.sendAsyncMessage("Contacts:GetRevision:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg });
+          }.bind(this)
+        );
+        break;
+      case "Contacts:GetCount":
+        if (!this.assertPermission(aMessage, "contacts-read")) {
+          return null;
+        }
+        this._db.getCount(
+          function(count) {
+            mm.sendAsyncMessage("Contacts:Count", {
+              requestID: msg.requestID,
+              count: count
+            });
+          },
+          function(aErrorMsg) {
+            mm.sendAsyncMessage("Contacts:Count:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg });
+          }.bind(this)
+        );
+        break;
+      case "Contacts:RegisterForMessages":
+        if (!aMessage.target.assertPermission("contacts-read")) {
+          return null;
+        }
+        if (DEBUG) debug("Register!");
+        if (this._children.indexOf(mm) == -1) {
+          this._children.push(mm);
+        }
+        break;
+      case "child-process-shutdown":
+        if (DEBUG) debug("Unregister");
+        let index = this._children.indexOf(mm);
+        if (index != -1) {
+          if (DEBUG) debug("Unregister index: " + index);
+          this._children.splice(index, 1);
+        }
+        cursorList = this._cursors.get(mm);
+        if (cursorList) {
+          for (let id of cursorList) {
+            this._db.clearDispatcher(id);
+          }
+          this._cursors.delete(mm);
+        }
+        break;
+      default:
+        if (DEBUG) debug("WRONG MESSAGE NAME: " + aMessage.name);
+    }
+  }
+}
+
+ContactService.init();
new file mode 100644
--- /dev/null
+++ b/dom/contacts/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; 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/.
+
+# Disable the tests on Android for now (bug 927869)
+if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
+    MOCHITEST_MANIFESTS += ['tests/mochitest.ini']
+    MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+EXTRA_COMPONENTS += [
+    'ContactManager.js',
+    'ContactManager.manifest',
+]
+
+EXTRA_JS_MODULES += [
+    'fallback/ContactDB.jsm',
+    'fallback/ContactService.jsm'
+]
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/chrome.ini
@@ -0,0 +1,44 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # Bug 1287455: takes too long to complete on Android
+support-files =
+  shared.js
+  file_contacts_basics.html
+  file_contacts_basics2.html
+  file_contacts_blobs.html
+  file_contacts_events.html
+  file_contacts_getall.html
+  file_contacts_getall2.html
+  file_contacts_international.html
+  file_contacts_substringmatching.html
+  file_contacts_substringmatchingVE.html
+  file_contacts_substringmatchingCL.html
+  test_migration_chrome.js
+  file_migration.html
+
+# renaming with "_a_" to execure before others, since we hardcode open of 
+# database and this messes up with mozContacts when done after mozContacts
+# did opened the database. those should really be xpcshell and not chrome
+# mochitests maybe ...
+[test_contacts_a_shutdown.xul]
+skip-if = buildapp == 'b2g'
+[test_contacts_a_upgrade.xul]
+skip-if = buildapp == 'b2g'
+[test_contacts_a_cache.xul]
+skip-if = buildapp == 'b2g'
+[test_contacts_basics.html]
+skip-if = (toolkit == 'gonk' && debug) #debug-only failure
+[test_contacts_basics2.html]
+skip-if = (toolkit == 'gonk' && debug) || (os == 'win' && os_version == '5.1') #debug-only failure, bug 967258 on XP
+[test_contacts_blobs.html]
+skip-if = (toolkit == 'gonk' && debug) #debug-only failure
+[test_contacts_events.html]
+[test_contacts_getall.html]
+skip-if = (toolkit == 'gonk' && debug) #debug-only failure
+[test_contacts_getall2.html]
+skip-if = (toolkit == 'gonk' && debug) #debug-only failure
+[test_contacts_international.html]
+[test_contacts_substringmatching.html]
+[test_contacts_substringmatchingVE.html]
+[test_contacts_substringmatchingCL.html]
+[test_migration.html]
+  support-files +=
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_basics.html
@@ -0,0 +1,787 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674720
+-->
+<head>
+  <title>Test for Bug 674720 WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674720">Mozilla Bug 674720</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var is = parent.is;
+
+var initialRev;
+
+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) {
+    revReq.onerror = function(e) {
+      then();
+    };
+  } else {
+    revReq.onerror = onFailure;
+  }
+}
+
+var req;
+
+var steps = [
+  function() {
+    req = mozContacts.getRevision();
+    req.onsuccess = function(e) {
+      initialRev = e.target.result;
+      next();
+    };
+
+    // Android does not support the revision function. Treat errors as success.
+    if (isAndroid) {
+      req.onerror = function(e) {
+        initialRev = 0;
+        next();
+      };
+    } else {
+      req.onerror = onFailure;
+    }
+  },
+  function () {
+    ok(true, "Deleting database");
+    checkRevision(0, "Initial revision is 0", function() {
+      req = mozContacts.clear();
+      req.onsuccess = function () {
+        ok(true, "Deleted the database");
+        checkCount(0, "No contacts after clear", function() {
+          checkRevision(1, "Revision was incremented on clear", next);
+        });
+      };
+      req.onerror = onFailure;
+    });
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    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({});
+    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(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(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(properties1);
+
+    mozContacts.oncontactchange = function(event) {
+      is(event.contactID, createResult1.id, "Same contactID");
+      is(event.reason, "create", "Same reason");
+      next();
+    }
+
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      sample_id1 = createResult1.id;
+      checkContacts(createResult1, properties1);
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 1");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[1].substring(0,3)};
+    req = mozContacts.find(options);
+    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, properties1);
+      // Some manual testing. Testint the testfunctions
+      // tel: [{type: ["work"], value: "123456", carrier: "testCarrier"} , {type: ["home", "fax"], value: "+55 (31) 9876-3456"}],
+      is(findResult1.tel[0].carrier, "testCarrier", "Same Carrier");
+      is(String(findResult1.tel[0].type), "work", "Same type");
+      is(findResult1.tel[0].value, "123456", "Same Value");
+      is(findResult1.tel[1].type[1], "fax", "Same type");
+      is(findResult1.tel[1].value, "+55 (31) 9876-3456", "Same Value");
+
+      is(findResult1.adr[0].countryName, "country 1", "Same country");
+
+      // email: [{type: ["work"], value: "x@y.com"}]
+      is(String(findResult1.email[0].type), "work", "Same Type");
+      is(findResult1.email[0].value, "x@y.com", "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for exact email");
+    var options = {filterBy: ["email"],
+                   filterOp: "equals",
+                   filterValue: properties1.email[0].value};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring and update");
+    mozContacts.oncontactchange = function(event) {
+       is(event.contactID, findResult1.id, "Same contactID");
+       is(event.reason, "update", "Same reason");
+     }
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0].substring(0,3)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      findResult1.jobTitle = ["new Job"];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(createResult1, properties1);
+      next();
+    };
+    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({name: ["newName"]});
+    req = navigator.mozContacts.save(createResult2);
+    req.onsuccess = function () {
+      ok(createResult2.id, "The contact now has an ID.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 2");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0].substring(0,3)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      checkContacts(createResult1, findResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Retrieving by name equality 1");
+    var options = {filterBy: ["name"],
+                   filterOp: "equals",
+                   filterValue: properties1.name[0]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      checkContacts(createResult1, findResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Retrieving by name equality 2");
+    var options = {filterBy: ["name"],
+                   filterOp: "equals",
+                   filterValue: properties1.name[1]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      checkContacts(createResult1, findResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Retrieving by name substring 1");
+    var options = {filterBy: ["name"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.name[0].substring(0,3).toLowerCase()};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      checkContacts(createResult1, findResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Retrieving by name substring 2");
+    var options = {filterBy: ["name"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.name[1].substring(0,3).toLowerCase()};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      checkContacts(createResult1, findResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Remove contact1");
+    mozContacts.oncontactchange = function(event) {
+      is(event.contactID, createResult1.id, "Same contactID");
+      is(event.reason, "remove", "Same reason");
+    }
+    req = navigator.mozContacts.remove(createResult1);
+    req.onsuccess = function () {
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 3");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[1].substring(0,3)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found no contact.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Remove contact2");
+    mozContacts.oncontactchange = function(event) {
+      is(event.contactID, createResult2.id, "Same contactID");
+      is(event.reason, "remove", "Same reason");
+    }
+    req = navigator.mozContacts.remove(createResult2);
+    req.onsuccess = function () {
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 4");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[1].substring(0,3)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found no contact.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    mozContacts.oncontactchange = function(event) {
+      is(event.contactID, "undefined", "Same contactID");
+      is(event.reason, "remove", "Same reason");
+    }
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact with 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();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring tel1");
+    var options = {filterBy: ["tel"],
+                   filterOp: "contains",
+                   filterValue: properties1.tel[1].value.substring(2,5)};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by tel exact");
+    var options = {filterBy: ["tel"],
+                   filterOp: "equals",
+                   filterValue: "+55 319 8 7 6 3456"};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by tel exact with substring");
+    var options = {filterBy: ["tel"],
+                   filterOp: "equals",
+                   filterValue: "3456"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found no contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by tel exact with substring");
+    var options = {filterBy: ["tel"],
+                   filterOp: "equals",
+                   filterValue: "+55 (31)"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found no contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by tel match national number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "3198763456"};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by tel match national format");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "0451 491934"};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by tel match entered number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "123456"};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by tel match international number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "+55 31 98763456"};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by match with field other than tel");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "match",
+                   filterValue: "my friends call me 555-4040"};
+    req = mozContacts.find(options);
+    req.onsuccess = onUnwantedSuccess;
+    req.onerror = function() {
+      ok(true, "Failed");
+      next();
+    }
+  },
+  function () {
+    ok(true, "Retrieving by substring tel2");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: "9876"};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring tel3");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: "98763456"};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 5");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0].substring(0,3)};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 6");
+    var options = {filterBy: ["familyName", "givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0].substring(0,3)};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring3, Testing multi entry");
+    var options = {filterBy: ["givenName", "familyName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.familyName[1].substring(0,3).toLowerCase()};
+    req = mozContacts.find(options);
+    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, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    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);
+      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(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();
+        };
+        req2.onerror = onFailure;
+      };
+      req.onerror = onFailure;
+    }
+  },
+  function() {
+    // Android does not support published/updated fields. Skip this.
+    if (isAndroid) {
+      next();
+      return;
+    }
+
+    ok(true, "Saving old contact, should abort!");
+    req = mozContacts.save(createResult1);
+    req.onsuccess = onUnwantedSuccess;
+    req.onerror   = function() { ok(true, "Successfully declined updating old contact!"); next(); };
+  },
+  function () {
+    ok(true, "Retrieving a specific contact by ID");
+    var options = {filterBy: ["id"],
+                   filterOp: "equals",
+                   filterValue: sample_id1};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving a specific contact by givenName");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "equals",
+                   filterValue: properties1.givenName[0]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, properties1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  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(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();
+        }
+        req2.onerror = onFailure;
+      };
+      req.onerror = onFailure;
+    }
+  },
+  function () {
+    ok(true, "Searching contacts by query");
+    var options = {filterBy: ["givenName", "email"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0].substring(0,4)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by query");
+    var options = {filterBy: ["givenName", "email"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts with multiple indices");
+    var options = {filterBy: ["email", "givenName"],
+                   filterOp: "equals",
+                   filterValue: properties1.givenName[1]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, properties1);
+      next();
+    };
+    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(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 () {
+          is(req2.result.length, 2, "Found exactly 2 contacts.");
+          ok(req2.result[0].id != req2.result[1].id, "Different ID");
+          next();
+        }
+        req2.onerror = onFailure;
+      };
+      req.onerror = onFailure;
+    }
+  },
+  function () {
+    ok(true, "Deleting contact" + findResult1);
+    req = mozContacts.remove(findResult1);
+    req.onsuccess = function () {
+      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;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.remove(findResult1);
+    req.onsuccess =  function () {
+      clearTemps();
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Test JSON.stringify output for mozContact objects");
+    var json = JSON.parse(JSON.stringify(new mozContact(properties1)));
+    checkContacts(json, properties1);
+    next();
+  },
+  function() {
+    ok(true, "Test slice");
+    var c = new mozContact();
+    c.email = [{ type: ["foo"], value: "bar@baz" }]
+    var arr = c.email;
+    is(arr[0].value, "bar@baz", "Should have the right value");
+    arr = arr.slice();
+    is(arr[0].value, "bar@baz", "Should have the right value after slicing");
+    next();
+  },
+  function () {
+    ok(true, "all done!\n");
+    clearTemps();
+
+    parent.SimpleTest.finish();
+  }
+];
+
+start_tests();
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_basics2.html
@@ -0,0 +1,1153 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674720
+-->
+<head>
+  <title>Test for Bug 674720 WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674720">Mozilla Bug 674720</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var is = parent.is;
+var isnot = parent.isnot;
+
+var req;
+
+var steps = [
+  function () {
+    ok(true, "Adding a new contact");
+    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(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.onsuccess = function () {
+      is(req.result.length, 2, "Found exactly 2 contact.");
+      checkContacts(req.result[1], properties1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    console.log("Searching contacts by query1");
+    var options = {filterBy: ["givenName", "email"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0].substring(0, 4)}
+    req = mozContacts.find(options)
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by query2");
+    var options = {filterBy: ["givenName", "email"],
+                   filterOp: "startsWith",
+                   filterValue: properties2.givenName[0].substring(0, 4)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.adr.length, 2, "Adr length 2");
+      checkContacts(findResult1, createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by tel");
+    var options = {filterBy: ["tel"],
+                   filterOp: "contains",
+                   filterValue: properties2.tel[0].value.substring(3, 7)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id2, "Same ID");
+      checkContacts(findResult1, createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by email");
+    var options = {filterBy: ["email"],
+                   filterOp: "startsWith",
+                   filterValue: properties2.email[0].value.substring(0, 4)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id2, "Same ID");
+      checkContacts(findResult1, createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding 20 contacts");
+    for (var i=0; i<19; i++) {
+      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);
+    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(defaultOptions);
+    req.onsuccess = function () {
+      is(req.result.length, 20, "20 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts with limit 10");
+    var options = { filterLimit: 10 };
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 10, "10 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts with limit 10 and sorted");
+    var options = { filterLimit: 10,
+                    sortBy: 'FamilyName',
+                    sortOrder: 'descending' };
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 10, "10 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts2");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties1.givenName[0].substring(0, 4)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 20, "20 Entries.");
+      checkContacts(createResult1, req.result[19]);
+      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)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      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 () {
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Testing clone contact");
+    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"];
+    req = mozContacts.save(cloned);
+    req.onsuccess = function () {
+      ok(cloned.id, "The contact now has an ID.");
+      is(cloned.email[0].value, "new email!", "Same Email");
+      isnot(createResult1.email[0].value, cloned.email[0].value, "Clone has different email");
+      is(String(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(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({name: ["XXX"],
+                                    givenName: ["XXX"],
+                                    email: [{value: "XXX"}],
+                                    tel: [{value: "XXX"}]
+                                   });
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function() {
+      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;
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    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(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(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(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;
+  },
+  function () {
+    ok(true, "Test sorting");
+    var options = {sortBy: "familyName",
+                   sortOrder: "ascending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 4, "4 results");
+      checkContacts(req.result[0], c1);
+      checkContacts(req.result[1], c2);
+      checkContacts(req.result[2], c3);
+      checkContacts(req.result[3], c4);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    var options = {sortBy: "familyName",
+                   sortOrder: "descending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 4, "4 results");
+      checkContacts(req.result[0], c4);
+      checkContacts(req.result[1], c3);
+      checkContacts(req.result[2], c2);
+      checkContacts(req.result[3], c1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    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;
+  },
+  function () {
+    ok(true, "Test sorting with empty string");
+    var options = {sortBy: "familyName",
+                   sortOrder: "ascending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 5, "5 results");
+      checkContacts(req.result[0], c5);
+      checkContacts(req.result[1], c1);
+      checkContacts(req.result[2], c2);
+      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({givenName: ["customTest"], yyy: "XXX"});
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function() {
+      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[0].givenName, ["customTest"], "same name");
+        ok(req2.result.yyy === undefined, "custom property undefined");
+        next();
+      }
+      req2.onerror = onFailure;
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    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(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(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;
+  },
+  function () {
+    // Android does not support published/updated fields. Skip this.
+    if (isAndroid) {
+      next();
+      return;
+    }
+
+    ok(true, "Test sorting with published");
+    var options = {sortBy: "familyName",
+                   sortOrder: "descending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 3, "3 results");
+      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.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact with 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, "Test category search with startsWith");
+    var options = {filterBy: ["category"],
+                   filterOp: "startsWith",
+                   filterValue: properties2.category[0]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "1 Entry.");
+      checkContacts(req.result[0], createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test category search with equals");
+    var options = {filterBy: ["category"],
+                   filterOp: "equals",
+                   filterValue: properties2.category[0]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "1 Entry.");
+      checkContacts(req.result[0], createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    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;
+  },
+  function () {
+    ok(true, "Test category search with equals");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   filterValue: "5"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "1 Entry.");
+      checkContacts(req.result[0], createResult1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding contact with invalid data");
+    var obj = {
+        honorificPrefix: [],
+        honorificSuffix: [{foo: "bar"}],
+        sex: 17,
+        genderIdentity: 18,
+        email: [{type: ["foo"], value: "bar"}]
+    };
+    obj.honorificPrefix.__defineGetter__('0',(function() {
+      var c = 0;
+      return function() {
+        if (c == 0) {
+          c++;
+          return "string";
+        } else {
+          return {foo:"bar"};
+        }
+      }
+    })());
+    createResult1 = new mozContact(obj);
+    createResult1.email.push({aeiou: "abcde"});
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      checkContacts(createResult1, {
+        honorificPrefix: ["string"],
+        honorificSuffix: ["[object Object]"],
+        sex: "17",
+        genderIdentity: "18",
+        email: [{type: ["foo"], value: "bar"}, {}]
+      });
+      next();
+    };
+  },
+  function () {
+    ok(true, "Adding contact with no number but carrier");
+    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({ 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({ 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 () {
+    ok(true, "Test numbersOnly search 2");
+    var options = {filterBy: ["givenName", "tel"],
+                   filterOp: "contains",
+                   filterValue: "a"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "1 Entry.");
+      checkContacts(req.result[0], createResult1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test numbersOnly search 3");
+    var options = {filterBy: ["givenName", "tel"],
+                   filterOp: "contains",
+                   filterValue: "b"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 0, "0 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test numbersOnly search 4");
+    var options = {filterBy: ["givenName", "tel"],
+                   filterOp: "contains",
+                   filterValue: "1a"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 0, "0 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test numbersOnly search 5");
+    var options = {filterBy: ["givenName", "tel"],
+                   filterOp: "contains",
+                   filterValue: "1(23)"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "1 Entry.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test numbersOnly search 6");
+    var options = {filterBy: ["givenName", "tel"],
+                   filterOp: "contains",
+                   filterValue: "1(23)a"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 0, "0 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Test that after setting array properties to scalar values the property os not a non-array")
+    const FIELDS = ["email","url","adr","tel","impp"];
+    createResult1 = new mozContact();
+    for (var prop of FIELDS) {
+      try {
+        createResult1[prop] = {type: ["foo"]};
+      } catch (e) {}
+      ok(createResult1[prop] === null ||
+         Array.isArray(createResult1[prop]), prop + " is array");
+    }
+    next();
+  },
+  function() {
+    ok(true, "Undefined properties of fields should be treated correctly");
+    var c = new mozContact({
+      adr: [{streetAddress: undefined}],
+      email: [{value: undefined}],
+      url: [{value: undefined}],
+      impp: [{value: undefined}],
+      tel: [{value: undefined}],
+    });
+    is(c.adr[0].streetAddress, undefined, "adr.streetAddress is undefined");
+    is(c.adr[0].locality, undefined, "adr.locality is undefined");
+    is(c.adr[0].pref, undefined, "adr.pref is undefined");
+    is(c.email[0].value, undefined, "email.value is undefined");
+    is(c.url[0].value, undefined, "url.value is undefined");
+    is(c.impp[0].value, undefined, "impp.value is undefined");
+    is(c.tel[0].value, undefined, "tel.value is undefined");
+    next();
+  },
+  function() {
+    ok(true, "Setting array properties to an empty array should work");
+    var c = new mozContact();
+    function testArrayProp(prop) {
+      is(c[prop], null, "property is initially null");
+      c[prop] = [];
+      ok(Array.isArray(c[prop]), "property is an array after setting");
+      is(c[prop].length, 0, "property has length 0 after setting");
+    }
+    testArrayProp("email");
+    testArrayProp("adr");
+    testArrayProp("tel");
+    testArrayProp("impp");
+    testArrayProp("url");
+    next();
+  },
+  function() {
+    ok(true, "Passing a mozContact with invalid data to save() should throw");
+    var c = new mozContact({
+      photo: [],
+      tel: []
+    });
+    c.photo.push({});
+    SimpleTest.doesThrow(()=>navigator.mozContacts.save(c), "Invalid data in Blob array");
+    c.tel.push(123);
+    SimpleTest.doesThrow(()=>navigator.mozContacts.save(c), "Invalid data in dictionary array");
+    next();
+  },
+  function() {
+    ok(true, "Inline changes to array properties should be seen by save");
+    var c = new mozContact({
+      name: [],
+      familyName: [],
+      givenName: [],
+      phoneticFamilyName: [],
+      phoneticGivenName: [],
+      nickname: [],
+      tel: [],
+      adr: [],
+      email: []
+    });
+    for (var prop of Object.getOwnPropertyNames(properties1)) {
+      if (!Array.isArray(properties1[prop])) {
+        continue;
+      }
+      for (var i = 0; i < properties1[prop].length; ++i) {
+        c[prop].push(properties1[prop][i]);
+      }
+    }
+    req = navigator.mozContacts.save(c);
+    req.onsuccess = function() {
+      req = navigator.mozContacts.find(defaultOptions);
+      req.onsuccess = function() {
+        is(req.result.length, 1, "Got 1 contact");
+        checkContacts(req.result[0], properties1);
+        next();
+      };
+      req.onerror = onFailure;
+    };
+    req.onerror = onFailure;
+  },
+  clearDatabase,
+  function() {
+    ok(true, "mozContact.init deprecation message");
+    var c = new mozContact();
+    SimpleTest.monitorConsole(next, [
+      { errorMessage: "mozContact.init is DEPRECATED. Use the mozContact constructor instead. " +
+                      "See https://developer.mozilla.org/docs/WebAPI/Contacts for details." }
+    ], /* forbidUnexpectedMsgs */ true);
+    c.init({name: ["Bar"]});
+    c.init({name: ["Bar"]});
+    SimpleTest.endMonitorConsole();
+  },
+  function() {
+    ok(true, "mozContact.init works as expected");
+    var c = new mozContact({name: ["Foo"]});
+    c.init({name: ["Bar"]});
+    is(c.name[0], "Bar", "Same name");
+    next();
+  },
+  function() {
+    ok(true, "mozContact.init without parameters");
+    var c = new mozContact({name: ["Foo"]});
+    c.init();
+    next();
+  },
+  function() {
+    ok(true, "mozContact.init resets properties");
+    var c = new mozContact({jobTitle: ["Software Engineer"]});
+    c.init({nickname: ["Jobless Johnny"]});
+    is(c.nickname[0], "Jobless Johnny", "Same nickname");
+    ok(!c.jobTitle, "jobTitle is not set");
+    next();
+  },
+  function() {
+    ok(true, "mozContacts.remove with an ID works");
+    var c = new mozContact({name: ["Ephemeral Jimmy"]});
+    req = navigator.mozContacts.save(c);
+    req.onsuccess = function() {
+      req = navigator.mozContacts.remove(c.id);
+      req.onsuccess = function() {
+        req = navigator.mozContacts.find({
+          filterBy: ["id"],
+          filterOp: "equals",
+          filterValue: c.id
+        });
+        req.onsuccess = function() {
+          is(req.result.length, 0, "Successfully removed contact by ID");
+          next();
+        };
+        req.onerror = onFailure;
+      };
+      req.onerror = onFailure;
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact");
+    createResult1 = new mozContact(properties3);
+    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(properties4);
+    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: "phoneticFamilyName"});
+    req.onsuccess = function () {
+      is(req.result.length, 2, "Found exactly 2 contact.");
+      checkContacts(req.result[1], properties3);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by query1");
+    var options = {filterBy: ["phoneticGivenName", "email"],
+                   filterOp: "startsWith",
+                   filterValue: properties3.phoneticGivenName[0].substring(0, 3)}
+    req = mozContacts.find(options)
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by query2");
+    var options = {filterBy: ["phoneticGivenName", "email"],
+                   filterOp: "startsWith",
+                   filterValue: properties4.phoneticGivenName[0].substring(0, 3)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.adr.length, 2, "Adr length 2");
+      checkContacts(findResult1, createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  clearDatabase,
+  function () {
+    ok(true, "Adding 20 contacts");
+    for (var i=0; i<19; i++) {
+      createResult1 = new mozContact(properties3);
+      req = mozContacts.save(createResult1);
+      req.onsuccess = function () {
+        ok(createResult1.id, "The contact now has an ID.");
+      };
+      req.onerror = onFailure;
+    };
+    createResult1 = new mozContact(properties3);
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkStrArray(createResult1.name, properties3.name, "Same Name");
+      checkCount(20, "20 contacts in DB", next);
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find(defaultOptions);
+    req.onsuccess = function () {
+      is(req.result.length, 20, "20 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts2");
+    var options = {filterBy: ["phoneticGivenName"],
+                   filterOp: "startsWith",
+                   filterValue: properties3.phoneticGivenName[0].substring(0, 3)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 20, "20 Entries.");
+      checkContacts(createResult1, req.result[19]);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts3");
+    var options = {filterBy: ["phoneticGivenName", "tel", "email"],
+                   filterOp: "startsWith",
+                   filterValue: properties3.phoneticGivenName[0].substring(0, 3)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 20, "20 Entries.");
+      checkContacts(createResult1, req.result[10]);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  clearDatabase,
+  function () {
+    ok(true, "Testing clone contact");
+    createResult1 = new mozContact(properties3);
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkStrArray(createResult1.phoneticFamilyName, properties3.phoneticFamilyName, "Same phoneticFamilyName");
+      checkStrArray(createResult1.phoneticGivenName, properties3.phoneticGivenName, "Same phoneticGivenName");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({sortBy: "phoneticGivenName"});
+    req.onsuccess = function () {
+      is(req.result.length, 1, "1 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  clearDatabase,
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c11);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c11, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c10);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c10, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c12);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c12, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c9);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c9, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    var options = {sortBy: "phoneticFamilyName",
+                   sortOrder: "ascending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 4, "4 results");
+      checkContacts(req.result[0], c9);
+      checkContacts(req.result[1], c10);
+      checkContacts(req.result[2], c11);
+      checkContacts(req.result[3], c12);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    var options = {sortBy: "phoneticFamilyName",
+                   sortOrder: "descending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 4, "4 results");
+      checkContacts(req.result[0], c12);
+      checkContacts(req.result[1], c11);
+      checkContacts(req.result[2], c10);
+      checkContacts(req.result[3], c9);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c13);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c13, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting with empty string");
+    var options = {sortBy: "phoneticFamilyName",
+                   sortOrder: "ascending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 5, "5 results");
+      checkContacts(req.result[0], c13);
+      checkContacts(req.result[1], c9);
+      checkContacts(req.result[2], c10);
+      checkContacts(req.result[3], c11);
+      checkContacts(req.result[4], c12);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  clearDatabase,
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c15);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c15, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c14);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c14, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Test sorting");
+    createResult1 = new mozContact(c16);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      checkContacts(c16, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    // Android does not support published/updated fields. Skip this.
+    if (isAndroid) {
+      next();
+      return;
+    }
+
+    ok(true, "Test sorting with published");
+    var options = {sortBy: "phoneticFamilyName",
+                   sortOrder: "descending"};
+    req = navigator.mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 3, "3 results");
+      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;
+  },
+  clearDatabase,
+  function () {
+    ok(true, "all done!\n");
+    parent.SimpleTest.finish();
+  }
+];
+
+function next() {
+  ok(true, "Begin!");
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    var i = index++;
+    steps[i]();
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+start_tests();
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_blobs.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674720
+-->
+<head>
+  <title>Test for Bug 674720 WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674720">Mozilla Bug 674720</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var is = parent.is;
+var isnot = parent.isnot;
+
+var utils = SpecialPowers.getDOMWindowUtils(window);
+
+function getView(size)
+{
+ var buffer = new ArrayBuffer(size);
+ var view = new Uint8Array(buffer);
+ is(buffer.byteLength, size, "Correct byte length");
+ return view;
+}
+
+function getRandomView(size)
+{
+ var view = getView(size);
+ for (var i = 0; i < size; i++) {
+   view[i] = parseInt(Math.random() * 255)
+ }
+ return view;
+}
+
+function getRandomBlob(size)
+{
+  return new Blob([getRandomView(size)], { type: "binary/random" });
+}
+
+function compareBuffers(buffer1, buffer2)
+{
+  if (buffer1.byteLength != buffer2.byteLength) {
+    return false;
+  }
+  var view1 = new Uint8Array(buffer1);
+  var view2 = new Uint8Array(buffer2);
+  for (var i = 0; i < buffer1.byteLength; i++) {
+    if (view1[i] != view2[i]) {
+      return false;
+    }
+  }
+  return true;
+}
+
+function verifyBuffers(buffer1, buffer2, isLast)
+{
+  ok(compareBuffers(buffer1, buffer2), "Correct blob data");
+  if (isLast)
+    next();
+}
+
+var randomBlob = getRandomBlob(1024);
+var randomBlob2 = getRandomBlob(1024);
+
+var properties1 = {
+  name: ["xTestname1"],
+  givenName: ["xTestname1"],
+  photo: [randomBlob]
+};
+
+var properties2 = {
+  name: ["yTestname2"],
+  givenName: ["yTestname2"],
+  photo: [randomBlob, randomBlob2]
+};
+
+var sample_id1;
+var createResult1;
+var findResult1;
+
+function verifyBlob(blob1, blob2, isLast)
+{
+  is(blob1 instanceof Blob, true,
+     "blob1 is an instance of DOMBlob");
+  is(blob2 instanceof Blob, true,
+     "blob2 is an instance of DOMBlob");
+  isnot(blob1 instanceof File, true,
+     "blob1 is an instance of File");
+  isnot(blob2 instanceof File, true,
+     "blob2 is an instance of File");
+  is(blob1.size, blob2.size, "Same size");
+  is(blob1.type, blob2.type, "Same type");
+
+  var buffer1;
+  var buffer2;
+
+  var reader1 = new FileReader();
+  reader1.readAsArrayBuffer(blob2);
+  reader1.onload = function(event) {
+    buffer2 = event.target.result;
+    if (buffer1) {
+      verifyBuffers(buffer1, buffer2, isLast);
+    }
+  }
+
+  var reader2 = new FileReader();
+  reader2.readAsArrayBuffer(blob1);
+  reader2.onload = function(event) {
+    buffer1 = event.target.result;
+    if (buffer2) {
+      verifyBuffers(buffer1, buffer2, isLast);
+    }
+  }
+}
+
+function verifyBlobArray(blobs1, blobs2)
+{
+  is(blobs1 instanceof Array, true, "blobs1 is an array object");
+  is(blobs2 instanceof Array, true, "blobs2 is an array object");
+  is(blobs1.length, blobs2.length, "Same length");
+
+  if (!blobs1.length) {
+    next();
+    return;
+  }
+
+  for (var i = 0; i < blobs1.length; i++) {
+    verifyBlob(blobs1[i], blobs2[i], i == blobs1.length - 1);
+  }
+}
+
+var req;
+
+var steps = [
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding contact with photo");
+    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: 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(findResult1.photo, properties1.photo);
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    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;
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "startsWith",
+                   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");
+      verifyBlobArray(findResult1.photo, properties2.photo);
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "all done!\n");
+
+    parent.SimpleTest.finish();
+  }
+];
+
+start_tests();
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_events.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=764667
+-->
+<head>
+  <title>Test for Bug 678695</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=764667">Mozilla Bug 764667</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+  
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 764667 **/
+
+var ok = parent.ok;
+var is = parent.is;
+
+var e = new MozContactChangeEvent("contactchanged", {contactID: "123", reason: "create"});
+ok(e, "Should have contactsChange event!");
+is(e.contactID, "123", "ID should be 123.");
+is(e.reason, "create", "Reason should be create.");
+
+e = new MozContactChangeEvent("contactchanged", {contactID: "test", reason: "test"});
+is(e.contactID, "test", "Name should be 'test'.");
+is(e.reason, "test", "Name should be 'test'.");
+
+e = new MozContactChangeEvent("contactchanged", {contactID: "a", reason: ""});
+is(e.contactID, "a", "Name should be a.");
+is(e.reason, "", "Value should be empty");
+
+parent.SimpleTest.finish();
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_getall.html
@@ -0,0 +1,156 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=836519
+-->
+<head>
+  <title>Mozilla Bug 836519</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=836519">Mozilla Bug 836519</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript;version=1.8">
+"use strict";
+
+var ok = parent.ok;
+var is = parent.is;
+var isnot = parent.isnot;
+
+let req;
+
+let steps = [
+  function start() {
+    SpecialPowers.Cc["@mozilla.org/tools/profiler;1"].getService(SpecialPowers.Ci.nsIProfiler).AddMarker("GETALL_START");
+    next();
+  },
+  clearDatabase,
+  addContacts,
+
+  function() {
+    ok(true, "Delete the current contact while iterating");
+    req = mozContacts.getAll({});
+    let count = 0;
+    let previousId = null;
+    req.onsuccess = function() {
+      if (req.result) {
+        ok(true, "on success");
+        if (previousId) {
+          isnot(previousId, req.result.id, "different contacts returned");
+        }
+        previousId = req.result.id;
+        count++;
+        let delReq = mozContacts.remove(req.result);
+        delReq.onsuccess = function() {
+          ok(true, "deleted current contact");
+          req.continue();
+        };
+      } else {
+        is(count, 40, "returned 40 contacts");
+        next();
+      }
+    };
+  },
+
+  clearDatabase,
+  addContacts,
+
+  function() {
+    ok(true, "Iterating through the contact list inside a cursor callback");
+    let count1 = 0, count2 = 0;
+    let req1 = mozContacts.getAll({});
+    let req2;
+    req1.onsuccess = function() {
+      if (count1 == 0) {
+        count1++;
+        req2 = mozContacts.getAll({});
+        req2.onsuccess = function() {
+          if (req2.result) {
+            count2++;
+            req2.continue();
+          } else {
+            is(count2, 40, "inner cursor returned 40 contacts");
+            req1.continue();
+          }
+        };
+      } else {
+        if (req1.result) {
+          count1++;
+          req1.continue();
+        } else {
+          is(count1, 40, "outer cursor returned 40 contacts");
+          next();
+        }
+      }
+    };
+  },
+
+  clearDatabase,
+  addContacts,
+
+  function() {
+    ok(true, "20 concurrent cursors");
+    const NUM_CURSORS = 20;
+    let completed = 0;
+    for (let i = 0; i < NUM_CURSORS; ++i) {
+      mozContacts.getAll({}).onsuccess = (function(i) {
+        let count = 0;
+        return function(event) {
+          let req = event.target;
+          if (req.result) {
+            count++;
+            req.continue();
+          } else {
+            is(count, 40, "cursor " + i + " returned 40 contacts");
+            if (++completed == NUM_CURSORS) {
+              next();
+            }
+          }
+        };
+      })(i);
+    }
+  },
+
+  clearDatabase,
+  addContacts,
+
+  function() {
+    if (!SpecialPowers.isMainProcess()) {
+      // We stop calling continue() intentionally here to see if the cursor gets
+      // cleaned up properly in the parent.
+      ok(true, "Leaking a cursor");
+      req = mozContacts.getAll({
+        sortBy: "familyName",
+        sortOrder: "ascending"
+      });
+      req.onsuccess = function(event) {
+        next();
+      };
+      req.onerror = onFailure;
+    } else {
+      next();
+    }
+  },
+
+  clearDatabase,
+
+  function() {
+    ok(true, "all done!\n");
+    SpecialPowers.Cc["@mozilla.org/tools/profiler;1"].getService(SpecialPowers.Ci.nsIProfiler).AddMarker("GETALL_END");
+    parent.SimpleTest.finish();
+  }
+];
+
+start_tests();
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_getall2.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=836519
+-->
+<head>
+  <title>Mozilla Bug 836519</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=836519">Mozilla Bug 836519</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript;version=1.8">
+"use strict";
+
+var ok = parent.ok;
+var is = parent.is;
+
+let req;
+
+let steps = [
+  clearDatabase,
+  function() {
+    // add a contact
+    createResult1 = new mozContact({});
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function() {
+      next();
+    };
+    req.onerror = onFailure;
+  },
+
+  getOne(),
+  getOne("Retrieving one contact with getAll - cached"),
+
+  clearDatabase,
+  addContacts,
+
+  getAll(),
+  getAll("Retrieving 40 contacts with getAll - cached"),
+
+  function() {
+    ok(true, "Deleting one contact");
+    req = mozContacts.remove(createResult1);
+    req.onsuccess = function() {
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Test cache invalidation");
+    req = mozContacts.getAll({});
+    let count = 0;
+    req.onsuccess = function(event) {
+      ok(true, "on success");
+      if (req.result) {
+        ok(true, "result is valid");
+        count++;
+        req.continue();
+      } else {
+        is(count, 39, "last contact - 39 contacts returned");
+        next();
+      }
+    };
+    req.onerror = onFailure;
+  },
+
+  clearDatabase,
+  addContacts,
+
+  function() {
+    ok(true, "Test cache consistency when deleting contact during getAll");
+    req = mozContacts.find({});
+    req.onsuccess = function(e) {
+      let lastContact = e.target.result[e.target.result.length-1];
+      req = mozContacts.getAll({});
+      let count = 0;
+      let firstResult = true;
+      req.onsuccess = function(event) {
+        ok(true, "on success");
+        if (firstResult) {
+          if (req.result) {
+            count++;
+          }
+          let delReq = mozContacts.remove(lastContact);
+          delReq.onsuccess = function() {
+            firstResult = false;
+            req.continue();
+          };
+        } else {
+          if (req.result) {
+            ok(true, "result is valid");
+            count++;
+            req.continue();
+          } else {
+            is(count, 40, "last contact - 40 contacts returned");
+            next();
+          }
+        }
+      };
+    };
+  },
+
+  clearDatabase,
+
+  function() {
+    ok(true, "all done!\n");
+    parent.SimpleTest.finish();
+  }
+];
+
+start_tests();
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_international.html
@@ -0,0 +1,277 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=815833
+-->
+<head>
+  <title>Test for Bug 815833 WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=815833">Mozilla Bug 815833</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var ise = parent.ise;
+
+var number1 = {
+  local: "7932012345",
+  international: "+557932012345"
+};
+
+var number2 = {
+  local: "7932012346",
+  international: "+557932012346"
+};
+
+var properties1 = {
+  name: ["Testname1"],
+  tel: [{type: ["work"], value: number1.local, carrier: "testCarrier"} , {type: ["home", "fax"], value: number2.local}],
+};
+
+var shortNumber = "888";
+var properties2 = {
+  name: ["Testname2"],
+  tel: [{type: ["work"], value: shortNumber, carrier: "testCarrier"}]
+};
+
+var number3 = {
+  local: "7932012345",
+  international: "+557932012345"
+};
+
+var properties3 = {
+  name: ["Testname2"],
+  tel: [{value: number3.international}]
+};
+
+var req;
+var createResult1;
+var findResult1;
+var sample_id1;
+
+var steps = [
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact1");
+    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(properties2);
+    req = navigator.mozContacts.save(createResult2);
+    req.onsuccess = function () {
+      ok(createResult2.id, "The contact now has an ID.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for local number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: number1.local};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.id, sample_id1, "Same ID");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for international number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: number1.international};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found exactly 0 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for a short number matching the prefix");
+    var shortNumber = number1.local.substring(0, 3);
+    var options = {filterBy: ["tel"],
+                   filterOp: "equals",
+                   filterValue: shortNumber};
+    req = mozContacts.find(options);
+    req.onsuccess = function() {
+      is(req.result.length, 0, "The prefix short number should not match any contact.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for a short number matching the suffix");
+    var shortNumber = number1.local.substring(number1.local.length - 3);
+    var options = {filterBy: ["tel"],
+                   filterOp: "equals",
+                   filterValue: shortNumber};
+    req = mozContacts.find(options);
+    req.onsuccess = function() {
+      is(req.result.length, 0, "The suffix short number should not match any contact.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for a short number matching a contact");
+    var options = {filterBy: ["tel"],
+                   filterOp: "equals",
+                   filterValue: shortNumber};
+    req = mozContacts.find(options);
+    req.onsuccess = function() {
+      is(req.result.length, 1, "Found the contact equally matching the shortNumber.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Modifying number");
+    if (!findResult1) {
+      SpecialPowers.executeSoon(next);
+    } else {
+      findResult1.tel[0].value = number2.local;
+      req = mozContacts.save(findResult1);
+      req.onsuccess = function () {
+        next();
+      };
+    }
+  },
+  function () {
+    ok(true, "Searching for local number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: number1.local};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found exactly 0 contact.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for local number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: number1.international};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found exactly 0 contact.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for local number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: number2.local};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.id, sample_id1, "Same ID");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for local number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "startsWith",
+                   filterValue: number2.international};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found exactly 1 contact.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a contact with a Brazilian country code");
+    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;
+  },
+  function () {
+    ok(true, "Searching for Brazilian number using local number");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: number3.local};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.id, sample_id1, "Same ID");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "all done!\n");
+    parent.SimpleTest.finish();
+  }
+];
+
+SpecialPowers.pushPrefEnv({
+  set: [
+    ["ril.lastKnownSimMcc", "000"]
+  ]
+}, start_tests);
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_substringmatching.html
@@ -0,0 +1,351 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=877302
+-->
+<head>
+  <title>Test for Bug 877302 substring matching for WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=877302">Mozilla Bug 877302</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var is = parent.is;
+
+var substringLength = 8;
+
+var prop = {
+  tel: [{value: "7932012345" }, {value: "7932012346"}]
+};
+
+var prop2 = {
+  tel: [{value: "01187654321" }]
+};
+
+var prop3 = {
+  tel: [{ value: "+43332112346" }]
+};
+
+var prop4 = {
+  tel: [{ value: "(0414) 233-9888" }]
+};
+
+var brazilianNumber = {
+  international1: "0041557932012345",
+  international2: "+557932012345"
+};
+
+var prop5 = {
+  tel: [{value: brazilianNumber.international2}]
+};
+
+var req;
+var steps = [
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding contact");
+    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;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      is(req.result.length, 1, "One contact.");
+      findResult1 = req.result[0];
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 1");
+    var length = prop.tel[0].value.length;
+    var num = prop.tel[0].value.substring(length - substringLength, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      is(findResult1.tel[0].value, "7932012345", "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 2");
+    var length = prop.tel[1].value.length;
+    var num = prop.tel[1].value.substring(length - substringLength, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      is(findResult1.tel[0].value, "7932012345", "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 3");
+    var length = prop.tel[0].value.length;
+    var num = prop.tel[0].value.substring(length - substringLength + 1, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found exactly 0 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 4");
+    var length = prop.tel[0].value.length;
+    var num = prop.tel[0].value.substring(length - substringLength - 1, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding contact");
+    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;
+  },
+  function () {
+    ok(true, "Retrieving by substring 5");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "87654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 6");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "01187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 7");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "909087654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 8");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "0411187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 9");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "90411187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 10");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "+551187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding contact");
+    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;
+  },
+  function () {
+    if (!isAndroid) { // Bug 905927
+      ok(true, "Retrieving by substring 1");
+      var length = prop3.tel[0].value.length;
+      var num = prop3.tel[0].value.substring(length - substringLength, length);
+      var options = {filterBy: ["tel"],
+                     filterOp: "match",
+                     filterValue: num};
+      req = mozContacts.find(options);
+      req.onsuccess = function () {
+        is(req.result.length, 0, "Found exactly 0 contacts.");
+        next();
+      };
+      req.onerror = onFailure;
+    } else {
+      SpecialPowers.executeSoon(next);
+    }
+  },
+  function () {
+    ok(true, "Adding contact");
+    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;
+  },
+  function () {
+    ok(true, "Retrieving by substring 1");
+    var num = "(0424) 233-9888"
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact with a Brazilian country code");
+    createResult1 = new mozContact(prop5);
+    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, "Searching for international number with prefix");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: brazilianNumber.international1};
+    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");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "all done!\n");
+    parent.SimpleTest.finish();
+  }
+];
+
+SpecialPowers.pushPrefEnv({
+  set: [
+    ["dom.phonenumber.substringmatching.BR", substringLength],
+    ["ril.lastKnownSimMcc", "724"]
+  ]
+}, start_tests);
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_substringmatchingCL.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=877302
+-->
+<head>
+  <title>Test for Bug 949537 substring matching for WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=949537">Mozilla Bug 949537</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var ise = parent.ise;
+
+var landlineNumber = "+56 2 27654321";
+
+var number = {
+  local: "87654321",
+  international: "+56 9 87654321"
+};
+
+var properties = {
+  name: ["Testname2"],
+  tel: [{value: number.international}]
+};
+
+var req;
+var steps = [
+  function () {
+    ok(true, "Adding a contact with a Chilean number");
+    createResult1 = new mozContact(properties);
+    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, "Searching for Chilean number with prefix");
+    req = mozContacts.find({
+      filterBy: ["tel"],
+      filterOp: "match",
+      filterValue: number.international
+    });
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.id, sample_id1, "Same ID");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching for Chilean number using local number");
+    req = mozContacts.find({
+      filterBy: ["tel"],
+      filterOp: "match",
+      filterValue: number.local
+    });
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found 0 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+
+  clearDatabase,
+
+  function () {
+    ok(true, "Adding contact with mobile number");
+    createResult1 = new mozContact({tel: [{value: number.international}]});
+    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 all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      is(req.result.length, 1, "One contact.");
+      findResult1 = req.result[0];
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by last 8 digits");
+    req = mozContacts.find({
+      filterBy: ["tel"],
+      filterOp: "match",
+      filterValue: number.international.slice(-8)
+    });
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.id, sample_id1, "Same ID");
+      is(findResult1.tel[0].value, number.international, "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by last 9 digits");
+    req = mozContacts.find({
+      filterBy: ["tel"],
+      filterOp: "match",
+      filterValue: number.international.slice(-9)
+    });
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.id, sample_id1, "Same ID");
+      is(findResult1.tel[0].value, number.international, "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by last 6 digits");
+    req = mozContacts.find({
+      filterBy: ["tel"],
+      filterOp: "match",
+      filterValue: number.international.slice(-6)
+    });
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found exactly zero contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+
+  clearDatabase,
+
+  function () {
+    ok(true, "Adding contact with landline number");
+    createResult1 = new mozContact({tel: [{value: landlineNumber}]});
+    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 all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      is(req.result.length, 1, "One contact.");
+      findResult1 = req.result[0];
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by last 7 digits (local number) with landline calling prefix");
+    req = mozContacts.find({
+      filterBy: ["tel"],
+      filterOp: "match",
+      filterValue: "022" + landlineNumber.slice(-7)
+    });
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      is(findResult1.id, sample_id1, "Same ID");
+      is(findResult1.tel[0].value, landlineNumber, "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+
+  clearDatabase,
+
+  function () {
+    ok(true, "all done!\n");
+    parent.SimpleTest.finish();
+  }
+];
+
+SpecialPowers.pushPrefEnv({
+  set: [
+    ["dom.phonenumber.substringmatching.CL", 8],
+    ["ril.lastKnownSimMcc", "730"]
+  ]
+}, start_tests);
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_contacts_substringmatchingVE.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=877302
+-->
+<head>
+  <title>Test for Bug 877302 substring matching for WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=877302">Mozilla Bug 877302</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="http://mochi.test:8888/tests/dom/contacts/tests/shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var is = parent.is;
+
+var prop = {
+  tel: [{value: "7932012345" }, {value: "7704143727591"}]
+};
+
+var prop2 = {
+  tel: [{value: "7932012345" }, {value: "+58 212 5551212"}]
+};
+
+var req;
+var steps = [
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding contact");
+    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;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      is(req.result.length, 1, "One contact.");
+      findResult1 = req.result[0];
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 1");
+    var length = prop.tel[0].value.length;
+    var num = "04143727591"
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      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(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;
+  },
+  function () {
+    ok(true, "Retrieving by substring 2");
+    var num = "5551212";
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      is(findResult1.tel[1].value, "+58 212 5551212", "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "all done!\n");
+    parent.SimpleTest.finish();
+  }
+];
+
+SpecialPowers.pushPrefEnv({
+  set: [
+    ["dom.phonenumber.substringmatching.VE", 7],
+    ["ril.lastKnownSimMcc", "734"]
+  ]
+}, start_tests);
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_migration.html
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Migration tests</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<h1>migration tests</h1>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.8" src="shared.js"></script>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var ok = parent.ok;
+var ise = parent.ise;
+
+var backend, contactsCount, allContacts;
+function loadChromeScript() {
+  var url = SimpleTest.getTestFileURL("test_migration_chrome.js");
+  backend = SpecialPowers.loadChromeScript(url);
+}
+
+function addBackendEvents() {
+  backend.addMessageListener("createDB.success", function(count) {
+    contactsCount = count;
+    ok(true, "Created the database");
+    next();
+  });
+  backend.addMessageListener("createDB.error", function(err) {
+    ok(false, err);
+    next();
+  });
+
+  backend.addMessageListener("deleteDB.success", function() {
+    ok(true, "Deleted the database");
+    next();
+  });
+  backend.addMessageListener("deleteDB.error", function(err) {
+    ok(false, err);
+    next();
+  });
+}
+
+function createDB(version) {
+  info("Will create the DB at version " + version);
+  backend.sendAsyncMessage("createDB", version);
+}
+
+function deleteDB() {
+  info("Will delete the DB.");
+  backend.sendAsyncMessage("deleteDB");
+}
+
+var steps = [
+  function setupChromeScript() {
+    loadChromeScript();
+    addBackendEvents();
+    next();
+  },
+
+  deleteDB, // let's be sure the DB does not exist yet
+  createDB.bind(null, 12),
+
+  function testAccessMozContacts() {
+    info("Checking we have the right number of contacts: " + contactsCount);
+    var req = mozContacts.getCount();
+    req.onsuccess = function onsuccess() {
+      ok(true, "Could access the mozContacts API");
+      is(this.result, contactsCount, "Contacts count is correct");
+      next();
+    };
+
+    req.onerror = function onerror() {
+      ok(false, "Couldn't access the mozContacts API");
+      next();
+    };
+  },
+
+  function testRetrieveAllContacts() {
+    /* if the migration does not work right, either we'll have an error, or the
+       contacts won't be migrated properly and thus will fail WebIDL conversion,
+       which will manifest as a timeout */
+    info("Checking the contacts are corrected to obey WebIDL constraints.  (upgrades 14 to 17)");
+    var req = mozContacts.find();
+    req.onsuccess = function onsuccess() {
+      if (this.result) {
+        is(this.result.length, contactsCount, "Contacts array length is correct");
+        allContacts = this.result;
+        next();
+      } else {
+        ok(false, "Could access the mozContacts API but got no contacts!");
+        next();
+      }
+    };
+
+    req.onerror = function onerror() {
+      ok(false, "Couldn't access the mozContacts API");
+      next();
+    };
+  },
+
+  function checkNameIndex() {
+    info("Checking name index migration (upgrades 17 to 19).");
+    if (!allContacts) {
+      next();
+    }
+
+    var count = allContacts.length;
+
+    function finishRequest() {
+      count--;
+      if (!count) {
+        next();
+      }
+    }
+
+    allContacts.forEach(function(contact) {
+      var name = contact.name && contact.name[0];
+      if (!name) {
+        count--;
+        return;
+      }
+
+      var req = mozContacts.find({
+        filterBy: ["name"],
+        filterValue: name,
+        filterOp: "equals"
+      });
+
+      req.onsuccess = function onsuccess() {
+        if (this.result) {
+          info("Found contact '" + name + "', checking it's the correct one.");
+          checkContacts(this.result[0], contact);
+        } else {
+          ok(false, "Could not find contact with name '" + name + "'");
+        }
+
+        finishRequest();
+      };
+
+      req.onerror = function onerror() {
+        ok(false, "Error while finding contact with name '" + name + "'!");
+        finishRequest();
+      }
+    });
+
+    if (!count) {
+      ok(false, "No contact had a name, this is unexpected.");
+      next();
+    }
+  },
+
+  function checkSubstringMatching() {
+    var subject = "0004567890"; // the last 7 digits are the same that at least one contact
+    info("Looking for a contact matching " + subject);
+    var req = mozContacts.find({
+      filterValue: subject,
+      filterOp: "match",
+      filterBy: ["tel"],
+      filterLimit: 1
+    });
+
+    req.onsuccess = function onsuccess() {
+      if (this.result && this.result[0]) {
+        ok(true, "Found a contact with number " + this.result[0].tel[0].value);
+      }
+      next();
+    };
+
+    req.onerror = function onerror() {
+      ok(false, "Error while finding contact for substring matching check!");
+      next();
+    };
+  },
+
+  deleteDB,
+
+  function finish() {
+    backend.destroy();
+    info("all done!\n");
+    parent.SimpleTest.finish();
+  }
+];
+
+// this is the Mcc for Brazil, so that we trigger the previous pref
+SpecialPowers.pushPrefEnv({"set": [["dom.phonenumber.substringmatching.BR", 7],
+                                   ["ril.lastKnownSimMcc", "724"]]}, start_tests);
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/file_permission_denied.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1081873
+-->
+<head>
+  <title>Test for Bug 1081873</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1081873">Mozilla Bug 1081873</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+parent.is("mozContacts" in navigator, false, "navigator.mozContacts must be inaccessible");
+parent.SimpleTest.finish();
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/mochitest.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+  shared.js
+  file_permission_denied.html
+
+[test_permission_denied.html]
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/shared.js
@@ -0,0 +1,503 @@
+"use strict";
+
+// Fix the environment to run Contacts tests
+SpecialPowers.importInMainProcess("resource://gre/modules/ContactService.jsm");
+
+// Some helpful global vars
+var isAndroid = (navigator.userAgent.indexOf("Android") !== -1);
+
+var defaultOptions = {
+  sortBy: "givenName",
+};
+
+// Make sure we only touch |navigator.mozContacts| after we have the necessary
+// permissions, or we'll race when checking the listen permission needed for the
+// oncontactchange event. This is only needed for tests because at first we have
+// the permission set to UNKNOWN_ACTION. That should never happen for real apps,
+// see dom/apps/PermissionsTable.jsm.
+var mozContacts;
+
+// To test sorting
+var c1 = {
+  name: ["a a"],
+  familyName: ["a"],
+  givenName: ["a"],
+};
+
+var c2 = {
+  name: ["b b"],
+  familyName: ["b"],
+  givenName: ["b"],
+};
+
+var c3 = {
+  name: ["c c", "a a", "b b"],
+  familyName: ["c","a","b"],
+  givenName: ["c","a","b"],
+};
+
+var c4 = {
+  name: ["c c", "a a", "c c"],
+  familyName: ["c","a","c"],
+  givenName: ["c","a","c"],
+};
+
+var c5 = {
+  familyName: [],
+  givenName: [],
+};
+
+var c6 = {
+  name: ["e"],
+  familyName: ["e","e","e"],
+  givenName: ["e","e","e"],
+};
+
+var c7 = {
+  name: ["e"],
+  familyName: ["e","e","e"],
+  givenName: ["e","e","e"],
+};
+
+var c8 = {
+  name: ["e"],
+  familyName: ["e","e","e"],
+  givenName: ["e","e","e"],
+};
+
+var adr1 = {
+  type: ["work"],
+  streetAddress: "street 1",
+  locality: "locality 1",
+  region: "region 1",
+  postalCode: "postal code 1",
+  countryName: "country 1"
+};
+
+var adr2 = {
+  type: ["home, fax"],
+  streetAddress: "street2",
+  locality: "locality2",
+  region: "region2",
+  postalCode: "postal code2",
+  countryName: "country2"
+};
+
+var properties1 = {
+  // please keep capital letters at the start of these names
+  name: ["Test1 TestFamilyName", "Test2 Wagner"],
+  familyName: ["TestFamilyName","Wagner"],
+  givenName: ["Test1","Test2"],
+  phoneticFamilyName: ["TestphoneticFamilyName1","TestphoneticFamilyName2"],
+  phoneticGivenName: ["TestphoneticGivenName1","TestphoneticGivenName2"],
+  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],
+  email: [{type: ["work"], value: "x@y.com"}],
+};
+
+var properties2 = {
+  name: ["dummyHonorificPrefix dummyGivenName dummyFamilyName dummyHonorificSuffix", "dummyHonorificPrefix2"],
+  familyName: ["dummyFamilyName"],
+  givenName: ["dummyGivenName"],
+  phoneticFamilyName: ["dummyphoneticFamilyName"],
+  phoneticGivenName: ["dummyphoneticGivenName"],
+  honorificPrefix: ["dummyHonorificPrefix","dummyHonorificPrefix2"],
+  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"],
+  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"]
+};
+
+// To test sorting(CJK)
+var c9 = {
+  phoneticFamilyName: ["a"],
+  phoneticGivenName: ["a"],
+};
+
+var c10 = {
+  phoneticFamilyName: ["b"],
+  phoneticGivenName: ["b"],
+};
+
+var c11 = {
+  phoneticFamilyName: ["c","a","b"],
+  phoneticGivenName: ["c","a","b"],
+};
+
+var c12 = {
+  phoneticFamilyName: ["c","a","c"],
+  phoneticGivenName: ["c","a","c"],
+};
+
+var c13 = {
+  phoneticFamilyName: [],
+  phoneticGivenName: [],
+};
+
+var c14 = {
+  phoneticFamilyName: ["e","e","e"],
+  phoneticGivenName: ["e","e","e"],
+};
+
+var c15 = {
+  phoneticFamilyName: ["e","e","e"],
+  phoneticGivenName: ["e","e","e"],
+};
+
+var c16 = {
+  phoneticFamilyName: ["e","e","e"],
+  phoneticGivenName: ["e","e","e"],
+};
+
+var properties3 = {
+  // please keep capital letters at the start of these names
+  name: ["Taro Yamada", "Ichiro Suzuki"],
+  familyName: ["Yamada","Suzuki"],
+  givenName: ["Taro","Ichiro"],
+  phoneticFamilyName: ["TestPhoneticFamilyYamada","TestPhoneticFamilySuzuki"],
+  phoneticGivenName: ["TestPhoneticGivenTaro","TestPhoneticGivenIchiro"],
+  nickname: ["phoneticNicktest"],
+  tel: [{type: ["work"], value: "123456", carrier: "testCarrier"} , {type: ["home", "fax"], value: "+55 (31) 9876-3456"}, {type: ["home"], value: "+49 451 491934"}],
+  adr: [adr1],
+  email: [{type: ["work"], value: "x@y.com"}],
+};
+
+var properties4 = {
+  name: ["dummyHonorificPrefix dummyTaro dummyYamada dummyHonorificSuffix", "dummyHonorificPrefix2"],
+  familyName: ["dummyYamada"],
+  givenName: ["dummyTaro"],
+  phoneticFamilyName: ["dummyTestPhoneticFamilyYamada"],
+  phoneticGivenName: ["dummyTestPhoneticGivenTaro"],
+  honorificPrefix: ["dummyPhoneticHonorificPrefix","dummyPhoneticHonorificPrefix2"],
+  honorificSuffix: ["dummyPhoneticHonorificSuffix"],
+  additionalName: ["dummyPhoneticAdditionalName"],
+  nickname: ["dummyPhoneticNickname"],
+  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"],
+  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"]
+};
+
+var sample_id1;
+var sample_id2;
+
+var createResult1;
+var createResult2;
+
+var findResult1;
+var findResult2;
+
+// DOMRequest helper functions
+function onUnwantedSuccess() {
+  ok(false, "onUnwantedSuccess: shouldn't get here");
+}
+
+function onFailure() {
+  ok(false, "in on Failure!");
+  next();
+}
+
+// Validation helper functions
+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) {
+  function normalize_falsy(v) {
+    if (!v || v == "null" || v == "undefined") {
+      return "";
+    }
+    return v;
+  }
+  function optArray(val) {
+    return Array.isArray(val) ? val : [val];
+  }
+  str1 = optArray(str1).map(normalize_falsy).filter(v => v != "");
+  str2 = optArray(str2).map(normalize_falsy).filter(v => v != "");
+  is(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;
+    }
+  }
+  is(!!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");
+  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 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");
+  checkStr(field1.value, field2.value, "Same value");
+  checkPref(field1.pref, field2.pref);
+}
+
+function checkTel(tel1, tel2) {
+  if (tel1 ^ tel2) {
+    ok(false, "Expected both tels to be either present or absent");
+    return;
+  }
+  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) {
+    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: " + JSON.stringify(array1) + " vs. " + JSON.stringify(array2) + ". (" + msg + ")");
+    return;
+  }
+  if (!array1 && !array2)  {
+    ok(true, msg);
+    return;
+  }
+  is(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.phoneticFamilyName, contact2.phoneticFamilyName, "Same phoneticFamilyName");
+  checkStrArray(contact1.phoneticGivenName, contact2.phoneticGivenName, "Same phoneticGivenName");
+  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 addContacts() {
+  ok(true, "Adding 40 contacts");
+  let req;
+  for (let i = 0; i < 39; ++i) {
+    properties1.familyName[0] = "Testname" + (i < 10 ? "0" + i : i);
+    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;
+  };
+  properties1.familyName[0] = "Testname39";
+  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.");
+    checkStrArray(createResult1.name, properties1.name, "Same Name");
+    next();
+  };
+  req.onerror = onFailure;
+}
+
+function getOne(msg) {
+  return function() {
+    ok(true, msg || "Retrieving one contact with getAll");
+    let req = mozContacts.getAll({});
+
+    let count = 0;
+    req.onsuccess = function(event) {
+      ok(true, "on success");
+      if (req.result) {
+        ok(true, "result is valid");
+        count++;
+        req.continue();
+      } else {
+        is(count, 1, "last contact - only one contact returned");
+        next();
+      }
+    };
+    req.onerror = onFailure;
+  };
+}
+
+function getAll(msg) {
+  return function() {
+    ok(true, msg || "Retrieving 40 contacts with getAll");
+    let req = mozContacts.getAll({
+      sortBy: "familyName",
+      sortOrder: "ascending"
+    });
+    let count = 0;
+    let result;
+    let props;
+    req.onsuccess = function(event) {
+      if (req.result) {
+        ok(true, "result is valid");
+        result = req.result;
+        properties1.familyName[0] = "Testname" + (count < 10 ? "0" + count : count);
+        is(result.familyName[0], properties1.familyName[0], "Same familyName");
+        count++;
+        req.continue();
+      } else {
+        is(count, 40, "last contact - 40 contacts returned");
+        next();
+      }
+    };
+    req.onerror = onFailure;
+  };
+}
+
+function clearTemps() {
+  sample_id1 = null;
+  sample_id2 = null;
+  createResult1 = null;
+  createResult2 = null;
+  findResult1 = null;
+  findResult2 = null;
+}
+
+function clearDatabase() {
+  ok(true, "Deleting database");
+  let req = mozContacts.clear()
+  req.onsuccess = function () {
+    ok(true, "Deleted the database");
+    next();
+  }
+  req.onerror = onFailure;
+}
+
+function checkCount(count, msg, then) {
+  var request = mozContacts.getCount();
+  request.onsuccess = function(e) {
+    is(e.target.result, count, msg);
+    then();
+  };
+  request.onerror = onFailure;
+}
+
+// Helper functions to run tests
+var index = 0;
+
+function next() {
+  info("Step " + index);
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    var i = index++;
+    steps[i]();
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+function start_tests() {
+  // Skip tests on Android < 4.0 due to test failures on tbpl (see bugs 897924 & 888891)
+  let androidVersion = SpecialPowers.Cc['@mozilla.org/system-info;1']
+                                    .getService(SpecialPowers.Ci.nsIPropertyBag2)
+                                    .getProperty('version');
+  if (!isAndroid || androidVersion >= 14) {
+    mozContacts = navigator.mozContacts;
+    next();
+  } else {
+    ok(true, "Skip tests on Android < 4.0 (bugs 897924 & 888891");
+    parent.SimpleTest.finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_a_cache.xul
@@ -0,0 +1,168 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<window title="Mozilla Bug 1114520"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <script type="application/javascript;version=1.7">
+  <![CDATA[
+  "use strict";
+
+  const { 'utils': Cu } = Components;
+  Cu.import("resource://gre/modules/ContactDB.jsm", window);
+
+  let contactsDB = new ContactDB();
+  contactsDB.init();
+
+  function makeFailure(reason, skipDelete) {
+    return function() {
+      ok(false, reason);
+      if (skipDelete) {
+        SimpleTest.finish();
+        return;
+      }
+      deleteDatabase(SimpleTest.finish);
+    };
+  };
+
+  function deleteDatabase(then) {
+    contactsDB.close();
+    let req = indexedDB.deleteDatabase(DB_NAME);
+    req.onsuccess = then;
+    req.onblocked = makeFailure("blocked", true);
+    req.onupgradeneeded = makeFailure("onupgradeneeded", true);
+    req.onerror = makeFailure("onerror", true);
+  };
+
+  function checkRevision(expectedRevision, then) {
+    contactsDB.getRevision(function(revision) {
+      ok(expectedRevision === revision, "Revision OK");
+      then();
+    }, makeFailure("Could not get revision"));
+  };
+
+  let CONTACT_PROPS = {
+    id: "ab74671e36be41b680f8f030e7e24ea2",
+    properties: {
+      name: ["Magnificentest foo bar the third"],
+      givenName: ["foo"],
+      familyName: ["bar"]
+    }
+  };
+
+  let ANOTHER_CONTACT_PROPS = {
+    id: "b461d53d548b4e8aaa8256911a415f79",
+    properties: {
+      name: ["Magnificentest foo bar the fourth"],
+      givenName: ["foo"],
+      familyName: ["bar"]
+    }
+  };
+
+  let Tests = [function() {
+    info("Deleting database");
+    deleteDatabase(next);
+  }, function() {
+    info("Checking initial revision");
+    checkRevision(0, next);
+  }, function() {
+    info("Save contact");
+    contactsDB.saveContact(CONTACT_PROPS, function() {
+      ok(true, "Saved contact successfully");
+      checkRevision(1, next);
+    }, makeFailure("Could not save contact"));
+  }, function() {
+    info("Save another contact");
+    contactsDB.saveContact(ANOTHER_CONTACT_PROPS, function() {
+      ok(true, "Saved contact successfully");
+      checkRevision(2, next);
+    }, makeFailure("Could not save contact"));
+  }, function() {
+    info("Get all contacts so cache is built");
+    contactsDB.getAll(function(contacts) {
+      ok(true, "Got all contacts " + contacts.length);
+      next();
+    }, makeFailure("Unexpected error getting contacts"), {
+      "sortBy":"givenName","sortOrder":"ascending"
+    });
+  }, function() {
+    info("Contacts cache should have both ids");
+    let contactsCount = 0;
+    contactsDB.newTxn("readonly", SAVED_GETALL_STORE_NAME, function(txn, store) {
+      store.openCursor().onsuccess = function(e) {
+        let cursor = e.target.result;
+        if (!cursor) {
+          makeFailure("Wrong cache")();
+          return;
+        }
+        ok(cursor.value.length == 2, "Both contacts ids are in the cache");
+        next();
+      };
+    }, null, makeFailure("Txn error"));
+  }, function() {
+    info("Remove contact " + CONTACT_PROPS.id);
+    contactsDB.removeContact(CONTACT_PROPS.id, function() {
+      ok(true, "Removed contact");
+      checkRevision(3, next);
+    }, makeFailure("Unexpected error removing contact "));
+  }, function() {
+    info("Check that contact has been removed for good");
+    contactsDB.newTxn("readonly", STORE_NAME, function(txn, store) {
+      let req = store.openCursor(IDBKeyRange.only(CONTACT_PROPS.id));
+      req.onsuccess = function(event) {
+        if (event.target.result) {
+          makeFailure("Should not have cursor")();
+          return;
+        }
+        ok(true, "Yep, the contact was removed");
+        next();
+      };
+      req.onerror = makeFailure("OpenCursor error");
+    });
+  }, function() {
+    info("Contacts cache should have only one id");
+    contactsDB.newTxn("readonly", SAVED_GETALL_STORE_NAME, function(txn, store) {
+      store.openCursor().onsuccess = function(e) {
+        let cursor = e.target.result;
+        if (!cursor) {
+          makeFailure("Missing cursor")();
+          return;
+        }
+        ok(cursor.value.length == 1, "Only one contacts id is in the cache");
+        ok(cursor.value[0] == ANOTHER_CONTACT_PROPS.id, "And it is the right id");
+        next();
+      };
+    }, null, makeFailure("Txn error"));
+  }, function() {
+    deleteDatabase(next);
+  }];
+
+  function next() {
+    let step = Tests.shift();
+    if (step) {
+      step();
+    } else {
+      info("All done");
+      SimpleTest.finish();
+    }
+  }
+
+  SimpleTest.waitForExplicitFinish();
+
+  next();
+
+  ]]>
+  </script>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114520"
+     target="_blank">Mozilla Bug 1114520</a>
+  </body>
+</window>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_a_shutdown.xul
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<window title="Mozilla Bug 945948"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <script type="application/javascript;version=1.7">
+  <![CDATA[
+  "use strict";
+  const { 'utils': Cu } = Components;
+  Cu.import("resource://gre/modules/ContactService.jsm", window);
+
+  //
+  // Mock message manager
+  //
+  function MockMessageManager() { }
+  MockMessageManager.prototype.assertPermission = function() { return true; };
+  MockMessageManager.prototype.sendAsyncMessage = function(name, data) { };
+
+  //
+  // Mock ContactDB
+  //
+  function MockContactDB() { }
+  MockContactDB.prototype.getAll = function(cb) {
+    cb([]);
+  };
+  MockContactDB.prototype.clearDispatcher = function() { }
+  MockContactDB.prototype.close = function() { }
+
+  let realContactDB = ContactService._db;
+
+  function before() {
+    ok(true, "Install mock ContactDB object");
+    ContactService._db = new MockContactDB();
+  }
+
+  function after() {
+    ok(true, "Restore real ContactDB object");
+    ContactService._db = realContactDB;
+  }
+
+  function steps() {
+    let mm1 = new MockMessageManager();
+    let mm2 = new MockMessageManager();
+
+    is(ContactService._cursors.size, 0, "Verify clean contact init");
+
+    ContactService.receiveMessage({
+      target: mm1,
+      name: "Contacts:GetAll",
+      data: { cursorId: 1 },
+      findOptions: {}
+    });
+    is(ContactService._cursors.size, 1, "Add contact cursor 1");
+
+    ContactService.receiveMessage({
+      target: mm2,
+      name: "Contacts:GetAll",
+      data: { cursorId: 2 },
+      findOptions: {}
+    });
+    is(ContactService._cursors.size, 2, "Add contact cursor 2");
+
+    ContactService.receiveMessage({
+      target: mm1,
+      name: "child-process-shutdown"
+    });
+    is(ContactService._cursors.size, 1, "Shutdown contact cursor 1");
+
+    ContactService.receiveMessage({
+      target: mm2,
+      name: "child-process-shutdown"
+    });
+    is(ContactService._cursors.size, 0, "Shutdown contact cursor 2");
+  }
+
+  function runTests() {
+    SimpleTest.waitForExplicitFinish();
+    try {
+      before();
+      steps();
+    } finally {
+      after();
+      SimpleTest.finish();
+    }
+  }
+
+  runTests();
+  ]]>
+  </script>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=945948"
+     target="_blank">Mozilla Bug 945948</a>
+  </body>
+</window>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_a_upgrade.xul
@@ -0,0 +1,271 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<window title="Mozilla Bug 889239"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <script type="application/javascript;version=1.7">
+  <![CDATA[
+  "use strict";
+
+  function checkStr(str1, str2, msg) {
+    if (str1 ^ str2) {
+      ok(false, "Expected both strings to be either present or absent");
+      return;
+    }
+    is(str1, str2, msg);
+  }
+
+  function checkStrArray(str1, str2, msg) {
+    // comparing /[null(,null)+]/ and undefined should pass
+    function nonNull(e) {
+      return e != null;
+    }
+    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);
+    }
+  }
+
+  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");
+    is(adr1.pref, adr2.pref, "Same 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");
+    is(tel1.pref, tel2.pref, "Same 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");
+    is(field1.pref, field2.pref, "Same pref");
+  }
+
+  function checkDBContacts(dbContact1, dbContact2) {
+    let contact1 = dbContact1.properties;
+    let contact2 = dbContact2.properties;
+
+    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");
+    checkStrArray(contact1.category, contact2.category, "Same 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");
+
+    is(contact1.email.length, contact2.email.length, "Same number of emails");
+    for (let i = 0; i < contact1.email.length; ++i) {
+      checkField(contact1.email[i], contact2.email[i]);
+    }
+
+    is(contact1.adr.length, contact2.adr.length, "Same number of adrs");
+    for (var i in contact1.adr) {
+      checkAddress(contact1.adr[i], contact2.adr[i]);
+    }
+
+    is(contact1.tel.length, contact2.tel.length, "Same number of tels");
+    for (var i in contact1.tel) {
+      checkTel(contact1.tel[i], contact2.tel[i]);
+    }
+
+    is(contact1.url.length, contact2.url.length, "Same number of urls");
+    for (var i in contact1.url) {
+      checkField(contact1.url[i], contact2.url[i]);
+    }
+
+    is(contact1.impp.length, contact2.impp.length, "Same number of impps");
+    for (var i in contact1.impp) {
+      checkField(contact1.impp[i], contact2.impp[i]);
+    }
+
+    // test search indexes
+    contact1 = dbContact1.search;
+    contact2 = dbContact2.search;
+    checkStrArray(contact1.category, contact2.category, "Same cateogry index");
+    checkStrArray(contact1.email, contact2.email, "Same email index");
+    checkStrArray(contact1.emailLowerCase, contact2.emailLowerCase, "Same emailLowerCase index");
+    checkStrArray(contact1.familyName, contact2.familyName, "Same familyName index");
+    checkStrArray(contact1.familyNameLowerCase, contact2.familyNameLowerCase, "Same familyNameLowerCase index");
+    checkStrArray(contact1.givenName, contact2.givenName, "Same givenName index");
+    checkStrArray(contact1.givenNameLowerCase, contact2.givenNameLowerCase, "Same givenNameLowerCase index");
+    checkStrArray(contact1.name, contact2.name, "Same name index");
+    checkStrArray(contact1.tel, contact2.tel, "Same tel index");
+    checkStrArray(contact1.telLowerCase, contact2.telLowerCase, "Same telLowerCase index");
+    checkStrArray(contact1.telMatch, contact2.telMatch, "Same telMatch index");
+  }
+
+  function makeFailure(reason) {
+    return function() {
+      ok(false, reason);
+      SimpleTest.finish();
+    };
+  };
+
+  const { 'utils': Cu } = Components;
+  Cu.import("resource://gre/modules/ContactDB.jsm", window);
+
+  let cdb = new ContactDB();
+  cdb.init();
+
+  let CONTACT_PROPS = {
+    id: "ab74671e36be41b680f8f030e7e24ea2",
+    properties: {
+      name: ["Magnificentest foo bar the third"],
+      givenName: ["foo"],
+      familyName: ["bar"],
+      honorificPrefix: ["magnificentest"],
+      honorificSuffix: ["the third"],
+      additionalName: ["addl"],
+      nickname: ["foo"],
+      tel: [
+        {type: ["mobile"], value: "+12345678901", carrier: "ACME Telecommunications", pref: true},
+        {type: ["home", "custom"], value: "7932012346", pref: false},
+      ],
+      email: [{type: ["work"], value: "a@b.c"}, {value: "b@c.d", pref: true}],
+      adr: [
+        {
+          type: ["home"],
+          streetAddress: "street 1",
+          locality: "locality 1",
+          region: "region 1",
+          postalCode: "postal code 1",
+          countryName: "country 1",
+        }
+      ],
+      impp: [{type: ["aim"], value:"im1", pref: true}, {value: "im2"}],
+      org: ["org1", "org2"],
+      jobTitle: ["boss", "superboss"],
+      bday: new Date("1980, 12, 01"),
+      note: ["bla bla bla"],
+      category: ["cat1", "cat2"],
+      url: [{type: ["work", "work2"], value: "www.1.com", pref: true}, {value: "www2.com"}],
+      anniversary: new Date("2000, 12, 01"),
+      sex: "male",
+      genderIdentity: "trisexual",
+      key: "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAC4jAAAuIwF4pT92AAACrElEQVQozwXBTW8bRRgA4Hfemf1er7/iJI4Tq7VFlEZN1VZIlapy4MQBTkXcuSH+G/APKnGAAyCVCqmtCHETp64db5zdtdf7NbMzw/OQH378HkCZpmmapqYMy8yrNnadS6026HC/Z7k+SCkEBwKEEKaUQtQAmlDqrucH23nH4BRkJVRcwmod5gcn6LehFgCaEIIalFZaEcLCq73w355RdvY7nfGQGVTlmRXfqMlrUaSUMUQkhCISJIggKj3/YBHt7PRbpy+cwbF7dN/0vEqTMoo3s0tmGAAAoJAgImMq3xZ5WTPbHj4Mho8Nf+QcPtZBLxEkqeQ2WmklkRCtNdNaI1KpVCnqOC3j5ZK++4vnm6xSWZpzwQtRV2mOiBoRpEKtNQAQggjQcCwqinRxJeKlWW93dlqEsa2QRZbF85nWBAAZY4YUgl9fRJWKVuWgmhwHhpD1+ZrfVjAN867rMCne//rq7OuXjWaLCVHnOWHgFDwMw+Tvi09PdhtJXoVC7bWDIi8Lg8qyMk3rYjLzvJh2O30hwK6TpiG7zWDcck9GR17D9wxDcH7/oNtElRa1aZuLDJN4S7/87tssLVg0/eZs/3h0D5R89vR0v+1AVT0YHX31ZDy9uv7IeJrryeyu2+nS50/PqOXM5qt8Nf/jv08UwTfN27vkchldLpPf/nx/nqSz5sbzhkTYzLRppzNYre/ycrMIZwqsHdf96fd/Xr354AYBr/jESWhgGb6zVSuGrrQS1j4Zk8nc2Hs7frFb3Phc6+fOKDGLKOJTHvlj2u85N4t6vbw7OM4YRVquboPdsPNZ9eb8pvfAOf2iN4dN3EzWadnoO5JY19Oo0TYtw1t8TBqBR9v7wbOXROLWtZ3PH937+ZfXrb6BUHEbXL+FCIfDw92e5zebg8GR54r/AaMVcBxE6hgPAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDEyLTA3LTIxVDEwOjUzOjE5LTA0OjAwYyXbYgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxMi0wNy0yMVQxMDo1MzoxOS0wNDowMBJ4Y94AAAARdEVYdGpwZWc6Y29sb3JzcGFjZQAyLHVVnwAAACB0RVh0anBlZzpzYW1wbGluZy1mYWN0b3IAMXgxLDF4MSwxeDHplfxwAAAAAElFTkSuQmCC"
+    }
+  };
+
+  function deleteDatabase(then) {
+    cdb.close();
+    let req = indexedDB.deleteDatabase(DB_NAME);
+    req.onsuccess = then;
+    req.onblocked = makeFailure("blocked");
+    req.onupgradeneeded = makeFailure("onupgradeneeded");
+    req.onerror = makeFailure("onerror");
+  }
+
+  function saveContact() {
+    // takes fast upgrade path
+    cdb.saveContact(CONTACT_PROPS,
+      function() {
+        ok(true, "Saved contact successfully");
+        next();
+      }
+    );
+  }
+
+  function getContact(callback) {
+    return function() {
+      let req = indexedDB.open(STORE_NAME, DB_VERSION);
+      req.onsuccess = function(event) {
+        let db = event.target.result;
+        let txn = db.transaction([STORE_NAME], "readonly");
+        txn.onabort = makeFailure("Failed to open transaction");
+        let r2 = txn.objectStore(STORE_NAME).get(CONTACT_PROPS.id);
+        r2.onsuccess = function() {
+          db.close();
+          callback(r2.result);
+        };
+        r2.onerror = makeFailure("Failed to get contact");
+      };
+    };
+  }
+
+  let savedContact;
+
+  let Tests = [
+    saveContact,
+
+    getContact(function(contact) {
+      savedContact = contact;
+      next();
+    }),
+
+    function() {
+      deleteDatabase(function() {
+        info("slow upgrade");
+        cdb.useFastUpgrade = false;
+        cdb.init();
+        next();
+      });
+    },
+
+    saveContact,
+
+    getContact(function(contact) {
+      checkDBContacts(savedContact, contact);
+      next();
+    }),
+  ];
+
+  function next() {
+    let step = Tests.shift();
+    if (step) {
+      step();
+    } else {
+      info("All done");
+      SimpleTest.finish();
+    }
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  next();
+
+  ]]>
+  </script>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=889239"
+     target="_blank">Mozilla Bug 889239</a>
+  </body>
+</window>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_basics.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_basics.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_basics2.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_basics2.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_blobs.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_blobs.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_events.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_events.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_getall.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_getall.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_getall2.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_getall2.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_international.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_international.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_substringmatching.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_substringmatching.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_substringmatchingCL.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_substringmatchingCL.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_substringmatchingVE.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_contacts_substringmatchingVE.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_migration.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+function run_tests() {
+  var iframe = document.querySelector("iframe");
+  iframe.src = "file_migration.html";
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = run_tests;
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_migration_chrome.js
@@ -0,0 +1,336 @@
+/* global
+    sendAsyncMessage,
+    addMessageListener,
+    indexedDB
+ */
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+var imports = {};
+
+Cu.import("resource://gre/modules/ContactDB.jsm", imports);
+Cu.import("resource://gre/modules/ContactService.jsm", imports);
+Cu.import("resource://gre/modules/Promise.jsm", imports);
+Cu.importGlobalProperties(["indexedDB"]);
+
+const {
+  STORE_NAME,
+  SAVED_GETALL_STORE_NAME,
+  REVISION_STORE,
+  DB_NAME,
+  ContactService,
+} = imports;