Merge b2g-inbound to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 08 Jul 2014 16:34:48 -0700
changeset 192918 ff4e6d5629039b2d5fd6363d79c1f8187e75f2cd
parent 192867 d35c1bf0f084b965970ccf3b9a1393082012b8dd (current diff)
parent 192917 e8982b4f59a92562e6b1e57b48bff34b0e2ba2e9 (diff)
child 192919 48de6f4f82af3a465d1dc84dbb0ba00dd4c912e1
child 192939 196d05832e121b29c0b6c975675d2a8c612ceff0
child 192947 087dcd6f346ac9d7945f763805316d02fd4838e7
child 193054 79b1fc2308b7be3fdc24a0988d60e66dd33472c0
push id7663
push userkwierso@gmail.com
push dateWed, 09 Jul 2014 03:08:08 +0000
treeherderfx-team@48de6f4f82af [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
Merge b2g-inbound to m-c a=merge
dom/nfc/tests/marionette/test_nfc_tag.js
dom/permission/PermissionPromptHelper.jsm
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -4,17 +4,16 @@
  * 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/. */
 
 Cu.import('resource://gre/modules/ContactService.jsm');
 Cu.import('resource://gre/modules/SettingsChangeNotifier.jsm');
 Cu.import('resource://gre/modules/DataStoreChangeNotifier.jsm');
 Cu.import('resource://gre/modules/AlarmService.jsm');
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
-Cu.import('resource://gre/modules/PermissionPromptHelper.jsm');
 Cu.import('resource://gre/modules/NotificationDB.jsm');
 Cu.import('resource://gre/modules/Payment.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');
 #ifdef MOZ_WIDGET_GONK
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,23 +14,23 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="bf9aaf39dd5a6491925a022db167c460f8207d34"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
@@ -123,14 +123,14 @@
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
   <!-- Emulator specific things -->
   <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
   <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="0e31f35a2a77301e91baa8a237aa9e9fa4076084"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
-  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="efd87a5797ca40fa2df256630c07e0dfb2f762dc"/>
+  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c61e5f15fd62888f2c33d7d542b5b65c38102e8b"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="832f4acaf481a19031e479a40b03d9ce5370ddee"/>
   <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="dd72bacb432efc5135a1f747d00aab91f898bddb"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="8b1365af38c9a653df97349ee53a3f5d64fd590a"/>
 </manifest>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="51da9b1981be481b92a59a826d4d78dc73d0989a"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,23 +14,23 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="bf9aaf39dd5a6491925a022db167c460f8207d34"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "3e612bc9b3d79a3fa36d2f38af4202abb0ead68f", 
+    "revision": "190172ac413ab6476a6d7df3999950ec756f96a4", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
   <project name="platform/development" path="development" revision="2460485184bc8535440bb63876d4e63ec1b4770c"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="740faa5d0060fb218b407cf224330654ddf833a5"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dde24450450514039bad6d8ab4fcb7e5d4d44e03"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="ee6e7320bb83409ebd4685fbd87a8ae033704182"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="e0a9ac010df3afaa47ba107192c05ac8b5516435"/>
   <project name="platform/development" path="development" revision="a384622f5fcb1d2bebb9102591ff7ae91fe8ed2d"/>
   <project name="device/common" path="device/common" revision="7c65ea240157763b8ded6154a17d3c033167afb7"/>
   <project name="device/sample" path="device/sample" revision="c328f3d4409db801628861baa8d279fb8855892f"/>
--- a/dom/apps/src/PermissionsInstaller.jsm
+++ b/dom/apps/src/PermissionsInstaller.jsm
@@ -162,17 +162,17 @@ this.PermissionsInstaller = {
                 ? PermissionsTable[permName]["certified"]
                 : PermissionsTable[permName][appStatus];
 
           let permValue = PERM_TO_STRING[permission];
           if (!aIsSystemUpdate && isPromptPermission) {
             // If it's not a system update, then we should keep the prompt
             // permissions that have been granted or denied previously.
             permValue =
-              PermissionSettingsModule.getPermission(permName,
+              PermissionSettingsModule.getPermission(expandedPermNames[idx],
                                                      aApp.manifestURL,
                                                      aApp.origin,
                                                      false);
             if (permValue === "unknown") {
               permValue = PERM_TO_STRING[permission];
             }
           }
 
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -158,18 +158,24 @@ this.PermissionsTable =  { geolocation: 
                            },
                            fmradio: {
                              app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
                            attention: {
                              app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "moz-attention": {
+                             app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
-                             certified: ALLOW_ACTION
+                             certified: ALLOW_ACTION,
+                             substitute: ["attention"]
                            },
                            "webapps-manage": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "backgroundservice": {
                              app: DENY_ACTION,
@@ -262,23 +268,35 @@ this.PermissionsTable =  { geolocation: 
                            },
                            "audio-channel-alarm": {
                              app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "audio-channel-telephony": {
                              app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "moz-audio-channel-telephony": {
+                             app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
-                             certified: ALLOW_ACTION
+                             certified: ALLOW_ACTION,
+                             substitute: ["audio-channel-telephony"]
                            },
                            "audio-channel-ringer": {
                              app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "moz-audio-channel-ringer": {
+                             app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
-                             certified: ALLOW_ACTION
+                             certified: ALLOW_ACTION,
+                             substitute: ["audio-channel-ringer"]
                            },
                            "audio-channel-publicnotification": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "open-remote-window": {
                              app: DENY_ACTION,
@@ -335,16 +353,25 @@ this.PermissionsTable =  { geolocation: 
                              app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "mobileid": {
                              app: DENY_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: PROMPT_ACTION
+                           },
+                           // This permission doesn't actually grant access to
+                           // anything. It exists only to check the correctness
+                           // of web prompt composed permissions in tests.
+                           "test-permission": {
+                             app: PROMPT_ACTION,
+                             privileged: PROMPT_ACTION,
+                             certified: ALLOW_ACTION,
+                             access: ["read", "write", "create"]
                            }
                          };
 
 /**
  * Append access modes to the permission name as suffixes.
  *   e.g. permission name 'contacts' with ['read', 'write'] =
  *   ['contacts-read', contacts-write']
  * @param string aPermName
--- a/dom/apps/tests/file_packaged_app.template.webapp
+++ b/dom/apps/tests/file_packaged_app.template.webapp
@@ -3,16 +3,17 @@
   "version" : "VERSIONTOKEN",
   "size" : PACKAGESIZETOKEN,
   "package_path": "PACKAGEPATHTOKEN",
   "description": "Updated even faster than Firefox, just to annoy slashdotters",
   "permissions": {
      "geolocation": {},
      "audio-capture": {},
      "video-capture": {},
+     "test-permission": {"access": "readonly"},
      "downloads": {}
    },
   "launch_path": "tests/dom/apps/tests/file_packaged_app.sjs",
   "developer": {
     "name": "DEVELOPERTOKEN",
     "url": "DEVELOPERURLTOKEN"
   },
   "default_locale": "en-US"
--- a/dom/apps/tests/test_packaged_app_update.html
+++ b/dom/apps/tests/test_packaged_app_update.html
@@ -90,28 +90,31 @@ function updateApp(aExpectedReady, aPrev
                  null, true);
 
 }
 
 var initialPermissionState = {
   "geolocation": "prompt",
   "audio-capture": "prompt",
   "video-capture": "prompt",
+  "test-permission-read": "prompt",
   "downloads": "deny"
 }
 
 var permissionsToSet = {
   "geolocation": "allow",
+  "test-permission-read": "allow",
   "audio-capture": "deny"
 }
 
 var permissionsToCheck = {
   "geolocation": "allow",
   "audio-capture": "deny",
   "video-capture": "prompt",
+  "test-permission-read": "allow",
   "downloads": "deny"
 }
 
 function validatePermissions(aList, aDontFail) {
   var gApp = PackagedTestHelper.gApp;
   var mozPermissions = window.navigator.mozPermissionSettings;
   var permission;
   for (permission in aList) {
--- a/dom/base/nsContentPermissionHelper.cpp
+++ b/dom/base/nsContentPermissionHelper.cpp
@@ -8,16 +8,17 @@
 #endif // MOZ_WIDGET_GONK
 #include "nsCOMPtr.h"
 #include "nsIDOMElement.h"
 #include "nsIPrincipal.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/PContentPermission.h"
 #include "mozilla/dom/PermissionMessageUtils.h"
 #include "mozilla/dom/PContentPermissionRequestParent.h"
+#include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/TabParent.h"
 #include "mozilla/unused.h"
 #include "nsComponentManagerUtils.h"
 #include "nsArrayUtils.h"
 #include "nsIMutableArray.h"
 #include "nsContentPermissionHelper.h"
 #include "nsCxPusher.h"
 #include "nsJSUtils.h"
@@ -353,8 +354,134 @@ nsContentPermissionRequestProxy::Allow(J
     MOZ_ASSERT(false, "SelectedChoices should be undefined or an JS object");
     return NS_ERROR_FAILURE;
   }
 
   unused << ContentPermissionRequestParent::Send__delete__(mParent, true, choices);
   mParent = nullptr;
   return NS_OK;
 }
+
+// RemotePermissionRequest
+
+// static
+uint32_t
+RemotePermissionRequest::ConvertArrayToPermissionRequest(
+                                nsIArray* aSrcArray,
+                                nsTArray<PermissionRequest>& aDesArray)
+{
+  uint32_t len = 0;
+  aSrcArray->GetLength(&len);
+  for (uint32_t i = 0; i < len; i++) {
+    nsCOMPtr<nsIContentPermissionType> cpt = do_QueryElementAt(aSrcArray, i);
+    nsAutoCString type;
+    nsAutoCString access;
+    cpt->GetType(type);
+    cpt->GetAccess(access);
+
+    nsCOMPtr<nsIArray> optionArray;
+    cpt->GetOptions(getter_AddRefs(optionArray));
+    uint32_t optionsLength = 0;
+    if (optionArray) {
+      optionArray->GetLength(&optionsLength);
+    }
+    nsTArray<nsString> options;
+    for (uint32_t j = 0; j < optionsLength; ++j) {
+      nsCOMPtr<nsISupportsString> isupportsString = do_QueryElementAt(optionArray, j);
+      if (isupportsString) {
+        nsString option;
+        isupportsString->GetData(option);
+        options.AppendElement(option);
+      }
+    }
+
+    aDesArray.AppendElement(PermissionRequest(type, access, options));
+  }
+  return len;
+}
+
+NS_IMPL_ISUPPORTS(RemotePermissionRequest, nsIContentPermissionRequest)
+
+RemotePermissionRequest::RemotePermissionRequest(
+  nsIContentPermissionRequest* aRequest,
+  nsPIDOMWindow* aWindow)
+  : mRequest(aRequest)
+  , mWindow(aWindow)
+{
+}
+
+// nsIContentPermissionRequest methods
+NS_IMETHODIMP
+RemotePermissionRequest::GetTypes(nsIArray** aTypes)
+{
+  NS_ASSERTION(mRequest, "We need a request");
+  return mRequest->GetTypes(aTypes);
+}
+
+NS_IMETHODIMP
+RemotePermissionRequest::GetPrincipal(nsIPrincipal **aRequestingPrincipal)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingPrincipal);
+
+  return mRequest->GetPrincipal(aRequestingPrincipal);
+}
+
+NS_IMETHODIMP
+RemotePermissionRequest::GetWindow(nsIDOMWindow** aRequestingWindow)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingWindow);
+
+  return mRequest->GetWindow(aRequestingWindow);
+}
+
+NS_IMETHODIMP
+RemotePermissionRequest::GetElement(nsIDOMElement** aRequestingElement)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingElement);
+  *aRequestingElement = nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+RemotePermissionRequest::Cancel()
+{
+  NS_ASSERTION(mRequest, "We need a request");
+  return mRequest->Cancel();
+}
+
+NS_IMETHODIMP
+RemotePermissionRequest::Allow(JS::HandleValue aChoices)
+{
+  NS_ASSERTION(mRequest, "We need a request");
+  return mRequest->Allow(aChoices);
+}
+
+// PCOMContentPermissionRequestChild
+bool
+RemotePermissionRequest::Recv__delete__(const bool& aAllow,
+                                        const nsTArray<PermissionChoice>& aChoices)
+{
+  if (aAllow && mWindow->IsCurrentInnerWindow()) {
+    // Convert choices to a JS val if any.
+    // {"type1": "choice1", "type2": "choiceA"}
+    AutoJSAPI jsapi;
+    if (NS_WARN_IF(!jsapi.Init(mWindow))) {
+      return true; // This is not an IPC error.
+    }
+    JSContext* cx = jsapi.cx();
+    JS::Rooted<JSObject*> obj(cx);
+    obj = JS_NewObject(cx, nullptr, JS::NullPtr(), JS::NullPtr());
+    for (uint32_t i = 0; i < aChoices.Length(); ++i) {
+      const nsString& choice = aChoices[i].choice();
+      const nsCString& type = aChoices[i].type();
+      JS::Rooted<JSString*> jChoice(cx, JS_NewUCStringCopyN(cx, choice.get(), choice.Length()));
+      JS::Rooted<JS::Value> vChoice(cx, StringValue(jChoice));
+      if (!JS_SetProperty(cx, obj, type.get(), vChoice)) {
+        return false;
+      }
+    }
+    JS::RootedValue val(cx, JS::ObjectValue(*obj));
+    (void) Allow(val);
+  } else {
+    (void) Cancel();
+  }
+  return true;
+}
--- a/dom/base/nsContentPermissionHelper.h
+++ b/dom/base/nsContentPermissionHelper.h
@@ -3,17 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef nsContentPermissionHelper_h
 #define nsContentPermissionHelper_h
 
 #include "nsIContentPermissionPrompt.h"
 #include "nsTArray.h"
 #include "nsIMutableArray.h"
+#include "PCOMContentPermissionRequestChild.h"
 
+class nsPIDOMWindow;
 class nsContentPermissionRequestProxy;
 
 // Forward declare IPC::Principal here which is defined in
 // PermissionMessageUtils.h. Include this file will transitively includes
 // "windows.h" and it defines
 //   #define CreateEvent CreateEventW
 //   #define LoadImage LoadImageW
 // That will mess up windows build.
@@ -78,9 +80,37 @@ class nsContentPermissionRequestProxy : 
  private:
   virtual ~nsContentPermissionRequestProxy();
 
   // Non-owning pointer to the ContentPermissionRequestParent object which owns this proxy.
   mozilla::dom::ContentPermissionRequestParent* mParent;
   nsTArray<mozilla::dom::PermissionRequest> mPermissionRequests;
 };
 
+/**
+ * RemotePermissionRequest will send a prompt ipdl request to b2g process.
+ */
+class RemotePermissionRequest : public nsIContentPermissionRequest
+                              , public PCOMContentPermissionRequestChild
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSICONTENTPERMISSIONREQUEST
+
+  RemotePermissionRequest(nsIContentPermissionRequest* aRequest,
+                          nsPIDOMWindow* aWindow);
+
+  // It will be called when prompt dismissed.
+  virtual bool Recv__delete__(const bool &aAllow,
+                              const nsTArray<PermissionChoice>& aChoices) MOZ_OVERRIDE;
+  virtual void IPDLRelease() MOZ_OVERRIDE { Release(); }
+
+  static uint32_t ConvertArrayToPermissionRequest(
+                                nsIArray* aSrcArray,
+                                nsTArray<PermissionRequest>& aDesArray);
+private:
+  virtual ~RemotePermissionRequest() {}
+
+  nsCOMPtr<nsIContentPermissionRequest> mRequest;
+  nsCOMPtr<nsPIDOMWindow>               mWindow;
+};
+
 #endif // nsContentPermissionHelper_h
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -65,16 +65,17 @@
 #include "mozilla/layers/ShadowLayers.h"
 
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/dom/IDBFactoryBinding.h"
 #include "mozilla/dom/indexedDB/IndexedDatabaseManager.h"
 #include "mozilla/dom/MutableFile.h"
 #include "mozilla/dom/MutableFileBinding.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
 #include "mozilla/dom/quota/PersistenceType.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "nsDOMBlobBuilder.h"
 #include "nsPrintfCString.h"
 #include "nsViewportInfo.h"
 #include "nsIFormControl.h"
 #include "nsIScriptError.h"
 #include "nsIAppShell.h"
@@ -83,16 +84,17 @@
 #include "nsDisplayList.h"
 #include "nsROCSSPrimitiveValue.h"
 #include "nsIBaseWindow.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "GeckoProfiler.h"
 #include "mozilla/Preferences.h"
 #include "nsIContentIterator.h"
+#include "nsContentPermissionHelper.h"
 
 #ifdef XP_WIN
 #undef GetClassName
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::layers;
@@ -3672,16 +3674,59 @@ nsDOMWindowUtils::SetAudioVolume(float a
 
 NS_IMETHODIMP
 nsDOMWindowUtils::XpconnectArgument(nsIDOMWindowUtils* aThis)
 {
   // Do nothing.
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDOMWindowUtils::AskPermission(nsIContentPermissionRequest* aRequest)
+{
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryReferent(mWindow);
+  nsRefPtr<RemotePermissionRequest> req =
+    new RemotePermissionRequest(aRequest, window->GetCurrentInnerWindow());
+
+    // for content process
+  if (XRE_GetProcessType() == GeckoProcessType_Content) {
+    MOZ_ASSERT(NS_IsMainThread()); // IPC can only be execute on main thread.
+
+    dom::TabChild* child = dom::TabChild::GetFrom(window->GetDocShell());
+    NS_ENSURE_TRUE(child, NS_ERROR_FAILURE);
+
+    nsCOMPtr<nsIArray> typeArray;
+    nsresult rv = req->GetTypes(getter_AddRefs(typeArray));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsTArray<PermissionRequest> permArray;
+    RemotePermissionRequest::ConvertArrayToPermissionRequest(typeArray, permArray);
+
+    nsCOMPtr<nsIPrincipal> principal;
+    rv = req->GetPrincipal(getter_AddRefs(principal));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    req->AddRef();
+    child->SendPContentPermissionRequestConstructor(req,
+                                                    permArray,
+                                                    IPC::Principal(principal));
+
+    req->Sendprompt();
+    return NS_OK;
+  }
+
+  // for chrome process
+  nsCOMPtr<nsIContentPermissionPrompt> prompt =
+    do_GetService(NS_CONTENT_PERMISSION_PROMPT_CONTRACTID);
+  if (prompt) {
+    prompt->Prompt(req);
+  }
+  return NS_OK;
+}
+
 NS_INTERFACE_MAP_BEGIN(nsTranslationNodeList)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
   NS_INTERFACE_MAP_ENTRY(nsITranslationNodeList)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_ADDREF(nsTranslationNodeList)
 NS_IMPL_RELEASE(nsTranslationNodeList)
 
--- a/dom/bluetooth2/tests/marionette/head.js
+++ b/dom/bluetooth2/tests/marionette/head.js
@@ -337,17 +337,17 @@ function setBluetoothEnabled(aEnabled) {
   let obj = {};
   obj["bluetooth.enabled"] = aEnabled;
   return setSettings(obj);
 }
 
 /**
  * Wait for one named BluetoothManager event.
  *
- * Resolve if that named event occurs.  Never reject.
+ * Resolve if that named event occurs. Never reject.
  *
  * Fulfill params: the DOMEvent passed.
  *
  * @param aEventName
  *        The name of the EventHandler.
  *
  * @return A deferred promise.
  */
@@ -362,17 +362,17 @@ function waitForManagerEvent(aEventName)
   });
 
   return deferred.promise;
 }
 
 /**
  * Wait for one named BluetoothAdapter event.
  *
- * Resolve if that named event occurs.  Never reject.
+ * Resolve if that named event occurs. Never reject.
  *
  * Fulfill params: the DOMEvent passed.
  *
  * @param aAdapter
  *        The BluetoothAdapter you want to use.
  * @param aEventName
  *        The name of the EventHandler.
  *
@@ -387,16 +387,128 @@ function waitForAdapterEvent(aAdapter, a
     ok(true, "BluetoothAdapter event '" + aEventName + "' got.");
     deferred.resolve(aEvent);
   });
 
   return deferred.promise;
 }
 
 /**
+ * Wait for 'onattributechanged' events for state changes of BluetoothAdapter
+ * with specified order.
+ *
+ * Resolve if those expected events occur in order. Never reject.
+ *
+ * Fulfill params: an array which contains every changed attributes during
+ *                 the waiting.
+ *
+ * @param aAdapter
+ *        The BluetoothAdapter you want to use.
+ * @param aStateChangesInOrder
+ *        An array which contains an expected order of BluetoothAdapterState.
+ *        Example 1: [enabling, enabled]
+ *        Example 2: [disabling, disabled]
+ *
+ * @return A deferred promise.
+ */
+function waitForAdapterStateChanged(aAdapter, aStateChangesInOrder) {
+  let deferred = Promise.defer();
+
+  let stateIndex = 0;
+  let prevStateIndex = 0;
+  let statesArray = [];
+  let changedAttrs = [];
+  aAdapter.onattributechanged = function(aEvent) {
+    for (let i in aEvent.attrs) {
+      changedAttrs.push(aEvent.attrs[i]);
+      switch (aEvent.attrs[i]) {
+        case "state":
+          log("  'state' changed to " + aAdapter.state);
+
+          // Received state change order may differ from expected one even though
+          // state changes in expected order, because the value of state may change
+          // again before we receive prior 'onattributechanged' event.
+          //
+          // For example, expected state change order [A,B,C] may result in
+          // received ones:
+          // - [A,C,C] if state becomes C before we receive 2nd 'onattributechanged'
+          // - [B,B,C] if state becomes B before we receive 1st 'onattributechanged'
+          // - [C,C,C] if state becomes C before we receive 1st 'onattributechanged'
+          // - [A,B,C] if all 'onattributechanged' are received in perfect timing
+          //
+          // As a result, we ensure only following conditions instead of exactly
+          // matching received and expected state change order.
+          // - Received state change order never reverse expected one. For example,
+          //   [B,A,C] should never occur with expected state change order [A,B,C].
+          // - The changed value of state in received state change order never
+          //   appears later than that in expected one. For example, [A,A,C] should
+          //   never occur with expected state change order [A,B,C].
+          let stateIndex = aStateChangesInOrder.indexOf(aAdapter.state);
+          if (stateIndex >= prevStateIndex && stateIndex + 1 > statesArray.length) {
+            statesArray.push(aAdapter.state);
+            prevStateIndex = stateIndex;
+
+            if (statesArray.length == aStateChangesInOrder.length) {
+              aAdapter.onattributechanged = null;
+              ok(true, "BluetoothAdapter event 'onattributechanged' got.");
+              deferred.resolve(changedAttrs);
+            }
+          } else {
+            ok(false, "The order of 'onattributechanged' events is unexpected.");
+          }
+
+          break;
+        case "name":
+          log("  'name' changed to " + aAdapter.name);
+          if (aAdapter.state == "enabling") {
+            isnot(aAdapter.name, "", "adapter.name");
+          }
+          else if (aAdapter.state == "disabling") {
+            is(aAdapter.name, "", "adapter.name");
+          }
+          break;
+        case "address":
+          log("  'address' changed to " + aAdapter.address);
+          if (aAdapter.state == "enabling") {
+            isnot(aAdapter.address, "", "adapter.address");
+          }
+          else if (aAdapter.state == "disabling") {
+            is(aAdapter.address, "", "adapter.address");
+          }
+          break;
+        case "discoverable":
+          log("  'discoverable' changed to " + aAdapter.discoverable);
+          if (aAdapter.state == "enabling") {
+            is(aAdapter.discoverable, true, "adapter.discoverable");
+          }
+          else if (aAdapter.state == "disabling") {
+            is(aAdapter.discoverable, false, "adapter.discoverable");
+          }
+          break;
+        case "discovering":
+          log("  'discovering' changed to " + aAdapter.discovering);
+          if (aAdapter.state == "enabling") {
+            is(aAdapter.discovering, true, "adapter.discovering");
+          }
+          else if (aAdapter.state == "disabling") {
+            is(aAdapter.discovering, false, "adapter.discovering");
+          }
+          break;
+        case "unknown":
+        default:
+          ok(false, "Unknown attribute '" + aEvent.attrs[i] + "' changed." );
+          break;
+      }
+    }
+  };
+
+  return deferred.promise;
+}
+
+/**
  * Flush permission settings and call |finish()|.
  */
 function cleanUp() {
   waitFor(function() {
     SpecialPowers.flushPermissions(function() {
       // Use ok here so that we have at least one test run.
       ok(true, "permissions flushed");
 
@@ -412,17 +524,17 @@ function startBluetoothTestBase(aPermiss
     .then(aTestCaseMain)
     .then(cleanUp, function() {
       ok(false, "Unhandled rejected promise.");
       cleanUp();
     });
 }
 
 function startBluetoothTest(aReenable, aTestCaseMain) {
-  startBluetoothTestBase(["settings-read", "settings-write"], function() {
+  startBluetoothTestBase([], function() {
     let origEnabled, needEnable;
     return Promise.resolve()
       .then(function() {
         origEnabled = getBluetoothEnabled();
 
         needEnable = !origEnabled;
         log("Original state of bluetooth is " + bluetoothManager.defaultAdapter.state);
 
--- a/dom/bluetooth2/tests/marionette/manifest.ini
+++ b/dom/bluetooth2/tests/marionette/manifest.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 b2g = true
 browser = false
 qemu = false
 
 [test_dom_BluetoothManager_API2.js]
+[test_dom_BluetoothAdapter_enable_API2.js]
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth2/tests/marionette/test_dom_BluetoothAdapter_enable_API2.js
@@ -0,0 +1,76 @@
+/* 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/. */
+
+///////////////////////////////////////////////////////////////////////////////
+// Test Purpose:
+//   To verify that enable/disable process of BluetoothAdapter is correct.
+//
+// Test Procedure:
+//   [0] Set Bluetooth permission and enable default adapter.
+//   [1] Disable Bluetooth and check the correctness of 'onattributechanged'.
+//   [2] Enable Bluetooth and check the correctness of 'onattributechanged'.
+//
+// Test Coverage:
+//   - BluetoothAdapter.enable()
+//   - BluetoothAdapter.disable()
+//   - BluetoothAdapter.onattributechanged()
+//   - BluetoothAdapter.address
+//   - BluetoothAdapter.state
+//
+///////////////////////////////////////////////////////////////////////////////
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+startBluetoothTest(true, function testCaseMain(aAdapter) {
+  log("Checking adapter attributes ...");
+
+  is(aAdapter.state, "enabled", "adapter.state");
+  isnot(aAdapter.address, "", "adapter.address");
+
+  // Since adapter has just been re-enabled, these properties should be 'false'.
+  is(aAdapter.discovering, false, "adapter.discovering");
+  is(aAdapter.discoverable, false, "adapter.discoverable");
+  // TODO: Check the correctness of name and address if we use emulator.
+  // is(aAdapter.name, EMULATOR_NAME, "adapter.name");
+  // is(aAdapter.address, EMULATOR_ADDRESS, "adapter.address");
+
+  log("  adapter.address: " + aAdapter.address);
+  log("  adapter.name: " + aAdapter.name);
+
+  let originalAddr = aAdapter.address;
+  let originalName = aAdapter.name;
+
+  return Promise.resolve()
+    .then(function() {
+      log("[1] Disable Bluetooth and check the correctness of 'onattributechanged'");
+      let promises = [];
+      promises.push(waitForAdapterStateChanged(aAdapter, ["disabling", "disabled"]));
+      promises.push(aAdapter.disable());
+      return Promise.all(promises);
+    })
+    .then(function(aResults) {
+      isnot(aResults[0].indexOf("address"), -1, "Indicator of 'address' changed event");
+      if (originalName != "") {
+        isnot(aResults[0].indexOf("name"), -1, "Indicator of 'name' changed event");
+      }
+      is(aAdapter.address, "", "adapter.address");
+      is(aAdapter.name, "", "adapter.name");
+    })
+    .then(function() {
+      log("[2] Enable Bluetooth and check the correctness of 'onattributechanged'");
+      let promises = [];
+      promises.push(waitForAdapterStateChanged(aAdapter, ["enabling", "enabled"]));
+      promises.push(aAdapter.enable());
+      return Promise.all(promises);
+    })
+    .then(function(aResults) {
+      isnot(aResults[0].indexOf("address"), -1, "Indicator of 'address' changed event");
+      if (originalName != "") {
+        isnot(aResults[0].indexOf("name"), -1, "Indicator of 'name' changed event");
+      }
+      is(aAdapter.address, originalAddr, "adapter.address");
+      is(aAdapter.name, originalName, "adapter.name");
+    })
+});
--- a/dom/cellbroadcast/tests/marionette/head.js
+++ b/dom/cellbroadcast/tests/marionette/head.js
@@ -1,15 +1,160 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const {Cc: Cc, Ci: Ci, Cr: Cr, Cu: Cu} = SpecialPowers;
 
 let Promise = Cu.import("resource://gre/modules/Promise.jsm").Promise;
 
+const PDU_DCS_CODING_GROUP_BITS          = 0xF0;
+const PDU_DCS_MSG_CODING_7BITS_ALPHABET  = 0x00;
+const PDU_DCS_MSG_CODING_8BITS_ALPHABET  = 0x04;
+const PDU_DCS_MSG_CODING_16BITS_ALPHABET = 0x08;
+
+const PDU_DCS_MSG_CLASS_BITS             = 0x03;
+const PDU_DCS_MSG_CLASS_NORMAL           = 0xFF;
+const PDU_DCS_MSG_CLASS_0                = 0x00;
+const PDU_DCS_MSG_CLASS_ME_SPECIFIC      = 0x01;
+const PDU_DCS_MSG_CLASS_SIM_SPECIFIC     = 0x02;
+const PDU_DCS_MSG_CLASS_TE_SPECIFIC      = 0x03;
+const PDU_DCS_MSG_CLASS_USER_1           = 0x04;
+const PDU_DCS_MSG_CLASS_USER_2           = 0x05;
+
+const GECKO_SMS_MESSAGE_CLASSES = {};
+GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]       = "normal";
+GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]            = "class-0";
+GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_ME_SPECIFIC]  = "class-1";
+GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_SIM_SPECIFIC] = "class-2";
+GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_TE_SPECIFIC]  = "class-3";
+GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_1]       = "user-1";
+GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_2]       = "user-2";
+
+const CB_MESSAGE_SIZE_GSM  = 88;
+const CB_MESSAGE_SIZE_ETWS = 56;
+
+const CB_GSM_MESSAGEID_ETWS_BEGIN = 0x1100;
+const CB_GSM_MESSAGEID_ETWS_END   = 0x1107;
+
+const CB_GSM_GEOGRAPHICAL_SCOPE_NAMES = [
+  "cell-immediate",
+  "plmn",
+  "location-area",
+  "cell"
+];
+
+const CB_ETWS_WARNING_TYPE_NAMES = [
+  "earthquake",
+  "tsunami",
+  "earthquake-tsunami",
+  "test",
+  "other"
+];
+
+const CB_DCS_LANG_GROUP_1 = [
+  "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi",
+  "no", "el", "tr", "hu", "pl", null
+];
+const CB_DCS_LANG_GROUP_2 = [
+  "cs", "he", "ar", "ru", "is", null, null, null, null, null,
+  null, null, null, null, null, null
+];
+
+/**
+ * Compose input number into specified number of semi-octets.
+ *
+ * @param: aNum
+ *         The number to be converted.
+ * @param: aNumSemiOctets
+ *         Number of semi-octects to be composed to.
+ *
+ * @return The composed Hex String.
+ */
+function buildHexStr(aNum, aNumSemiOctets) {
+  let str = aNum.toString(16);
+  ok(str.length <= aNumSemiOctets);
+  while (str.length < aNumSemiOctets) {
+    str = "0" + str;
+  }
+  return str;
+}
+
+/**
+ * Helper function to decode the given DCS into encoding type, language,
+ * language indicator and message class.
+ *
+ * @param: aDcs
+ *         The DCS to be decoded.
+ *
+ * @return [encoding, language, hasLanguageIndicator,
+ *          GECKO_SMS_MESSAGE_CLASSES[messageClass]]
+ */
+function decodeGsmDataCodingScheme(aDcs) {
+  let language = null;
+  let hasLanguageIndicator = false;
+  let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET;
+  let messageClass = PDU_DCS_MSG_CLASS_NORMAL;
+
+  switch (aDcs & PDU_DCS_CODING_GROUP_BITS) {
+    case 0x00: // 0000
+      language = CB_DCS_LANG_GROUP_1[aDcs & 0x0F];
+      break;
+
+    case 0x10: // 0001
+      switch (aDcs & 0x0F) {
+        case 0x00:
+          hasLanguageIndicator = true;
+          break;
+        case 0x01:
+          encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET;
+          hasLanguageIndicator = true;
+          break;
+      }
+      break;
+
+    case 0x20: // 0010
+      language = CB_DCS_LANG_GROUP_2[aDcs & 0x0F];
+      break;
+
+    case 0x40: // 01xx
+    case 0x50:
+    //case 0x60:
+    //case 0x70:
+    case 0x90: // 1001
+      encoding = (aDcs & 0x0C);
+      if (encoding == 0x0C) {
+        encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET;
+      }
+      messageClass = (aDcs & PDU_DCS_MSG_CLASS_BITS);
+      break;
+
+    case 0xF0:
+      encoding = (aDcs & 0x04) ? PDU_DCS_MSG_CODING_8BITS_ALPHABET
+                              : PDU_DCS_MSG_CODING_7BITS_ALPHABET;
+      switch(aDcs & PDU_DCS_MSG_CLASS_BITS) {
+        case 0x01: messageClass = PDU_DCS_MSG_CLASS_USER_1; break;
+        case 0x02: messageClass = PDU_DCS_MSG_CLASS_USER_2; break;
+        case 0x03: messageClass = PDU_DCS_MSG_CLASS_TE_SPECIFIC; break;
+      }
+      break;
+
+    case 0x30: // 0011 (Reserved)
+    case 0x80: // 1000 (Reserved)
+    case 0xA0: // 1010..1100 (Reserved)
+    case 0xB0:
+    case 0xC0:
+      break;
+    default:
+      throw new Error("Unsupported CBS data coding scheme: " + aDcs);
+  }
+
+  return [encoding, language, hasLanguageIndicator,
+          GECKO_SMS_MESSAGE_CLASSES[messageClass]];
+}
+
 /**
  * Push required permissions and test if |navigator.mozCellBroadcast| exists.
  * Resolve if it does, reject otherwise.
  *
  * Fulfill params:
  *   cbManager -- an reference to navigator.mozCellBroadcast.
  *
  * Reject params: (none)
@@ -143,17 +288,17 @@ function waitForManagerEvent(aEventName)
 function sendMultipleRawCbsToEmulatorAndWait(aPdus) {
   let promises = [];
 
   promises.push(waitForManagerEvent("received"));
   for (let pdu of aPdus) {
     promises.push(sendRawCbsToEmulator(pdu));
   }
 
-  return Promise.all(promises);
+  return Promise.all(promises).then(aResults => aResults[0].message);
 }
 
 /**
  * Flush permission settings and call |finish()|.
  */
 function cleanUp() {
   waitFor(function() {
     SpecialPowers.flushPermissions(function() {
--- a/dom/cellbroadcast/tests/marionette/test_cellbroadcast_etws.js
+++ b/dom/cellbroadcast/tests/marionette/test_cellbroadcast_etws.js
@@ -1,268 +1,215 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-MARIONETTE_TIMEOUT = 10000;
-
-const CB_MESSAGE_SIZE_ETWS = 56;
-
-const CB_GSM_GEOGRAPHICAL_SCOPE_NAMES = [
-  "cell-immediate",
-  "plmn",
-  "location-area",
-  "cell"
-];
-
-const CB_ETWS_WARNING_TYPE_NAMES = [
-  "earthquake",
-  "tsunami",
-  "earthquake-tsunami",
-  "test",
-  "other"
-];
-
-SpecialPowers.addPermission("cellbroadcast", true, document);
-SpecialPowers.addPermission("mobileconnection", true, document);
-
-let cbs = window.navigator.mozCellBroadcast;
-ok(cbs instanceof window.MozCellBroadcast,
-   "mozCellBroadcast is instanceof " + cbs.constructor);
+MARIONETTE_TIMEOUT = 20000;
+MARIONETTE_HEAD_JS = 'head.js';
 
-let pendingEmulatorCmdCount = 0;
-function sendCellBroadcastMessage(pdu, callback) {
-  pendingEmulatorCmdCount++;
-
-  let cmd = "cbs pdu " + pdu;
-  runEmulatorCmd(cmd, function(result) {
-    pendingEmulatorCmdCount--;
-
-    is(result[0], "OK", "Emulator response");
-
-    if (callback) {
-      window.setTimeout(callback, 0);
-    }
-  });
-}
-
-function buildHexStr(n, numSemiOctets) {
-  let str = n.toString(16);
-  ok(str.length <= numSemiOctets);
-  while (str.length < numSemiOctets) {
-    str = "0" + str;
-  }
-  return str;
-}
-
-function seq(end, begin) {
-  let result = [];
-  for (let i = begin || 0; i < end; i++) {
-    result.push(i);
-  }
-  return result;
-}
+function testReceiving_ETWS_MessageAttributes() {
+  log("Test receiving ETWS Primary Notification - Message Attributes");
 
-function repeat(func, array, oncomplete) {
-  (function do_call(index) {
-    let next = index < (array.length - 1) ? do_call.bind(null, index + 1) : oncomplete;
-    func.apply(null, [array[index], next]);
-  })(0);
-}
-
-function doTestHelper(pdu, nextTest, checkFunc) {
-  cbs.addEventListener("received", function onreceived(event) {
-    cbs.removeEventListener("received", onreceived);
-
-    checkFunc(event.message);
-
-    window.setTimeout(nextTest, 0);
-  });
-
-  if (Array.isArray(pdu)) {
-    repeat(sendCellBroadcastMessage, pdu);
-  } else {
-    sendCellBroadcastMessage(pdu);
-  }
-}
-
-/**
- * Tests receiving Cell Broadcast messages, event instance type, all attributes
- * of CellBroadcastMessage exist.
- */
-function testEtwsMessageAttributes() {
-  log("Test ETWS Primary Notification message attributes");
-
-  cbs.addEventListener("received", function onreceived(event) {
-    cbs.removeEventListener("received", onreceived);
-
-    // Bug 838542: following check throws an exception and fails this case.
-    // ok(event instanceof MozCellBroadcastEvent,
-    //    "event is instanceof " + event.constructor)
-    ok(event, "event is valid");
-
-    let message = event.message;
-    ok(message, "event.message is valid");
-
+  let verifyCBMessage = (aMessage) => {
     // Attributes other than `language` and `body` should always be assigned.
-    ok(message.gsmGeographicalScope != null, "message.gsmGeographicalScope");
-    ok(message.messageCode != null, "message.messageCode");
-    ok(message.messageId != null, "message.messageId");
-    ok('language' in message, "message.language");
-    ok(message.language == null, "message.language");
-    ok('body' in message, "message.body");
-    ok(message.body == null, "message.body");
-    is(message.messageClass, "normal", "message.messageClass");
-    ok(message.timestamp != null, "message.timestamp");
-    ok(message.etws != null, "message.etws");
-    ok(message.etws.warningType != null, "message.etws.warningType");
-    ok(message.etws.emergencyUserAlert != null,
-       "message.etws.emergencyUserAlert");
-    ok(message.etws.popup != null, "message.etws.popup");
-    ok(message.cdmaServiceCategory != null, "message.cdmaServiceCategory");
-
-    window.setTimeout(testReceiving_ETWS_GeographicalScope, 0);
-  });
+    ok(aMessage.gsmGeographicalScope != null, "aMessage.gsmGeographicalScope");
+    ok(aMessage.messageCode != null, "aMessage.messageCode");
+    ok(aMessage.messageId != null, "aMessage.messageId");
+    ok('language' in aMessage, "aMessage.language");
+    ok(aMessage.language == null, "aMessage.language");
+    ok('body' in aMessage, "aMessage.body");
+    ok(aMessage.body == null, "aMessage.body");
+    is(aMessage.messageClass, "normal", "aMessage.messageClass");
+    ok(aMessage.timestamp != null, "aMessage.timestamp");
+    ok(aMessage.etws != null, "aMessage.etws");
+    ok(aMessage.etws.warningType != null, "aMessage.etws.warningType");
+    ok(aMessage.etws.emergencyUserAlert != null,
+       "aMessage.etws.emergencyUserAlert");
+    ok(aMessage.etws.popup != null, "aMessage.etws.popup");
+    ok(aMessage.cdmaServiceCategory != null, "aMessage.cdmaServiceCategory");
+  };
 
   // Here we use a simple ETWS message for test.
   let pdu = buildHexStr(0, CB_MESSAGE_SIZE_ETWS * 2); // 6 octets
-  sendCellBroadcastMessage(pdu);
+
+  return sendMultipleRawCbsToEmulatorAndWait([pdu])
+    .then((aMessage) => verifyCBMessage(aMessage));
 }
 
 function testReceiving_ETWS_GeographicalScope() {
   log("Test receiving ETWS Primary Notification - Geographical Scope");
 
-  function do_test(gs, nextTest) {
-    // Here we use a simple ETWS message for test.
-    let pdu = buildHexStr(((gs & 0x03) << 14), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 2) * 2);
+  let promise = Promise.resolve();
+
+  let verifyCBMessage = (aMessage, aGsName) => {
+    is(aMessage.gsmGeographicalScope, aGsName,
+       "aMessage.gsmGeographicalScope");
+  };
 
-    doTestHelper(pdu, nextTest, function(message) {
-      is(message.gsmGeographicalScope, CB_GSM_GEOGRAPHICAL_SCOPE_NAMES[gs],
-         "message.gsmGeographicalScope");
-    });
-  }
+  CB_GSM_GEOGRAPHICAL_SCOPE_NAMES.forEach(function(aGsName, aIndex) {
+    let pdu = buildHexStr(((aIndex & 0x03) << 14), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 2) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aGsName));
+  });
 
-  repeat(do_test, seq(CB_GSM_GEOGRAPHICAL_SCOPE_NAMES.length),
-         testReceiving_ETWS_MessageCode);
+  return promise;
 }
 
 function testReceiving_ETWS_MessageCode() {
   log("Test receiving ETWS Primary Notification - Message Code");
 
+  let promise = Promise.resolve();
+
   // Message Code has 10 bits, and is ORed into a 16 bits 'serial' number. Here
   // we test every single bit to verify the operation doesn't go wrong.
-  let messageCodesToTest = [
+  let messageCodes = [
     0x000, 0x001, 0x002, 0x004, 0x008, 0x010, 0x020, 0x040,
     0x080, 0x100, 0x200, 0x251
   ];
 
-  function do_test(messageCode, nextTest) {
-    let pdu = buildHexStr(((messageCode & 0x3FF) << 4), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 2) * 2);
+  let verifyCBMessage = (aMessage, aMsgCode) => {
+    is(aMessage.messageCode, aMsgCode, "aMessage.messageCode");
+  };
 
-    doTestHelper(pdu, nextTest, function(message) {
-      is(message.messageCode, messageCode, "message.messageCode");
-    });
-  }
+  messageCodes.forEach(function(aMsgCode) {
+    let pdu = buildHexStr(((aMsgCode & 0x3FF) << 4), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 2) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMsgCode));
+  });
 
-  repeat(do_test, messageCodesToTest, testReceiving_ETWS_MessageId);
+  return promise;
 }
 
 function testReceiving_ETWS_MessageId() {
   log("Test receiving ETWS Primary Notification - Message Identifier");
 
+  let promise = Promise.resolve();
+
   // Message Identifier has 16 bits, but no bitwise operation is needed.
   // Test some selected values only.
-  let messageIdsToTest = [
-    0x0000, 0x0001, 0x0010, 0x0100, 0x1000, 0x1111, 0x8888, 0x8811
+  let messageIds = [
+    0x0000, 0x0001, 0x0010, 0x0100, 0x1000, 0x1111, 0x8888, 0x8811,
   ];
 
-  function do_test(messageId, nextTest) {
-    let pdu = buildHexStr(0, 4)
-            + buildHexStr((messageId & 0xFFFF), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 4) * 2);
+  let verifyCBMessage = (aMessage, aMessageId) => {
+    is(aMessage.messageId, aMessageId, "aMessage.messageId");
+  };
 
-    doTestHelper(pdu, nextTest, function(message) {
-      is(message.messageId, messageId, "message.messageId");
-    });
-  }
+  messageIds.forEach(function(aMessageId) {
+    let pdu = buildHexStr(0, 4)
+            + buildHexStr((aMessageId & 0xFFFF), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 4) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMessageId));
+  });
 
-  repeat(do_test, messageIdsToTest, testReceiving_ETWS_Timestamp);
+  return promise;
 }
 
 function testReceiving_ETWS_Timestamp() {
   log("Test receiving ETWS Primary Notification - Timestamp");
 
-  // Here we use a simple ETWS message for test.
-  let pdu = buildHexStr(0, 12); // 6 octets
-  doTestHelper(pdu, testReceiving_ETWS_WarningType, function(message) {
+  let verifyCBMessage = (aMessage) => {
     // Cell Broadcast messages do not contain a timestamp field (however, ETWS
     // does). We only check the timestamp doesn't go too far (60 seconds) here.
-    let msMessage = message.timestamp;
+    let msMessage = aMessage.timestamp;
     let msNow = Date.now();
-    ok(Math.abs(msMessage - msNow) < (1000 * 60), "message.timestamp");
-  });
+    ok(Math.abs(msMessage - msNow) < (1000 * 60), "aMessage.timestamp");
+  };
+
+  // Here we use a simple ETWS message for test.
+  let pdu = buildHexStr(0, 12); // 6 octets
+
+  return sendMultipleRawCbsToEmulatorAndWait([pdu])
+    .then((aMessage) => verifyCBMessage(aMessage));
 }
 
 function testReceiving_ETWS_WarningType() {
   log("Test receiving ETWS Primary Notification - Warning Type");
 
+  let promise = Promise.resolve();
+
   // Warning Type has 7 bits, and is ORed into a 16 bits 'WarningType' field.
   // Here we test every single bit to verify the operation doesn't go wrong.
-  let warningTypesToTest = [
+  let warningTypes = [
     0x00, 0x01, 0x02, 0x03, 0x04, 0x08, 0x10, 0x20, 0x40, 0x41
   ];
 
-  function do_test(warningType, nextTest) {
-    let pdu = buildHexStr(0, 8)
-            + buildHexStr(((warningType & 0x7F) << 9), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 6) * 2);
-
-    doTestHelper(pdu, nextTest, function(message) {
-      ok(message.etws, "message.etws");
-      is(message.etws.warningType, CB_ETWS_WARNING_TYPE_NAMES[warningType],
-         "message.etws.warningType");
-    });
-  }
+  let verifyCBMessage = (aMessage, aWarningType) => {
+    ok(aMessage.etws, "aMessage.etws");
+    is(aMessage.etws.warningType, CB_ETWS_WARNING_TYPE_NAMES[aWarningType],
+       "aMessage.etws.warningType");
+  };
 
-  repeat(do_test, warningTypesToTest, testReceiving_ETWS_EmergencyUserAlert);
-}
+  warningTypes.forEach(function(aWarningType) {
+    let pdu = buildHexStr(0, 8)
+            + buildHexStr(((aWarningType & 0x7F) << 9), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 6) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aWarningType));
+  });
 
-function doTestEmergencyUserAlert_or_Popup(name, mask, nextTest) {
-  let pdu = buildHexStr(0, 8)
-          + buildHexStr(mask, 4)
-          + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 6) * 2);
-  doTestHelper(pdu, nextTest, function(message) {
-    ok(message.etws != null, "message.etws");
-    is(message.etws[name], mask != 0, "message.etws." + name);
-  });
+  return promise;
 }
 
 function testReceiving_ETWS_EmergencyUserAlert() {
   log("Test receiving ETWS Primary Notification - Emergency User Alert");
 
-  repeat(doTestEmergencyUserAlert_or_Popup.bind(null, "emergencyUserAlert"),
-         [0x100, 0x000], testReceiving_ETWS_Popup);
+  let promise = Promise.resolve();
+
+  let emergencyUserAlertMasks = [0x100, 0x000];
+
+  let verifyCBMessage = (aMessage, aMask) => {
+    ok(aMessage.etws != null, "aMessage.etws");
+    is(aMessage.etws.emergencyUserAlert, aMask != 0, "aMessage.etws.emergencyUserAlert");
+  };
+
+  emergencyUserAlertMasks.forEach(function(aMask) {
+    let pdu = buildHexStr(0, 8)
+            + buildHexStr(aMask, 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 6) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMask));
+  });
+
+  return promise;
 }
 
 function testReceiving_ETWS_Popup() {
   log("Test receiving ETWS Primary Notification - Popup");
 
-  repeat(doTestEmergencyUserAlert_or_Popup.bind(null, "popup"),
-         [0x80, 0x000], cleanUp);
+  let promise = Promise.resolve();
+
+  let popupMasks = [0x80, 0x000];
+
+  let verifyCBMessage = (aMessage, aMask) => {
+    ok(aMessage.etws != null, "aMessage.etws");
+    is(aMessage.etws.popup, aMask != 0, "aMessage.etws.popup");
+  };
+
+  popupMasks.forEach(function(aMask) {
+    let pdu = buildHexStr(0, 8)
+            + buildHexStr(aMask, 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_ETWS - 6) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMask));
+  });
+
+  return promise;
 }
 
-function cleanUp() {
-  if (pendingEmulatorCmdCount > 0) {
-    window.setTimeout(cleanUp, 100);
-    return;
-  }
-
-  SpecialPowers.removePermission("mobileconnection", document);
-  SpecialPowers.removePermission("cellbroadcast", true, document);
-
-  finish();
-}
-
-waitFor(testEtwsMessageAttributes, function() {
-  return navigator.mozMobileConnections[0].voice.connected;
+startTestCommon(function testCaseMain() {
+  return testReceiving_ETWS_MessageAttributes()
+    .then(() => testReceiving_ETWS_GeographicalScope())
+    .then(() => testReceiving_ETWS_MessageCode())
+    .then(() => testReceiving_ETWS_MessageId())
+    .then(() => testReceiving_ETWS_Timestamp())
+    .then(() => testReceiving_ETWS_WarningType())
+    .then(() => testReceiving_ETWS_EmergencyUserAlert())
+    .then(() => testReceiving_ETWS_Popup());
 });
-
--- a/dom/cellbroadcast/tests/marionette/test_cellbroadcast_gsm.js
+++ b/dom/cellbroadcast/tests/marionette/test_cellbroadcast_gsm.js
@@ -1,481 +1,345 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-MARIONETTE_TIMEOUT = 30000;
-
-const PDU_DCS_CODING_GROUP_BITS          = 0xF0;
-const PDU_DCS_MSG_CODING_7BITS_ALPHABET  = 0x00;
-const PDU_DCS_MSG_CODING_8BITS_ALPHABET  = 0x04;
-const PDU_DCS_MSG_CODING_16BITS_ALPHABET = 0x08;
-
-const PDU_DCS_MSG_CLASS_BITS             = 0x03;
-const PDU_DCS_MSG_CLASS_NORMAL           = 0xFF;
-const PDU_DCS_MSG_CLASS_0                = 0x00;
-const PDU_DCS_MSG_CLASS_ME_SPECIFIC      = 0x01;
-const PDU_DCS_MSG_CLASS_SIM_SPECIFIC     = 0x02;
-const PDU_DCS_MSG_CLASS_TE_SPECIFIC      = 0x03;
-const PDU_DCS_MSG_CLASS_USER_1           = 0x04;
-const PDU_DCS_MSG_CLASS_USER_2           = 0x05;
-
-const GECKO_SMS_MESSAGE_CLASSES = {};
-GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]       = "normal";
-GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]            = "class-0";
-GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_ME_SPECIFIC]  = "class-1";
-GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_SIM_SPECIFIC] = "class-2";
-GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_TE_SPECIFIC]  = "class-3";
-GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_1]       = "user-1";
-GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_2]       = "user-2";
-
-const CB_MESSAGE_SIZE_GSM  = 88;
-
-const CB_GSM_MESSAGEID_ETWS_BEGIN = 0x1100;
-const CB_GSM_MESSAGEID_ETWS_END   = 0x1107;
-
-const CB_GSM_GEOGRAPHICAL_SCOPE_NAMES = [
-  "cell-immediate",
-  "plmn",
-  "location-area",
-  "cell"
-];
-
-const CB_ETWS_WARNING_TYPE_NAMES = [
-  "earthquake",
-  "tsunami",
-  "earthquake-tsunami",
-  "test",
-  "other"
-];
-
-const CB_DCS_LANG_GROUP_1 = [
-  "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi",
-  "no", "el", "tr", "hu", "pl", null
-];
-const CB_DCS_LANG_GROUP_2 = [
-  "cs", "he", "ar", "ru", "is", null, null, null, null, null,
-  null, null, null, null, null, null
-];
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
 
 const CB_MAX_CONTENT_7BIT = Math.floor((CB_MESSAGE_SIZE_GSM - 6) * 8 / 7);
 const CB_MAX_CONTENT_UCS2 = Math.floor((CB_MESSAGE_SIZE_GSM - 6) / 2);
 
 const BODY_7BITS = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
                  + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
                  + "@@@@@@@@@@@@@"; // 93 ascii chars.
 const BODY_7BITS_IND = BODY_7BITS.substr(3);
 const BODY_UCS2 = "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                 + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                 + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                 + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                 + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
                 + "\u0000"; // 41 unicode chars.
 const BODY_UCS2_IND = BODY_UCS2.substr(1);
 
-SpecialPowers.addPermission("cellbroadcast", true, document);
-SpecialPowers.addPermission("mobileconnection", true, document);
-
 is(BODY_7BITS.length,     CB_MAX_CONTENT_7BIT,     "BODY_7BITS.length");
 is(BODY_7BITS_IND.length, CB_MAX_CONTENT_7BIT - 3, "BODY_7BITS_IND.length");
 is(BODY_UCS2.length,      CB_MAX_CONTENT_UCS2,     "BODY_UCS2.length");
-is(BODY_UCS2_IND.length,  CB_MAX_CONTENT_UCS2 - 1, "BODY_UCS2_IND.length")
-
-let cbs = window.navigator.mozCellBroadcast;
-ok(cbs instanceof window.MozCellBroadcast,
-   "mozCellBroadcast is instanceof " + cbs.constructor);
-
-let pendingEmulatorCmdCount = 0;
-function sendCellBroadcastMessage(pdu, callback) {
-  pendingEmulatorCmdCount++;
-
-  let cmd = "cbs pdu " + pdu;
-  runEmulatorCmd(cmd, function(result) {
-    pendingEmulatorCmdCount--;
-
-    is(result[0], "OK", "Emulator response");
-
-    if (callback) {
-      window.setTimeout(callback, 0);
-    }
-  });
-}
+is(BODY_UCS2_IND.length,  CB_MAX_CONTENT_UCS2 - 1, "BODY_UCS2_IND.length");
 
-function buildHexStr(n, numSemiOctets) {
-  let str = n.toString(16);
-  ok(str.length <= numSemiOctets);
-  while (str.length < numSemiOctets) {
-    str = "0" + str;
-  }
-  return str;
-}
-
-function seq(end, begin) {
-  let result = [];
-  for (let i = begin || 0; i < end; i++) {
-    result.push(i);
-  }
-  return result;
-}
-
-function repeat(func, array, oncomplete) {
-  (function do_call(index) {
-    let next = index < (array.length - 1) ? do_call.bind(null, index + 1) : oncomplete;
-    func.apply(null, [array[index], next]);
-  })(0);
-}
+function testReceiving_GSM_MessageAttributes() {
+  log("Test receiving GSM Cell Broadcast - Message Attributes");
 
-function doTestHelper(pdu, nextTest, checkFunc) {
-  cbs.addEventListener("received", function onreceived(event) {
-    cbs.removeEventListener("received", onreceived);
-
-    checkFunc(event.message);
-
-    window.setTimeout(nextTest, 0);
-  });
-
-  if (Array.isArray(pdu)) {
-    repeat(sendCellBroadcastMessage, pdu);
-  } else {
-    sendCellBroadcastMessage(pdu);
-  }
-}
-
-/**
- * Tests receiving Cell Broadcast messages, event instance type, all attributes
- * of CellBroadcastMessage exist.
- */
-function testGsmMessageAttributes() {
-  log("Test GSM Cell Broadcast message attributes");
-
-  cbs.addEventListener("received", function onreceived(event) {
-    cbs.removeEventListener("received", onreceived);
-
-    // Bug 838542: following check throws an exception and fails this case.
-    // ok(event instanceof MozCellBroadcastEvent,
-    //    "event is instanceof " + event.constructor)
-    ok(event, "event is valid");
-
-    let message = event.message;
-    ok(message, "event.message is valid");
-
+  let verifyCBMessage = (aMessage) => {
     // Attributes other than `language` and `body` should always be assigned.
-    ok(message.gsmGeographicalScope != null, "message.gsmGeographicalScope");
-    ok(message.messageCode != null, "message.messageCode");
-    ok(message.messageId != null, "message.messageId");
-    ok(message.language != null, "message.language");
-    ok(message.body != null, "message.body");
-    ok(message.messageClass != null, "message.messageClass");
-    ok(message.timestamp != null, "message.timestamp");
-    ok('etws' in message, "message.etws");
-    if (message.etws) {
-      ok('warningType' in message.etws, "message.etws.warningType");
-      ok(message.etws.emergencyUserAlert != null, "message.etws.emergencyUserAlert");
-      ok(message.etws.popup != null, "message.etws.popup");
+    ok(aMessage.gsmGeographicalScope != null, "aMessage.gsmGeographicalScope");
+    ok(aMessage.messageCode != null, "aMessage.messageCode");
+    ok(aMessage.messageId != null, "aMessage.messageId");
+    ok(aMessage.language != null, "aMessage.language");
+    ok(aMessage.body != null, "aMessage.body");
+    ok(aMessage.messageClass != null, "aMessage.messageClass");
+    ok(aMessage.timestamp != null, "aMessage.timestamp");
+    ok('etws' in aMessage, "aMessage.etws");
+    if (aMessage.etws) {
+      ok('warningType' in aMessage.etws, "aMessage.etws.warningType");
+      ok(aMessage.etws.emergencyUserAlert != null, "aMessage.etws.emergencyUserAlert");
+      ok(aMessage.etws.popup != null, "aMessage.etws.popup");
     }
-    ok(message.cdmaServiceCategory != null, "message.cdmaServiceCategory");
-
-    window.setTimeout(testReceiving_GSM_GeographicalScope, 0);
-  });
+    ok(aMessage.cdmaServiceCategory != null, "aMessage.cdmaServiceCategory");
+  };
 
   // Here we use a simple GSM message for test.
   let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2);
-  sendCellBroadcastMessage(pdu);
+
+  return sendMultipleRawCbsToEmulatorAndWait([pdu])
+    .then((aMessage) => verifyCBMessage(aMessage));
 }
 
 function testReceiving_GSM_GeographicalScope() {
   log("Test receiving GSM Cell Broadcast - Geographical Scope");
 
-  function do_test(gs, nextTest) {
-    let pdu = buildHexStr(((gs & 0x03) << 14), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 2) * 2);
+  let promise = Promise.resolve();
+
+  let verifyCBMessage = (aMessage, aGsName) => {
+    is(aMessage.gsmGeographicalScope, aGsName,
+       "aMessage.gsmGeographicalScope");
+  };
 
-    doTestHelper(pdu, nextTest, function(message) {
-      is(message.gsmGeographicalScope, CB_GSM_GEOGRAPHICAL_SCOPE_NAMES[gs],
-         "message.gsmGeographicalScope");
-    });
-  }
+  CB_GSM_GEOGRAPHICAL_SCOPE_NAMES.forEach(function(aGsName, aIndex) {
+    let pdu = buildHexStr(((aIndex & 0x03) << 14), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 2) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aGsName));
+  });
 
-  repeat(do_test, seq(CB_GSM_GEOGRAPHICAL_SCOPE_NAMES.length),
-         testReceiving_GSM_MessageCode);
+  return promise;
 }
 
 function testReceiving_GSM_MessageCode() {
   log("Test receiving GSM Cell Broadcast - Message Code");
 
+  let promise = Promise.resolve();
+
   // Message Code has 10 bits, and is ORed into a 16 bits 'serial' number. Here
   // we test every single bit to verify the operation doesn't go wrong.
-  let messageCodesToTest = [
+  let messageCodes = [
     0x000, 0x001, 0x002, 0x004, 0x008, 0x010, 0x020, 0x040,
     0x080, 0x100, 0x200, 0x251
   ];
 
-  function do_test(messageCode, nextTest) {
-    let pdu = buildHexStr(((messageCode & 0x3FF) << 4), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 2) * 2);
+  let verifyCBMessage = (aMessage, aMsgCode) => {
+    is(aMessage.messageCode, aMsgCode, "aMessage.messageCode");
+  };
 
-    doTestHelper(pdu, nextTest, function(message) {
-      is(message.messageCode, messageCode, "message.messageCode");
-    });
-  }
+  messageCodes.forEach(function(aMsgCode) {
+    let pdu = buildHexStr(((aMsgCode & 0x3FF) << 4), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 2) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMsgCode));
+  });
 
-  repeat(do_test, messageCodesToTest, testReceiving_GSM_MessageId);
+  return promise;
 }
 
 function testReceiving_GSM_MessageId() {
   log("Test receiving GSM Cell Broadcast - Message Identifier");
 
+  let promise = Promise.resolve();
+
   // Message Identifier has 16 bits, but no bitwise operation is needed.
   // Test some selected values only.
-  let messageIdsToTest = [
+  let messageIds = [
     0x0000, 0x0001, 0x0010, 0x0100, 0x1000, 0x1111, 0x8888, 0x8811,
   ];
 
-  function do_test(messageId, nextTest) {
-    let pdu = buildHexStr(0, 4)
-            + buildHexStr((messageId & 0xFFFF), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2);
-
-    doTestHelper(pdu, nextTest, function(message) {
-      is(message.messageId, messageId, "message.messageId");
-      ok(message.etws == null, "message.etws");
-    });
-  }
-
-  repeat(do_test, messageIdsToTest, testReceiving_GSM_Language_and_Body);
-}
-
-// Copied from GsmPDUHelper.readCbDataCodingScheme
-function decodeDataCodingScheme(dcs) {
-  let language = null;
-  let hasLanguageIndicator = false;
-  let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET;
-  let messageClass = PDU_DCS_MSG_CLASS_NORMAL;
-
-  switch (dcs & PDU_DCS_CODING_GROUP_BITS) {
-    case 0x00: // 0000
-      language = CB_DCS_LANG_GROUP_1[dcs & 0x0F];
-      break;
-
-    case 0x10: // 0001
-      switch (dcs & 0x0F) {
-        case 0x00:
-          hasLanguageIndicator = true;
-          break;
-        case 0x01:
-          encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET;
-          hasLanguageIndicator = true;
-          break;
-      }
-      break;
+  let verifyCBMessage = (aMessage, aMessageId) => {
+    is(aMessage.messageId, aMessageId, "aMessage.messageId");
+    ok(aMessage.etws == null, "aMessage.etws");
+  };
 
-    case 0x20: // 0010
-      language = CB_DCS_LANG_GROUP_2[dcs & 0x0F];
-      break;
-
-    case 0x40: // 01xx
-    case 0x50:
-    //case 0x60:
-    //case 0x70:
-    case 0x90: // 1001
-      encoding = (dcs & 0x0C);
-      if (encoding == 0x0C) {
-        encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET;
-      }
-      messageClass = (dcs & PDU_DCS_MSG_CLASS_BITS);
-      break;
+  messageIds.forEach(function(aMessageId) {
+    let pdu = buildHexStr(0, 4)
+            + buildHexStr((aMessageId & 0xFFFF), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMessageId));
+  });
 
-    case 0xF0:
-      encoding = (dcs & 0x04) ? PDU_DCS_MSG_CODING_8BITS_ALPHABET
-                              : PDU_DCS_MSG_CODING_7BITS_ALPHABET;
-      switch(dcs & PDU_DCS_MSG_CLASS_BITS) {
-        case 0x01: messageClass = PDU_DCS_MSG_CLASS_USER_1; break;
-        case 0x02: messageClass = PDU_DCS_MSG_CLASS_USER_2; break;
-        case 0x03: messageClass = PDU_DCS_MSG_CLASS_TE_SPECIFIC; break;
-      }
-      break;
-
-    case 0x30: // 0011 (Reserved)
-    case 0x80: // 1000 (Reserved)
-    case 0xA0: // 1010..1100 (Reserved)
-    case 0xB0:
-    case 0xC0:
-      break;
-    default:
-      throw new Error("Unsupported CBS data coding scheme: " + dcs);
-  }
-
-  return [encoding, language, hasLanguageIndicator,
-          GECKO_SMS_MESSAGE_CLASSES[messageClass]];
+  return promise;
 }
 
 function testReceiving_GSM_Language_and_Body() {
   log("Test receiving GSM Cell Broadcast - Language & Body");
 
-  function do_test(dcs) {
-    let encoding, language, indicator, messageClass;
+  let promise = Promise.resolve();
+
+  let testDcs = [];
+  dcs = 0;
+  while (dcs <= 0xFF) {
     try {
-      [encoding, language, indicator, messageClass] = decodeDataCodingScheme(dcs);
+      let dcsInfo = { dcs: dcs };
+      [ dcsInfo.encoding, dcsInfo.language,
+        dcsInfo.indicator, dcsInfo.messageClass ] = decodeGsmDataCodingScheme(dcs);
+      testDcs.push(dcsInfo);
     } catch (e) {
       // Unsupported coding group, skip.
-      let nextGroup = (dcs & PDU_DCS_CODING_GROUP_BITS) + 0x10;
-      window.setTimeout(do_test.bind(null, nextGroup), 0);
-      return;
+      let dcs = (dcs & PDU_DCS_CODING_GROUP_BITS) + 0x10;
+    }
+    dcs++;
+  }
+
+  let verifyCBMessage = (aMessage, aDcsInfo) => {
+    if (aDcsInfo.language) {
+      is(aMessage.language, aDcsInfo.language, "aMessage.language");
+    } else if (aDcsInfo.indicator) {
+      is(aMessage.language, "@@", "aMessage.language");
+    } else {
+      ok(aMessage.language == null, "aMessage.language");
     }
 
-    let pdu = buildHexStr(0, 8)
-            + buildHexStr(dcs, 2)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 5) * 2);
-
-    let nextTest = (dcs < 0xFF) ? do_test.bind(null, dcs + 1)
-                                 : testReceiving_GSM_Timestamp;
-    doTestHelper(pdu, nextTest, function(message) {
-      if (language) {
-        is(message.language, language, "message.language");
-      } else if (indicator) {
-        is(message.language, "@@", "message.language");
-      } else {
-        ok(message.language == null, "message.language");
-      }
+    switch (aDcsInfo.encoding) {
+      case PDU_DCS_MSG_CODING_7BITS_ALPHABET:
+        is(aMessage.body, aDcsInfo.indicator ? BODY_7BITS_IND : BODY_7BITS, "aMessage.body");
+        break;
+      case PDU_DCS_MSG_CODING_8BITS_ALPHABET:
+        ok(aMessage.body == null, "aMessage.body");
+        break;
+      case PDU_DCS_MSG_CODING_16BITS_ALPHABET:
+        is(aMessage.body, aDcsInfo.indicator ? BODY_UCS2_IND : BODY_UCS2, "aMessage.body");
+        break;
+    }
 
-      switch (encoding) {
-        case PDU_DCS_MSG_CODING_7BITS_ALPHABET:
-          is(message.body, indicator ? BODY_7BITS_IND : BODY_7BITS, "message.body");
-          break;
-        case PDU_DCS_MSG_CODING_8BITS_ALPHABET:
-          ok(message.body == null, "message.body");
-          break;
-        case PDU_DCS_MSG_CODING_16BITS_ALPHABET:
-          is(message.body, indicator ? BODY_UCS2_IND : BODY_UCS2, "message.body");
-          break;
-      }
+    is(aMessage.messageClass, aDcsInfo.messageClass, "aMessage.messageClass");
+  };
 
-      is(message.messageClass, messageClass, "message.messageClass");
-    });
-  }
+  testDcs.forEach(function(aDcsInfo) {
+    let pdu = buildHexStr(0, 8)
+            + buildHexStr(aDcsInfo.dcs, 2)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 5) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aDcsInfo));
+  });
 
-  do_test(0);
+  return promise;
 }
 
 function testReceiving_GSM_Timestamp() {
   log("Test receiving GSM Cell Broadcast - Timestamp");
 
-  let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2);
-  doTestHelper(pdu, testReceiving_GSM_WarningType, function(message) {
+  let verifyCBMessage = (aMessage) => {
     // Cell Broadcast messages do not contain a timestamp field (however, ETWS
     // does). We only check the timestamp doesn't go too far (60 seconds) here.
-    let msMessage = message.timestamp;
+    let msMessage = aMessage.timestamp;
     let msNow = Date.now();
-    ok(Math.abs(msMessage - msNow) < (1000 * 60), "message.timestamp");
-  });
+    ok(Math.abs(msMessage - msNow) < (1000 * 60), "aMessage.timestamp");
+  };
+
+  let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2);
+
+  return sendMultipleRawCbsToEmulatorAndWait([pdu])
+    .then((aMessage) => verifyCBMessage(aMessage));
 }
 
 function testReceiving_GSM_WarningType() {
   log("Test receiving GSM Cell Broadcast - Warning Type");
 
-  let messageIdsToTest = [];
+  let promise = Promise.resolve();
+
+  let messageIds = [];
   for (let i = CB_GSM_MESSAGEID_ETWS_BEGIN; i <= CB_GSM_MESSAGEID_ETWS_END; i++) {
-    messageIdsToTest.push(i);
+    messageIds.push(i);
   }
 
-  function do_test(messageId, nextTest) {
-    let pdu = buildHexStr(0, 4)
-            + buildHexStr((messageId & 0xFFFF), 4)
-            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2);
-
-    doTestHelper(pdu, nextTest, function(message) {
-      is(message.messageId, messageId, "message.messageId");
-      ok(message.etws != null, "message.etws");
+  let verifyCBMessage = (aMessage, aMessageId) => {
+    is(aMessage.messageId, aMessageId, "aMessage.messageId");
+    ok(aMessage.etws != null, "aMessage.etws");
 
-      let offset = messageId - CB_GSM_MESSAGEID_ETWS_BEGIN;
-      if (offset < CB_ETWS_WARNING_TYPE_NAMES.length) {
-        is(message.etws.warningType, CB_ETWS_WARNING_TYPE_NAMES[offset],
-           "message.etws.warningType");
-      } else {
-        ok(message.etws.warningType == null, "message.etws.warningType");
-      }
-    });
-  }
+    let offset = aMessageId - CB_GSM_MESSAGEID_ETWS_BEGIN;
+    if (offset < CB_ETWS_WARNING_TYPE_NAMES.length) {
+      is(aMessage.etws.warningType, CB_ETWS_WARNING_TYPE_NAMES[offset],
+         "aMessage.etws.warningType");
+    } else {
+      ok(aMessage.etws.warningType == null, "aMessage.etws.warningType");
+    }
+  };
 
-  repeat(do_test, messageIdsToTest, testReceiving_GSM_EmergencyUserAlert);
-}
+  messageIds.forEach(function(aMessageId) {
+    let pdu = buildHexStr(0, 4)
+            + buildHexStr((aMessageId & 0xFFFF), 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMessageId));
+  });
 
-function doTestEmergencyUserAlert_or_Popup(name, mask, nextTest) {
-  let pdu = buildHexStr(mask, 4)
-          + buildHexStr(CB_GSM_MESSAGEID_ETWS_BEGIN, 4)
-          + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2);
-
-  doTestHelper(pdu, nextTest, function(message) {
-    is(message.messageId, CB_GSM_MESSAGEID_ETWS_BEGIN, "message.messageId");
-    ok(message.etws != null, "message.etws");
-    is(message.etws[name], mask != 0, "message.etws." + name);
-  });
+  return promise;
 }
 
 function testReceiving_GSM_EmergencyUserAlert() {
   log("Test receiving GSM Cell Broadcast - Emergency User Alert");
 
-  repeat(doTestEmergencyUserAlert_or_Popup.bind(null, "emergencyUserAlert"),
-         [0x2000, 0x0000], testReceiving_GSM_Popup);
+  let promise = Promise.resolve();
+
+  let emergencyUserAlertMasks = [0x2000, 0x0000];
+
+  let verifyCBMessage = (aMessage, aMask) => {
+    is(aMessage.messageId, CB_GSM_MESSAGEID_ETWS_BEGIN, "aMessage.messageId");
+    ok(aMessage.etws != null, "aMessage.etws");
+    is(aMessage.etws.emergencyUserAlert, aMask != 0, "aMessage.etws.emergencyUserAlert");
+  };
+
+  emergencyUserAlertMasks.forEach(function(aMask) {
+    let pdu = buildHexStr(aMask, 4)
+            + buildHexStr(CB_GSM_MESSAGEID_ETWS_BEGIN, 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMask));
+  });
+
+  return promise;
 }
 
 function testReceiving_GSM_Popup() {
   log("Test receiving GSM Cell Broadcast - Popup");
 
-  repeat(doTestEmergencyUserAlert_or_Popup.bind(null, "popup"),
-         [0x1000, 0x0000], testReceiving_GSM_Multipart);
+  let promise = Promise.resolve();
+
+  let popupMasks = [0x1000, 0x0000];
+
+  let verifyCBMessage = (aMessage, aMask) => {
+    is(aMessage.messageId, CB_GSM_MESSAGEID_ETWS_BEGIN, "aMessage.messageId");
+    ok(aMessage.etws != null, "aMessage.etws");
+    is(aMessage.etws.popup, aMask != 0, "aMessage.etws.popup");
+  };
+
+  popupMasks.forEach(function(aMask) {
+    let pdu = buildHexStr(aMask, 4)
+            + buildHexStr(CB_GSM_MESSAGEID_ETWS_BEGIN, 4)
+            + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2);
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+      .then((aMessage) => verifyCBMessage(aMessage, aMask));
+  });
+
+  return promise;
 }
 
 function testReceiving_GSM_Multipart() {
   log("Test receiving GSM Cell Broadcast - Multipart Messages");
 
-  function do_test(numParts, nextTest) {
+  let promise = Promise.resolve();
+
+  // According to 9.4.1.2.4 Page Parameter in TS 23.041, the maximal Number of
+  // pages per CB message is 15.
+  let numParts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
+
+  let verifyCBMessage = (aMessage, aNumParts) => {
+      is(aMessage.body.length, (aNumParts * CB_MAX_CONTENT_7BIT),
+         "aMessage.body");
+  };
+
+  numParts.forEach(function(aNumParts) {
     let pdus = [];
-    for (let i = 1; i <= numParts; i++) {
+    for (let i = 1; i <= aNumParts; i++) {
       let pdu = buildHexStr(0, 10)
-              + buildHexStr((i << 4) + numParts, 2)
+              + buildHexStr((i << 4) + aNumParts, 2)
               + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 6) * 2);
       pdus.push(pdu);
     }
+    promise = promise
+      .then(() => sendMultipleRawCbsToEmulatorAndWait(pdus))
+      .then((aMessage) => verifyCBMessage(aMessage, aNumParts));
+  });
 
-    doTestHelper(pdus, nextTest, function(message) {
-      is(message.body.length, (numParts * CB_MAX_CONTENT_7BIT),
-         "message.body");
-    });
-  }
-
-  repeat(do_test, seq(16, 1), testReceiving_GSM_ServiceCategory);
+  return promise;
 }
 
 function testReceiving_GSM_ServiceCategory() {
   log("Test receiving GSM Cell Broadcast - Service Category");
 
-  cbs.addEventListener("received", function onreceived(event) {
-    cbs.removeEventListener("received", onreceived);
-
-    let message = event.message;
-
+  let verifyCBMessage = (aMessage) => {
     // Bug 910091
     // "Service Category" is not defined in GSM.  We should always get '0' here.
-    is(message.cdmaServiceCategory, 0, "message.cdmaServiceCategory");
-
-    window.setTimeout(cleanUp, 0);
-  });
+    is(aMessage.cdmaServiceCategory, 0, "aMessage.cdmaServiceCategory");
+  };
 
   let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2);
-  sendCellBroadcastMessage(pdu);
+  return sendMultipleRawCbsToEmulatorAndWait([pdu])
+    .then((aMessage) => verifyCBMessage(aMessage));
 }
 
-function cleanUp() {
-  if (pendingEmulatorCmdCount > 0) {
-    window.setTimeout(cleanUp, 100);
-    return;
-  }
-
-  SpecialPowers.removePermission("mobileconnection", document);
-  SpecialPowers.removePermission("cellbroadcast", true, document);
-
-  finish();
-}
-
-waitFor(testGsmMessageAttributes, function() {
-  return navigator.mozMobileConnections[0].voice.connected;
+startTestCommon(function testCaseMain() {
+  return testReceiving_GSM_MessageAttributes()
+    .then(() => testReceiving_GSM_GeographicalScope())
+    .then(() => testReceiving_GSM_MessageCode())
+    .then(() => testReceiving_GSM_MessageId())
+    .then(() => testReceiving_GSM_Language_and_Body())
+    .then(() => testReceiving_GSM_Timestamp())
+    .then(() => testReceiving_GSM_WarningType())
+    .then(() => testReceiving_GSM_EmergencyUserAlert())
+    .then(() => testReceiving_GSM_Popup())
+    .then(() => testReceiving_GSM_Multipart())
+    .then(() => testReceiving_GSM_ServiceCategory());
 });
-
--- a/dom/cellbroadcast/tests/marionette/test_cellbroadcast_multi_sim.js
+++ b/dom/cellbroadcast/tests/marionette/test_cellbroadcast_multi_sim.js
@@ -1,34 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_TIMEOUT = 10000;
 MARIONETTE_HEAD_JS = 'head.js';
 
 const BODY_7BITS = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
                  + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
                  + "@@@@@@@@@@@@@"; // 93 ascii chars.
-const CB_PDU_SIZE = 88;
 
-function testReceivingMultiSIM() {
-  let CB_PDU = "";
-  while (CB_PDU.length < CB_PDU_SIZE * 2) {
-    CB_PDU += "00";
-  }
+function testReceiving_MultiSIM() {
+  log("Test receiving GSM Cell Broadcast - Multi-SIM");
+
+  let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2);
 
   let verifyCBMessage = (aMessage, aServiceId) => {
     log("Verify CB message received from serviceId: " + aServiceId);
     is(aMessage.body, BODY_7BITS, "Checking message body.");
     is(aMessage.serviceId, aServiceId, "Checking serviceId.");
   };
 
   return selectModem(1)
-    .then(() => sendMultipleRawCbsToEmulatorAndWait([CB_PDU]))
-    .then((results) => verifyCBMessage(results[0].message, 1))
+    .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+    .then((aMessage) => verifyCBMessage(aMessage, 1))
     .then(() => selectModem(0))
-    .then(() => sendMultipleRawCbsToEmulatorAndWait([CB_PDU]))
-    .then((results) => verifyCBMessage(results[0].message, 0));
+    .then(() => sendMultipleRawCbsToEmulatorAndWait([pdu]))
+    .then((aMessage) => verifyCBMessage(aMessage, 0));
 }
 
 startTestCommon(function testCaseMain() {
-  return runIfMultiSIM(testReceivingMultiSIM);
+  return runIfMultiSIM(testReceiving_MultiSIM);
 });
--- a/dom/contacts/ContactManager.js
+++ b/dom/contacts/ContactManager.js
@@ -180,29 +180,16 @@ ContactManager.prototype = {
         }
         break;
       case "Contacts:GetAll:Return:KO":
         req = this.getRequest(msg.requestID);
         if (req) {
           Services.DOMRequest.fireError(req.cursor, msg.errorMsg);
         }
         break;
-      case "PermissionPromptHelper:AskPermission:OK":
-        if (DEBUG) debug("id: " + msg.requestID);
-        req = this.getRequest(msg.requestID);
-        if (!req) {
-          break;
-        }
-
-        if (msg.result == Ci.nsIPermissionManager.ALLOW_ACTION) {
-          req.allow();
-        } else {
-          req.cancel();
-        }
-        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);
@@ -230,16 +217,17 @@ ContactManager.prototype = {
   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";
@@ -250,48 +238,52 @@ ContactManager.prototype = {
       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(this._window.document.nodePrincipal, type);
+      Services.perms.testExactPermissionFromPrincipal(principal, type);
     if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
       aAllowCallback();
       return;
+    } else if (permValue == Ci.nsIPermissionManager.DENY_ACTION) {
+      aCancelCallback();
     }
 
-    let requestID = this.getRequestId({
-      request: aRequest,
-      allow: function() {
-        aAllowCallback();
-      }.bind(this),
-      cancel : function() {
-        if (aCancelCallback) {
-          aCancelCallback()
-        } else if (aRequest) {
-          Services.DOMRequest.fireError(aRequest, "Not Allowed");
-        }
-      }.bind(this)
-    });
-
-    let principal = this._window.document.nodePrincipal;
-    cpmm.sendAsyncMessage("PermissionPromptHelper:AskPermission", {
+    // Create an array with a single nsIContentPermissionType element.
+    let type = {
       type: "contacts",
       access: access,
-      requestID: requestID,
-      origin: principal.origin,
-      appID: principal.appId,
-      browserFlag: principal.isInBrowserElement,
-      windowID: this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID
-    });
+      options: null,
+      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: aAllowCallback,
+      cancel: aCancelCallback,
+      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: {}};
 
@@ -459,17 +451,16 @@ ContactManager.prototype = {
 
   init: function(aWindow) {
     // DOMRequestIpcHelper.initHelper sets this._window
     this.initDOMRequestHelper(aWindow, ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO",
                               "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO",
                               "Contact:Save:Return:OK", "Contact:Save:Return:KO",
                               "Contact:Remove:Return:OK", "Contact:Remove:Return:KO",
                               "Contact:Changed",
-                              "PermissionPromptHelper:AskPermission:OK",
                               "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO",
                               "Contacts:Count",
                               "Contacts:Revision", "Contacts:GetRevision:Return:KO",]);
 
 
     let allowCallback = function() {
       cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
       this.hasListenPermission = true;
--- a/dom/contacts/tests/shared.js
+++ b/dom/contacts/tests/shared.js
@@ -1,14 +1,13 @@
 "use strict";
 
 // Fix the environment to run Contacts tests
 if (SpecialPowers.isMainProcess()) {
   SpecialPowers.Cu.import("resource://gre/modules/ContactService.jsm");
-  SpecialPowers.Cu.import("resource://gre/modules/PermissionPromptHelper.jsm");
 }
 
 SpecialPowers.addPermission("contacts-write", true, document);
 SpecialPowers.addPermission("contacts-read", true, document);
 SpecialPowers.addPermission("contacts-create", true, document);
 
 // Some helpful global vars
 var isAndroid = (navigator.userAgent.indexOf("Android") !== -1);
--- a/dom/inputmethod/mochitest/file_inputmethod.html
+++ b/dom/inputmethod/mochitest/file_inputmethod.html
@@ -1,18 +1,25 @@
 <html>
 <body>
 <script>
   var im = navigator.mozInputMethod;
   if (im) {
-    // Automatically append location hash to current input field.
-    im.oninputcontextchange = function() {
-      var ctx = im.inputcontext;
-      if (ctx) {
-        intervalId = setTimeout(function() {
-          ctx.replaceSurroundingText(location.hash);
-        }, 0);
-      }
-    };
+    im.oninputcontextchange = onIcc;
+
+    if (im.inputcontext) {
+      onIcc();
+    }
+  }
+
+  function onIcc() {
+    var ctx = im.inputcontext;
+    if (ctx) {
+      ctx.replaceSurroundingText(location.hash).then(function() {
+        /* Happy flow */
+      }, function(err) {
+        dump('ReplaceSurroundingText failed ' + err + '\n');
+      });
+    }
   }
 </script>
 </body>
 </html>
--- a/dom/inputmethod/mochitest/mochitest.ini
+++ b/dom/inputmethod/mochitest/mochitest.ini
@@ -6,12 +6,13 @@ support-files =
   file_inputmethod.html
   file_test_app.html
   file_test_sendkey_cancel.html
   file_test_sms_app.html
 
 [test_basic.html]
 [test_bug944397.html]
 [test_bug949059.html]
+[test_bug953044.html]
 [test_bug960946.html]
 [test_bug978918.html]
 [test_delete_focused_element.html]
 [test_sendkey_cancel.html]
--- a/dom/inputmethod/mochitest/test_bug944397.html
+++ b/dom/inputmethod/mochitest/test_bug944397.html
@@ -18,94 +18,94 @@ https://bugzilla.mozilla.org/show_bug.cg
 inputmethod_setup(function() {
   runTest();
 });
 
 // The frame script running in file_test_app.html.
 function appFrameScript() {
   let input = content.document.getElementById('test-input');
   input.oninput = function() {
-    dump('oninput was called in file_test_app.html.');
-    sendAsyncMessage('test:InputMethod:oninput', {});
+    sendAsyncMessage('test:InputMethod:oninput', {
+      value: input.value
+    });
   };
-
-  /*
-   * Bug 957213. Sometimes we need to refocus the input field to avoid
-   * intermittent test failure.
-   */
-  content.setInterval(function() {
-    input.focus();
-  }, 500);
 }
 
 function runTest() {
-  let timeoutId = null;
-
-  // Create an app frame to recieve keyboard inputs.
-  let app = document.createElement('iframe');
-  app.src = 'file_test_app.html';
-  app.setAttribute('mozbrowser', true);
-  document.body.appendChild(app);
-  app.addEventListener('mozbrowserloadend', function() {
-    let mm = SpecialPowers.getBrowserFrameMessageManager(app);
-    mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
-    mm.addMessageListener("test:InputMethod:oninput", function() {
-      ok(true, 'Keyboard input was received.');
-      clearTimeout(timeoutId);
-      inputmethod_cleanup();
-    });
-  });
+  let app, keyboard;
 
-  // Create a browser frame to load the input method app.
-  let keyboard = document.createElement('iframe');
-  keyboard.setAttribute('mozbrowser', true);
-  document.body.appendChild(keyboard);
-
-  // Bug 953044 setInputMethodActive(false) before input method app loads should
-  // always succeed.
-  let req = keyboard.setInputMethodActive(false);
-  req.onsuccess = function() {
-    ok(true, 'setInputMethodActive before loading succeeded.');
-  };
-
-  req.onerror = function() {
-   ok(false, 'setInputMethodActive before loading failed: ' + this.error.name);
-   clearTimeout(timeoutId);
-   inputmethod_cleanup();
-  };
+  /**
+   * So this test does the following:
+   * 1. Create a mozbrowser iframe with a text field in it, and focus the text field
+   * 2. 100ms. after loading we create new keyboard iframe, that will try to execute
+   *    replaceSurroundingText on the current active inputcontext
+   * 3. That should trigger 'input' event on the said text field
+   * 4. And if that happens we know everything is OK
+   */
 
   let path = location.pathname;
-  let imeUrl = location.protocol + '//' + location.host +
-               path.substring(0, path.lastIndexOf('/')) +
-               '/file_inputmethod.html#data';
-  SpecialPowers.pushPermissions([{
-    type: 'input',
-    allow: true,
-    context: imeUrl
-  }], function() {
-     let req = keyboard.setInputMethodActive(true);
+  let basePath = location.protocol + '//' + location.host +
+               path.substring(0, path.lastIndexOf('/'));
 
-     req.onsuccess = function() {
-       ok(true, 'setInputMethodActive succeeded.');
-     };
+  // STEP 1: Create an app frame to recieve keyboard inputs.
+  function step1() {
+    app = document.createElement('iframe');
+    app.src = basePath + '/file_test_app.html';
+    app.setAttribute('mozbrowser', true);
+    document.body.appendChild(app);
+    app.addEventListener('mozbrowserloadend', function() {
+      let mm = SpecialPowers.getBrowserFrameMessageManager(app);
+      mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
+      mm.addMessageListener("test:InputMethod:oninput", function(ev) {
+        step4(SpecialPowers.wrap(ev).json.value);
+      });
+
+      step2();
+    });
+  }
+
+  function step2() {
+    // STEP 2a: Create a browser frame to load the input method app.
+    keyboard = document.createElement('iframe');
+    keyboard.setAttribute('mozbrowser', true);
+    document.body.appendChild(keyboard);
+
+    // STEP 2b: Grant input privileges to the keyboard iframe
+    let imeUrl = basePath + '/file_inputmethod.html#data';
 
-     req.onerror = function() {
-       ok(false, 'setInputMethodActive failed: ' + this.error.name);
-       inputmethod_cleanup();
-     };
+    SpecialPowers.pushPermissions([{
+      type: 'input',
+      allow: true,
+      context: imeUrl
+    }], function() {
+      // STEP 2c: Tell Gecko to use this iframe as its keyboard app
+      let req = keyboard.setInputMethodActive(true);
+
+      req.onsuccess = function() {
+        ok(true, 'setInputMethodActive succeeded.');
+      };
 
-     // Loads the input method app to the browser frame after a delay.
-     SpecialPowers.DOMWindowUtils.focus(app);
-     setTimeout(function() {
-       keyboard.src = imeUrl;
-       timeoutId = setTimeout(function() {
-         inputmethod_cleanup();
-         ok(false, 'Failed to generate keyboard input.');
-       }, 20000);
-     }, 100);
-  });
+      req.onerror = function() {
+        ok(false, 'setInputMethodActive failed: ' + this.error.name);
+        inputmethod_cleanup();
+      };
+
+      // STEP 3: Loads the input method app to the browser frame after a delay.
+      setTimeout(function() {
+        keyboard.src = imeUrl;
+      }, 100);
+    });
+  }
+
+  function step4(val) {
+    ok(true, 'Keyboard input was received.');
+    is(val, '#dataYuan', 'Input value');
+    inputmethod_cleanup();
+  }
+
+  step1();
 }
 
 </script>
 </pre>
 </body>
 </html>
 
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug953044.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=953044
+-->
+<head>
+  <title>Basic test for InputMethod API.</title>
+  <script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript;version=1.7" src="inputmethod_common.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=953044">Mozilla Bug 953044</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+  runTest();
+});
+
+function runTest() {
+  // Create an app frame to recieve keyboard inputs.
+  let app = document.createElement('iframe');
+  app.src = 'file_test_app.html';
+  app.setAttribute('mozbrowser', true);
+  document.body.appendChild(app);
+
+  // Create a browser frame to load the input method app.
+  let keyboard = document.createElement('iframe');
+  keyboard.setAttribute('mozbrowser', true);
+  document.body.appendChild(keyboard);
+
+  // Bug 953044 setInputMethodActive(false) before input method app loads should
+  // always succeed.
+  let req = keyboard.setInputMethodActive(false);
+  req.onsuccess = function() {
+    ok(true, 'setInputMethodActive before loading succeeded.');
+    inputmethod_cleanup();
+  };
+
+  req.onerror = function() {
+    ok(false, 'setInputMethodActive before loading failed: ' + this.error.name);
+    inputmethod_cleanup();
+  };
+}
+
+</script>
+</pre>
+</body>
+</html>
+
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -42,18 +42,19 @@ interface nsIDOMBlob;
 interface nsIDOMFile;
 interface nsIFile;
 interface nsIDOMClientRect;
 interface nsIURI;
 interface nsIDOMEventTarget;
 interface nsIRunnable;
 interface nsICompositionStringSynthesizer;
 interface nsITranslationNodeList;
+interface nsIContentPermissionRequest;
 
-[scriptable, uuid(46e3f206-9a8f-4d66-8248-7ec6a37de45a)]
+[scriptable, uuid(ca202fa7-7b8f-4814-acc3-a8545f67320b)]
 interface nsIDOMWindowUtils : nsISupports {
 
   /**
    * Image animation mode of the window. When this attribute's value
    * is changed, the implementation should set all images in the window
    * to the given value. That is, when set to kDontAnimMode, all images
    * will stop animating. The attribute's value must be one of the
    * animationMode values from imgIContainer.
@@ -1658,16 +1659,22 @@ interface nsIDOMWindowUtils : nsISupport
      */
     attribute float audioVolume;
 
     /**
      * This method doesn't do anything useful.  It was solely added for the
      * purpose of the test for bug 503926.
      */
     void xpconnectArgument(in nsIDOMWindowUtils aThis);
+
+    /**
+     * Helper for JS components that need to send permission requests with
+     * e10s support properly.
+     */
+     void askPermission(in nsIContentPermissionRequest aRequest);
 };
 
 [scriptable, uuid(c694e359-7227-4392-a138-33c0cc1f15a6)]
 interface nsITranslationNodeList : nsISupports {
   readonly attribute unsigned long length;
   nsIDOMNode item(in unsigned long index);
 
   // A translation root is a block element, or an inline element
--- a/dom/ipc/PCOMContentPermissionRequestChild.h
+++ b/dom/ipc/PCOMContentPermissionRequestChild.h
@@ -5,16 +5,17 @@
 #ifndef PCOMContentPermissionRequestChild_h
 #define PCOMContentPermissionRequestChild_h
 
 #include "mozilla/dom/PContentPermissionRequestChild.h"
 // Microsoft's API Name hackery sucks
 // XXXbz Doing this in a header is a gigantic footgun.  See
 // https://bugzilla.mozilla.org/show_bug.cgi?id=932421#c3 for why.
 #undef CreateEvent
+#undef LoadImage
 
 /*
   PContentPermissionRequestChild implementations also are
   XPCOM objects.  Addref() is called on their implementation
   before SendPContentPermissionRequestConstructor is called.
   When Dealloc is called, IPDLRelease() is called.
   Implementations of this method are expected to call
   Release() on themselves.  See Bug 594261 for more
--- a/dom/media/MediaPermissionGonk.cpp
+++ b/dom/media/MediaPermissionGonk.cpp
@@ -30,48 +30,16 @@
 #define VIDEO_PERMISSION_NAME "video-capture"
 
 using namespace mozilla::dom;
 
 namespace mozilla {
 
 static MediaPermissionManager *gMediaPermMgr = nullptr;
 
-static uint32_t
-ConvertArrayToPermissionRequest(nsIArray* aSrcArray,
-                                nsTArray<PermissionRequest>& aDesArray)
-{
-  uint32_t len = 0;
-  aSrcArray->GetLength(&len);
-  for (uint32_t i = 0; i < len; i++) {
-    nsCOMPtr<nsIContentPermissionType> cpt = do_QueryElementAt(aSrcArray, i);
-    nsAutoCString type;
-    nsAutoCString access;
-    cpt->GetType(type);
-    cpt->GetAccess(access);
-
-    nsCOMPtr<nsIArray> optionArray;
-    cpt->GetOptions(getter_AddRefs(optionArray));
-    uint32_t optionsLength = 0;
-    optionArray->GetLength(&optionsLength);
-    nsTArray<nsString> options;
-    for (uint32_t j = 0; j < optionsLength; ++j) {
-      nsCOMPtr<nsISupportsString> isupportsString = do_QueryElementAt(optionArray, j);
-      if (isupportsString) {
-        nsString option;
-        isupportsString->GetData(option);
-        options.AppendElement(option);
-      }
-    }
-
-    aDesArray.AppendElement(PermissionRequest(type, access, options));
-  }
-  return len;
-}
-
 static void
 CreateDeviceNameList(nsTArray<nsCOMPtr<nsIMediaDevice> > &aDevices,
                      nsTArray<nsString> &aDeviceNameList)
 {
   for (uint32_t i = 0; i < aDevices.Length(); ++i) {
      nsString name;
      nsresult rv = aDevices[i]->GetName(name);
      NS_ENSURE_SUCCESS_VOID(rv);
@@ -444,17 +412,17 @@ MediaDeviceSuccessCallback::DoPrompt(nsR
     dom::TabChild* child = dom::TabChild::GetFrom(window->GetDocShell());
     NS_ENSURE_TRUE(child, NS_ERROR_FAILURE);
 
     nsCOMPtr<nsIArray> typeArray;
     rv = req->GetTypes(getter_AddRefs(typeArray));
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsTArray<PermissionRequest> permArray;
-    ConvertArrayToPermissionRequest(typeArray, permArray);
+    RemotePermissionRequest::ConvertArrayToPermissionRequest(typeArray, permArray);
 
     nsCOMPtr<nsIPrincipal> principal;
     rv = req->GetPrincipal(getter_AddRefs(principal));
     NS_ENSURE_SUCCESS(rv, rv);
 
     req->AddRef();
     child->SendPContentPermissionRequestConstructor(req,
                                                     permArray,
--- a/dom/mobileid/moz.build
+++ b/dom/mobileid/moz.build
@@ -1,7 +1,9 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 PARALLEL_DIRS += ['interfaces', 'src']
+
+TEST_DIRS += ['test']
new file mode 100644
--- /dev/null
+++ b/dom/mobileid/test/mochitest.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = (buildapp != 'b2g')
+
+[test_mobileid_basics.html]
+[test_mobileid_no_permission.html]
new file mode 100644
--- /dev/null
+++ b/dom/mobileid/test/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ['mochitest.ini']
new file mode 100644
--- /dev/null
+++ b/dom/mobileid/test/test_mobileid_basics.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for navigator.getMobileIdAssertion</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const MOCK_CID = SpecialPowers.wrap(SpecialPowers.Components)
+                              .ID("{4cb9b8b3-bc8c-46c0-a2b6-2eb0b1ffce94}");
+const MOBILE_ID_SERVICE_CONTRACT_ID = "@mozilla.org/mobileidentity-service;1";
+
+function finish() {
+  SpecialPowers.wrap(SpecialPowers.Components).manager
+               .QueryInterface(SpecialPowers.Ci.nsIComponentRegistrar)
+               .unregisterFactory(MOCK_CID, mockMobileIdService);
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var mockMobileIdService = {
+  QueryInterface: function(aIID) {
+    if (SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsISupports) ||
+        SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsIMobileIdentityService)) {
+      return this;
+    }
+    throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
+  },
+
+  createInstance: function(aOuter, aIID) {
+    if (aOuter != null) {
+      throw SpecialPowers.Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+
+  getMobileIdAssertion: function(aWindow, aOptions) {
+    return new Promise(function(resolve, reject) {
+      resolve(aOptions);
+    });
+  }
+};
+mockMobileIdService = SpecialPowers.wrapCallbackObject(mockMobileIdService);
+
+SpecialPowers.wrap(SpecialPowers.Components).manager
+             .QueryInterface(SpecialPowers.Ci.nsIComponentRegistrar)
+             .registerFactory(MOCK_CID, "mobileid service",
+                              MOBILE_ID_SERVICE_CONTRACT_ID,
+                              mockMobileIdService);
+
+// Tests
+
+SpecialPowers.pushPermissions([{"type": "mobileid",
+                               "allow": 1,
+                               "context": document}], function() {
+  ok("getMobileIdAssertion" in navigator,
+     "navigator.getMobileIdAssertion should exist");
+
+  var options = { forceSelection: true };
+  var promise = navigator.getMobileIdAssertion(options)
+  .then(function(result) {
+    ok(promise instanceof Promise, "Should return a Promise");
+    is(result.forceSelection, options.forceSelection,
+       "MobileIdentityService should receive correct options");
+    finish();
+  });
+});
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/mobileid/test/test_mobileid_no_permission.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for navigator.getMobileIdAssertion - No permission</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+ok(!("getMobileIdAssertion" in navigator),
+   "navigator.getMobileIdAssertion should NOT exist");
+
+SimpleTest.finish();
+</script>
+</pre>
+</body>
+</html>
--- a/dom/nfc/tests/marionette/head.js
+++ b/dom/nfc/tests/marionette/head.js
@@ -77,16 +77,26 @@ let emulator = (function() {
     this.run(cmd, function(result) {
       is(result.pop(), "OK", "set NDEF data of tag" + re);
       deferred.resolve();
     });
 
     return deferred.promise;
   };
 
+  function clearTagData(re) {
+    let deferred = Promise.defer();
+    let cmd = "nfc tag clear " + re;
+
+    this.run(cmd, function(result) {
+      is(result.pop(), "OK", "clear tag" + re);
+      deferred.resolve();
+    });
+  }
+
   function snepPutNdef(dsap, ssap, flags, tnf, type, payload, id) {
     let deferred = Promise.defer();
     let cmd = "nfc snep put " + dsap + " " + ssap + " [" + flags + "," +
                                                            tnf + "," +
                                                            type + "," +
                                                            payload + "," +
                                                            id + "]";
     this.run(cmd, function(result) {
@@ -98,16 +108,17 @@ let emulator = (function() {
   };
 
   return {
     run: run,
     activateRE: activateRE,
     deactivate: deactivate,
     notifyDiscoverRE: notifyDiscoverRE,
     setTagData: setTagData,
+    clearTagData: clearTagData,
     snepPutNdef: snepPutNdef
   };
 }());
 
 function toggleNFC(enabled) {
   let deferred = Promise.defer();
 
   let req;
--- a/dom/nfc/tests/marionette/manifest.ini
+++ b/dom/nfc/tests/marionette/manifest.ini
@@ -5,11 +5,11 @@ qemu=true
 
 [test_ndef.js]
 [test_nfc_enabled.js]
 [test_nfc_manager_tech_discovered.js]
 [test_nfc_manager_tech_discovered_ndef.js]
 [test_nfc_manager_tech_lost.js]
 [test_nfc_peer.js]
 [test_nfc_peer_sendndef.js]
-[test_nfc_tag.js]
+[test_nfc_read_tag.js]
 [test_nfc_checkP2PRegistration.js]
 [test_nfc_error_messages.js]
new file mode 100644
--- /dev/null
+++ b/dom/nfc/tests/marionette/test_nfc_read_tag.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 30000;
+MARIONETTE_HEAD_JS = "head.js";
+
+let url = "http://www.mozilla.org";
+
+const T1T_RE_INDEX   = 2;
+const T2T_RE_INDEX   = 3;
+const T3T_RE_INDEX   = 4;
+const T4T_RE_INDEX   = 5;
+
+function testUrlTagDiscover(re) {
+  log("Running \'testUrlTagDiscover\'");
+  // TODO : Make flag value readable.
+  let flag = 0xd0;
+  let tnf = NDEF.TNF_WELL_KNOWN;
+  let type = "U";
+  let payload = url;
+
+  window.navigator.mozSetMessageHandler("nfc-manager-tech-discovered", function(msg) {
+    log("Received \'nfc-manager-tech-ndiscovered\'");
+    is(msg.type, "techDiscovered", "check for correct message type");
+    let index = msg.techList.indexOf("NDEF");
+    isnot(index, -1, "check for \'NDEF\' in tech list");
+
+    let records = Cu.waiveXrays(msg.records);
+    ok(records.length > 0);
+
+    is(tnf, records[0].tnf, "check for TNF field in NDEF");
+    is(type, NfcUtils.toUTF8(records[0].type), "check for type field in NDEF");
+    is(payload, NfcUtils.toUTF8(records[0].payload), "check for payload field in NDEF");
+
+    toggleNFC(false).then(runNextTest);
+  });
+
+  toggleNFC(true)
+  .then(() => emulator.setTagData(re, flag, tnf, btoa(type), btoa(payload)))
+  .then(() => emulator.activateRE(re));
+}
+
+function testEmptyTagDiscover(re) {
+  log("Running \'testEmptyTagDiscover\'");
+
+  window.navigator.mozSetMessageHandler("nfc-manager-tech-discovered", function(msg) {
+    log("Received \'nfc-manager-tech-ndiscovered\'");
+    is(msg.type, "techDiscovered", "check for correct message type");
+    let index = msg.techList.indexOf("NDEF");
+    isnot(index, -1, "check for \'NDEF\' in tech list");
+
+    let records = msg.records;
+    ok(records == null);
+
+    toggleNFC(false).then(runNextTest);
+  });
+
+  toggleNFC(true)
+  .then(() => emulator.clearTagData(re))
+  .then(() => emulator.activateRE(re));
+}
+
+function testUrlT1TDiscover() {
+  testUrlTagDiscover(T1T_RE_INDEX);
+}
+
+function testUrlT2TDiscover() {
+  testUrlTagDiscover(T2T_RE_INDEX);
+}
+
+function testUrlT3TDiscover() {
+  testUrlTagDiscover(T3T_RE_INDEX);
+}
+
+function testUrlT4TDiscover() {
+  testUrlTagDiscover(T4T_RE_INDEX);
+}
+
+function testEmptyT1TDiscover() {
+  testEmptyTagDiscover(T1T_RE_INDEX);
+}
+
+function testEmptyT2TDiscover() {
+  testEmptyTagDiscover(T2T_RE_INDEX);
+}
+
+function testEmptyT3TDiscover() {
+  testEmptyTagDiscover(T3T_RE_INDEX);
+}
+
+function testEmptyT4TDiscover() {
+  testEmptyTagDiscover(T4T_RE_INDEX);
+}
+
+let tests = [
+    testUrlT1TDiscover,
+    testUrlT2TDiscover,
+    testUrlT3TDiscover,
+    testUrlT4TDiscover,
+    testEmptyT1TDiscover,
+    testEmptyT2TDiscover,
+    testEmptyT3TDiscover,
+    testEmptyT4TDiscover
+];
+
+SpecialPowers.pushPermissions(
+  [{'type': 'nfc-manager', 'allow': true, context: document}], runTests);
deleted file mode 100644
--- a/dom/nfc/tests/marionette/test_nfc_tag.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-MARIONETTE_TIMEOUT = 30000;
-MARIONETTE_HEAD_JS = "head.js";
-
-let url = "http://www.mozilla.org";
-
-// TODO : Get this from emulator console command.
-const T1T_RE_INDEX = 2;
-const T2T_RE_INDEX = 3;
-const T3T_RE_INDEX = 4;
-const T4T_RE_INDEX = 5;
-
-function testUrlTagDiscover(re) {
-  log("Running \'testUrlTagDiscover\'");
-  // TODO : Make flag value readable.
-  let flag = 0xd0;
-  let tnf = NDEF.TNF_WELL_KNOWN;
-  let type = "U";
-  let payload = url;
-
-  window.navigator.mozSetMessageHandler("nfc-manager-tech-discovered", function(msg) {
-    log("Received \'nfc-manager-tech-ndiscovered\'");
-    is(msg.type, "techDiscovered", "check for correct message type");
-    let index = msg.techList.indexOf("NDEF");
-    isnot(index, -1, "check for \'NDEF\' in tech list");
-
-    let records = Cu.waiveXrays(msg.records);
-    ok(records.length > 0);
-
-    is(tnf, records[0].tnf, "check for TNF field in NDEF");
-    is(type, NfcUtils.toUTF8(records[0].type), "check for type field in NDEF");
-    is(payload, NfcUtils.toUTF8(records[0].payload), "check for payload field in NDEF");
-
-    toggleNFC(false).then(runNextTest);
-  });
-
-  toggleNFC(true)
-  .then(() => emulator.setTagData(re, flag, tnf, btoa(type), btoa(payload)))
-  .then(() => emulator.activateRE(re));
-}
-
-function testUrlT1TDiscover() {
-  testUrlTagDiscover(T1T_RE_INDEX);
-}
-
-function testUrlT2TDiscover() {
-  testUrlTagDiscover(T2T_RE_INDEX);
-}
-
-function testUrlT3TDiscover() {
-  testUrlTagDiscover(T3T_RE_INDEX);
-}
-
-function testUrlT4TDiscover() {
-  testUrlTagDiscover(T4T_RE_INDEX);
-}
-
-let tests = [
-  testUrlT1TDiscover,
-  testUrlT2TDiscover,
-  testUrlT3TDiscover,
-  testUrlT4TDiscover
-];
-
-SpecialPowers.pushPermissions(
-  [{'type': 'nfc-manager', 'allow': true, context: document}], runTests);
deleted file mode 100644
--- a/dom/permission/PermissionPromptHelper.jsm
+++ /dev/null
@@ -1,146 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* PermissionPromptHelper checks the permissionDB for a given permission
- * name and performs prompting if needed.
- * Usage: send PermissionPromptHelper:AskPermission via the FrameMessageManager with:
- * |origin|, |appID|, |browserFlag| -> used for getting the principal and
- * |type| and |access| to call testExactPermissionFromPrincipal.
- * Note that |access| isn't currently used.
- * Other arugments are:
- * requestID: ID that gets returned with the result message.
- *
- * Once the permission is checked, it returns with the message
- * "PermissionPromptHelper:AskPermission:OK"
- * The result contains the |result| e.g.Ci.nsIPermissionManager.ALLOW_ACTION
- * and a requestID that
- */
-
-"use strict";
-
-let DEBUG = 0;
-let debug;
-if (DEBUG)
-  debug = function (s) { dump("-*- Permission Prompt Helper component: " + s + "\n"); }
-else
-  debug = function (s) {}
-
-const Cu = Components.utils;
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-
-this.EXPORTED_SYMBOLS = ["PermissionPromptHelper"];
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
-                                   "@mozilla.org/parentprocessmessagemanager;1",
-                                   "nsIMessageListenerManager");
-
-XPCOMUtils.defineLazyServiceGetter(this, "permissionPromptService",
-                                   "@mozilla.org/permission-prompt-service;1",
-                                   "nsIPermissionPromptService");
-
-let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
-
-this.PermissionPromptHelper = {
-  init: function init() {
-    debug("Init");
-    ppmm.addMessageListener("PermissionPromptHelper:AskPermission", this);
-    Services.obs.addObserver(this, "profile-before-change", false);
-  },
-
-  askPermission: function askPermission(aMessage, aCallbacks) {
-    let msg = aMessage.json;
-
-    let access = msg.type;
-    if (msg.access) {
-      access = access + "-" + msg.access;
-    }
-
-    let uri = Services.io.newURI(msg.origin, null, null);
-    let principal =
-      Services.scriptSecurityManager.getAppCodebasePrincipal(uri, msg.appID, msg.browserFlag);
-
-    let permValue =
-      Services.perms.testExactPermissionFromPrincipal(principal, access);
-
-    if (permValue == Ci.nsIPermissionManager.DENY_ACTION ||
-        permValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
-      aCallbacks.cancel();
-      return;
-    }
-
-    if (permValue == Ci.nsIPermissionManager.PROMPT_ACTION) {
-
-      // create the options from permission request.
-      let options = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
-      if (msg.options) {
-        for (let option of options) {
-          options.appendElement(option);
-        }
-      }
-
-      // create an array with a nsIContentPermissionType element
-      let type = {
-        type: msg.type,
-        access: msg.access ? msg.access : "unused",
-        options: 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: aCallbacks.allow,
-        cancel: aCallbacks.cancel,
-        window: Services.wm.getOuterWindowWithId(msg.windowID)
-      };
-
-      permissionPromptService.getPermission(request);
-      return;
-    }
-
-    if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
-      aCallbacks.allow();
-      return;
-    }
-  },
-
-  observe: function observe(aSubject, aTopic, aData) {
-    ppmm.removeMessageListener("PermissionPromptHelper:AskPermission", this);
-    Services.obs.removeObserver(this, "profile-before-change");
-    ppmm = null;
-  },
-
-  receiveMessage: function receiveMessage(aMessage) {
-    debug("PermissionPromptHelper::receiveMessage " + aMessage.name);
-    let mm = aMessage.target;
-    let msg = aMessage.data;
-
-    let result;
-    if (aMessage.name == "PermissionPromptHelper:AskPermission") {
-      this.askPermission(aMessage, {
-        cancel: function() {
-          mm.sendAsyncMessage("PermissionPromptHelper:AskPermission:OK",
-                              { result: Ci.nsIPermissionManager.DENY_ACTION,
-                                requestID: msg.requestID });
-        },
-        allow: function(aChoice) {
-          mm.sendAsyncMessage("PermissionPromptHelper:AskPermission:OK",
-                              { result: Ci.nsIPermissionManager.ALLOW_ACTION,
-                                choice: aChoice,
-                                requestID: msg.requestID });
-        }
-      });
-    }
-  }
-}
-
-PermissionPromptHelper.init();
--- a/dom/permission/moz.build
+++ b/dom/permission/moz.build
@@ -9,11 +9,10 @@ TEST_DIRS += ['tests']
 EXTRA_COMPONENTS += [
     'PermissionPromptService.js',
     'PermissionPromptService.manifest',
     'PermissionSettings.js',
     'PermissionSettings.manifest',
 ]
 
 EXTRA_JS_MODULES += [
-    'PermissionPromptHelper.jsm',
     'PermissionSettings.jsm',
 ]
--- a/mobile/android/chrome/content/WebappRT.js
+++ b/mobile/android/chrome/content/WebappRT.js
@@ -4,17 +4,16 @@
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
-Cu.import("resource://gre/modules/PermissionPromptHelper.jsm");
 Cu.import("resource://gre/modules/ContactService.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
 
 function pref(name, value) {
   return {
     name: name,
--- a/services/mobileid/MobileIdentityCredentialsStore.jsm
+++ b/services/mobileid/MobileIdentityCredentialsStore.jsm
@@ -31,32 +31,33 @@ this.MobileIdentityCredentialsStore.prot
   },
 
   upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
     log.debug("upgradeSchema");
     /**
      * We will be storing objects like:
      *  {
      *    msisdn: <string> (key),
-     *    iccId: <string> (index),
+     *    iccId: <string> (index, optional),
+     *    deviceIccIds: <array>,
      *    origin: <array> (index),
-     *    msisdnSessionToken: <string>
+     *    msisdnSessionToken: <string>,
      *  }
      */
     let objectStore = aDb.createObjectStore(CREDENTIALS_STORE_NAME, {
       keyPath: "msisdn"
     });
 
     objectStore.createIndex("iccId", "iccId", { unique: true });
     objectStore.createIndex("origin", "origin", { unique: true, multiEntry: true });
   },
 
-  add: function(aIccId, aMsisdn, aOrigin, aSessionToken) {
+  add: function(aIccId, aMsisdn, aOrigin, aSessionToken, aDeviceIccIds) {
     log.debug("put " + aIccId + ", " + aMsisdn + ", " + aOrigin + ", " +
-              aSessionToken);
+              aSessionToken + ", " + aDeviceIccIds);
     if (!aOrigin || !aSessionToken) {
       return Promise.reject(ERROR_INTERNAL_DB_ERROR);
     }
 
     let deferred = Promise.defer();
 
     // We first try get an existing record for the given MSISDN.
     this.newTxn(
@@ -77,17 +78,18 @@ this.MobileIdentityCredentialsStore.prot
             }
             cursor.update(record);
           } else {
             // Otherwise, we store a new record.
             record = {
               iccId: aIccId,
               msisdn: aMsisdn,
               origin: [aOrigin],
-              sessionToken: aSessionToken
+              sessionToken: aSessionToken,
+              deviceIccIds: aDeviceIccIds
             };
             aStore.add(record);
           }
           deferred.resolve();
         };
         cursorReq.onerror = function(aEvent) {
           log.error(aEvent.target.error);
           deferred.reject(ERROR_INTERNAL_DB_ERROR);
@@ -164,10 +166,92 @@ this.MobileIdentityCredentialsStore.prot
       CREDENTIALS_STORE_NAME,
       (aTxn, aStore) => {
         aStore.delete(aMsisdn);
       },
       deferred.resolve,
       deferred.reject
     );
     return deferred.promise;
+  },
+
+  removeValue: function(aMsisdn, aKey, aValue) {
+    log.debug("Removing " + aKey + " with value " + aValue);
+    if (!aMsisdn || !aKey) {
+      return Promise.reject();
+    }
+
+    let deferred = Promise.defer();
+    this.newTxn(
+      "readwrite",
+      CREDENTIALS_STORE_NAME,
+      (aTxn, aStore) => {
+        let range = IDBKeyRange.only(aMsisdn);
+        let cursorReq = aStore.openCursor(range);
+        cursorReq.onsuccess = function(aEvent) {
+          let cursor = aEvent.target.result;
+          let record;
+          if (!cursor || !cursor.value) {
+            return Promise.resolve();
+          }
+          record = cursor.value;
+          if (!record[aKey]) {
+            return Promise.reject();
+          }
+          if (aValue) {
+            let index = record[aKey].indexOf(aValue);
+            if (index != -1) {
+              record[aKey].splice(index, 1);
+            }
+          } else {
+            record[aKey] = undefined;
+          }
+          log.debug("Removal done ${}", record);
+          cursor.update(record);
+          deferred.resolve();
+        };
+        cursorReq.onerror = function(aEvent) {
+          log.error(aEvent.target.error);
+          deferred.reject(ERROR_INTERNAL_DB_ERROR);
+        };
+      }, null, deferred.reject);
+
+    return deferred.promise;
+  },
+
+  removeOrigin: function(aMsisdn, aOrigin) {
+    log.debug("removeOrigin " + aMsisdn + " " + aOrigin);
+    return this.removeValue(aMsisdn, "origin", aOrigin);
+  },
+
+  setDeviceIccIds: function(aMsisdn, aDeviceIccIds) {
+    log.debug("Setting icc ids " + aDeviceIccIds + " for " + aMsisdn);
+    if (!aMsisdn) {
+      return Promise.reject();
+    }
+
+    let deferred = Promise.defer();
+    this.newTxn(
+      "readwrite",
+      CREDENTIALS_STORE_NAME,
+      (aTxn, aStore) => {
+        let range = IDBKeyRange.only(aMsisdn);
+        let cursorReq = aStore.openCursor(range);
+        cursorReq.onsuccess = function(aEvent) {
+          let cursor = aEvent.target.result;
+          let record;
+          if (!cursor || !cursor.value) {
+            return Promise.resolve();
+          }
+          record = cursor.value;
+          record.deviceIccIds = aDeviceIccIds;
+          cursor.update(record);
+          deferred.resolve();
+        };
+        cursorReq.onerror = function(aEvent) {
+          log.error(aEvent.target.error);
+          deferred.reject(ERROR_INTERNAL_DB_ERROR);
+        };
+      }, null, deferred.reject);
+
+    return deferred.promise;
   }
 };
--- a/services/mobileid/MobileIdentityManager.jsm
+++ b/services/mobileid/MobileIdentityManager.jsm
@@ -1,15 +1,15 @@
 /* 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";
 
-this.EXPORTED_SYMBOLS = [];
+this.EXPORTED_SYMBOLS = ["MobileIdentityManager"];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
 Cu.import("resource://gre/modules/MobileIdentityUIGlueCommon.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
@@ -57,24 +57,23 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsIRadioInterfaceLayer");
 
 XPCOMUtils.defineLazyServiceGetter(this, "iccProvider",
                                    "@mozilla.org/ril/content-helper;1",
                                    "nsIIccProvider");
 #endif
 
 
-let MobileIdentityManager = {
+this.MobileIdentityManager = {
 
   init: function() {
     log.debug("MobileIdentityManager init");
     Services.obs.addObserver(this, "xpcom-shutdown", false);
     ppmm.addMessageListener(GET_ASSERTION_IPC_MSG, this);
     this.messageManagers = {};
-    // TODO: Store keyPairs and certificates in disk. Bug 1021605.
     this.keyPairs = {};
     this.certificates = {};
   },
 
   receiveMessage: function(aMessage) {
     log.debug("Received " + aMessage.name);
 
     if (aMessage.name !== GET_ASSERTION_IPC_MSG) {
@@ -102,21 +101,20 @@ let MobileIdentityManager = {
   },
 
 
   /*********************************************************
    * Getters
    ********************************************************/
 
   get iccInfo() {
-#ifdef MOZ_B2G_RIL
     if (this._iccInfo) {
       return this._iccInfo;
     }
-
+#ifdef MOZ_B2G_RIL
     this._iccInfo = [];
     for (let i = 0; i < gRil.numRadioInterfaces; i++) {
       let rilContext = gRil.getRadioInterface(i).rilContext;
       if (!rilContext) {
         log.warn("Tried to get the RIL context for an invalid service ID " + i);
         continue;
       }
       let info = rilContext.iccInfo;
@@ -148,16 +146,36 @@ let MobileIdentityManager = {
       });
     }
 
     return this._iccInfo;
 #endif
     return null;
   },
 
+  get iccIds() {
+#ifdef MOZ_B2G_RIL
+    if (this._iccIds) {
+      return this._iccIds;
+    }
+
+    this._iccIds = [];
+    if (!this.iccInfo) {
+      return this._iccIds;
+    }
+
+    for (let i = 0; i < this.iccInfo.length; i++) {
+      this._iccIds.push(this.iccInfo[i].iccId);
+    }
+
+    return this._iccIds;
+#endif
+    return null;
+  },
+
   get credStore() {
     if (!this._credStore) {
       this._credStore = new MobileIdentityCredentialsStore();
       this._credStore.init();
     }
     return this._credStore;
   },
 
@@ -234,17 +252,19 @@ let MobileIdentityManager = {
 
   getVerificationOptions: function() {
     log.debug("getVerificationOptions");
     // We try to get if we already have credentials for any of the inserted
     // SIM cards if any is available and we try to get the possible
     // verification mechanisms for these SIM cards.
     // All this information will be stored in iccInfo.
     if (!this.iccInfo || !this.iccInfo.length) {
-      return Promise.resolve();
+      let deferred = Promise.defer();
+      deferred.resolve(null);
+      return deferred.promise;
     }
 
     let promises = [];
     for (let i = 0; i < this.iccInfo.length; i++) {
       promises.push(this.getVerificationOptionsForIcc(i));
     }
     return Promise.all(promises);
   },
@@ -295,16 +315,35 @@ let MobileIdentityManager = {
         deferred.resolve(signedCert.cert);
       },
       deferred.reject
     );
     return deferred.promise;
   },
 
   /*********************************************************
+   * Setters (for test only purposes)
+   ********************************************************/
+  set ui(aUi) {
+    this._ui = aUi;
+  },
+
+  set credStore(aCredStore) {
+    this._credStore = aCredStore;
+  },
+
+  set client(aClient) {
+    this._client = aClient;
+  },
+
+  set iccInfo(aIccInfo) {
+    this._iccInfo = aIccInfo;
+  },
+
+  /*********************************************************
    * UI callbacks
    ********************************************************/
 
   onUICancel: function() {
     log.debug("UI cancel");
     if (this.activeVerificationFlow) {
       this.activeVerificationFlow.cleanup(true);
     }
@@ -522,27 +561,27 @@ let MobileIdentityManager = {
         phoneInfoArray.push(phoneInfo);
       }
     }
 
     return this.ui.startFlow(aManifestURL, phoneInfoArray)
     .then(
       (result) => {
         if (!result ||
-            (!result.phoneNumber && !result.serviceId)) {
+            (!result.phoneNumber && (result.serviceId === undefined))) {
           return Promise.reject(ERROR_INTERNAL_INVALID_PROMPT_RESULT);
         }
 
         let msisdn;
         let mcc;
 
         // If the user selected one of the existing SIM cards we have to check
         // that we either have the MSISDN for that SIM or we can do a silent
         // verification that does not require us to have the MSISDN in advance.
-        if (result.serviceId) {
+        if (result.serviceId !== undefined) {
           let icc = this.iccInfo[result.serviceId];
           log.debug("icc ${}", icc);
           if (!icc || !icc.msisdn && !icc.canDoSilentVerification) {
             return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
           }
           msisdn = icc.msisdn;
           mcc = icc.mcc;
         } else {
@@ -592,17 +631,17 @@ let MobileIdentityManager = {
         // If we have an exisiting credentials, we add its associated
         // phone number information to the list of choices to present
         // to the user within the selection prompt.
         let phoneInfo;
         if (aCreds) {
           phoneInfo = new MobileIdentityUIGluePhoneInfo(
             aCreds.msisdn,
             null,           // operator
-            null,           // service ID
+            undefined,      // service ID
             !!aCreds.iccId, // external
             true            // primary
           );
         }
         return this.prompt(aPrincipal, aManifestURL, phoneInfo);
       }
     )
     .then(
@@ -618,42 +657,71 @@ let MobileIdentityManager = {
 
         // We might already have credentials for the user selected icc. In
         // that case, we update the credentials store with the new origin and
         // return the credentials.
         if (promptResult.serviceId) {
           let creds = this.iccInfo[promptResult.serviceId].credentials;
           if (creds) {
             this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
-                               creds.sessionToken);
+                               creds.sessionToken, this.iccIds);
             return creds;
           }
         }
 
         // Or we might already have credentials for the selected phone
         // number and so we do the same: update the credentials store with the
         // new origin and return the credentials.
         return this.credStore.getByMsisdn(promptResult.msisdn)
         .then(
           (creds) => {
             if (creds) {
               this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
-                                 creds.sessionToken);
+                                 creds.sessionToken, this.iccIds);
               return creds;
             }
             // Otherwise, we need to verify the new number selected by the
             // user.
             return this.verificationFlow(promptResult, aPrincipal.origin);
           }
         );
       }
     );
   },
 
   /*********************************************************
+   * Credentials check
+   *********************************************************/
+
+  checkNewCredentials: function(aOldCreds, aNewCreds, aOrigin) {
+    // If there were previous credentials and the user changed her
+    // choice, we need to remove the origin from the old credentials.
+    if (aNewCreds.msisdn != aOldCreds.msisdn) {
+      return this.credStore.removeOrigin(aOldCreds.msisdn,
+                                         aOrigin)
+      .then(
+        () => {
+          return aNewCreds;
+        }
+      );
+    } else {
+      // Otherwise, we update the status of the SIM cards in the device
+      // so we know that the user decided not to take the chance to change
+      // her selection. We won't bother her again until a new SIM card
+      // change is detected.
+      return this.credStore.setDeviceIccIds(aOldCreds.msisdn, this.iccIds)
+      .then(
+        () => {
+          return aOldCreds;
+        }
+      );
+    }
+  },
+
+  /*********************************************************
    * Assertion generation
    ********************************************************/
 
   generateAssertion: function(aCredentials, aOrigin) {
     if (!aCredentials.sessionToken) {
       return Promise.reject(ERROR_INTERNAL_INVALID_TOKEN);
     }
 
@@ -680,17 +748,18 @@ let MobileIdentityManager = {
               if (error) {
                 log.error("Error generating assertion " + err);
                 deferred.reject(error);
                 return;
               }
               this.credStore.add(aCredentials.iccId,
                                  aCredentials.msisdn,
                                  aOrigin,
-                                 aCredentials.sessionToken)
+                                 aCredentials.sessionToken,
+                                 this.iccIds)
               .then(
                 () => {
                   deferred.resolve(assertion);
                 }
               );
             });
           }, deferred.reject
         );
@@ -706,51 +775,80 @@ let MobileIdentityManager = {
     let uri = Services.io.newURI(aPrincipal.origin, null, null);
     let principal = securityManager.getAppCodebasePrincipal(
       uri, aPrincipal.appid, aPrincipal.isInBrowserElement);
     let manifestURL = appsService.getManifestURLByLocalId(aPrincipal.appId);
 
     // First of all we look if we already have credentials for this origin.
     // If we don't have credentials it means that it is the first time that
     // the caller requested an assertion.
-    return this.credStore.getByOrigin(aPrincipal.origin)
+    this.credStore.getByOrigin(aPrincipal.origin)
     .then(
       (creds) => {
         log.debug("creds ${creds} - ${origin}", { creds: creds,
                                                   origin: aPrincipal.origin });
         if (!creds || !creds.sessionToken) {
           log.debug("No credentials");
           return;
         }
 
-        // Even if we already have credentials for this origin, the consumer of
-        // the API might want to force the identity selection dialog.
+        // Even if we already have credentials for this origin, the consumer
+        // of the API might want to force the identity selection dialog.
         if (aOptions.forceSelection) {
-          return this.promptAndVerify(principal, manifestURL, creds);
+          return this.promptAndVerify(principal, manifestURL, creds)
+          .then(
+            (newCreds) => {
+              return this.checkNewCredentials(creds, newCreds,
+                                              principal.origin);
+            }
+          );
         }
 
-        // It is possible that the ICC associated with the stored
-        // credentials is not present in the device anymore, so we ask the
-        // user if she still wants to use it anyway or if she prefers to use
-        // another phone number.
-        // If the credentials are associated with an external SIM or there is
-        // no SIM in the device, we just return the credentials.
-        if (this.iccInfo && creds.iccId) {
-          for (let i = 0; i < this.iccInfo.length; i++) {
-            if (this.iccInfo[i].iccId == creds.iccId) {
-              return creds;
-            }
-          }
-          // At this point we know that the SIM associated with the credentials
-          // is not present in the device any more, so we need to ask the user
-          // what to do.
-          return this.promptAndVerify(principal, manifestURL, creds);
+        // SIM change scenario.
+
+        // It is possible that the SIM cards inserted in the device at the
+        // moment of the previous verification where we obtained the credentials
+        // has changed. In that case, we need to let the user knowabout this
+        // situation. Otherwise, we just return the credentials.
+        log.debug("Looking for SIM changes. Credentials ICCS ${creds} " +
+                  "Device ICCS ${device}", { creds: creds.deviceIccIds,
+                                             device: this.iccIds });
+        let simChanged = (creds.deviceIccIds == null && this.iccIds != null) ||
+                         (creds.deviceIccIds != null && this.iccIds == null);
+
+        if (!simChanged &&
+            creds.deviceIccIds != null &&
+            this.IccIds != null) {
+          simChanged = creds.deviceIccIds.length != this.iccIds.length;
         }
 
-        return creds;
+        if (!simChanged &&
+            creds.deviceIccIds != null &&
+            this.IccIds != null) {
+          let intersection = creds.deviceIccIds.filter((n) => {
+            return this.iccIds.indexOf(n) != -1;
+          });
+          simChanged = intersection.length != creds.deviceIccIds.length ||
+                       intersection.length != this.iccIds.length;
+        }
+
+        if (!simChanged) {
+          return creds;
+        }
+
+        // At this point we know that the SIM associated with the credentials
+        // is not present in the device any more or a new SIM has been detected,
+        // so we need to ask the user what to do.
+        return this.promptAndVerify(principal, manifestURL, creds)
+        .then(
+          (newCreds) => {
+            return this.checkNewCredentials(creds, newCreds,
+                                            principal.origin);
+          }
+        );
       }
     )
     .then(
       (creds) => {
         // Even if we have credentails it is possible that the user has
         // removed the permission to share its mobile id with this origin, so
         // we check the permission and if it is not granted, we ask the user
         // before generating and sharing the assertion.
@@ -758,17 +856,18 @@ let MobileIdentityManager = {
         // is already granted and stored so we just progress the credentials.
         if (creds) {
           let permission = permissionManager.testPermissionFromPrincipal(
             principal,
             MOBILEID_PERM
           );
           if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
             return creds;
-          } else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
+          } else if (permission == Ci.nsIPermissionManager.DENY_ACTION ||
+                     permission == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
             return Promise.reject(ERROR_PERMISSION_DENIED);
           }
           return this.promptAndVerify(principal, manifestURL, creds);
         }
         return this.promptAndVerify(principal, manifestURL);
       }
     )
     .then(
--- a/services/mobileid/moz.build
+++ b/services/mobileid/moz.build
@@ -1,16 +1,18 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 PARALLEL_DIRS += ['interfaces']
 
+TEST_DIRS += ['tests']
+
 EXTRA_JS_MODULES += [
     'MobileIdentityClient.jsm',
     'MobileIdentityCommon.jsm',
     'MobileIdentityCredentialsStore.jsm',
     'MobileIdentitySmsMoMtVerificationFlow.jsm',
     'MobileIdentitySmsMtVerificationFlow.jsm',
     'MobileIdentityUIGlueCommon.jsm',
     'MobileIdentityVerificationFlow.jsm'
new file mode 100644
--- /dev/null
+++ b/services/mobileid/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['xpcshell/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/services/mobileid/tests/xpcshell/head.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+"use strict";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+(function initMobileIdTestingInfrastructure() {
+  do_get_profile();
+
+  Services.prefs.setCharPref("services.mobileid.loglevel", "Debug");
+
+}).call(this);
new file mode 100644
--- /dev/null
+++ b/services/mobileid/tests/xpcshell/test_mobileid_manager.js
@@ -0,0 +1,1235 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Cm = Components.manager;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// We need to set the server url pref before importing MobileIdentityManager.
+// Otherwise, it won't be able to import it as getting the non existing pref
+// will throw.
+Services.prefs.setCharPref("services.mobileid.server.uri",
+                           "https://dummyurl.com");
+
+// Set do_printging on.
+Services.prefs.setCharPref("services.mobileid.loglevel", "do_print");
+
+Cu.import("resource://gre/modules/MobileIdentityManager.jsm");
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+
+const DEBUG = false;
+
+const GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion";
+const GET_ASSERTION_RETURN_OK = "MobileId:GetAssertion:Return:OK";
+const GET_ASSERTION_RETURN_KO = "MobileId:GetAssertion:Return:KO";
+
+// === Globals ===
+
+const ORIGIN = "app://afakeorigin";
+const PRINCIPAL = {
+  origin: ORIGIN,
+  appId: "123"
+};
+const PHONE_NUMBER = "+34666555444";
+const ANOTHER_PHONE_NUMBER = "+44123123123";
+const VERIFICATION_CODE = "123456";
+const SESSION_TOKEN = "aSessionToken";
+const ICC_ID = "aIccId";
+const MNC = "aMnc";
+const MCC = 214;
+const OPERATOR = "aOperator";
+const CERTIFICATE = "eyJhbGciOiJEUzI1NiJ9.eyJsYXN0QXV0aEF0IjoxNDA0NDY5NzkyODc3LCJ2ZXJpZmllZE1TSVNETiI6IiszMTYxNzgxNTc1OCIsInB1YmxpYy1rZXkiOnsiYWxnb3JpdGhtIjoiRFMiLCJ5IjoiNGE5YzkzNDY3MWZhNzQ3YmM2ZjMyNjE0YTg1MzUyZjY5NDcwMDdhNTRkMDAxMDY4OWU5ZjJjZjc0ZGUwYTEwZTRlYjlmNDk1ZGFmZTA0NGVjZmVlNDlkN2YwOGU4ODQyMDJiOTE5OGRhNWZhZWE5MGUzZjRmNzE1YzZjNGY4Yjc3MGYxZTU4YWZhNDM0NzVhYmFiN2VlZGE1MmUyNjk2YzFmNTljNzMzYjFlYzBhNGNkOTM1YWIxYzkyNzAxYjNiYTA5ZDRhM2E2MzNjNTJmZjE2NGYxMWY3OTg1YzlmZjY3ZThmZDFlYzA2NDU3MTdkMjBiNDE4YmM5M2YzYzVkNCIsInAiOiJmZjYwMDQ4M2RiNmFiZmM1YjQ1ZWFiNzg1OTRiMzUzM2Q1NTBkOWYxYmYyYTk5MmE3YThkYWE2ZGMzNGY4MDQ1YWQ0ZTZlMGM0MjlkMzM0ZWVlYWFlZmQ3ZTIzZDQ4MTBiZTAwZTRjYzE0OTJjYmEzMjViYTgxZmYyZDVhNWIzMDVhOGQxN2ViM2JmNGEwNmEzNDlkMzkyZTAwZDMyOTc0NGE1MTc5MzgwMzQ0ZTgyYTE4YzQ3OTMzNDM4Zjg5MWUyMmFlZWY4MTJkNjljOGY3NWUzMjZjYjcwZWEwMDBjM2Y3NzZkZmRiZDYwNDYzOGMyZWY3MTdmYzI2ZDAyZTE3IiwicSI6ImUyMWUwNGY5MTFkMWVkNzk5MTAwOGVjYWFiM2JmNzc1OTg0MzA5YzMiLCJnIjoiYzUyYTRhMGZmM2I3ZTYxZmRmMTg2N2NlODQxMzgzNjlhNjE1NGY0YWZhOTI5NjZlM2M4MjdlMjVjZmE2Y2Y1MDhiOTBlNWRlNDE5ZTEzMzdlMDdhMmU5ZTJhM2NkNWRlYTcwNGQxNzVmOGViZjZhZjM5N2Q2OWUxMTBiOTZhZmIxN2M3YTAzMjU5MzI5ZTQ4MjliMGQwM2JiYzc4OTZiMTViNGFkZTUzZTEzMDg1OGNjMzRkOTYyNjlhYTg5MDQxZjQwOTEzNmM3MjQyYTM4ODk1YzlkNWJjY2FkNGYzODlhZjFkN2E0YmQxMzk4YmQwNzJkZmZhODk2MjMzMzk3YSJ9LCJwcmluY2lwYWwiOiIwMzgxOTgyYS0xZTgzLTI1NjYtNjgzZS05MDRmNDA0NGM1MGRAbXNpc2RuLWRldi5zdGFnZS5tb3phd3MubmV0IiwiaWF0IjoxNDA0NDY5NzgyODc3LCJleHAiOjE0MDQ0OTEzOTI4NzcsImlzcyI6Im1zaXNkbi1kZXYuc3RhZ2UubW96YXdzLm5ldCJ9."
+
+// === Helpers ===
+
+function addPermission(aOrigin, aAction) {
+  let uri = Cc["@mozilla.org/network/io-service;1"]
+              .getService(Ci.nsIIOService)
+              .newURI(aOrigin, null, null);
+  let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"]
+                     .getService(Ci.nsIScriptSecurityManager)
+                     .getNoAppCodebasePrincipal(uri);
+  let pm = Cc["@mozilla.org/permissionmanager;1"]
+             .getService(Ci.nsIPermissionManager);
+  pm.addFromPrincipal(_principal, MOBILEID_PERM, aAction);
+}
+
+function removePermission(aOrigin) {
+  let uri = Cc["@mozilla.org/network/io-service;1"]
+              .getService(Ci.nsIIOService)
+              .newURI(aOrigin, null, null);
+  let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"]
+                     .getService(Ci.nsIScriptSecurityManager)
+                     .getNoAppCodebasePrincipal(uri);
+  let pm = Cc["@mozilla.org/permissionmanager;1"]
+             .getService(Ci.nsIPermissionManager);
+  pm.removeFromPrincipal(_principal, MOBILEID_PERM);
+}
+
+// === Mocks ===
+
+let Mock = function(aOptions) {
+  if (!aOptions) {
+    aOptions = {};
+  }
+  this._options = aOptions;
+  this._spied = {};
+};
+
+Mock.prototype = {
+  _: function(aMethod) {
+    DEBUG && do_print("_ " + aMethod + JSON.stringify(this._spied));
+    let self = this;
+    return {
+      callsLength: function(aNumberOfCalls) {
+        if (aNumberOfCalls == 0) {
+          do_check_eq(self._spied[aMethod], undefined);
+          return;
+        }
+        do_check_eq(self._spied[aMethod].length, aNumberOfCalls);
+      },
+      call: function(aCallNumber) {
+        return {
+          arg: function(aArgNumber, aValue) {
+            let _arg = self._spied[aMethod][aCallNumber - 1][aArgNumber - 1];
+            if (Array.isArray(aValue)) {
+              do_check_eq(_arg.length, aValue.length)
+              for (let i = 0; i < _arg.length; i++) {
+                do_check_eq(_arg[i], aValue[i]);
+              }
+              return;
+            }
+
+            if (typeof aValue === 'object') {
+              do_check_eq(JSON.stringify(_arg), JSON.stringify(aValue));
+              return;
+            }
+
+            do_check_eq(_arg, aValue);
+          }
+        }
+      }
+    }
+  },
+
+  _spy: function(aMethod, aArgs) {
+    DEBUG && do_print(aMethod + " - " + JSON.stringify(aArgs));
+    if (!this._spied[aMethod]) {
+      this._spied[aMethod] = [];
+    }
+    this._spied[aMethod].push(aArgs);
+  },
+
+  getSpiedCalls: function(aMethod) {
+    return this._spied[aMethod];
+  }
+};
+
+// UI Glue mock up.
+let MockUi = function(aOptions) {
+  Mock.call(this, aOptions);
+};
+
+MockUi.prototype = {
+  __proto__: Mock.prototype,
+
+  _startFlowResult: {
+    phoneNumber: PHONE_NUMBER
+  },
+
+  _verifyCodePromptResult: {
+    verificationCode: VERIFICATION_CODE
+  },
+
+  startFlow: function() {
+    this._spy("startFlow", arguments);
+    return Promise.resolve(this._options.startFlowResult ||
+                           this._startFlowResult);
+  },
+
+  verificationCodePrompt: function() {
+    this._spy("verifyCodePrompt", arguments);
+    return Promise.resolve(this._options.verificationCodePromptResult ||
+                           this._verifyCodePromptResult);
+  },
+
+  verify: function() {
+    this._spy("verify", arguments);
+  },
+
+  error: function() {
+    this._spy("error", arguments);
+  },
+
+  verified: function() {
+    this._spy("verified", arguments);
+  },
+
+  set oncancel(aCallback) {
+  },
+
+  set onresendcode(aCallback) {
+  }
+};
+
+// Save original credential store instance.
+const kMobileIdentityCredStore = MobileIdentityManager.credStore;
+
+// Credentials store mock up.
+let MockCredStore = function(aOptions) {
+  Mock.call(this, aOptions);
+};
+
+MockCredStore.prototype = {
+  __proto__: Mock.prototype,
+
+  _getByOriginResult: null,
+
+  _getByMsisdnResult: null,
+
+  _getByIccIdResult: null,
+
+  getByOrigin: function() {
+    this._spy("getByOrigin", arguments);
+    return Promise.resolve(this._options.getByOriginResult ||
+                           this._getByOriginResult);
+  },
+
+  getByMsisdn: function() {
+    this._spy("getByMsisdn", arguments);
+    return Promise.resolve(this._options.getByMsisdnResult ||
+                           this._getByMsisdnResult);
+  },
+
+  getByIccId: function() {
+    this._spy("getByIccId", arguments);
+    return Promise.resolve(this._options.getByIccIdResult ||
+                           this._getByIccIdResult);
+  },
+
+  add: function() {
+    this._spy("add", arguments);
+    return Promise.resolve();
+  },
+
+  setDeviceIccIds: function() {
+    this._spy("setDeviceIccIds", arguments);
+    return Promise.resolve();
+  },
+
+  removeOrigin: function() {
+    this._spy("removeOrigin", arguments);
+    return Promise.resolve();
+  }
+};
+
+// Save original client instance.
+const kMobileIdentityClient = MobileIdentityManager.client;
+
+// Client mock up.
+let MockClient = function(aOptions) {
+  Mock.call(this, aOptions);
+};
+
+MockClient.prototype = {
+
+  __proto__: Mock.prototype,
+
+  _discoverResult: {
+    verificationMethods: ["sms/momt", "sms/mt"],
+    verificationDetails: {
+      "sms/mt": {
+        mtSender: "123",
+        url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
+      },
+      "sms/momt": {
+        mtSender: "123",
+        moVerifier: "234"
+      }
+    }
+  },
+
+  _registerResult: {
+    msisdnSessionToken: SESSION_TOKEN
+  },
+
+  _smsMtVerifyResult: {},
+
+  _verifyCodeResult: {
+    msisdn: PHONE_NUMBER
+  },
+
+  _signResult: {
+    cert: CERTIFICATE
+  },
+
+  hawk: {
+    now: function() {
+      return Date.now();
+    }
+  },
+
+  discover: function() {
+    this._spy("discover", arguments);
+    return Promise.resolve(this._options.discoverResult ||
+                           this._discoverResult);
+  },
+
+  register: function() {
+    this._spy("register", arguments);
+    return Promise.resolve(this._options.registerResult ||
+                           this._registerResult);
+  },
+
+  smsMtVerify: function() {
+    this._spy("smsMtVerify", arguments);
+    return Promise.resolve(this._options.smsMtVerifyResult ||
+                           this._smsMtVerifyResult);
+  },
+
+  verifyCode: function() {
+    this._spy("verifyCode", arguments);
+    return Promise.resolve(this._options.verifyCodeResult ||
+                           this._verifyCodeResult);
+  },
+
+  sign: function() {
+    this._spy("sign", arguments);
+    return Promise.resolve(this._options.signResult || this._signResult);
+  }
+};
+
+// The test rely on having an app registered. Otherwise, it will throw.
+// Override XULAppInfo.
+const XUL_APP_INFO_UUID = Components.ID("{84fdc459-d96d-421c-9bff-a8193233ae75}");
+const XUL_APP_INFO_CONTRACT_ID = "@mozilla.org/xre/app-info;1";
+
+let (XULAppInfo = {
+  vendor: "Mozilla",
+  name: "MobileIdTest",
+  ID: "{230de50e-4cd1-11dc-8314-0800200b9a66}",
+  version: "1",
+  appBuildID: "2007010101",
+  platformVersion: "",
+  platformBuildID: "2007010101",
+  inSafeMode: false,
+  logConsoleErrors: true,
+  OS: "XPCShell",
+  XPCOMABI: "noarch-spidermonkey",
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIXULAppInfo,
+    Ci.nsIXULRuntime,
+  ])
+}) {
+  let XULAppInfoFactory = {
+    createInstance: function (outer, iid) {
+      if (outer != null) {
+        throw Cr.NS_ERROR_NO_AGGREGATION;
+      }
+      return XULAppInfo.QueryInterface(iid);
+    }
+  };
+  Cm.QueryInterface(Ci.nsIComponentRegistrar)
+    .registerFactory(XUL_APP_INFO_UUID,
+                     "XULAppInfo",
+                     XUL_APP_INFO_CONTRACT_ID,
+                     XULAppInfoFactory);
+}
+
+// === Global cleanup ===
+
+function cleanup() {
+  MobileIdentityManager.credStore = kMobileIdentityCredStore;
+  MobileIdentityManager.client = kMobileIdentityClient;
+  MobileIdentityManager.ui = null;
+  MobileIdentityManager.iccInfo = null;
+}
+
+// Unregister mocks and restore original code.
+do_register_cleanup(cleanup);
+
+// === Tests ===
+function run_test() {
+  run_next_test();
+}
+
+add_test(function() {
+  do_print("= Initial state =");
+  do_check_neq(MobileIdentityManager, undefined);
+  run_next_test();
+});
+
+add_test(function() {
+  do_print("= No credentials - No ICC - User MSISDN - External - OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let ui = new MockUi();
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore();
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient({
+    discoverResult: {
+      verificationMethods: ["sms/mt"],
+      verificationDetails: {
+        "sms/mt": {
+          mtSender: "123",
+          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
+        }
+      }
+    }
+  });
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(1);
+      credStore._("getByMsisdn").call(1).arg(1, PHONE_NUMBER);
+      credStore._("add").callsLength(1);
+      credStore._("add").call(1).arg(1, undefined);
+      credStore._("add").call(1).arg(2, PHONE_NUMBER);
+      credStore._("add").call(1).arg(3, ORIGIN);
+      credStore._("add").call(1).arg(4, SESSION_TOKEN);
+      credStore._("add").call(1).arg(5, null);
+
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("startFlow").call(1).arg(1, "");
+      ui._("startFlow").call(1).arg(2, []);
+      ui._("verifyCodePrompt").callsLength(1);
+      ui._("verifyCodePrompt").call(1).arg(1, 3);
+      ui._("verify").callsLength(1);
+
+      // MockClient.
+      client._("discover").callsLength(1);
+      client._("discover").call(1).arg(1, PHONE_NUMBER);
+      client._("register").callsLength(1);
+      client._("smsMtVerify").callsLength(1);
+      client._("smsMtVerify").call(1).arg(1, SESSION_TOKEN);
+      client._("smsMtVerify").call(1).arg(2, PHONE_NUMBER);
+      client._("smsMtVerify").call(1).arg(3, true);
+      client._("verifyCode").callsLength(1);
+      client._("verifyCode").call(1).arg(1, SESSION_TOKEN);
+      client._("verifyCode").call(1).arg(2, {
+        verificationCode: VERIFICATION_CODE
+      });
+      client._("sign").callsLength(1);
+      client._("sign").call(1).arg(1, SESSION_TOKEN);
+      client._("sign").call(1).arg(2, CERTIFICATE_LIFETIME);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= No credentials - No icc - User MSISDN - External - KO -" +
+           " ERROR_INTERNAL_CANNOT_VERIFY_SELECTION =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let _sessionToken = Date.now();
+
+  let ui = new MockUi();
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore();
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient({
+    discoverResult: {
+      verificationMethods: ["other"],
+      verificationDetails: {
+        "other": {}
+      }
+    },
+    registerResult: {
+      msisdnSessionToken: _sessionToken
+    }
+  });
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_KO);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+      do_check_eq(aData.error, ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(1);
+      credStore._("getByMsisdn").call(1).arg(1, PHONE_NUMBER);
+      credStore._("add").callsLength(0);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("startFlow").call(1).arg(1, "");
+      ui._("startFlow").call(1).arg(2, []);
+      ui._("verifyCodePrompt").callsLength(0);
+      ui._("verify").callsLength(0);
+      ui._("error").callsLength(1);
+      ui._("error").call(1).arg(1, ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
+
+      // MockClient.
+      client._("discover").callsLength(1);
+      client._("discover").call(1).arg(1, PHONE_NUMBER);
+      client._("register").callsLength(0);
+      client._("smsMtVerify").callsLength(0);
+      client._("verifyCode").callsLength(0);
+      client._("sign").callsLength(0);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= No credentials - No icc - User MSISDN - External - KO -" +
+           " INTERNAL_INVALID_PROMPT_RESULT =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let _sessionToken = Date.now();
+
+  let ui = new MockUi({
+    startFlowResult: {}
+  });
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore();
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient();
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_KO);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+      do_check_eq(aData.error, ERROR_INTERNAL_INVALID_PROMPT_RESULT);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(0);
+      credStore._("add").callsLength(0);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("startFlow").call(1).arg(1, "");
+      ui._("startFlow").call(1).arg(2, []);
+      ui._("verifyCodePrompt").callsLength(0);
+      ui._("verify").callsLength(0);
+      ui._("error").callsLength(1);
+      ui._("error").call(1).arg(1, ERROR_INTERNAL_INVALID_PROMPT_RESULT);
+
+      // MockClient.
+      client._("discover").callsLength(0);
+      client._("register").callsLength(0);
+      client._("smsMtVerify").callsLength(0);
+      client._("verifyCode").callsLength(0);
+      client._("sign").callsLength(0);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= No credentials - No icc - User MSISDN - External - KO -" +
+           " ERROR_INVALID_ASSERTION =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let _sessionToken = Date.now();
+
+  let ui = new MockUi();
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore();
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient({
+    discoverResult: {
+      verificationMethods: ["sms/mt"],
+      verificationDetails: {
+        "sms/mt": {
+          mtSender: "123",
+          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
+        }
+      }
+    },
+    signResult: {
+      cert: "aInvalidCert"
+    },
+    registerResult: {
+      msisdnSessionToken: _sessionToken
+    }
+  });
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_KO);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+      do_check_eq(aData.error, ERROR_INVALID_ASSERTION);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(1);
+      credStore._("getByMsisdn").call(1).arg(1, PHONE_NUMBER);
+      credStore._("add").callsLength(1);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("startFlow").call(1).arg(1, "");
+      ui._("startFlow").call(1).arg(2, []);
+      ui._("verifyCodePrompt").callsLength(1);
+      ui._("verifyCodePrompt").call(1).arg(1, 3);
+      ui._("verify").callsLength(1);
+      ui._("error").callsLength(1);
+      ui._("error").call(1).arg(1, ERROR_INVALID_ASSERTION);
+
+      // MockClient.
+      client._("discover").callsLength(1);
+      client._("discover").call(1).arg(1, PHONE_NUMBER);
+      client._("register").callsLength(1);
+      client._("smsMtVerify").callsLength(1);
+      client._("smsMtVerify").call(1).arg(1, _sessionToken);
+      client._("smsMtVerify").call(1).arg(2, PHONE_NUMBER);
+      client._("smsMtVerify").call(1).arg(3, true);
+      client._("verifyCode").callsLength(1);
+      client._("verifyCode").call(1).arg(1, _sessionToken);
+      client._("verifyCode").call(1).arg(2, {
+        verificationCode: VERIFICATION_CODE
+      });
+      client._("sign").callsLength(1);
+      client._("sign").call(1).arg(1, _sessionToken);
+      client._("sign").call(1).arg(2, CERTIFICATE_LIFETIME);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= Existing credentials - No Icc - Permission - OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let ui = new MockUi();
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: {
+      sessionToken: SESSION_TOKEN,
+      msisdn: PHONE_NUMBER,
+      origin: ORIGIN,
+      deviceIccIds: null
+    }
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient();
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+
+      removePermission(ORIGIN);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  addPermission(ORIGIN, Ci.nsIPermissionManager.ALLOW_ACTION);
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {}
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= Existing credentials - No Icc - Prompt permission - OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let ui = new MockUi();
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: {
+      sessionToken: SESSION_TOKEN,
+      msisdn: PHONE_NUMBER,
+      origin: ORIGIN,
+      deviceIccIds: null
+    }
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient();
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+
+      removePermission(ORIGIN);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  addPermission(ORIGIN, Ci.nsIPermissionManager.PROMPT_ACTION);
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {}
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= Existing credentials - No Icc - Permission denied - OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let ui = new MockUi();
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: {
+      sessionToken: SESSION_TOKEN,
+      msisdn: PHONE_NUMBER,
+      origin: ORIGIN,
+      deviceIccIds: null
+    }
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient();
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_KO);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+      do_check_eq(aData.error, ERROR_PERMISSION_DENIED);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+
+      // MockUI.
+      ui._("startFlow").callsLength(0);
+      ui._("error").callsLength(1);
+      ui._("error").call(1).arg(1, ERROR_PERMISSION_DENIED);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  removePermission(ORIGIN);
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {}
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= Existing credentials - No Icc - SIM change/Same choice - " +
+           "OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let ui = new MockUi();
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: {
+      sessionToken: SESSION_TOKEN,
+      msisdn: PHONE_NUMBER,
+      origin: ORIGIN,
+      deviceIccIds: [ICC_ID]
+    }
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient();
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("add").callsLength(1);
+      credStore._("add").call(1).arg(1, undefined);
+      credStore._("add").call(1).arg(2, PHONE_NUMBER);
+      credStore._("add").call(1).arg(3, ORIGIN);
+      credStore._("add").call(1).arg(4, SESSION_TOKEN);
+      credStore._("add").call(1).arg(5, null);
+      credStore._("setDeviceIccIds").callsLength(1);
+      credStore._("setDeviceIccIds").call(1).arg(1, PHONE_NUMBER);
+      credStore._("setDeviceIccIds").call(1).arg(2, null);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("verifyCodePrompt").callsLength(0);
+      ui._("verify").callsLength(0);
+
+      // MockClient.
+      client._("discover").callsLength(0);
+      client._("register").callsLength(0);
+      client._("smsMtVerify").callsLength(0);
+      client._("verifyCode").callsLength(0);
+      client._("sign").callsLength(0);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {}
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= Existing credentials - No Icc - SIM change/Different choice - " +
+           "OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let _sessionToken = Date.now();
+
+  let existingCredentials = {
+    sessionToken: SESSION_TOKEN,
+    msisdn: PHONE_NUMBER,
+    origin: ORIGIN,
+    deviceIccIds: [ICC_ID]
+  };
+
+  let ui = new MockUi({
+    startFlowResult: {
+      phoneNumber: ANOTHER_PHONE_NUMBER
+    }
+  });
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: existingCredentials
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient({
+    verifyCodeResult: ANOTHER_PHONE_NUMBER,
+    discoverResult: {
+      verificationMethods: ["sms/mt"],
+      verificationDetails: {
+        "sms/mt": {
+          mtSender: "123",
+          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
+        }
+      }
+    },
+    registerResult: {
+      msisdnSessionToken: _sessionToken
+    }
+  });
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(1);
+      credStore._("getByMsisdn").call(1).arg(1, ANOTHER_PHONE_NUMBER);
+      credStore._("add").callsLength(1);
+      credStore._("add").call(1).arg(1, undefined);
+      credStore._("add").call(1).arg(2, ANOTHER_PHONE_NUMBER);
+      credStore._("add").call(1).arg(3, ORIGIN);
+      credStore._("add").call(1).arg(4, _sessionToken);
+      credStore._("add").call(1).arg(5, null);
+      credStore._("setDeviceIccIds").callsLength(0);
+      credStore._("removeOrigin").callsLength(1);
+      credStore._("removeOrigin").call(1).arg(1, PHONE_NUMBER);
+      credStore._("removeOrigin").call(1).arg(2, ORIGIN);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("verifyCodePrompt").callsLength(1);
+      ui._("verify").callsLength(1);
+
+      // MockClient.
+      client._("discover").callsLength(1);
+      client._("register").callsLength(1);
+      client._("smsMtVerify").callsLength(1);
+      client._("verifyCode").callsLength(1);
+      client._("sign").callsLength(1);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {}
+    }
+  });
+});
+
+add_test(function() {
+  do_print("= Existing credentials - No Icc - forceSelection/same - " +
+           "OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let _sessionToken = Date.now();
+
+  let existingCredentials = {
+    sessionToken: _sessionToken,
+    msisdn: PHONE_NUMBER,
+    origin: ORIGIN,
+    deviceIccIds: []
+  };
+
+  let ui = new MockUi({
+    startFlowResult: {
+      phoneNumber: PHONE_NUMBER
+    }
+  });
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: existingCredentials
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient();
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(0);
+      credStore._("add").callsLength(1);
+      credStore._("add").call(1).arg(1, undefined);
+      credStore._("add").call(1).arg(2, PHONE_NUMBER);
+      credStore._("add").call(1).arg(3, ORIGIN);
+      credStore._("add").call(1).arg(4, _sessionToken);
+      credStore._("add").call(1).arg(5, null);
+      credStore._("setDeviceIccIds").callsLength(1);
+      credStore._("removeOrigin").callsLength(0);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("verifyCodePrompt").callsLength(0);
+      ui._("verify").callsLength(0);
+
+      // MockClient.
+      client._("discover").callsLength(0);
+      client._("register").callsLength(0);
+      client._("smsMtVerify").callsLength(0);
+      client._("verifyCode").callsLength(0);
+      client._("sign").callsLength(1);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {
+        forceSelection: true
+      }
+    }
+  });
+});
+add_test(function() {
+  do_print("= Existing credentials - No Icc - forceSelection/different - " +
+           "OK result =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let _sessionToken = Date.now();
+
+  let existingCredentials = {
+    sessionToken: SESSION_TOKEN,
+    msisdn: PHONE_NUMBER,
+    origin: ORIGIN,
+    deviceIccIds: []
+  };
+
+  let ui = new MockUi({
+    startFlowResult: {
+      phoneNumber: ANOTHER_PHONE_NUMBER
+    }
+  });
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: existingCredentials
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient({
+    verifyCodeResult: ANOTHER_PHONE_NUMBER,
+    discoverResult: {
+      verificationMethods: ["sms/mt"],
+      verificationDetails: {
+        "sms/mt": {
+          mtSender: "123",
+          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
+        }
+      }
+    },
+    registerResult: {
+      msisdnSessionToken: _sessionToken
+    }
+  });
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(1);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(1);
+      credStore._("getByMsisdn").call(1).arg(1, ANOTHER_PHONE_NUMBER);
+      credStore._("add").callsLength(1);
+      credStore._("add").call(1).arg(1, undefined);
+      credStore._("add").call(1).arg(2, ANOTHER_PHONE_NUMBER);
+      credStore._("add").call(1).arg(3, ORIGIN);
+      credStore._("add").call(1).arg(4, _sessionToken);
+      credStore._("add").call(1).arg(5, null);
+      credStore._("setDeviceIccIds").callsLength(0);
+      credStore._("removeOrigin").callsLength(1);
+      credStore._("removeOrigin").call(1).arg(1, PHONE_NUMBER);
+      credStore._("removeOrigin").call(1).arg(2, ORIGIN);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("verifyCodePrompt").callsLength(1);
+      ui._("verify").callsLength(1);
+
+      // MockClient.
+      client._("discover").callsLength(1);
+      client._("register").callsLength(1);
+      client._("smsMtVerify").callsLength(1);
+      client._("verifyCode").callsLength(1);
+      client._("sign").callsLength(1);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {
+        forceSelection: true
+      }
+    }
+  });
+});
new file mode 100644
--- /dev/null
+++ b/services/mobileid/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = head.js
+tail =
+
+[test_mobileid_manager.js]