Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 22 Sep 2014 15:51:51 -0400
changeset 206588 3c7c479d5040ed723d1adf0a392e61e7378a0a43
parent 206587 ed8de2cc24f30bef0cd08e8e0a36a372eec96a5e (current diff)
parent 206544 f4037194394efcad03d5076860387bcf32ff98a0 (diff)
child 206589 499a2a39ac53d286cd61798defa0e99d82c007e4
push idunknown
push userunknown
push dateunknown
reviewersmerge
milestone35.0a1
Merge m-c to inbound. a=merge
dom/system/gonk/tests/test_ril_worker_mmi_parseMMI.js
dom/telephony/test/marionette/test_mmi_code.js
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--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="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--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="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <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="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
   <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"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--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="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--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="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <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="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--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="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
   <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"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "a491e07757d721d4bea7302cfbb2b18460a3820d", 
+    "revision": "36ec1e83a5bc8dc4106bd3fa1a506f1085611ba2", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--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="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- 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="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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,17 +12,17 @@
   <!--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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
   <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"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--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="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <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="3802009e1ab6c3ddfc3eb15522e3140a96b33336"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1fd99454b6cd5da4f2c58f928fc04c6d03f478f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b81855b6b67f285d6f27a4f8c1cfe2e0387ea57c"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f3e998242fb9a857cf50f5bf3a02304a530ea617"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -692,22 +692,25 @@ pref("plugin.state.npciscowebcommunicato
 pref("plugin.state.ciscowebcommunicator", 2);
 #endif
 
 // McAfee Security Scanner detection plugin, bug 980772
 #ifdef XP_WIN
 pref("plugin.state.npmcafeemss", 2);
 #endif
 
-// Cisco VGConnect for directv.com, bug 981403
+// Cisco VGConnect for directv.com, bug 981403 & bug 1051772
 #ifdef XP_WIN
 pref("plugin.state.npplayerplugin", 2);
 #endif
 #ifdef XP_MACOSX
 pref("plugin.state.playerplugin", 2);
+pref("plugin.state.playerplugin.dtv", 2);
+pref("plugin.state.playerplugin.ciscodrm", 2);
+pref("plugin.state.playerplugin.charter", 2);
 #endif
 
 // Cisco Jabber Client, bug 981905
 #ifdef XP_WIN
 pref("plugin.state.npchip", 2);
 #endif
 #ifdef XP_MACOSX
 pref("plugin.state.cisco jabber guest plug-in", 2);
@@ -838,16 +841,25 @@ pref("plugin.state.np_prsnl", 2);
 #endif
 #ifdef XP_MACOSX
 pref("plugin.state.personalplugin", 2);
 #endif
 #ifdef UNIX_BUT_NOT_MAC
 pref("plugin.state.libplugins", 2);
 #endif
 
+// Novell iPrint Client, bug 1036693
+#ifdef XP_WIN
+pref("plugin.state.npnipp", 2);
+pref("plugin.state.npnisp", 2);
+#endif
+#ifdef XP_MACOSX
+pref("plugin.state.iprint", 2);
+#endif
+
 #ifdef XP_MACOSX
 pref("browser.preferences.animateFadeIn", true);
 #else
 pref("browser.preferences.animateFadeIn", false);
 #endif
 
 // Toggles between the two Preferences implementations, pop-up window and in-content
 #ifdef NIGHTLY_BUILD
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -109,17 +109,17 @@ loop.Client = (function($) {
      */
     _requestCallUrlInternal: function(nickname, cb) {
       var sessionType;
       if (this.mozLoop.userProfile) {
         sessionType = this.mozLoop.LOOP_SESSION_TYPE.FXA;
       } else {
         sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST;
       }
-      
+
       this.mozLoop.hawkRequest(sessionType, "/call-url/", "POST",
                                {callerId: nickname},
         function (error, responseText) {
           if (error) {
             this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
             this._failureHandler(cb, error);
             return;
           }
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -401,16 +401,17 @@ loop.conversation = (function(OT, mozL10
         return;
       }
 
       var callType = this._conversation.get("selectedCallType");
       var videoStream = callType === "audio" ? false : true;
 
       /*jshint newcap:false*/
       this.loadReactComponent(sharedViews.ConversationView({
+        initiate: true,
         sdk: OT,
         model: this._conversation,
         video: {enabled: videoStream}
       }));
     },
 
     /**
      * Handles a error starting the session
@@ -435,17 +436,18 @@ loop.conversation = (function(OT, mozL10
       var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
         product: navigator.mozLoop.getLoopCharPref("feedback.product"),
         platform: appVersionInfo.OS,
         channel: appVersionInfo.channel,
         version: appVersionInfo.version
       });
 
       this.loadReactComponent(sharedViews.FeedbackView({
-        feedbackApiClient: feedbackClient
+        feedbackApiClient: feedbackClient,
+        onAfterFeedbackReceived: window.close.bind(window)
       }));
     }
   });
 
   /**
    * Panel initialisation.
    */
   function init() {
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -401,16 +401,17 @@ loop.conversation = (function(OT, mozL10
         return;
       }
 
       var callType = this._conversation.get("selectedCallType");
       var videoStream = callType === "audio" ? false : true;
 
       /*jshint newcap:false*/
       this.loadReactComponent(sharedViews.ConversationView({
+        initiate: true,
         sdk: OT,
         model: this._conversation,
         video: {enabled: videoStream}
       }));
     },
 
     /**
      * Handles a error starting the session
@@ -435,17 +436,18 @@ loop.conversation = (function(OT, mozL10
       var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
         product: navigator.mozLoop.getLoopCharPref("feedback.product"),
         platform: appVersionInfo.OS,
         channel: appVersionInfo.channel,
         version: appVersionInfo.version
       });
 
       this.loadReactComponent(sharedViews.FeedbackView({
-        feedbackApiClient: feedbackClient
+        feedbackApiClient: feedbackClient,
+        onAfterFeedbackReceived: window.close.bind(window)
       }));
     }
   });
 
   /**
    * Panel initialisation.
    */
   function init() {
--- a/browser/components/loop/content/shared/js/feedbackApiClient.js
+++ b/browser/components/loop/content/shared/js/feedbackApiClient.js
@@ -46,17 +46,18 @@ loop.FeedbackAPIClient = (function($, _)
      */
     _supportedFields: ["happy",
                        "category",
                        "description",
                        "product",
                        "platform",
                        "version",
                        "channel",
-                       "user_agent"],
+                       "user_agent",
+                       "url"],
 
     /**
      * Creates a formatted payload object compliant with the Feedback API spec
      * against validated field data.
      *
      * @param  {Object} fields Feedback initial values.
      * @return {Object}        Formatted payload object.
      * @throws {Error}         If provided values are invalid
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -25,35 +25,37 @@ loop.shared.views = (function(_, OT, l10
    * - {Function} action  Function to be executed on click.
    * - {Enabled}  enabled Stream activation status (default: true).
    */
   var MediaControlButton = React.createClass({displayName: 'MediaControlButton',
     propTypes: {
       scope: React.PropTypes.string.isRequired,
       type: React.PropTypes.string.isRequired,
       action: React.PropTypes.func.isRequired,
-      enabled: React.PropTypes.bool.isRequired
+      enabled: React.PropTypes.bool.isRequired,
+      visible: React.PropTypes.bool.isRequired
     },
 
     getDefaultProps: function() {
-      return {enabled: true};
+      return {enabled: true, visible: true};
     },
 
     handleClick: function() {
       this.props.action();
     },
 
     _getClasses: function() {
       var cx = React.addons.classSet;
       // classes
       var classesObj = {
         "btn": true,
         "media-control": true,
         "local-media": this.props.scope === "local",
-        "muted": !this.props.enabled
+        "muted": !this.props.enabled,
+        "hide": !this.props.visible
       };
       classesObj["btn-mute-" + this.props.type] = true;
       return cx(classesObj);
     },
 
     _getTitle: function(enabled) {
       var prefix = this.props.enabled ? "mute" : "unmute";
       var suffix = "button_title";
@@ -73,18 +75,18 @@ loop.shared.views = (function(_, OT, l10
   });
 
   /**
    * Conversation controls.
    */
   var ConversationToolbar = React.createClass({displayName: 'ConversationToolbar',
     getDefaultProps: function() {
       return {
-        video: {enabled: true},
-        audio: {enabled: true}
+        video: {enabled: true, visible: true},
+        audio: {enabled: true, visible: true}
       };
     },
 
     propTypes: {
       video: React.PropTypes.object.isRequired,
       audio: React.PropTypes.object.isRequired,
       hangup: React.PropTypes.func.isRequired,
       publishStream: React.PropTypes.func.isRequired
@@ -98,97 +100,108 @@ loop.shared.views = (function(_, OT, l10
       this.props.publishStream("video", !this.props.video.enabled);
     },
 
     handleToggleAudio: function() {
       this.props.publishStream("audio", !this.props.audio.enabled);
     },
 
     render: function() {
-      /* jshint ignore:start */
+      var cx = React.addons.classSet;
       return (
         React.DOM.ul({className: "conversation-toolbar"}, 
           React.DOM.li({className: "conversation-toolbar-btn-box"}, 
             React.DOM.button({className: "btn btn-hangup", onClick: this.handleClickHangup, 
                     title: l10n.get("hangup_button_title")}, 
               l10n.get("hangup_button_caption2")
             )
           ), 
           React.DOM.li({className: "conversation-toolbar-btn-box"}, 
             MediaControlButton({action: this.handleToggleVideo, 
                                 enabled: this.props.video.enabled, 
+                                visible: this.props.video.visible, 
                                 scope: "local", type: "video"})
           ), 
           React.DOM.li({className: "conversation-toolbar-btn-box"}, 
             MediaControlButton({action: this.handleToggleAudio, 
                                 enabled: this.props.audio.enabled, 
+                                visible: this.props.audio.visible, 
                                 scope: "local", type: "audio"})
           )
         )
       );
-      /* jshint ignore:end */
     }
   });
 
+  /**
+   * Conversation view.
+   */
   var ConversationView = React.createClass({displayName: 'ConversationView',
     mixins: [Backbone.Events],
 
     propTypes: {
       sdk: React.PropTypes.object.isRequired,
-      model: React.PropTypes.object.isRequired
+      video: React.PropTypes.object,
+      audio: React.PropTypes.object,
+      initiate: React.PropTypes.bool
     },
 
     // height set to 100%" to fix video layout on Google Chrome
     // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
     publisherConfig: {
       insertMode: "append",
       width: "100%",
       height: "100%",
       style: {
         bugDisplayMode: "off",
         buttonDisplayMode: "off",
         nameDisplayMode: "off"
       }
     },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {
-        video: {enabled: true},
-        audio: {enabled: true}
+        initiate: true,
+        video: {enabled: true, visible: true},
+        audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
       return {
         video: this.props.video,
         audio: this.props.audio
       };
     },
 
     componentWillMount: function() {
-      this.publisherConfig.publishVideo = this.props.video.enabled;
+      if (this.props.initiate) {
+        this.publisherConfig.publishVideo = this.props.video.enabled;
+      }
     },
 
     componentDidMount: function() {
-      this.listenTo(this.props.model, "session:connected",
-                                      this.startPublishing);
-      this.listenTo(this.props.model, "session:stream-created",
-                                      this._streamCreated);
-      this.listenTo(this.props.model, ["session:peer-hungup",
-                                       "session:network-disconnected",
-                                       "session:ended"].join(" "),
-                                       this.stopPublishing);
-
-      this.props.model.startSession();
+      if (this.props.initiate) {
+        this.listenTo(this.props.model, "session:connected",
+                                        this.startPublishing);
+        this.listenTo(this.props.model, "session:stream-created",
+                                        this._streamCreated);
+        this.listenTo(this.props.model, ["session:peer-hungup",
+                                         "session:network-disconnected",
+                                         "session:ended"].join(" "),
+                                         this.stopPublishing);
+        this.props.model.startSession();
+      }
 
       /**
        * OT inserts inline styles into the markup. Using a listener for
        * resize events helps us trigger a full width/height on the element
        * so that they update to the correct dimensions.
-       * */
+       * XXX: this should be factored as a mixin.
+       */
       window.addEventListener('orientationchange', this.updateVideoContainer);
       window.addEventListener('resize', this.updateVideoContainer);
     },
 
     updateVideoContainer: function() {
       var localStreamParent = document.querySelector('.local .OT_publisher');
       var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
       if (localStreamParent) {
@@ -277,20 +290,22 @@ loop.shared.views = (function(_, OT, l10
         this.setState({video: {enabled: enabled}});
       }
     },
 
     /**
      * Unpublishes local stream.
      */
     stopPublishing: function() {
-      // Unregister listeners for publisher events
-      this.stopListening(this.publisher);
+      if (this.publisher) {
+        // Unregister listeners for publisher events
+        this.stopListening(this.publisher);
 
-      this.props.model.session.unpublish(this.publisher);
+        this.props.model.session.unpublish(this.publisher);
+      }
     },
 
     render: function() {
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
         "local-stream-audio": !this.state.video.enabled
       });
@@ -357,17 +372,17 @@ loop.shared.views = (function(_, OT, l10
       sendFeedback: React.PropTypes.func,
       reset:        React.PropTypes.func
     },
 
     getInitialState: function() {
       return {category: "", description: ""};
     },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {pending: false};
     },
 
     _getCategories: function() {
       return {
         audio_quality: l10n.get("feedback_category_audio_quality"),
         video_quality: l10n.get("feedback_category_video_quality"),
         disconnected : l10n.get("feedback_category_was_disconnected"),
@@ -462,18 +477,26 @@ loop.shared.views = (function(_, OT, l10
           )
         )
       );
     }
   });
 
   /**
    * Feedback received view.
+   *
+   * Props:
+   * - {Function} onAfterFeedbackReceived Function to execute after the
+   *   WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
    */
   var FeedbackReceived = React.createClass({displayName: 'FeedbackReceived',
+    propTypes: {
+      onAfterFeedbackReceived: React.PropTypes.func
+    },
+
     getInitialState: function() {
       return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
     },
 
     componentDidMount: function() {
       this._timer = setInterval(function() {
         this.setState({countdown: this.state.countdown - 1});
       }.bind(this), 1000);
@@ -483,17 +506,19 @@ loop.shared.views = (function(_, OT, l10
       if (this._timer) {
         clearInterval(this._timer);
       }
     },
 
     render: function() {
       if (this.state.countdown < 1) {
         clearInterval(this._timer);
-        window.close();
+        if (this.props.onAfterFeedbackReceived) {
+          this.props.onAfterFeedbackReceived();
+        }
       }
       return (
         FeedbackLayout({title: l10n.get("feedback_thank_you_heading")}, 
           React.DOM.p({className: "info thank-you"}, 
             l10n.get("feedback_window_will_close_in2", {
               countdown: this.state.countdown,
               num: this.state.countdown
             }))
@@ -504,25 +529,26 @@ loop.shared.views = (function(_, OT, l10
 
   /**
    * Feedback view.
    */
   var FeedbackView = React.createClass({displayName: 'FeedbackView',
     propTypes: {
       // A loop.FeedbackAPIClient instance
       feedbackApiClient: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func,
       // The current feedback submission flow step name
       step: React.PropTypes.oneOf(["start", "form", "finished"])
     },
 
     getInitialState: function() {
       return {pending: false, step: this.props.step || "start"};
     },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {step: "start"};
     },
 
     reset: function() {
       this.setState(this.getInitialState());
     },
 
     handleHappyClick: function() {
@@ -547,17 +573,20 @@ loop.shared.views = (function(_, OT, l10
         console.error("Unable to send user feedback", err);
       }
       this.setState({pending: false, step: "finished"});
     },
 
     render: function() {
       switch(this.state.step) {
         case "finished":
-          return FeedbackReceived(null);
+          return (
+            FeedbackReceived({
+              onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
+          );
         case "form":
           return FeedbackForm({feedbackApiClient: this.props.feedbackApiClient, 
                                sendFeedback: this.sendFeedback, 
                                reset: this.reset, 
                                pending: this.state.pending});
         default:
           return (
             FeedbackLayout({title: 
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -25,35 +25,37 @@ loop.shared.views = (function(_, OT, l10
    * - {Function} action  Function to be executed on click.
    * - {Enabled}  enabled Stream activation status (default: true).
    */
   var MediaControlButton = React.createClass({
     propTypes: {
       scope: React.PropTypes.string.isRequired,
       type: React.PropTypes.string.isRequired,
       action: React.PropTypes.func.isRequired,
-      enabled: React.PropTypes.bool.isRequired
+      enabled: React.PropTypes.bool.isRequired,
+      visible: React.PropTypes.bool.isRequired
     },
 
     getDefaultProps: function() {
-      return {enabled: true};
+      return {enabled: true, visible: true};
     },
 
     handleClick: function() {
       this.props.action();
     },
 
     _getClasses: function() {
       var cx = React.addons.classSet;
       // classes
       var classesObj = {
         "btn": true,
         "media-control": true,
         "local-media": this.props.scope === "local",
-        "muted": !this.props.enabled
+        "muted": !this.props.enabled,
+        "hide": !this.props.visible
       };
       classesObj["btn-mute-" + this.props.type] = true;
       return cx(classesObj);
     },
 
     _getTitle: function(enabled) {
       var prefix = this.props.enabled ? "mute" : "unmute";
       var suffix = "button_title";
@@ -73,18 +75,18 @@ loop.shared.views = (function(_, OT, l10
   });
 
   /**
    * Conversation controls.
    */
   var ConversationToolbar = React.createClass({
     getDefaultProps: function() {
       return {
-        video: {enabled: true},
-        audio: {enabled: true}
+        video: {enabled: true, visible: true},
+        audio: {enabled: true, visible: true}
       };
     },
 
     propTypes: {
       video: React.PropTypes.object.isRequired,
       audio: React.PropTypes.object.isRequired,
       hangup: React.PropTypes.func.isRequired,
       publishStream: React.PropTypes.func.isRequired
@@ -98,97 +100,108 @@ loop.shared.views = (function(_, OT, l10
       this.props.publishStream("video", !this.props.video.enabled);
     },
 
     handleToggleAudio: function() {
       this.props.publishStream("audio", !this.props.audio.enabled);
     },
 
     render: function() {
-      /* jshint ignore:start */
+      var cx = React.addons.classSet;
       return (
         <ul className="conversation-toolbar">
           <li className="conversation-toolbar-btn-box">
             <button className="btn btn-hangup" onClick={this.handleClickHangup}
                     title={l10n.get("hangup_button_title")}>
               {l10n.get("hangup_button_caption2")}
             </button>
           </li>
           <li className="conversation-toolbar-btn-box">
             <MediaControlButton action={this.handleToggleVideo}
                                 enabled={this.props.video.enabled}
+                                visible={this.props.video.visible}
                                 scope="local" type="video" />
           </li>
           <li className="conversation-toolbar-btn-box">
             <MediaControlButton action={this.handleToggleAudio}
                                 enabled={this.props.audio.enabled}
+                                visible={this.props.audio.visible}
                                 scope="local" type="audio" />
           </li>
         </ul>
       );
-      /* jshint ignore:end */
     }
   });
 
+  /**
+   * Conversation view.
+   */
   var ConversationView = React.createClass({
     mixins: [Backbone.Events],
 
     propTypes: {
       sdk: React.PropTypes.object.isRequired,
-      model: React.PropTypes.object.isRequired
+      video: React.PropTypes.object,
+      audio: React.PropTypes.object,
+      initiate: React.PropTypes.bool
     },
 
     // height set to 100%" to fix video layout on Google Chrome
     // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
     publisherConfig: {
       insertMode: "append",
       width: "100%",
       height: "100%",
       style: {
         bugDisplayMode: "off",
         buttonDisplayMode: "off",
         nameDisplayMode: "off"
       }
     },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {
-        video: {enabled: true},
-        audio: {enabled: true}
+        initiate: true,
+        video: {enabled: true, visible: true},
+        audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
       return {
         video: this.props.video,
         audio: this.props.audio
       };
     },
 
     componentWillMount: function() {
-      this.publisherConfig.publishVideo = this.props.video.enabled;
+      if (this.props.initiate) {
+        this.publisherConfig.publishVideo = this.props.video.enabled;
+      }
     },
 
     componentDidMount: function() {
-      this.listenTo(this.props.model, "session:connected",
-                                      this.startPublishing);
-      this.listenTo(this.props.model, "session:stream-created",
-                                      this._streamCreated);
-      this.listenTo(this.props.model, ["session:peer-hungup",
-                                       "session:network-disconnected",
-                                       "session:ended"].join(" "),
-                                       this.stopPublishing);
-
-      this.props.model.startSession();
+      if (this.props.initiate) {
+        this.listenTo(this.props.model, "session:connected",
+                                        this.startPublishing);
+        this.listenTo(this.props.model, "session:stream-created",
+                                        this._streamCreated);
+        this.listenTo(this.props.model, ["session:peer-hungup",
+                                         "session:network-disconnected",
+                                         "session:ended"].join(" "),
+                                         this.stopPublishing);
+        this.props.model.startSession();
+      }
 
       /**
        * OT inserts inline styles into the markup. Using a listener for
        * resize events helps us trigger a full width/height on the element
        * so that they update to the correct dimensions.
-       * */
+       * XXX: this should be factored as a mixin.
+       */
       window.addEventListener('orientationchange', this.updateVideoContainer);
       window.addEventListener('resize', this.updateVideoContainer);
     },
 
     updateVideoContainer: function() {
       var localStreamParent = document.querySelector('.local .OT_publisher');
       var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
       if (localStreamParent) {
@@ -277,20 +290,22 @@ loop.shared.views = (function(_, OT, l10
         this.setState({video: {enabled: enabled}});
       }
     },
 
     /**
      * Unpublishes local stream.
      */
     stopPublishing: function() {
-      // Unregister listeners for publisher events
-      this.stopListening(this.publisher);
+      if (this.publisher) {
+        // Unregister listeners for publisher events
+        this.stopListening(this.publisher);
 
-      this.props.model.session.unpublish(this.publisher);
+        this.props.model.session.unpublish(this.publisher);
+      }
     },
 
     render: function() {
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
         "local-stream-audio": !this.state.video.enabled
       });
@@ -357,17 +372,17 @@ loop.shared.views = (function(_, OT, l10
       sendFeedback: React.PropTypes.func,
       reset:        React.PropTypes.func
     },
 
     getInitialState: function() {
       return {category: "", description: ""};
     },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {pending: false};
     },
 
     _getCategories: function() {
       return {
         audio_quality: l10n.get("feedback_category_audio_quality"),
         video_quality: l10n.get("feedback_category_video_quality"),
         disconnected : l10n.get("feedback_category_was_disconnected"),
@@ -462,18 +477,26 @@ loop.shared.views = (function(_, OT, l10
           </form>
         </FeedbackLayout>
       );
     }
   });
 
   /**
    * Feedback received view.
+   *
+   * Props:
+   * - {Function} onAfterFeedbackReceived Function to execute after the
+   *   WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
    */
   var FeedbackReceived = React.createClass({
+    propTypes: {
+      onAfterFeedbackReceived: React.PropTypes.func
+    },
+
     getInitialState: function() {
       return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
     },
 
     componentDidMount: function() {
       this._timer = setInterval(function() {
         this.setState({countdown: this.state.countdown - 1});
       }.bind(this), 1000);
@@ -483,17 +506,19 @@ loop.shared.views = (function(_, OT, l10
       if (this._timer) {
         clearInterval(this._timer);
       }
     },
 
     render: function() {
       if (this.state.countdown < 1) {
         clearInterval(this._timer);
-        window.close();
+        if (this.props.onAfterFeedbackReceived) {
+          this.props.onAfterFeedbackReceived();
+        }
       }
       return (
         <FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
           <p className="info thank-you">{
             l10n.get("feedback_window_will_close_in2", {
               countdown: this.state.countdown,
               num: this.state.countdown
             })}</p>
@@ -504,25 +529,26 @@ loop.shared.views = (function(_, OT, l10
 
   /**
    * Feedback view.
    */
   var FeedbackView = React.createClass({
     propTypes: {
       // A loop.FeedbackAPIClient instance
       feedbackApiClient: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func,
       // The current feedback submission flow step name
       step: React.PropTypes.oneOf(["start", "form", "finished"])
     },
 
     getInitialState: function() {
       return {pending: false, step: this.props.step || "start"};
     },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {step: "start"};
     },
 
     reset: function() {
       this.setState(this.getInitialState());
     },
 
     handleHappyClick: function() {
@@ -547,17 +573,20 @@ loop.shared.views = (function(_, OT, l10
         console.error("Unable to send user feedback", err);
       }
       this.setState({pending: false, step: "finished"});
     },
 
     render: function() {
       switch(this.state.step) {
         case "finished":
-          return <FeedbackReceived />;
+          return (
+            <FeedbackReceived
+              onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
+          );
         case "form":
           return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
                                sendFeedback={this.sendFeedback}
                                reset={this.reset}
                                pending={this.state.pending} />;
         default:
           return (
             <FeedbackLayout title={
--- a/browser/components/loop/standalone/Makefile
+++ b/browser/components/loop/standalone/Makefile
@@ -8,16 +8,19 @@
 
 # XXX In the interest of making the build logic simpler and
 # more maintainable, we should be trying to implement new
 # functionality in Gruntfile.js rather than here.
 # Bug 1066176 tracks moving all functionality currently here
 # to the Gruntfile and getting rid of this Makefile entirely.
 
 LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
+LOOP_FEEDBACK_API_URL := $(shell echo $${LOOP_FEEDBACK_API_URL-"https://input.allizom.org/api/v1/feedback"})
+LOOP_FEEDBACK_PRODUCT_NAME := $(shell echo $${LOOP_FEEDBACK_PRODUCT_NAME-Loop})
+
 NODE_LOCAL_BIN=./node_modules/.bin
 
 install: npm_install tos
 
 npm_install:
 	@npm install
 
 test:
@@ -62,9 +65,11 @@ remove_old_config:
 
 # The services development deployment, however, still wants a static config
 # file, and needs an easy way to generate one.  This target is for folks
 # working with that deployment.
 .PHONY: config
 config:
 	@echo "var loop = loop || {};" > content/config.js
 	@echo "loop.config = loop.config || {};" >> content/config.js
-	@echo "loop.config.serverUrl          = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
+	@echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
+	@echo "loop.config.feedbackApiUrl = '`echo $(LOOP_FEEDBACK_API_URL)`';" >> content/config.js
+	@echo "loop.config.feedbackProductName = '`echo $(LOOP_FEEDBACK_PRODUCT_NAME)`';" >> content/config.js
--- a/browser/components/loop/standalone/README.md
+++ b/browser/components/loop/standalone/README.md
@@ -24,18 +24,22 @@ folks deploying the development server w
 
     $ make config
 
 It will read the configuration from the following env variables and generate the
 appropriate configuration file:
 
 - `LOOP_SERVER_URL` defines the root url of the loop server, without trailing
   slash (default: `http://localhost:5000`).
-- `LOOP_PENDING_CALL_TIMEOUT` defines the amount of time a pending outgoing call
-  should be considered timed out, in milliseconds (default: `20000`).
+- `LOOP_FEEDBACK_API_URL` sets the root URL for the
+  [input API](https://input.mozilla.org/); defaults to the input stage server
+  (https://input.allizom.org/api/v1/feedback). **Don't forget to set this
+  value to the production server URL when deploying to production.**
+- `LOOP_FEEDBACK_PRODUCT_NAME` defines the product name to be sent to the input
+  API (defaults: Loop).
 
 Usage
 -----
 
 For development, run a local static file server:
 
     $ make runserver
 
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -202,8 +202,46 @@ body,
  * Left / Right padding elements
  * used to center components
  * */
 .flex-padding-1 {
   display: flex;
   flex: 1;
 }
 
+/**
+ * Feedback form overlay (standalone only)
+ */
+.standalone .ended-conversation {
+  position: relative;
+  height: 100%;
+  background-color: #444;
+  text-align: left; /* as backup */
+  text-align: start;
+}
+
+.standalone .ended-conversation .feedback {
+  position: absolute;
+  width: 50%;
+  max-width: 400px;
+  margin: 10px auto;
+  top: 20px;
+  left: 10%;
+  right: 10%;
+  background: #FFF;
+  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
+  border-radius: 3px;
+  z-index: 1002; /* ensures the form is always on top of the control bar */
+}
+
+.standalone .ended-conversation .local-stream {
+  /* Hide  local media stream when feedback form is shown. */
+  display: none;
+}
+
+@media screen and (max-width:640px) {
+  .standalone .ended-conversation .feedback {
+    width: 92%;
+    top: 10%;
+    left: 5px;
+    right: 5px;
+  }
+}
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -36,16 +36,17 @@
     <script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
 
     <!-- app scripts -->
     <script type="text/javascript" src="config.js"></script>
     <script type="text/javascript" src="shared/js/utils.js"></script>
     <script type="text/javascript" src="shared/js/models.js"></script>
     <script type="text/javascript" src="shared/js/mixins.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
+    <script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="shared/js/websocket.js"></script>
     <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/webapp.js"></script>
 
     <script>
       // Wait for all the localization notes to load
       window.addEventListener('localized', function() {
         loop.webapp.init();
--- a/browser/components/loop/standalone/content/js/standaloneClient.js
+++ b/browser/components/loop/standalone/content/js/standaloneClient.js
@@ -117,17 +117,17 @@ loop.StandaloneClient = (function($) {
         dataType:    "json",
         data: JSON.stringify({callType: callType})
       });
 
       req.done(function(sessionData) {
         try {
           cb(null, this._validate(sessionData, expectedCallsProperties));
         } catch (err) {
-          console.log("Error requesting call info", err);
+          console.error("Error requesting call info", err.message);
           cb(err);
         }
       }.bind(this));
 
       req.fail(this._failureHandler.bind(this, cb));
     },
   };
 
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -1,41 +1,35 @@
 /** @jsx React.DOM */
 
 /* 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/. */
 
 /* global loop:true, React */
-/* jshint newcap:false */
+/* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.webapp = (function($, _, OT, mozL10n) {
   "use strict";
 
   loop.config = loop.config || {};
   loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
 
   var sharedModels = loop.shared.models,
       sharedViews = loop.shared.views;
 
   /**
-   * App router.
-   * @type {loop.webapp.WebappRouter}
-   */
-  var router;
-
-  /**
    * Homepage view.
    */
   var HomeView = React.createClass({displayName: 'HomeView',
     render: function() {
       return (
         React.DOM.p(null, mozL10n.get("welcome"))
-      )
+      );
     }
   });
 
   /**
    * Unsupported Browsers view.
    */
   var UnsupportedBrowserView = React.createClass({displayName: 'UnsupportedBrowserView',
     render: function() {
@@ -99,28 +93,26 @@ loop.webapp = (function($, _, OT, mozL10
    * Expired call URL view.
    */
   var CallUrlExpiredView = React.createClass({displayName: 'CallUrlExpiredView',
     propTypes: {
       helper: React.PropTypes.object.isRequired
     },
 
     render: function() {
-      /* jshint ignore:start */
       return (
         React.DOM.div({className: "expired-url-info"}, 
           React.DOM.div({className: "info-panel"}, 
             React.DOM.div({className: "firefox-logo"}), 
             React.DOM.h1(null, mozL10n.get("call_url_unavailable_notification_heading")), 
             React.DOM.h4(null, mozL10n.get("call_url_unavailable_notification_message2"))
           ), 
           PromoteFirefoxView({helper: this.props.helper})
         )
       );
-      /* jshint ignore:end */
     }
   });
 
   var ConversationBranding = React.createClass({displayName: 'ConversationBranding',
     render: function() {
       return (
         React.DOM.h1({className: "standalone-header-title"}, 
           React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname")
@@ -141,28 +133,26 @@ loop.webapp = (function($, _, OT, mozL10
         "hide": !this.props.urlCreationDateString.length
       });
 
       var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
         "call_url_creation_date": this.props.urlCreationDateString
       });
 
       return (
-        /* jshint ignore:start */
         React.DOM.header({className: "standalone-header header-box container-box"}, 
           ConversationBranding(null), 
           React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}), 
           React.DOM.h3({className: "call-url"}, 
             conversationUrl
           ), 
           React.DOM.h4({className: urlCreationDateClasses}, 
             callUrlCreationDateString
           )
         )
-        /* jshint ignore:end */
       );
     }
   });
 
   var ConversationFooter = React.createClass({displayName: 'ConversationFooter',
     render: function() {
       return (
         React.DOM.div({className: "standalone-footer container-box"}, 
@@ -171,17 +161,17 @@ loop.webapp = (function($, _, OT, mozL10
       );
     }
   });
 
   var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
     getInitialState: function() {
       return {
         callState: this.props.callState || "connecting"
-      }
+      };
     },
 
     propTypes: {
       websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
                       .isRequired
     },
 
     componentDidMount: function() {
@@ -195,17 +185,16 @@ loop.webapp = (function($, _, OT, mozL10
 
     _cancelOutgoingCall: function() {
       this.props.websocket.cancel();
     },
 
     render: function() {
       var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
       return (
-        /* jshint ignore:start */
         React.DOM.div({className: "container"}, 
           React.DOM.div({className: "container-box"}, 
             React.DOM.header({className: "pending-header header-box"}, 
               ConversationBranding(null)
             ), 
 
             React.DOM.div({id: "cameraPreview"}), 
 
@@ -224,55 +213,49 @@ loop.webapp = (function($, _, OT, mozL10
                 )
               ), 
               React.DOM.div({className: "flex-padding-1"})
             )
           ), 
 
           ConversationFooter(null)
         )
-        /* jshint ignore:end */
       );
     }
   });
 
   /**
    * Conversation launcher view. A ConversationModel is associated and attached
    * as a `model` property.
+   *
+   * Required properties:
+   * - {loop.shared.models.ConversationModel}    model    Conversation model.
+   * - {loop.shared.models.NotificationCollection} notifications
    */
   var StartConversationView = React.createClass({displayName: 'StartConversationView',
-    /**
-     * Constructor.
-     *
-     * Required options:
-     * - {loop.shared.models.ConversationModel}    model    Conversation model.
-     * - {loop.shared.models.NotificationCollection} notifications
-     *
-     */
+    propTypes: {
+      model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
+                                       .isRequired,
+      // XXX Check more tightly here when we start injecting window.loop.*
+      notifications: React.PropTypes.object.isRequired,
+      client: React.PropTypes.object.isRequired
+    },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {showCallOptionsMenu: false};
     },
 
     getInitialState: function() {
       return {
         urlCreationDateString: '',
         disableCallButton: false,
         showCallOptionsMenu: this.props.showCallOptionsMenu
       };
     },
 
-    propTypes: {
-      model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
-                                       .isRequired,
-      // XXX Check more tightly here when we start injecting window.loop.*
-      notifications: React.PropTypes.object.isRequired,
-      client: React.PropTypes.object.isRequired
-    },
-
     componentDidMount: function() {
       // Listen for events & hide dropdown menu if user clicks away
       window.addEventListener("click", this.clickHandler);
       this.props.model.listenTo(this.props.model, "session:error",
                                 this._onSessionError);
       this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"),
                                            this._setConversationTimestamp);
     },
@@ -343,17 +326,16 @@ loop.webapp = (function($, _, OT, mozL10
         "visually-hidden": !this.state.showCallOptionsMenu
       });
       var tosClasses = React.addons.classSet({
         "terms-service": true,
         hide: (localStorage.getItem("has-seen-tos") === "true")
       });
 
       return (
-        /* jshint ignore:start */
         React.DOM.div({className: "container"}, 
           React.DOM.div({className: "container-box"}, 
 
             ConversationHeader({
               urlCreationDateString: this.state.urlCreationDateString}), 
 
             React.DOM.p({className: "standalone-btn-label"}, 
               mozL10n.get("initiate_call_button_label2")
@@ -402,17 +384,47 @@ loop.webapp = (function($, _, OT, mozL10
             ), 
 
             React.DOM.p({className: tosClasses, 
                dangerouslySetInnerHTML: {__html: tosHTML}})
           ), 
 
           ConversationFooter(null)
         )
-        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
+   * Ended conversation view.
+   */
+  var EndedConversationView = React.createClass({displayName: 'EndedConversationView',
+    propTypes: {
+      conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
+                         .isRequired,
+      sdk: React.PropTypes.object.isRequired,
+      feedbackApiClient: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func.isRequired
+    },
+
+    render: function() {
+      return (
+        React.DOM.div({className: "ended-conversation"}, 
+          sharedViews.FeedbackView({
+            feedbackApiClient: this.props.feedbackApiClient, 
+            onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}
+          ), 
+          sharedViews.ConversationView({
+            initiate: false, 
+            sdk: this.props.sdk, 
+            model: this.props.conversation, 
+            audio: {enabled: false, visible: false}, 
+            video: {enabled: false, visible: false}}
+          )
+        )
       );
     }
   });
 
   /**
    * This view manages the outgoing conversation views - from
    * call initiation through to the actual conversation and call end.
    *
@@ -421,17 +433,18 @@ loop.webapp = (function($, _, OT, mozL10
   var OutgoingConversationView = React.createClass({displayName: 'OutgoingConversationView',
     propTypes: {
       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
-      sdk: React.PropTypes.object.isRequired
+      sdk: React.PropTypes.object.isRequired,
+      feedbackApiClient: React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         callStatus: "start"
       };
     },
 
@@ -445,61 +458,82 @@ loop.webapp = (function($, _, OT, mozL10
       this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
       this.props.conversation.on("session:connection-error", this._notifyError, this);
     },
 
     componentDidUnmount: function() {
       this.props.conversation.off(null, null, this);
     },
 
+    shouldComponentUpdate: function(nextProps, nextState) {
+      // Only rerender if current state has actually changed
+      return nextState.callStatus !== this.state.callStatus;
+    },
+
+    callStatusSwitcher: function(status) {
+      return function() {
+        this.setState({callStatus: status});
+      }.bind(this);
+    },
+
     /**
      * Renders the conversation views.
      */
     render: function() {
       switch (this.state.callStatus) {
         case "failure":
-        case "end":
         case "start": {
           return (
             StartConversationView({
               model: this.props.conversation, 
               notifications: this.props.notifications, 
               client: this.props.client}
             )
           );
         }
         case "pending": {
           return PendingConversationView({websocket: this._websocket});
         }
         case "connected": {
           return (
             sharedViews.ConversationView({
+              initiate: true, 
               sdk: this.props.sdk, 
               model: this.props.conversation, 
               video: {enabled: this.props.conversation.hasVideoStream("outgoing")}}
             )
           );
         }
+        case "end": {
+          return (
+            EndedConversationView({
+              sdk: this.props.sdk, 
+              conversation: this.props.conversation, 
+              feedbackApiClient: this.props.feedbackApiClient, 
+              onAfterFeedbackReceived: this.callStatusSwitcher("start")}
+            )
+          );
+        }
         case "expired": {
           return (
             CallUrlExpiredView({helper: this.props.helper})
           );
         }
         default: {
-          return HomeView(null)
+          return HomeView(null);
         }
       }
     },
 
     /**
      * Notify the user that the connection was not possible
      * @param {{code: number, message: string}} error
      */
     _notifyError: function(error) {
-      console.log(error);
+      console.error(error);
       this.props.notifications.errorL10n("connection_error_see_console_notification");
       this.setState({callStatus: "end"});
     },
 
     /**
      * Peer hung up. Notifies the user and ends the call.
      *
      * Event properties:
@@ -623,23 +657,25 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     /**
      * Handles call rejection.
      *
      * @param {String} reason The reason the call was terminated.
      */
     _handleCallTerminated: function(reason) {
-      this.setState({callStatus: "end"});
-      // For reasons other than cancel, display some notification text.
       if (reason !== "cancel") {
         // XXX This should really display the call failed view - bug 1046959
         // will implement this.
         this.props.notifications.errorL10n("call_timeout_notification_text");
       }
+      // redirects the user to the call start view
+      // XXX should switch callStatus to failed for specific reasons when we
+      // get the call failed view; for now, switch back to start.
+      this.setState({callStatus: "start"});
     },
 
     /**
      * Handles ending a call by resetting the view to the start state.
      */
     _endCall: function() {
       this.setState({callStatus: "end"});
     },
@@ -652,17 +688,18 @@ loop.webapp = (function($, _, OT, mozL10
   var WebappRootView = React.createClass({displayName: 'WebappRootView',
     propTypes: {
       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
-      sdk: React.PropTypes.object.isRequired
+      sdk: React.PropTypes.object.isRequired,
+      feedbackApiClient: React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         unsupportedDevice: this.props.helper.isIOS(navigator.platform),
         unsupportedBrowser: !this.props.sdk.checkSystemRequirements(),
       };
     },
@@ -674,17 +711,18 @@ loop.webapp = (function($, _, OT, mozL10
         return UnsupportedBrowserView(null);
       } else if (this.props.conversation.get("loopToken")) {
         return (
           OutgoingConversationView({
              client: this.props.client, 
              conversation: this.props.conversation, 
              helper: this.props.helper, 
              notifications: this.props.notifications, 
-             sdk: this.props.sdk}
+             sdk: this.props.sdk, 
+             feedbackApiClient: this.props.feedbackApiClient}
           )
         );
       } else {
         return HomeView(null);
       }
     }
   });
 
@@ -716,41 +754,49 @@ loop.webapp = (function($, _, OT, mozL10
     var helper = new WebappHelper();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var notifications = new sharedModels.NotificationCollection();
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: OT
     });
+    var feedbackApiClient = new loop.FeedbackAPIClient(
+      loop.config.feedbackApiUrl, {
+        product: loop.config.feedbackProductName,
+        user_agent: navigator.userAgent,
+        url: document.location.origin
+      });
 
     // Obtain the loopToken and pass it to the conversation
     var locationHash = helper.locationHash();
     if (locationHash) {
       conversation.set("loopToken", locationHash.match(/\#call\/(.*)/)[1]);
     }
 
     React.renderComponent(WebappRootView({
       client: client, 
       conversation: conversation, 
       helper: helper, 
       notifications: notifications, 
-      sdk: OT}
+      sdk: OT, 
+      feedbackApiClient: feedbackApiClient}
     ), document.querySelector("#main"));
 
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = mozL10n.language.code;
     document.documentElement.dir = mozL10n.language.direction;
   }
 
   return {
     CallUrlExpiredView: CallUrlExpiredView,
     PendingConversationView: PendingConversationView,
     StartConversationView: StartConversationView,
     OutgoingConversationView: OutgoingConversationView,
+    EndedConversationView: EndedConversationView,
     HomeView: HomeView,
     UnsupportedBrowserView: UnsupportedBrowserView,
     UnsupportedDeviceView: UnsupportedDeviceView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRootView: WebappRootView
   };
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -1,41 +1,35 @@
 /** @jsx React.DOM */
 
 /* 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/. */
 
 /* global loop:true, React */
-/* jshint newcap:false */
+/* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.webapp = (function($, _, OT, mozL10n) {
   "use strict";
 
   loop.config = loop.config || {};
   loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
 
   var sharedModels = loop.shared.models,
       sharedViews = loop.shared.views;
 
   /**
-   * App router.
-   * @type {loop.webapp.WebappRouter}
-   */
-  var router;
-
-  /**
    * Homepage view.
    */
   var HomeView = React.createClass({
     render: function() {
       return (
         <p>{mozL10n.get("welcome")}</p>
-      )
+      );
     }
   });
 
   /**
    * Unsupported Browsers view.
    */
   var UnsupportedBrowserView = React.createClass({
     render: function() {
@@ -99,28 +93,26 @@ loop.webapp = (function($, _, OT, mozL10
    * Expired call URL view.
    */
   var CallUrlExpiredView = React.createClass({
     propTypes: {
       helper: React.PropTypes.object.isRequired
     },
 
     render: function() {
-      /* jshint ignore:start */
       return (
         <div className="expired-url-info">
           <div className="info-panel">
             <div className="firefox-logo" />
             <h1>{mozL10n.get("call_url_unavailable_notification_heading")}</h1>
             <h4>{mozL10n.get("call_url_unavailable_notification_message2")}</h4>
           </div>
           <PromoteFirefoxView helper={this.props.helper} />
         </div>
       );
-      /* jshint ignore:end */
     }
   });
 
   var ConversationBranding = React.createClass({
     render: function() {
       return (
         <h1 className="standalone-header-title">
           <strong>{mozL10n.get("brandShortname")}</strong> {mozL10n.get("clientShortname")}
@@ -141,28 +133,26 @@ loop.webapp = (function($, _, OT, mozL10
         "hide": !this.props.urlCreationDateString.length
       });
 
       var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
         "call_url_creation_date": this.props.urlCreationDateString
       });
 
       return (
-        /* jshint ignore:start */
         <header className="standalone-header header-box container-box">
           <ConversationBranding />
           <div className="loop-logo" title="Firefox WebRTC! logo"></div>
           <h3 className="call-url">
             {conversationUrl}
           </h3>
           <h4 className={urlCreationDateClasses} >
             {callUrlCreationDateString}
           </h4>
         </header>
-        /* jshint ignore:end */
       );
     }
   });
 
   var ConversationFooter = React.createClass({
     render: function() {
       return (
         <div className="standalone-footer container-box">
@@ -171,17 +161,17 @@ loop.webapp = (function($, _, OT, mozL10
       );
     }
   });
 
   var PendingConversationView = React.createClass({
     getInitialState: function() {
       return {
         callState: this.props.callState || "connecting"
-      }
+      };
     },
 
     propTypes: {
       websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
                       .isRequired
     },
 
     componentDidMount: function() {
@@ -195,17 +185,16 @@ loop.webapp = (function($, _, OT, mozL10
 
     _cancelOutgoingCall: function() {
       this.props.websocket.cancel();
     },
 
     render: function() {
       var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
       return (
-        /* jshint ignore:start */
         <div className="container">
           <div className="container-box">
             <header className="pending-header header-box">
               <ConversationBranding />
             </header>
 
             <div id="cameraPreview"></div>
 
@@ -224,55 +213,49 @@ loop.webapp = (function($, _, OT, mozL10
                 </span>
               </button>
               <div className="flex-padding-1"></div>
             </div>
           </div>
 
           <ConversationFooter />
         </div>
-        /* jshint ignore:end */
       );
     }
   });
 
   /**
    * Conversation launcher view. A ConversationModel is associated and attached
    * as a `model` property.
+   *
+   * Required properties:
+   * - {loop.shared.models.ConversationModel}    model    Conversation model.
+   * - {loop.shared.models.NotificationCollection} notifications
    */
   var StartConversationView = React.createClass({
-    /**
-     * Constructor.
-     *
-     * Required options:
-     * - {loop.shared.models.ConversationModel}    model    Conversation model.
-     * - {loop.shared.models.NotificationCollection} notifications
-     *
-     */
+    propTypes: {
+      model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
+                                       .isRequired,
+      // XXX Check more tightly here when we start injecting window.loop.*
+      notifications: React.PropTypes.object.isRequired,
+      client: React.PropTypes.object.isRequired
+    },
 
-    getInitialProps: function() {
+    getDefaultProps: function() {
       return {showCallOptionsMenu: false};
     },
 
     getInitialState: function() {
       return {
         urlCreationDateString: '',
         disableCallButton: false,
         showCallOptionsMenu: this.props.showCallOptionsMenu
       };
     },
 
-    propTypes: {
-      model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
-                                       .isRequired,
-      // XXX Check more tightly here when we start injecting window.loop.*
-      notifications: React.PropTypes.object.isRequired,
-      client: React.PropTypes.object.isRequired
-    },
-
     componentDidMount: function() {
       // Listen for events & hide dropdown menu if user clicks away
       window.addEventListener("click", this.clickHandler);
       this.props.model.listenTo(this.props.model, "session:error",
                                 this._onSessionError);
       this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"),
                                            this._setConversationTimestamp);
     },
@@ -343,17 +326,16 @@ loop.webapp = (function($, _, OT, mozL10
         "visually-hidden": !this.state.showCallOptionsMenu
       });
       var tosClasses = React.addons.classSet({
         "terms-service": true,
         hide: (localStorage.getItem("has-seen-tos") === "true")
       });
 
       return (
-        /* jshint ignore:start */
         <div className="container">
           <div className="container-box">
 
             <ConversationHeader
               urlCreationDateString={this.state.urlCreationDateString} />
 
             <p className="standalone-btn-label">
               {mozL10n.get("initiate_call_button_label2")}
@@ -402,17 +384,47 @@ loop.webapp = (function($, _, OT, mozL10
             </div>
 
             <p className={tosClasses}
                dangerouslySetInnerHTML={{__html: tosHTML}}></p>
           </div>
 
           <ConversationFooter />
         </div>
-        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
+   * Ended conversation view.
+   */
+  var EndedConversationView = React.createClass({
+    propTypes: {
+      conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
+                         .isRequired,
+      sdk: React.PropTypes.object.isRequired,
+      feedbackApiClient: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func.isRequired
+    },
+
+    render: function() {
+      return (
+        <div className="ended-conversation">
+          <sharedViews.FeedbackView
+            feedbackApiClient={this.props.feedbackApiClient}
+            onAfterFeedbackReceived={this.props.onAfterFeedbackReceived}
+          />
+          <sharedViews.ConversationView
+            initiate={false}
+            sdk={this.props.sdk}
+            model={this.props.conversation}
+            audio={{enabled: false, visible: false}}
+            video={{enabled: false, visible: false}}
+          />
+        </div>
       );
     }
   });
 
   /**
    * This view manages the outgoing conversation views - from
    * call initiation through to the actual conversation and call end.
    *
@@ -421,17 +433,18 @@ loop.webapp = (function($, _, OT, mozL10
   var OutgoingConversationView = React.createClass({
     propTypes: {
       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
-      sdk: React.PropTypes.object.isRequired
+      sdk: React.PropTypes.object.isRequired,
+      feedbackApiClient: React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         callStatus: "start"
       };
     },
 
@@ -445,61 +458,82 @@ loop.webapp = (function($, _, OT, mozL10
       this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
       this.props.conversation.on("session:connection-error", this._notifyError, this);
     },
 
     componentDidUnmount: function() {
       this.props.conversation.off(null, null, this);
     },
 
+    shouldComponentUpdate: function(nextProps, nextState) {
+      // Only rerender if current state has actually changed
+      return nextState.callStatus !== this.state.callStatus;
+    },
+
+    callStatusSwitcher: function(status) {
+      return function() {
+        this.setState({callStatus: status});
+      }.bind(this);
+    },
+
     /**
      * Renders the conversation views.
      */
     render: function() {
       switch (this.state.callStatus) {
         case "failure":
-        case "end":
         case "start": {
           return (
             <StartConversationView
               model={this.props.conversation}
               notifications={this.props.notifications}
               client={this.props.client}
             />
           );
         }
         case "pending": {
           return <PendingConversationView websocket={this._websocket} />;
         }
         case "connected": {
           return (
             <sharedViews.ConversationView
+              initiate={true}
               sdk={this.props.sdk}
               model={this.props.conversation}
               video={{enabled: this.props.conversation.hasVideoStream("outgoing")}}
             />
           );
         }
+        case "end": {
+          return (
+            <EndedConversationView
+              sdk={this.props.sdk}
+              conversation={this.props.conversation}
+              feedbackApiClient={this.props.feedbackApiClient}
+              onAfterFeedbackReceived={this.callStatusSwitcher("start")}
+            />
+          );
+        }
         case "expired": {
           return (
             <CallUrlExpiredView helper={this.props.helper} />
           );
         }
         default: {
-          return <HomeView />
+          return <HomeView />;
         }
       }
     },
 
     /**
      * Notify the user that the connection was not possible
      * @param {{code: number, message: string}} error
      */
     _notifyError: function(error) {
-      console.log(error);
+      console.error(error);
       this.props.notifications.errorL10n("connection_error_see_console_notification");
       this.setState({callStatus: "end"});
     },
 
     /**
      * Peer hung up. Notifies the user and ends the call.
      *
      * Event properties:
@@ -623,23 +657,25 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     /**
      * Handles call rejection.
      *
      * @param {String} reason The reason the call was terminated.
      */
     _handleCallTerminated: function(reason) {
-      this.setState({callStatus: "end"});
-      // For reasons other than cancel, display some notification text.
       if (reason !== "cancel") {
         // XXX This should really display the call failed view - bug 1046959
         // will implement this.
         this.props.notifications.errorL10n("call_timeout_notification_text");
       }
+      // redirects the user to the call start view
+      // XXX should switch callStatus to failed for specific reasons when we
+      // get the call failed view; for now, switch back to start.
+      this.setState({callStatus: "start"});
     },
 
     /**
      * Handles ending a call by resetting the view to the start state.
      */
     _endCall: function() {
       this.setState({callStatus: "end"});
     },
@@ -652,17 +688,18 @@ loop.webapp = (function($, _, OT, mozL10
   var WebappRootView = React.createClass({
     propTypes: {
       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
-      sdk: React.PropTypes.object.isRequired
+      sdk: React.PropTypes.object.isRequired,
+      feedbackApiClient: React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         unsupportedDevice: this.props.helper.isIOS(navigator.platform),
         unsupportedBrowser: !this.props.sdk.checkSystemRequirements(),
       };
     },
@@ -675,16 +712,17 @@ loop.webapp = (function($, _, OT, mozL10
       } else if (this.props.conversation.get("loopToken")) {
         return (
           <OutgoingConversationView
              client={this.props.client}
              conversation={this.props.conversation}
              helper={this.props.helper}
              notifications={this.props.notifications}
              sdk={this.props.sdk}
+             feedbackApiClient={this.props.feedbackApiClient}
           />
         );
       } else {
         return <HomeView />;
       }
     }
   });
 
@@ -716,41 +754,49 @@ loop.webapp = (function($, _, OT, mozL10
     var helper = new WebappHelper();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var notifications = new sharedModels.NotificationCollection();
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: OT
     });
+    var feedbackApiClient = new loop.FeedbackAPIClient(
+      loop.config.feedbackApiUrl, {
+        product: loop.config.feedbackProductName,
+        user_agent: navigator.userAgent,
+        url: document.location.origin
+      });
 
     // Obtain the loopToken and pass it to the conversation
     var locationHash = helper.locationHash();
     if (locationHash) {
       conversation.set("loopToken", locationHash.match(/\#call\/(.*)/)[1]);
     }
 
     React.renderComponent(<WebappRootView
       client={client}
       conversation={conversation}
       helper={helper}
       notifications={notifications}
       sdk={OT}
+      feedbackApiClient={feedbackApiClient}
     />, document.querySelector("#main"));
 
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = mozL10n.language.code;
     document.documentElement.dir = mozL10n.language.direction;
   }
 
   return {
     CallUrlExpiredView: CallUrlExpiredView,
     PendingConversationView: PendingConversationView,
     StartConversationView: StartConversationView,
     OutgoingConversationView: OutgoingConversationView,
+    EndedConversationView: EndedConversationView,
     HomeView: HomeView,
     UnsupportedBrowserView: UnsupportedBrowserView,
     UnsupportedDeviceView: UnsupportedDeviceView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRootView: WebappRootView
   };
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties
+++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties
@@ -41,8 +41,37 @@ legal_text_and_links=By using this produ
 terms_of_use_link_text=Terms of use
 privacy_notice_link_text=Privacy notice
 brandShortname=Firefox
 clientShortname=WebRTC!
 ## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
 call_url_creation_date_label=(from {{call_url_creation_date}})
 call_progress_connecting_description=Connecting…
 call_progress_ringing_description=Ringing…
+
+feedback_call_experience_heading2=How was your conversation?
+feedback_what_makes_you_sad=What makes you sad?
+feedback_thank_you_heading=Thank you for your feedback!
+feedback_category_audio_quality=Audio quality
+feedback_category_video_quality=Video quality
+feedback_category_was_disconnected=Was disconnected
+feedback_category_confusing=Confusing
+feedback_category_other=Other:
+feedback_custom_category_text_placeholder=What went wrong?
+feedback_submit_button=Submit
+feedback_back_button=Back
+## LOCALIZATION NOTE (feedback_window_will_close_in2):
+## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
+## In this item, don't translate the part between {{..}}
+feedback_window_will_close_in2={[ plural(countdown) ]}
+feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
+feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
+feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
+feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
+feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
+
+## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
+## a signed-in to signed-in user call.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
+feedback_rejoin_button=Rejoin
+## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
+## an abusive user.
+feedback_report_user_button=Report User
--- a/browser/components/loop/standalone/server.js
+++ b/browser/components/loop/standalone/server.js
@@ -2,27 +2,31 @@
  * 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/. */
 
 var express = require('express');
 var app = express();
 
 var port = process.env.PORT || 3000;
 var loopServerPort = process.env.LOOP_SERVER_PORT || 5000;
+var feedbackApiUrl = process.env.LOOP_FEEDBACK_API_URL ||
+                     "https://input.allizom.org/api/v1/feedback";
+var feedbackProductName = process.env.LOOP_FEEDBACK_PRODUCT_NAME || "Loop";
 
 function getConfigFile(req, res) {
   "use strict";
 
   res.set('Content-Type', 'text/javascript');
-  res.send(
-    "var loop = loop || {};" +
-    "loop.config = loop.config || {};" +
-    "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" +
-    "loop.config.pendingCallTimeout = 20000;"
-  );
+  res.send([
+    "var loop = loop || {};",
+    "loop.config = loop.config || {};",
+    "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';",
+    "loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';",
+    "loop.config.feedbackProductName = '" + feedbackProductName + "';",
+  ].join("\n"));
 }
 
 app.get('/content/config.js', getConfigFile);
 
 // This lets /test/ be mapped to the right place for running tests
 app.use('/', express.static(__dirname + '/../'));
 
 // Magic so that the legal content works both in the standalone server
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -103,18 +103,17 @@ describe("loop.conversation", function()
   });
 
   describe("ConversationRouter", function() {
     var conversation, client;
 
     beforeEach(function() {
       client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
-        sdk: {},
-        pendingCallTimeout: 1000,
+        sdk: {}
       });
       sandbox.spy(conversation, "setIncomingSessionData");
       sandbox.stub(conversation, "setOutgoingSessionData");
     });
 
     describe("Routes", function() {
       var router;
 
--- a/browser/components/loop/test/shared/feedbackApiClient_test.js
+++ b/browser/components/loop/test/shared/feedbackApiClient_test.js
@@ -133,16 +133,23 @@ describe("loop.FeedbackAPIClient", funct
 
       it("should send user_agent information when provided", function() {
         client.send({user_agent: "MOZAGENT"}, function(){});
 
         var parsed = JSON.parse(requests[0].requestBody);
         expect(parsed.user_agent).eql("MOZAGENT");
       });
 
+      it("should send url information when provided", function() {
+        client.send({url: "http://fake.invalid"}, function(){});
+
+        var parsed = JSON.parse(requests[0].requestBody);
+        expect(parsed.url).eql("http://fake.invalid");
+      });
+
       it("should throw on invalid feedback data", function() {
         expect(function() {
           client.send("invalid data", function(){});
         }).to.Throw(/Invalid/);
       });
 
       it("should throw on unsupported field name", function() {
         expect(function() {
--- a/browser/components/loop/test/shared/router_test.js
+++ b/browser/components/loop/test/shared/router_test.js
@@ -55,18 +55,17 @@ describe("loop.shared.router", function(
 
     beforeEach(function() {
       TestRouter = loop.shared.router.BaseConversationRouter.extend({
         endCall: sandbox.spy()
       });
       conversation = new loop.shared.models.ConversationModel({
         loopToken: "fakeToken"
       }, {
-        sdk: {},
-        pendingCallTimeout: 1000
+        sdk: {}
       });
     });
 
     describe("#constructor", function() {
       it("should require a ConversationModel instance", function() {
         expect(function() {
           new TestRouter({ client: {} });
         }).to.Throw(Error, /missing required conversation/);
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -182,34 +182,46 @@ describe("loop.shared.views", function()
         publishAudio: sandbox.spy(),
         publishVideo: sandbox.spy()
       }, Backbone.Events);
       fakeSDK = {
         initPublisher: sandbox.stub().returns(fakePublisher),
         initSession: sandbox.stub().returns(fakeSession)
       };
       model = new sharedModels.ConversationModel(fakeSessionData, {
-        sdk: fakeSDK,
-        pendingCallTimeout: 1000
+        sdk: fakeSDK
       });
     });
 
     describe("#componentDidMount", function() {
-      it("should start a session", function() {
+      it("should start a session by default", function() {
         sandbox.stub(model, "startSession");
 
         mountTestComponent({
           sdk: fakeSDK,
           model: model,
           video: {enabled: true}
         });
 
         sinon.assert.calledOnce(model.startSession);
       });
 
+      it("shouldn't start a session if initiate is false", function() {
+        sandbox.stub(model, "startSession");
+
+        mountTestComponent({
+          initiate: false,
+          sdk: fakeSDK,
+          model: model,
+          video: {enabled: true}
+        });
+
+        sinon.assert.notCalled(model.startSession);
+      });
+
       it("should set the correct stream publish options", function() {
 
         var component = mountTestComponent({
           sdk: fakeSDK,
           model: model,
           video: {enabled: false}
         });
 
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -31,16 +31,17 @@
     mocha.setup('bdd');
   </script>
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
+  <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../standalone/content/js/standaloneClient.js"></script>
   <script src="../../standalone/content/js/webapp.js"></script>
  <!-- Test scripts -->
   <script src="standalone_client_test.js"></script>
   <script src="webapp_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -8,34 +8,39 @@ var expect = chai.expect;
 var TestUtils = React.addons.TestUtils;
 
 describe("loop.webapp", function() {
   "use strict";
 
   var sharedModels = loop.shared.models,
       sharedViews = loop.shared.views,
       sandbox,
-      notifications;
+      notifications,
+      feedbackApiClient;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     notifications = new sharedModels.NotificationCollection();
+    feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", {
+      product: "Loop"
+    });
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#init", function() {
     var conversationSetStub;
 
     beforeEach(function() {
       sandbox.stub(React, "renderComponent");
       sandbox.stub(loop.webapp.WebappHelper.prototype,
                    "locationHash").returns("#call/fake-Token");
+      loop.config.feedbackApiUrl = "http://fake.invalid";
       conversationSetStub =
         sandbox.stub(sharedModels.ConversationModel.prototype, "set");
     });
 
     it("should create the WebappRootView", function() {
       loop.webapp.init();
 
       sinon.assert.calledOnce(React.renderComponent);
@@ -72,17 +77,18 @@ describe("loop.webapp", function() {
         sdk: {}
       });
       conversation.set("loopToken", "fakeToken");
       ocView = mountTestComponent({
         helper: new loop.webapp.WebappHelper(),
         client: client,
         conversation: conversation,
         notifications: notifications,
-        sdk: {}
+        sdk: {},
+        feedbackApiClient: feedbackApiClient
       });
     });
 
     describe("start", function() {
       it("should display the StartConversationView", function() {
         TestUtils.findRenderedComponentWithType(ocView,
           loop.webapp.StartConversationView);
       });
@@ -300,26 +306,26 @@ describe("loop.webapp", function() {
           });
       });
 
       describe("session:ended", function() {
         it("should set display the StartConversationView", function() {
           conversation.trigger("session:ended");
 
           TestUtils.findRenderedComponentWithType(ocView,
-            loop.webapp.StartConversationView);
+            loop.webapp.EndedConversationView);
         });
       });
 
       describe("session:peer-hungup", function() {
         it("should set display the StartConversationView", function() {
           conversation.trigger("session:peer-hungup");
 
           TestUtils.findRenderedComponentWithType(ocView,
-            loop.webapp.StartConversationView);
+            loop.webapp.EndedConversationView);
         });
 
         it("should notify the user", function() {
           conversation.trigger("session:peer-hungup");
 
           sinon.assert.calledOnce(notifications.warnL10n);
           sinon.assert.calledWithExactly(notifications.warnL10n,
                                          "peer_ended_conversation2");
@@ -328,17 +334,17 @@ describe("loop.webapp", function() {
       });
 
       describe("session:network-disconnected", function() {
         it("should display the StartConversationView",
           function() {
             conversation.trigger("session:network-disconnected");
 
             TestUtils.findRenderedComponentWithType(ocView,
-              loop.webapp.StartConversationView);
+              loop.webapp.EndedConversationView);
           });
 
         it("should notify the user", function() {
           conversation.trigger("session:network-disconnected");
 
           sinon.assert.calledOnce(notifications.warnL10n);
           sinon.assert.calledWithExactly(notifications.warnL10n,
                                          "network_disconnected");
@@ -469,18 +475,20 @@ describe("loop.webapp", function() {
   describe("WebappRootView", function() {
     var webappHelper, sdk, conversationModel, client, props;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.webapp.WebappRootView({
         client: client,
         helper: webappHelper,
+        notifications: notifications,
         sdk: sdk,
-        conversation: conversationModel
+        conversation: conversationModel,
+        feedbackApiClient: feedbackApiClient
       }));
     }
 
     beforeEach(function() {
       webappHelper = new loop.webapp.WebappHelper();
       sdk = {
         checkSystemRequirements: function() { return true; }
       };
@@ -767,16 +775,42 @@ describe("loop.webapp", function() {
         );
         tos = view.getDOMNode().querySelector(".terms-service");
 
         expect(tos.classList.contains("hide")).to.equal(true);
       });
     });
   });
 
+  describe("EndedConversationView", function() {
+    var view, conversation;
+
+    beforeEach(function() {
+      conversation = new sharedModels.ConversationModel({}, {
+        sdk: {}
+      });
+      view = React.addons.TestUtils.renderIntoDocument(
+        loop.webapp.EndedConversationView({
+          conversation: conversation,
+          sdk: {},
+          feedbackApiClient: feedbackApiClient,
+          onAfterFeedbackReceived: function(){}
+        })
+      );
+    });
+
+    it("should render a ConversationView", function() {
+      TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
+    });
+
+    it("should render a FeedbackView", function() {
+      TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
+    });
+  });
+
   describe("PromoteFirefoxView", function() {
     describe("#render", function() {
       it("should not render when using Firefox", function() {
         var comp = TestUtils.renderIntoDocument(loop.webapp.PromoteFirefoxView({
           helper: {isFirefox: function() { return true; }}
         }));
 
         expect(comp.getDOMNode().querySelectorAll("h3").length).eql(0);
--- a/browser/components/loop/ui/fake-l10n.js
+++ b/browser/components/loop/ui/fake-l10n.js
@@ -4,16 +4,20 @@
 
 /**
  * /!\ FIXME: THIS IS A HORRID HACK which fakes both the mozL10n and webL10n
  * objects and makes them returning the string id and serialized vars if any,
  * for any requested string id.
  * @type {Object}
  */
 navigator.mozL10n = document.mozL10n = {
+  initialize: function(){},
+
+  getDirection: function(){},
+
   get: function(stringId, vars) {
 
     // upcase the first letter
     var readableStringId = stringId.replace(/^./, function(match) {
       "use strict";
       return match.toUpperCase();
     }).replace(/_/g, " ");  // and convert _ chars to spaces
 
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -2,11 +2,12 @@
  * 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/. */
 
 /**
  * Faking the mozLoop object which doesn't exist in regular web pages.
  * @type {Object}
  */
 navigator.mozLoop = {
+  ensureRegistered: function() {},
   getLoopCharPref: function() {},
   getLoopBoolPref: function() {}
 };
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -32,17 +32,17 @@
 .showcase-menu > a {
   margin-right: .5em;
   padding: .4rem;
   margin-top: .2rem;
 }
 
 .showcase > section {
   position: relative;
-  padding-top: 12em;
+  padding-top: 14em;
   clear: both;
 }
 
 .showcase > section > h1 {
   margin: 1em 0;
   border-bottom: 1px solid #aaa;
 }
 
@@ -144,8 +144,14 @@
   max-width: 120px;
 }
 
 .conversation .media.nested .remote {
   /* Height of obsolute box covers media control buttons. UI showcase only.
    * When tokbox inserts the markup into the page the problem goes away */
   bottom: auto;
 }
+
+.standalone .ended-conversation .remote_wrapper,
+.standalone .video-layout-wrapper {
+  /* Removes the fake video image for ended conversations */
+  background: none;
+}
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -18,16 +18,17 @@
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
   var CallUrlExpiredView    = loop.webapp.CallUrlExpiredView;
   var PendingConversationView = loop.webapp.PendingConversationView;
   var StartConversationView = loop.webapp.StartConversationView;
+  var EndedConversationView = loop.webapp.EndedConversationView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var ConversationView = loop.shared.views.ConversationView;
   var FeedbackView = loop.shared.views.FeedbackView;
 
   // Local helpers
   function returnTrue() {
@@ -333,16 +334,29 @@
             Example({summary: "Firefox User"}, 
               CallUrlExpiredView({helper: {isFirefox: returnTrue}})
             ), 
             Example({summary: "Non-Firefox User"}, 
               CallUrlExpiredView({helper: {isFirefox: returnFalse}})
             )
           ), 
 
+          Section({name: "EndedConversationView"}, 
+            Example({summary: "Displays the feedback form"}, 
+              React.DOM.div({className: "standalone"}, 
+                EndedConversationView({sdk: mockSDK, 
+                                       video: {enabled: true}, 
+                                       audio: {enabled: true}, 
+                                       conversation: mockConversationModel, 
+                                       feedbackApiClient: stageFeedbackApiClient, 
+                                       onAfterFeedbackReceived: noop})
+              )
+            )
+          ), 
+
           Section({name: "AlertMessages"}, 
             Example({summary: "Various alerts"}, 
               React.DOM.div({className: "alert alert-warning"}, 
                 React.DOM.button({className: "close"}), 
                 React.DOM.p({className: "message"}, 
                   "The person you were calling has ended the conversation."
                 )
               ), 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -18,16 +18,17 @@
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
   var CallUrlExpiredView    = loop.webapp.CallUrlExpiredView;
   var PendingConversationView = loop.webapp.PendingConversationView;
   var StartConversationView = loop.webapp.StartConversationView;
+  var EndedConversationView = loop.webapp.EndedConversationView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var ConversationView = loop.shared.views.ConversationView;
   var FeedbackView = loop.shared.views.FeedbackView;
 
   // Local helpers
   function returnTrue() {
@@ -333,16 +334,29 @@
             <Example summary="Firefox User">
               <CallUrlExpiredView helper={{isFirefox: returnTrue}} />
             </Example>
             <Example summary="Non-Firefox User">
               <CallUrlExpiredView helper={{isFirefox: returnFalse}} />
             </Example>
           </Section>
 
+          <Section name="EndedConversationView">
+            <Example summary="Displays the feedback form">
+              <div className="standalone">
+                <EndedConversationView sdk={mockSDK}
+                                       video={{enabled: true}}
+                                       audio={{enabled: true}}
+                                       conversation={mockConversationModel}
+                                       feedbackApiClient={stageFeedbackApiClient}
+                                       onAfterFeedbackReceived={noop} />
+              </div>
+            </Example>
+          </Section>
+
           <Section name="AlertMessages">
             <Example summary="Various alerts">
               <div className="alert alert-warning">
                 <button className="close"></button>
                 <p className="message">
                   The person you were calling has ended the conversation.
                 </p>
               </div>
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -70,16 +70,18 @@ let UI = {
     let autoInstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper");
     if (autoInstallADBHelper && !Devices.helperAddonInstalled) {
       GetAvailableAddons().then(addons => {
         addons.adb.install();
       }, console.error);
     }
     Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
 
+    this.lastConnectedRuntime = Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime");
+
     this.setupDeck();
   },
 
   openLastProject: function() {
     let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation");
     let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
     if (lastProjectLocation && shouldRestore) {
       let lastProject = AppProjects.get(lastProjectLocation);
@@ -120,16 +122,17 @@ let UI = {
     }
   },
 
   appManagerUpdate: function(event, what, details) {
     // Got a message from app-manager.js
     switch (what) {
       case "runtimelist":
         this.updateRuntimeList();
+        this.autoConnectRuntime();
         break;
       case "connection":
         this.updateRuntimeButton();
         this.updateCommands();
         break;
       case "project":
         this.updateTitle();
         this.destroyToolbox();
@@ -140,16 +143,17 @@ let UI = {
         break;
       case "project-is-not-running":
       case "project-is-running":
       case "list-tabs-response":
         this.updateCommands();
         break;
       case "runtime":
         this.updateRuntimeButton();
+        this.saveLastConnectedRuntime();
         break;
       case "project-validated":
         this.updateTitle();
         this.updateCommands();
         this.updateProjectButton();
         this.updateProjectEditorHeader();
         break;
       case "install-progress":
@@ -338,32 +342,71 @@ let UI = {
           this.hidePanels();
           this.dismissErrorNotification();
           this.connectToRuntime(r);
         }, true);
       }
     }
   },
 
+  autoConnectRuntime: function () {
+    // Automatically reconnect to the previously selected runtime,
+    // if available and has an ID
+    if (AppManager.selectedRuntime || !this.lastConnectedRuntime) {
+      return;
+    }
+    let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/);
+
+    type = type.toLowerCase();
+
+    // Local connection is mapped to AppManager.runtimeList.custom array
+    if (type == "local") {
+      type = "custom";
+    }
+
+    // We support most runtimes except simulator, that needs to be manually
+    // launched
+    if (type == "usb" || type == "wifi" || type == "custom") {
+      for (let runtime of AppManager.runtimeList[type]) {
+        // Some runtimes do not expose getID function and don't support
+        // autoconnect (like remote connection)
+        if (typeof(runtime.getID) == "function" && runtime.getID() == id) {
+          this.connectToRuntime(runtime);
+        }
+      }
+    }
+  },
+
   connectToRuntime: function(runtime) {
     let name = runtime.getName();
     let promise = AppManager.connectToRuntime(runtime);
     return this.busyUntil(promise, "connecting to runtime");
   },
 
   updateRuntimeButton: function() {
     let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label");
     if (!AppManager.selectedRuntime) {
       labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label"));
     } else {
       let name = AppManager.selectedRuntime.getName();
       labelNode.setAttribute("value", name);
     }
   },
 
+  saveLastConnectedRuntime: function () {
+    if (AppManager.selectedRuntime &&
+        typeof(AppManager.selectedRuntime.getID) === "function") {
+      this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" + AppManager.selectedRuntime.getID();
+    } else {
+      this.lastConnectedRuntime = "";
+    }
+    Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime",
+                               this.lastConnectedRuntime);
+  },
+
   /********** PROJECTS **********/
 
   // Panel & button
 
   updateProjectButton: function() {
     let buttonNode = document.querySelector("#project-panel-button");
     let labelNode = buttonNode.querySelector(".panel-button-label");
     let imageNode = buttonNode.querySelector(".panel-button-image");
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -646,17 +646,25 @@ exports.AppManager = AppManager = {
     Devices.off("addon-status-updated", this._updateUSBRuntimes);
   },
   _updateUSBRuntimes: function() {
     this.runtimeList.usb = [];
     for (let id of Devices.available()) {
       let r = new USBRuntime(id);
       this.runtimeList.usb.push(r);
       r.updateNameFromADB().then(
-        () => this.update("runtimelist"), () => {});
+        () => {
+          this.update("runtimelist");
+          // Also update the runtime button label, if the currently selected
+          // runtime name changes
+          if (r == this.selectedRuntime) {
+            this.update("runtime");
+          }
+        },
+        () => {});
     }
     this.update("runtimelist");
   },
 
   get isWiFiScanningEnabled() {
     return Services.prefs.getBoolPref(WIFI_SCANNING_PREF);
   },
   scanForWiFiRuntimes: function() {
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -8,21 +8,31 @@ const {Services} = Cu.import("resource:/
 const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
 const {DebuggerServer} = require("resource://gre/modules/devtools/dbg-server.jsm");
 const discovery = require("devtools/toolkit/discovery/discovery");
 const promise = require("promise");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
+// These type strings are used for logging events to Telemetry
+let RuntimeTypes = {
+  usb: "USB",
+  wifi: "WIFI",
+  simulator: "SIMULATOR",
+  remote: "REMOTE",
+  local: "LOCAL"
+};
+
 function USBRuntime(id) {
   this.id = id;
 }
 
 USBRuntime.prototype = {
+  type: RuntimeTypes.usb,
   connect: function(connection) {
     let device = Devices.getByName(this.id);
     if (!device) {
       return promise.reject("Can't find device: " + this.getName());
     }
     return device.connect().then((port) => {
       connection.host = "localhost";
       connection.port = port;
@@ -54,16 +64,17 @@ USBRuntime.prototype = {
   },
 }
 
 function WiFiRuntime(deviceName) {
   this.deviceName = deviceName;
 }
 
 WiFiRuntime.prototype = {
+  type: RuntimeTypes.wifi,
   connect: function(connection) {
     let service = discovery.getRemoteService("devtools", this.deviceName);
     if (!service) {
       return promise.reject("Can't find device: " + this.getName());
     }
     connection.host = service.host;
     connection.port = service.port;
     connection.connect();
@@ -77,16 +88,17 @@ WiFiRuntime.prototype = {
   },
 }
 
 function SimulatorRuntime(version) {
   this.version = version;
 }
 
 SimulatorRuntime.prototype = {
+  type: RuntimeTypes.simulator,
   connect: function(connection) {
     let port = ConnectionManager.getFreeTCPPort();
     let simulator = Simulator.getByVersion(this.version);
     if (!simulator || !simulator.launch) {
       return promise.reject("Can't find simulator: " + this.getName());
     }
     return simulator.launch({port: port}).then(() => {
       connection.host = "localhost";
@@ -100,32 +112,37 @@ SimulatorRuntime.prototype = {
     return this.version;
   },
   getName: function() {
     return Simulator.getByVersion(this.version).appinfo.label;
   },
 }
 
 let gLocalRuntime = {
+  type: RuntimeTypes.local,
   connect: function(connection) {
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
     }
     connection.host = null; // Force Pipe transport
     connection.port = null;
     connection.connect();
     return promise.resolve();
   },
   getName: function() {
     return Strings.GetStringFromName("local_runtime");
   },
+  getID: function () {
+    return "local";
+  }
 }
 
 let gRemoteRuntime = {
+  type: RuntimeTypes.remote,
   connect: function(connection) {
     let win = Services.wm.getMostRecentWindow("devtools:webide");
     if (!win) {
       return promise.reject();
     }
     let ret = {value: connection.host + ":" + connection.port};
     let title = Strings.GetStringFromName("remote_runtime_promptTitle");
     let message = Strings.GetStringFromName("remote_runtime_promptMessage");
--- a/browser/devtools/webide/test/chrome.ini
+++ b/browser/devtools/webide/test/chrome.ini
@@ -26,8 +26,9 @@ support-files =
 
 [test_basic.html]
 [test_newapp.html]
 [test_import.html]
 [test_runtime.html]
 [test_manifestUpdate.html]
 [test_addons.html]
 [test_deviceinfo.html]
+[test_autoconnect_runtime.html]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/test/test_autoconnect_runtime.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+
+<html>
+
+  <head>
+    <meta charset="utf8">
+    <title></title>
+
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+    <script type="application/javascript;version=1.8" src="head.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  </head>
+
+  <body>
+
+    <script type="application/javascript;version=1.8">
+      window.onload = function() {
+        SimpleTest.waitForExplicitFinish();
+
+        Task.spawn(function* () {
+
+          Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+          DebuggerServer.init(function () { return true; });
+          DebuggerServer.addBrowserActors();
+
+          let win = yield openWebIDE();
+
+          let fakeRuntime = {
+            type: "USB",
+            connect: function(connection) {
+              ok(connection, win.AppManager.connection, "connection is valid");
+              connection.host = null; // force connectPipe
+              connection.connect();
+              return promise.resolve();
+            },
+
+            getID: function() {
+              return "fakeRuntime";
+            },
+
+            getName: function() {
+              return "fakeRuntime";
+            }
+          };
+          win.AppManager.runtimeList.usb.push(fakeRuntime);
+          win.AppManager.update("runtimelist");
+
+          let panelNode = win.document.querySelector("#runtime-panel");
+          let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+          is(items.length, 1, "Found one runtime button");
+
+          let deferred = promise.defer();
+          win.AppManager.connection.once(
+              win.Connection.Events.CONNECTED,
+              () => deferred.resolve());
+          items[0].click();
+
+          ok(win.document.querySelector("window").className, "busy", "UI is busy");
+          yield win.UI._busyPromise;
+          is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
+
+          yield nextTick();
+
+          yield closeWebIDE(win);
+
+          is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
+
+          win = yield openWebIDE();
+
+          win.AppManager.runtimeList.usb.push(fakeRuntime);
+          win.AppManager.update("runtimelist");
+
+          yield waitForUpdate(win, "list-tabs-response");
+
+          is(Object.keys(DebuggerServer._connections).length, 1, "Automatically reconnected");
+
+          yield win.Cmds.disconnectRuntime();
+
+          yield closeWebIDE(win);
+
+          DebuggerServer.destroy();
+
+          SimpleTest.finish();
+        });
+      }
+
+
+    </script>
+  </body>
+</html>
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -10,8 +10,9 @@ pref("devtools.webide.lastprojectlocatio
 pref("devtools.webide.restoreLastProject", true);
 pref("devtools.webide.enableLocalRuntime", false);
 pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
 pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
 pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
 pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
 pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
 pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
+pref("devtools.webide.lastConnectedRuntime", "");
--- a/browser/themes/shared/devtools/webaudioeditor.inc.css
+++ b/browser/themes/shared/devtools/webaudioeditor.inc.css
@@ -100,34 +100,38 @@ g.edgePath.param-connection {
 .theme-dark .nodes g.selected rect {
   fill: #1d4f73; /* Select Highlight Blue */
 }
 
 .theme-light .nodes g.selected rect {
   fill: #4c9ed9; /* Select Highlight Blue */
 }
 
-/* Text in nodes */
+/* Text in nodes and edges */
 text {
-  cursor: pointer;
+  cursor: default; /* override the "text" cursor */
   font-weight: 300;
   font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
   font-size: 14px;
 }
 
 .theme-dark text {
   fill: #b6babf; /* Grey foreground text */
 }
 .theme-light text {
   fill: #585959; /* Grey foreground text */
 }
 .theme-light g.selected text {
   fill: #f0f1f2; /* Toolbars */
 }
 
+.nodes text {
+  cursor: pointer;
+}
+
 /**
  * Inspector Styles
  */
 
 #web-audio-inspector-title {
   margin: 6px;
 }
 
--- a/content/media/test/can_play_type_ogg.js
+++ b/content/media/test/can_play_type_ogg.js
@@ -1,38 +1,78 @@
-function check_ogg(v, enabled) {
+
+function check_ogg(v, enabled, finish) {
   function check(type, expected) {
     is(v.canPlayType(type), enabled ? expected : "", type);
   }
 
-  // Ogg types
-  check("video/ogg", "maybe");
-  check("audio/ogg", "maybe");
-  check("application/ogg", "maybe");
+  function basic_test() {
+    return new Promise(function(resolve, reject) {
+      // Ogg types
+      check("video/ogg", "maybe");
+      check("audio/ogg", "maybe");
+      check("application/ogg", "maybe");
 
-  // Supported Ogg codecs
-  check("audio/ogg; codecs=vorbis", "probably");
-  check("video/ogg; codecs=vorbis", "probably");
-  check("video/ogg; codecs=vorbis,theora", "probably");
-  check("video/ogg; codecs=\"vorbis, theora\"", "probably");
-  check("video/ogg; codecs=theora", "probably");
+      // Supported Ogg codecs
+      check("audio/ogg; codecs=vorbis", "probably");
+      check("video/ogg; codecs=vorbis", "probably");
+      check("video/ogg; codecs=vorbis,theora", "probably");
+      check("video/ogg; codecs=\"vorbis, theora\"", "probably");
+      check("video/ogg; codecs=theora", "probably");
+
+      resolve();
+    });
+  }
 
   // Verify Opus support
-  var OpusEnabled = undefined;
-  try {
-    OpusEnabled = SpecialPowers.getBoolPref("media.opus.enabled");
-  } catch (ex) {
-    // SpecialPowers failed, perhaps because Opus isn't compiled in
-    console.log("media.opus.enabled pref not found; skipping Opus validation");
+  function verify_opus_support() {
+    return new Promise(function(resolve, reject) {
+      var OpusEnabled = undefined;
+      try {
+        OpusEnabled = SpecialPowers.getBoolPref("media.opus.enabled");
+      } catch (ex) {
+        // SpecialPowers failed, perhaps because Opus isn't compiled in
+        console.log("media.opus.enabled pref not found; skipping Opus validation");
+      }
+      if (OpusEnabled != undefined) {
+        resolve();
+      } else {
+        reject();
+      }
+    });
   }
-  if (OpusEnabled !== undefined) {
-    SpecialPowers.setBoolPref("media.opus.enabled", true);
-    check("audio/ogg; codecs=opus", "probably");
-    SpecialPowers.setBoolPref("media.opus.enabled", false);
-    check("audio/ogg; codecs=opus", "");
-    SpecialPowers.setBoolPref("media.opus.enabled", OpusEnabled);
+
+  function opus_enable() {
+    return new Promise(function(resolve, reject) {
+      SpecialPowers.pushPrefEnv({"set": [['media.opus.enabled', true]]},
+                                function() {
+                                  check("audio/ogg; codecs=opus", "probably");
+                                  resolve();
+                                });
+    });
   }
 
-  // Unsupported Ogg codecs
-  check("video/ogg; codecs=xyz", "");
-  check("video/ogg; codecs=xyz,vorbis", "");
-  check("video/ogg; codecs=vorbis,xyz", "");
+  function opus_disable() {
+    return new Promise(function(resolve, reject) {
+      SpecialPowers.pushPrefEnv({"set": [['media.opus.enabled', false]]},
+                                function() {
+                                  check("audio/ogg; codecs=opus", "");
+                                  resolve();
+                                });
+    });
+  }
+
+  function unspported_ogg() {
+    // Unsupported Ogg codecs
+    check("video/ogg; codecs=xyz", "");
+    check("video/ogg; codecs=xyz,vorbis", "");
+    check("video/ogg; codecs=vorbis,xyz", "");
+
+    finish.call();
+  }
+
+  basic_test()
+  .then(verify_opus_support)
+  .then(opus_enable)
+  .then(opus_disable)
+  .then(unspported_ogg, unspported_ogg);
+
 }
--- a/content/media/test/mochitest.ini
+++ b/content/media/test/mochitest.ini
@@ -17,17 +17,17 @@
 # throws an error (and does not cause a crash or hang), just add it to
 # gErrorTests in manifest.js.
 
 # To test for a specific bug in handling a specific resource type, make the
 # test first check canPlayType for the type, and if it's not supported, just
 # do ok(true, "Type not supported") and stop the test.
 
 [DEFAULT]
-skip-if = buildapp == 'mulet' || (buildapp == 'b2g' && (toolkit != 'gonk' || debug)) # b2g-debug,b2g-desktop(bug 918299)
+skip-if = buildapp == 'mulet' || (buildapp == 'b2g' && toolkit != 'gonk') # b2g-desktop(bug 918299)
 support-files =
   320x240.ogv
   320x240.ogv^headers^
   448636.ogv
   448636.ogv^headers^
   VID_0001.ogg
   VID_0001.ogg^headers^
   allowed.sjs
@@ -315,18 +315,18 @@ skip-if = buildapp == 'mulet' || os == '
 [test_bug919265.html]
 [test_bug957847.html]
 [test_bug1018933.html]
 [test_can_play_type.html]
 [test_can_play_type_mpeg.html]
 skip-if = buildapp == 'b2g' # bug 1021675
 [test_can_play_type_no_ogg.html]
 [test_can_play_type_ogg.html]
-skip-if = buildapp == 'b2g' || e10s # b2g(bug 1021675)
 [test_chaining.html]
+skip-if = toolkit == 'gonk' && debug
 [test_clone_media_element.html]
 [test_closing_connections.html]
 [test_constants.html]
 [test_contentDuration1.html]
 [test_contentDuration2.html]
 [test_contentDuration3.html]
 [test_contentDuration4.html]
 [test_contentDuration5.html]
@@ -362,28 +362,34 @@ skip-if = toolkit == 'gonk' && !debug # 
 [test_mediarecorder_getencodeddata.html]
 [test_mediarecorder_record_4ch_audiocontext.html]
 [test_mediarecorder_record_audiocontext.html]
 [test_mediarecorder_record_audiocontext_mlk.html]
 [test_mediarecorder_record_audionode.html]
 [test_mediarecorder_record_gum_video_timeslice.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' # mimetype check, bug 969289
 [test_mediarecorder_record_immediate_stop.html]
+skip-if = toolkit == 'gonk' && debug
 [test_mediarecorder_record_no_timeslice.html]
+skip-if = toolkit == 'gonk' && debug
 [test_mediarecorder_record_nosrc.html]
 [test_mediarecorder_record_session.html]
 [test_mediarecorder_record_startstopstart.html]
+skip-if = toolkit == 'gonk' && debug
 [test_mediarecorder_record_stopms.html]
 [test_mediarecorder_record_timeslice.html]
+skip-if = toolkit == 'gonk' && debug
 [test_mediarecorder_reload_crash.html]
 [test_mediarecorder_unsupported_src.html]
 [test_mediarecorder_record_getdata_afterstart.html]
+skip-if = toolkit == 'gonk' && debug
 [test_mediatrack_consuming_mediaresource.html]
 [test_mediatrack_consuming_mediastream.html]
 [test_mediatrack_events.html]
+skip-if = toolkit == 'gonk' && debug # bug 1065924
 [test_mediatrack_parsing_ogg.html]
 [test_mediatrack_replay_from_end.html]
 [test_metadata.html]
 [test_mixed_principals.html]
 skip-if = true # bug 567954 and intermittent leaks
 [test_mozHasAudio.html]
 [test_networkState.html]
 [test_new_audio.html]
--- a/content/media/test/test_can_play_type_no_ogg.html
+++ b/content/media/test/test_can_play_type_no_ogg.html
@@ -19,20 +19,24 @@ a Bug 469247</a>
 
 <video id="v"></video>
 
 <pre id="test">
 <script src="can_play_type_ogg.js"></script>
 <script>
 
 SimpleTest.waitForExplicitFinish();
+
+function finish() {
+  mediaTestCleanup();
+  SimpleTest.finish();
+}
+
 SpecialPowers.pushPrefEnv({"set": [["media.ogg.enabled", false]]},
   function() {
-    check_ogg(document.getElementById('v'), false);
-    mediaTestCleanup();
-    SimpleTest.finish();
+    check_ogg(document.getElementById('v'), false, finish);
   }
 );
 
 </script>
 </pre>
 </body>
 </html>
--- a/content/media/test/test_can_play_type_ogg.html
+++ b/content/media/test/test_can_play_type_ogg.html
@@ -16,15 +16,22 @@ a Bug 469247</a>
 <div id="content" style="display: none">
 </div>
 
 <video id="v"></video>
 
 <pre id="test">
 <script src="can_play_type_ogg.js"></script>
 <script>
-check_ogg(document.getElementById('v'), true);
+
+SimpleTest.waitForExplicitFinish();
 
-mediaTestCleanup();
+function finish() {
+  mediaTestCleanup();
+  SimpleTest.finish();
+}
+
+check_ogg(document.getElementById('v'), true, finish);
+
 </script>
 </pre>
 </body>
 </html>
--- a/dom/apps/AppsUtils.jsm
+++ b/dom/apps/AppsUtils.jsm
@@ -17,17 +17,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/FileUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
   "resource://gre/modules/WebappOSUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 
-// Shared code for AppsServiceChild.jsm, Webapps.jsm and Webapps.js
+// Shared code for AppsServiceChild.jsm, TrustedHostedAppsUtils.jsm,
+// Webapps.jsm and Webapps.js
 
 this.EXPORTED_SYMBOLS =
   ["AppsUtils", "ManifestHelper", "isAbsoluteURI", "mozIApplication"];
 
 function debug(s) {
   //dump("-*- AppsUtils.jsm: " + s + "\n");
 }
 
@@ -111,16 +112,94 @@ function _setAppProperties(aObj, aApp) {
 this.AppsUtils = {
   // Clones a app, without the manifest.
   cloneAppObject: function(aApp) {
     let obj = {};
     _setAppProperties(obj, aApp);
     return obj;
   },
 
+  // Creates a nsILoadContext object with a given appId and isBrowser flag.
+  createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
+    return {
+       associatedWindow: null,
+       topWindow : null,
+       appId: aAppId,
+       isInBrowserElement: aIsBrowser,
+       usePrivateBrowsing: false,
+       isContent: false,
+
+       isAppOfType: function(appType) {
+         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+       },
+
+       QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
+                                              Ci.nsIInterfaceRequestor,
+                                              Ci.nsISupports]),
+       getInterface: function(iid) {
+         if (iid.equals(Ci.nsILoadContext))
+           return this;
+         throw Cr.NS_ERROR_NO_INTERFACE;
+       }
+     }
+  },
+
+  // Sends data downloaded from aRequestChannel to a file
+  // identified by aId and aFileName.
+  getFile: function(aRequestChannel, aId, aFileName) {
+    let deferred = Promise.defer();
+
+    // Staging the file in TmpD until all the checks are done.
+    let file = FileUtils.getFile("TmpD", ["webapps", aId, aFileName], true);
+
+    // We need an output stream to write the channel content to the out file.
+    let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
+                         .createInstance(Ci.nsIFileOutputStream);
+    // write, create, truncate
+    outputStream.init(file, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
+    let bufferedOutputStream =
+      Cc['@mozilla.org/network/buffered-output-stream;1']
+        .createInstance(Ci.nsIBufferedOutputStream);
+    bufferedOutputStream.init(outputStream, 1024);
+
+    // Create a listener that will give data to the file output stream.
+    let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
+                     .createInstance(Ci.nsISimpleStreamListener);
+
+    listener.init(bufferedOutputStream, {
+      onStartRequest: function(aRequest, aContext) {
+        // Nothing to do there anymore.
+      },
+
+      onStopRequest: function(aRequest, aContext, aStatusCode) {
+        bufferedOutputStream.close();
+        outputStream.close();
+
+        if (!Components.isSuccessCode(aStatusCode)) {
+          deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: true});
+          return;
+        }
+
+        // If we get a 4XX or a 5XX http status, bail out like if we had a
+        // network error.
+        let responseStatus = aRequestChannel.responseStatus;
+        if (responseStatus >= 400 && responseStatus <= 599) {
+          // unrecoverable error, don't bug the user
+          deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: false});
+          return;
+        }
+
+        deferred.resolve(file);
+      }
+    });
+    aRequestChannel.asyncOpen(listener, null);
+
+    return deferred.promise;
+  },
+
   getAppByManifestURL: function getAppByManifestURL(aApps, aManifestURL) {
     debug("getAppByManifestURL " + aManifestURL);
     // This could be O(1) if |webapps| was a dictionary indexed on manifestURL
     // which should be the unique app identifier.
     // It's currently O(n).
     for (let id in aApps) {
       let app = aApps[id];
       if (app.manifestURL == aManifestURL) {
--- a/dom/apps/PermissionsTable.jsm
+++ b/dom/apps/PermissionsTable.jsm
@@ -43,17 +43,17 @@ this.PermissionsTable =  { geolocation: 
                              app: DENY_ACTION,
                              trusted: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION,
                              substitute: ["geolocation"]
                            },
                            camera: {
                              app: DENY_ACTION,
-                             trusted: DENY_ACTION,
+                             trusted: PROMPT_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: ALLOW_ACTION
                            },
                            alarms: {
                              app: ALLOW_ACTION,
                              trusted: ALLOW_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
@@ -94,38 +94,38 @@ this.PermissionsTable =  { geolocation: 
                              app: DENY_ACTION,
                              trusted: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION,
                              access: ["read"]
                            },
                            "device-storage:pictures": {
                              app: DENY_ACTION,
-                             trusted: DENY_ACTION,
+                             trusted: PROMPT_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: ALLOW_ACTION,
                              access: ["read", "write", "create"]
                            },
                            "device-storage:videos": {
                              app: DENY_ACTION,
-                             trusted: DENY_ACTION,
+                             trusted: PROMPT_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: ALLOW_ACTION,
                              access: ["read", "write", "create"]
                            },
                            "device-storage:music": {
                              app: DENY_ACTION,
-                             trusted: DENY_ACTION,
+                             trusted: PROMPT_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: ALLOW_ACTION,
                              access: ["read", "write", "create"]
                            },
                            "device-storage:sdcard": {
                              app: DENY_ACTION,
-                             trusted: DENY_ACTION,
+                             trusted: PROMPT_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: ALLOW_ACTION,
                              access: ["read", "write", "create"]
                            },
                            sms: {
                              app: DENY_ACTION,
                              trusted: DENY_ACTION,
                              privileged: DENY_ACTION,
@@ -363,17 +363,17 @@ this.PermissionsTable =  { geolocation: 
                              app: DENY_ACTION,
                              trusted: DENY_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION,
                              substitute: ["audio-channel-ringer"]
                            },
                            "audio-channel-publicnotification": {
                              app: DENY_ACTION,
-                             trusted: DENY_ACTION,
+                             trusted: ALLOW_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "open-remote-window": {
                              app: DENY_ACTION,
                              trusted: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
--- a/dom/apps/StoreTrustAnchor.jsm
+++ b/dom/apps/StoreTrustAnchor.jsm
@@ -11,16 +11,18 @@ this.EXPORTED_SYMBOLS = [
   "TrustedRootCertificate"
 ];
 
 const APP_TRUSTED_ROOTS= ["AppMarketplaceProdPublicRoot",
                           "AppMarketplaceProdReviewersRoot",
                           "AppMarketplaceDevPublicRoot",
                           "AppMarketplaceDevReviewersRoot",
                           "AppMarketplaceStageRoot",
+                          "TrustedHostedAppPublicRoot",
+                          "TrustedHostedAppTestRoot",
                           "AppXPCShellRoot"];
 
 this.TrustedRootCertificate = {
   _index: Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot,
   get index() {
     return this._index;
   },
   set index(aIndex) {
--- a/dom/apps/TrustedHostedAppsUtils.jsm
+++ b/dom/apps/TrustedHostedAppsUtils.jsm
@@ -1,23 +1,31 @@
 /* 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/. */
 
-/* global Components, Services, dump */
+/* global Components, Services, dump, AppsUtils, NetUtil, XPCOMUtils */
 
 "use strict";
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
+const Cr = Components.results;
+const signatureFileExtension = ".sig";
 
 this.EXPORTED_SYMBOLS = ["TrustedHostedAppsUtils"];
 
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
 
 #ifdef MOZ_WIDGET_ANDROID
 // On Android, define the "debug" function as a binding of the "d" function
 // from the AndroidLog module so it gets the "debug" priority and a log tag.
 // We always report debug messages on Android because it's unnecessary
 // to restrict reporting, per bug 1003469.
 let debug = Cu
   .import("resource://gre/modules/AndroidLog.jsm", {})
@@ -27,18 +35,16 @@ let debug = Cu
 // The pref is only checked once, on startup, so restart after changing it.
 let debug = Services.prefs.getBoolPref("dom.mozApps.debug") ?
   aMsg => dump("-*- TrustedHostedAppsUtils.jsm : " + aMsg + "\n") :
   () => {};
 #endif
 
 /**
  * Verification functions for Trusted Hosted Apps.
- * (Manifest signature verification is in Webapps.jsm as part of
- * regular signature verification.)
  */
 this.TrustedHostedAppsUtils = {
 
   /**
    * Check if the given host is pinned in the CA pinning database.
    */
   isHostPinned: function (aUrl) {
     let uri;
@@ -60,17 +66,18 @@ this.TrustedHostedAppsUtils = {
       siteSecurityService = Cc["@mozilla.org/ssservice;1"]
         .getService(Ci.nsISiteSecurityService);
     } catch (e) {
       debug("nsISiteSecurityService error: " + e);
       // unrecoverable error, don't bug the user
       throw "CERTDB_ERROR";
     }
 
-    if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP, uri.host, 0)) {
+    if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
+                                         uri.host, 0)) {
       debug("\tvalid certificate pinning for host: " + uri.host + "\n");
       return true;
     }
 
     debug("\tHost NOT pinned: " + uri.host + "\n");
     return false;
   },
 
@@ -95,17 +102,17 @@ this.TrustedHostedAppsUtils = {
       directives
         .map(aDirective => aDirective.trim().split(" "))
         .filter(aList => aList.length > 1)
         // we only restrict on requiredDirectives
         .filter(aList => (requiredDirectives.indexOf(aList[0]) != -1))
         .forEach(aList => {
           // aList[0] contains the directive name.
           // aList[1..n] contains sources.
-          let directiveName = aList.shift()
+          let directiveName = aList.shift();
           let sources = aList;
 
           if ((-1 == validDirectives.indexOf(directiveName))) {
             validDirectives.push(directiveName);
           }
           whiteList.push(...sources.filter(
              // 'self' is checked separately during manifest check
             aSource => (aSource !="'self'" && whiteList.indexOf(aSource) == -1)
@@ -139,10 +146,127 @@ this.TrustedHostedAppsUtils = {
     }
 
     if (!domainWhitelist.list.every(aUrl => this.isHostPinned(aUrl))) {
       debug("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
       return false;
     }
 
     return true;
+  },
+
+  _verifySignedFile: function(aManifestStream, aSignatureStream, aCertDb) {
+    let deferred = Promise.defer();
+
+    let root = Ci.nsIX509CertDB.TrustedHostedAppPublicRoot;
+    try {
+      // Check if we should use the test certificates.
+      // Please note that this should be changed if we ever allow chages to the
+      // prefs since that would create a way for an attacker to use the test
+      // root for real apps.
+      let useTrustedAppTestCerts = Services.prefs
+        .getBoolPref("dom.mozApps.use_trustedapp_test_certs");
+      if (useTrustedAppTestCerts) {
+        root = Ci.nsIX509CertDB.TrustedHostedAppTestRoot;
+      }
+    } catch (ex) { }
+
+    aCertDb.verifySignedManifestAsync(
+      root, aManifestStream, aSignatureStream,
+      function(aRv, aCert) {
+        debug("Signature verification returned code, cert & root: " + aRv + " " + aCert + " " + root);
+        if (Components.isSuccessCode(aRv)) {
+          deferred.resolve(aCert);
+        } else if (aRv == Cr.NS_ERROR_FILE_CORRUPTED ||
+                   aRv == Cr.NS_ERROR_SIGNED_MANIFEST_FILE_INVALID) {
+          deferred.reject("MANIFEST_SIGNATURE_FILE_INVALID");
+        } else {
+          deferred.reject("MANIFEST_SIGNATURE_VERIFICATION_ERROR");
+        }
+      }
+    );
+
+    return deferred.promise;
+  },
+
+  verifySignedManifest: function(aApp, aAppId) {
+    let deferred = Promise.defer();
+
+    let certDb;
+    try {
+      certDb = Cc["@mozilla.org/security/x509certdb;1"]
+                 .getService(Ci.nsIX509CertDB);
+    } catch (e) {
+      debug("nsIX509CertDB error: " + e);
+      // unrecoverable error, don't bug the user
+      throw "CERTDB_ERROR";
+    }
+
+    let mRequestChannel = NetUtil.newChannel(aApp.manifestURL)
+                                 .QueryInterface(Ci.nsIHttpChannel);
+    mRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+    mRequestChannel.notificationCallbacks =
+      AppsUtils.createLoadContext(aAppId, false);
+
+    // The manifest signature must be located at the same path as the
+    // manifest and have the same file name, only the file extension
+    // should differ. Any fragment or query parameter will be ignored.
+    let signatureURL;
+    try {
+      let mURL = Cc["@mozilla.org/network/io-service;1"]
+        .getService(Ci.nsIIOService)
+        .newURI(aApp.manifestURL, null, null)
+        .QueryInterface(Ci.nsIURL);
+      signatureURL = mURL.prePath +
+        mURL.directory + mURL.fileBaseName + signatureFileExtension;
+    } catch(e) {
+      deferred.reject("SIGNATURE_PATH_INVALID");
+      return;
+    }
+
+    let sRequestChannel = NetUtil.newChannel(signatureURL)
+      .QueryInterface(Ci.nsIHttpChannel);
+    sRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+    sRequestChannel.notificationCallbacks =
+      AppsUtils.createLoadContext(aAppId, false);
+    let getAsyncFetchCallback = (resolve, reject) =>
+        (aInputStream, aResult) => {
+          if (!Components.isSuccessCode(aResult)) {
+            debug("Failed to download file");
+            reject("MANIFEST_FILE_UNAVAILABLE");
+            return;
+          }
+          resolve(aInputStream);
+        };
+
+    Promise.all([
+      new Promise((resolve, reject) => {
+        NetUtil.asyncFetch(mRequestChannel,
+                           getAsyncFetchCallback(resolve, reject));
+      }),
+      new Promise((resolve, reject) => {
+        NetUtil.asyncFetch(sRequestChannel,
+                           getAsyncFetchCallback(resolve, reject));
+      })
+    ]).then(([aManifestStream, aSignatureStream]) => {
+      this._verifySignedFile(aManifestStream, aSignatureStream, certDb)
+        .then(deferred.resolve, deferred.reject);
+    }, deferred.reject);
+
+    return deferred.promise;
+  },
+
+  verifyManifest: function(aData) {
+    return new Promise((resolve, reject) => {
+      // sanity check on manifest host's CA (proper CA check with
+      // pinning is done by regular networking code)
+      if (!this.isHostPinned(aData.app.manifestURL)) {
+        reject("TRUSTED_APPLICATION_HOST_CERTIFICATE_INVALID");
+        return;
+      }
+      if (!this.verifyCSPWhiteList(aData.app.manifest.csp)) {
+        reject("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
+        return;
+      }
+      this.verifySignedManifest(aData.app, aData.appId).then(resolve, reject);
+    });
   }
 };
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -2016,17 +2016,17 @@ this.DOMApplicationRegistry = {
         xhr.setRequestHeader(aHeader.name, aHeader.value);
       });
       xhr.responseType = "json";
       if (app.etag) {
         debug("adding manifest etag:" + app.etag);
         xhr.setRequestHeader("If-None-Match", app.etag);
       }
       xhr.channel.notificationCallbacks =
-        this.createLoadContext(app.installerAppId, app.installerIsBrowser);
+        AppsUtils.createLoadContext(app.installerAppId, app.installerIsBrowser);
 
       xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false);
       xhr.addEventListener("error", (function() {
         sendError("NETWORK_ERROR");
       }).bind(this), false);
 
       debug("Checking manifest at " + aData.manifestURL);
       xhr.send(null);
@@ -2047,40 +2047,16 @@ this.DOMApplicationRegistry = {
         extraHeaders.push({ name: "X-MOZ-B2G-DEVICE",
                             value: device || "unknown" });
       }
 #endif
       doRequest.call(this, aResult[0].manifest, extraHeaders);
     });
   },
 
-  // Creates a nsILoadContext object with a given appId and isBrowser flag.
-  createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
-    return {
-       associatedWindow: null,
-       topWindow : null,
-       appId: aAppId,
-       isInBrowserElement: aIsBrowser,
-       usePrivateBrowsing: false,
-       isContent: false,
-
-       isAppOfType: function(appType) {
-         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-       },
-
-       QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
-                                              Ci.nsIInterfaceRequestor,
-                                              Ci.nsISupports]),
-       getInterface: function(iid) {
-         if (iid.equals(Ci.nsILoadContext))
-           return this;
-         throw Cr.NS_ERROR_NO_INTERFACE;
-       }
-     }
-  },
 
   updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) {
     debug("updatePackagedApp");
 
     // Store the new update manifest.
     let dir = this._getAppDir(aId).path;
     let manFile = OS.Path.join(dir, "staged-update.webapp");
     yield this._writeFile(manFile, JSON.stringify(aNewManifest));
@@ -2311,71 +2287,62 @@ this.DOMApplicationRegistry = {
                                      JSON.stringify(aData));
       }
     }).bind(this);
 
     // We may already have the manifest (e.g. AutoInstall),
     // in which case we don't need to load it.
     if (app.manifest) {
       if (checkManifest()) {
-        if (this.kTrustedHosted == this.appKind(app, app.manifest)) {
-          // sanity check on manifest host's CA
-          // (proper CA check with pinning is done by regular networking code)
-          if (!TrustedHostedAppsUtils.isHostPinned(app.manifestURL)) {
-            sendError("TRUSTED_APPLICATION_HOST_CERTIFICATE_INVALID");
-            return;
-          }
-
-          // Signature of the manifest should be verified here.
-          // Bug 1059216.
-
-          if (!TrustedHostedAppsUtils.verifyCSPWhiteList(app.manifest.csp)) {
-            sendError("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
-            return;
-          }
+        debug("Installed manifest check OK");
+        if (this.kTrustedHosted !== this.appKind(app, app.manifest)) {
+          installApp();
+          return;
         }
-
-        installApp();
+        TrustedHostedAppsUtils.verifyManifest(aData)
+        	.then(installApp, sendError);
+      } else {
+        debug("Installed manifest check failed");
+        // checkManifest() sends error before return
       }
       return;
     }
 
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("GET", app.manifestURL, true);
     xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
-    xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
-                                                               aData.isBrowser);
+    xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId,
+                                                                    aData.isBrowser);
     xhr.responseType = "json";
 
     xhr.addEventListener("load", (function() {
       if (xhr.status == 200) {
         if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
                                                 xhr.getResponseHeader("content-type"))) {
           sendError("INVALID_MANIFEST_CONTENT_TYPE");
           return;
         }
 
         app.manifest = xhr.response;
         if (checkManifest()) {
+          debug("Downloaded manifest check OK");
           app.etag = xhr.getResponseHeader("Etag");
-          if (this.kTrustedHosted == this.appKind(app, app.manifest)) {
-            // checking trusted host for pinning is not needed here, since
-            // network code will have already done that
-
-            // Signature of the manifest should be verified here.
-            // Bug 1059216.
-
-            if (!TrustedHostedAppsUtils.verifyCSPWhiteList(app.manifest.csp)) {
-              sendError("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
-              return;
-            }
+          if (this.kTrustedHosted !== this.appKind(app, app.manifest)) {
+            installApp();
+            return;
           }
 
-          installApp();
+          debug("App kind: " + this.kTrustedHosted);
+          TrustedHostedAppsUtils.verifyManifest(aData)
+            .then(installApp, sendError);
+          return;
+        } else {
+          debug("Downloaded manifest check failed");
+          // checkManifest() sends error before return
         }
       } else {
         sendError("MANIFEST_URL_ERROR");
       }
     }).bind(this), false);
 
     xhr.addEventListener("error", (function() {
       sendError("NETWORK_ERROR");
@@ -2450,18 +2417,18 @@ this.DOMApplicationRegistry = {
       }
       return;
     }
 
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("GET", app.manifestURL, true);
     xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
-    xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
-                                                               aData.isBrowser);
+    xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId,
+                                                                    aData.isBrowser);
     xhr.responseType = "json";
 
     xhr.addEventListener("load", (function() {
       if (xhr.status == 200) {
         if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
                                                 xhr.getResponseHeader("content-type"))) {
           sendError("INVALID_MANIFEST_CONTENT_TYPE");
           return;
@@ -3214,62 +3181,25 @@ this.DOMApplicationRegistry = {
       eventType: "progress",
       manifestURL: aNewApp.manifestURL
     });
   },
 
   _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
     let deferred = Promise.defer();
 
-    // Staging the zip in TmpD until all the checks are done.
-    let zipFile =
-      FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
-
-    // We need an output stream to write the channel content to the zip file.
-    let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
-                         .createInstance(Ci.nsIFileOutputStream);
-    // write, create, truncate
-    outputStream.init(zipFile, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
-    let bufferedOutputStream =
-      Cc['@mozilla.org/network/buffered-output-stream;1']
-        .createInstance(Ci.nsIBufferedOutputStream);
-    bufferedOutputStream.init(outputStream, 1024);
-
-    // Create a listener that will give data to the file output stream.
-    let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
-                     .createInstance(Ci.nsISimpleStreamListener);
-
-    listener.init(bufferedOutputStream, {
-      onStartRequest: function(aRequest, aContext) {
-        // Nothing to do there anymore.
-      },
-
-      onStopRequest: function(aRequest, aContext, aStatusCode) {
-        bufferedOutputStream.close();
-        outputStream.close();
-
-        if (!Components.isSuccessCode(aStatusCode)) {
-          deferred.reject("NETWORK_ERROR");
-          return;
-        }
-
-        // If we get a 4XX or a 5XX http status, bail out like if we had a
-        // network error.
-        let responseStatus = aRequestChannel.responseStatus;
-        if (responseStatus >= 400 && responseStatus <= 599) {
-          // unrecoverable error, don't bug the user
-          aOldApp.downloadAvailable = false;
-          deferred.reject("NETWORK_ERROR");
-          return;
-        }
-
-        deferred.resolve(zipFile);
+    AppsUtils.getFile(aRequestChannel, aId, "application.zip").then((aFile) => {
+      deferred.resolve(aFile);
+    }, function(rejectStatus) {
+      debug("Failed to download package file: " + rejectStatus.msg);
+      if (!rejectStatus.downloadAvailable) {
+        aOldApp.downloadAvailable = false;
       }
+      deferred.reject(rejectStatus.msg);
     });
-    aRequestChannel.asyncOpen(listener, null);
 
     // send a first progress event to correctly set the DOM object's properties
     this._sendDownloadProgressEvent(aNewApp, 0);
 
     return deferred.promise;
   },
 
   /**
--- a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp
+++ b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp
@@ -1215,17 +1215,19 @@ private:
 void
 BluetoothHfpManager::Disconnect(BluetoothProfileController* aController)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mController);
 
   if (!sBluetoothHfpInterface) {
     BT_LOGR("sBluetoothHfpInterface is null");
-    aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE));
+    if (aController) {
+      aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE));
+    }
     return;
   }
 
   mController = aController;
 
   sBluetoothHfpInterface->Disconnect(mDeviceAddress,
                                      new DisconnectResultHandler(this));
 }
--- a/dom/devicestorage/nsDeviceStorage.cpp
+++ b/dom/devicestorage/nsDeviceStorage.cpp
@@ -1659,16 +1659,23 @@ DeviceStorageFile::GetStatus(nsAString& 
   }
   bool isFormatting;
   rv = vol->GetIsFormatting(&isFormatting);
   NS_ENSURE_SUCCESS_VOID(rv);
   if (isFormatting) {
     aStatus.AssignLiteral("unavailable");
     return;
   }
+  bool isUnmounting;
+  rv = vol->GetIsUnmounting(&isUnmounting);
+  NS_ENSURE_SUCCESS_VOID(rv);
+  if (isUnmounting) {
+    aStatus.AssignLiteral("unavailable");
+    return;
+  }
   int32_t volState;
   rv = vol->GetState(&volState);
   NS_ENSURE_SUCCESS_VOID(rv);
   if (volState == nsIVolume::STATE_MOUNTED) {
     aStatus.AssignLiteral("available");
   }
 #endif
 }
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1888,37 +1888,40 @@ ContentChild::RecvFilePathUpdate(const n
 bool
 ContentChild::RecvFileSystemUpdate(const nsString& aFsName,
                                    const nsString& aVolumeName,
                                    const int32_t& aState,
                                    const int32_t& aMountGeneration,
                                    const bool& aIsMediaPresent,
                                    const bool& aIsSharing,
                                    const bool& aIsFormatting,
-                                   const bool& aIsFake)
+                                   const bool& aIsFake,
+                                   const bool& aIsUnmounting)
 {
 #ifdef MOZ_WIDGET_GONK
     nsRefPtr<nsVolume> volume = new nsVolume(aFsName, aVolumeName, aState,
                                              aMountGeneration, aIsMediaPresent,
-                                             aIsSharing, aIsFormatting, aIsFake);
+                                             aIsSharing, aIsFormatting, aIsFake,
+                                             aIsUnmounting);
 
     nsRefPtr<nsVolumeService> vs = nsVolumeService::GetSingleton();
     if (vs) {
         vs->UpdateVolume(volume);
     }
 #else
     // Remove warnings about unused arguments
     unused << aFsName;
     unused << aVolumeName;
     unused << aState;
     unused << aMountGeneration;
     unused << aIsMediaPresent;
     unused << aIsSharing;
     unused << aIsFormatting;
     unused << aIsFake;
+    unused << aIsUnmounting;
 #endif
     return true;
 }
 
 bool
 ContentChild::RecvNotifyProcessPriorityChanged(
     const hal::ProcessPriority& aPriority)
 {
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -307,17 +307,18 @@ public:
                                     const nsCString& aReason) MOZ_OVERRIDE;
     virtual bool RecvFileSystemUpdate(const nsString& aFsName,
                                       const nsString& aVolumeName,
                                       const int32_t& aState,
                                       const int32_t& aMountGeneration,
                                       const bool& aIsMediaPresent,
                                       const bool& aIsSharing,
                                       const bool& aIsFormatting,
-                                      const bool& aIsFake) MOZ_OVERRIDE;
+                                      const bool& aIsFake,
+                                      const bool& aIsUnmounting) MOZ_OVERRIDE;
 
     virtual bool RecvNuwaFork() MOZ_OVERRIDE;
 
     virtual bool
     RecvNotifyProcessPriorityChanged(const hal::ProcessPriority& aPriority) MOZ_OVERRIDE;
     virtual bool RecvMinimizeMemoryUsage() MOZ_OVERRIDE;
 
     virtual bool RecvLoadAndRegisterSheet(const URIParams& aURI,
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -2645,33 +2645,36 @@ ContentParent::Observe(nsISupports* aSub
         nsString volName;
         nsString mountPoint;
         int32_t  state;
         int32_t  mountGeneration;
         bool     isMediaPresent;
         bool     isSharing;
         bool     isFormatting;
         bool     isFake;
+        bool     isUnmounting;
 
         vol->GetName(volName);
         vol->GetMountPoint(mountPoint);
         vol->GetState(&state);
         vol->GetMountGeneration(&mountGeneration);
         vol->GetIsMediaPresent(&isMediaPresent);
         vol->GetIsSharing(&isSharing);
         vol->GetIsFormatting(&isFormatting);
         vol->GetIsFake(&isFake);
+        vol->GetIsUnmounting(&isUnmounting);
 
 #ifdef MOZ_NUWA_PROCESS
         if (!(IsNuwaReady() && IsNuwaProcess()))
 #endif
         {
             unused << SendFileSystemUpdate(volName, mountPoint, state,
                                            mountGeneration, isMediaPresent,
-                                           isSharing, isFormatting, isFake);
+                                           isSharing, isFormatting, isFake,
+                                           isUnmounting);
         }
     } else if (!strcmp(aTopic, "phone-state-changed")) {
         nsString state(aData);
         unused << SendNotifyPhoneStateChange(state);
     }
 #endif
 #ifdef ACCESSIBILITY
     // Make sure accessibility is running in content process when accessibility
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -296,16 +296,17 @@ struct VolumeInfo {
   nsString name;
   nsString mountPoint;
   int32_t volState;
   int32_t mountGeneration;
   bool isMediaPresent;
   bool isSharing;
   bool isFormatting;
   bool isFake;
+  bool isUnmounting;
 };
 
 union MaybeFileDesc {
     FileDescriptor;
     void_t;
 };
 
 intr protocol PContent
@@ -444,17 +445,17 @@ child:
 
     FilePathUpdate(nsString storageType, nsString storageName, nsString filepath,
                    nsCString reasons);
 
     // Note: Any changes to this structure should also be changed in
     // VolumeInfo above.
     FileSystemUpdate(nsString fsName, nsString mountPoint, int32_t fsState,
                      int32_t mountGeneration, bool isMediaPresent,
-                     bool isSharing, bool isFormatting, bool isFake);
+                     bool isSharing, bool isFormatting, bool isFake, bool isUnmounting);
 
     // Ask the Nuwa process to create a new child process.
     NuwaFork();
 
     NotifyProcessPriorityChanged(ProcessPriority priority);
     MinimizeMemoryUsage();
 
     /**
--- a/dom/mobileconnection/gonk/MobileConnectionService.js
+++ b/dom/mobileconnection/gonk/MobileConnectionService.js
@@ -20,16 +20,18 @@ const GONK_MOBILECONNECTIONSERVICE_CONTR
 const GONK_MOBILECONNECTIONSERVICE_CID =
   Components.ID("{0c9c1a96-2c72-4c55-9e27-0ca73eb16f63}");
 const MOBILECONNECTIONINFO_CID =
   Components.ID("{8162b3c0-664b-45f6-96cd-f07b4e193b0e}");
 const MOBILENETWORKINFO_CID =
   Components.ID("{a6c8416c-09b4-46d1-bf29-6520d677d085}");
 const MOBILECELLINFO_CID =
   Components.ID("{0635d9ab-997e-4cdf-84e7-c1883752dff3}");
+const TELEPHONYCALLBACK_CID =
+  Components.ID("{6e1af17e-37f3-11e4-aed3-60a44c237d2b}");
 
 const NS_XPCOM_SHUTDOWN_OBSERVER_ID      = "xpcom-shutdown";
 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID  = "nsPref:changed";
 const NS_NETWORK_ACTIVE_CHANGED_TOPIC_ID = "network-active-changed";
 
 const kPrefRilDebuggingEnabled = "ril.debugging.enabled";
 
 XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger",
@@ -39,16 +41,20 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager",
                                    "@mozilla.org/network/manager;1",
                                    "nsINetworkManager");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gRadioInterfaceLayer",
                                    "@mozilla.org/ril;1",
                                    "nsIRadioInterfaceLayer");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gGonkTelephonyService",
+                                  "@mozilla.org/telephony/telephonyservice;1",
+                                  "nsIGonkTelephonyService");
+
 let DEBUG = RIL.DEBUG_RIL;
 function debug(s) {
   dump("MobileConnectionService: " + s + "\n");
 }
 
 function MobileNetworkInfo() {
   this.shortName = null;
   this.longName = null;
@@ -129,16 +135,51 @@ function MMIResult(aOptions) {
   this.additionalInformation = aOptions.additionalInformation;
 }
 MMIResult.prototype = {
   __exposedProps__ : {serviceCode: 'r',
                       statusMessage: 'r',
                       additionalInformation: 'r'},
 };
 
+/**
+ * Wrap a MobileConnectionCallback to a TelephonyCallback.
+ */
+function TelephonyCallback(aCallback) {
+  this.callback = aCallback;
+}
+TelephonyCallback.prototype = {
+  QueryInterface:   XPCOMUtils.generateQI([Ci.nsITelephonyCallback]),
+  classID:          TELEPHONYCALLBACK_CID,
+
+  notifyDialMMI: function(mmiServiceCode) {
+    this.serviceCode = mmiServiceCode;
+  },
+
+  notifyDialMMISuccess: function(result) {
+    this.callback.notifySendCancelMmiSuccess(result);
+  },
+
+  notifyDialMMIError: function(error) {
+    this.callback.notifyError(error, "", this.serviceCode);
+  },
+
+  notifyDialMMIErrorWithInfo: function(error, info) {
+    this.callback.notifyError(error, "", this.serviceCode, info);
+  },
+
+  notifyDialError: function() {
+    throw Cr.NS_ERROR_UNEXPECTED;
+  },
+
+  notifyDialSuccess: function() {
+    throw Cr.NS_ERROR_UNEXPECTED;
+  },
+};
+
 function MobileConnectionProvider(aClientId, aRadioInterface) {
   this._clientId = aClientId;
   this._radioInterface = aRadioInterface;
   this._operatorInfo = new MobileNetworkInfo();
   // An array of nsIMobileConnectionListener instances.
   this._listeners = [];
 
   this.supportedNetworkTypes = this._getSupportedNetworkTypes();
@@ -183,17 +224,17 @@ MobileConnectionProvider.prototype = {
     let supportedNetworkTypes = libcutils.property_get(key, "").split(",");
 
     // If mozRIL system property is not available, fallback to AOSP system
     // property for support network types.
     if (supportedNetworkTypes.length === 1 && supportedNetworkTypes[0] === "") {
       key = "ro.telephony.default_network";
       let indexString = libcutils.property_get(key, "");
       let index = parseInt(indexString, 10);
-      if (DEBUG) this._debug("Fallback to " + key + ": " + index)
+      if (DEBUG) this._debug("Fallback to " + key + ": " + index);
 
       let networkTypes = RIL.RIL_PREFERRED_NETWORK_TYPE_TO_GECKO[index];
       supportedNetworkTypes = networkTypes ?
         networkTypes.replace("-auto", "", "g").split("/") :
         RIL.GECKO_SUPPORTED_NETWORK_TYPES_DEFAULT.split(",");
     }
 
     for (let type of supportedNetworkTypes) {
@@ -377,20 +418,17 @@ MobileConnectionProvider.prototype = {
         isUpdated = true;
         aDestInfo[key] = aSrcInfo[key];
       }
     }
     return isUpdated;
   },
 
   _rulesToCallForwardingOptions: function(aRules) {
-    for (let i = 0; i < aRules.length; i++) {
-      let info = new CallForwardingOptions(aRules[i]);
-      aRules[i] = info;
-    }
+    return aRules.map(rule => new CallForwardingOptions(rule));
   },
 
   _dispatchNotifyError: function(aCallback, aErrorMsg) {
     Services.tm.currentThread.dispatch(() => aCallback.notifyError(aErrorMsg),
                                        Ci.nsIThread.DISPATCH_NORMAL);
   },
 
   registerListener: function(aListener) {
@@ -510,16 +548,23 @@ MobileConnectionProvider.prototype = {
     if (this.radioState === aRadioState) {
       return;
     }
 
     this.radioState = aRadioState;
     this.deliverListenerEvent("notifyRadioStateChanged");
   },
 
+  notifyCFStateChanged: function(aAction, aReason, aNumber, aTimeSeconds,
+                                 aServiceClass) {
+    this.deliverListenerEvent("notifyCFStateChanged",
+                              [true, aAction, aReason, aNumber, aTimeSeconds,
+                                aServiceClass]);
+  },
+
   getSupportedNetworkTypes: function(aTypes) {
     aTypes.value = this.supportedNetworkTypes.slice();
     return aTypes.value.length;
   },
 
   getNetworks: function(aCallback) {
     this._radioInterface.sendWorkerMessage("getAvailableNetworks", null,
                                            (function(aResponse) {
@@ -675,59 +720,18 @@ MobileConnectionProvider.prototype = {
       }
 
       aCallback.notifySuccessWithBoolean(aResponse.enabled);
       return false;
     }).bind(this));
   },
 
   sendMMI: function(aMmi, aCallback) {
-    this._radioInterface.sendWorkerMessage("sendMMI", {mmi: aMmi},
-                                           (function(aResponse) {
-      aResponse.serviceCode = aResponse.mmiServiceCode || "";
-      // We expect to have an IMEI at this point if the request was supposed
-      // to query for the IMEI, so getting a successful reply from the RIL
-      // without containing an actual IMEI number is considered an error.
-      if (aResponse.serviceCode === RIL.MMI_KS_SC_IMEI &&
-          !aResponse.statusMessage) {
-        aResponse.errorMsg = aResponse.errorMsg ||
-                             RIL.GECKO_ERROR_GENERIC_FAILURE;
-      }
-
-      if (aResponse.errorMsg) {
-        if (aResponse.additionalInformation) {
-          aCallback.notifyError(aResponse.errorMsg, "",
-                                aResponse.serviceCode,
-                                aResponse.additionalInformation);
-        } else {
-          aCallback.notifyError(aResponse.errorMsg, "",
-                                aResponse.serviceCode);
-        }
-        return false;
-      }
-
-      if (aResponse.isSetCallForward) {
-        this.deliverListenerEvent("notifyCFStateChanged",
-                                  [!aResponse.errorMsg, aResponse.action,
-                                   aResponse.reason, aResponse.number,
-                                   aResponse.timeSeconds, aResponse.serviceClass]);
-      }
-
-      // MMI query call forwarding options request returns a set of rules that
-      // will be exposed in the form of an array of MozCallForwardingOptions
-      // instances.
-      if (aResponse.serviceCode === RIL.MMI_KS_SC_CALL_FORWARDING &&
-          aResponse.additionalInformation) {
-        this._rulesToCallForwardingOptions(aResponse.additionalInformation);
-      }
-
-      let mmiResult = new MMIResult(aResponse);
-      aCallback.notifySendCancelMmiSuccess(mmiResult);
-      return false;
-    }).bind(this));
+    let telephonyCallback = new TelephonyCallback(aCallback);
+    gGonkTelephonyService.dialMMI(this._clientId, aMmi, telephonyCallback);
   },
 
   cancelMMI: function(aCallback) {
     this._radioInterface.sendWorkerMessage("cancelUSSD", null,
                                            (function(aResponse) {
       if (aResponse.errorMsg) {
         aCallback.notifyError(aResponse.errorMsg);
         return false;
@@ -757,21 +761,19 @@ MobileConnectionProvider.prototype = {
 
     this._radioInterface.sendWorkerMessage("setCallForward", options,
                                            (function(aResponse) {
       if (aResponse.errorMsg) {
         aCallback.notifyError(aResponse.errorMsg);
         return false;
       }
 
-      this.deliverListenerEvent("notifyCFStateChanged",
-                                [!aResponse.errorMsg, aResponse.action,
-                                 aResponse.reason, aResponse.number,
-                                 aResponse.timeSeconds, aResponse.serviceClass]);
-
+      this.notifyCFStateChanged(aResponse.action, aResponse.reason,
+                                aResponse.number, aResponse.timeSeconds,
+                                aResponse.serviceClass);
       aCallback.notifySuccess();
       return false;
     }).bind(this));
   },
 
   getCallForwarding: function(aReason, aCallback) {
     if (!this._isValidCallForwardingReason(aReason)){
       this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_INVALID_PARAMETER);
@@ -781,19 +783,18 @@ MobileConnectionProvider.prototype = {
     this._radioInterface.sendWorkerMessage("queryCallForwardStatus",
                                            {reason: aReason},
                                            (function(aResponse) {
       if (aResponse.errorMsg) {
         aCallback.notifyError(aResponse.errorMsg);
         return false;
       }
 
-      let infos = aResponse.rules;
-      this._rulesToCallForwardingOptions(infos);
-      aCallback.notifyGetCallForwardingSuccess(infos);
+      aCallback.notifyGetCallForwardingSuccess(
+        this._rulesToCallForwardingOptions(aResponse.rules));
       return false;
     }).bind(this));
   },
 
   setCallBarring: function(aOptions, aCallback) {
     if (!this._isValidCallBarringOptions(aOptions, true)) {
       this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_INVALID_PARAMETER);
       return;
@@ -1211,16 +1212,27 @@ MobileConnectionService.prototype = {
     if (provider.lastKnownHomeNetwork === aNetwork) {
       return;
     }
 
     provider.lastKnownHomeNetwork = aNetwork;
     provider.deliverListenerEvent("notifyLastKnownHomeNetworkChanged");
   },
 
+  notifyCFStateChanged: function(aClientId, aAction, aReason, aNumber,
+                                 aTimeSeconds, aServiceClass) {
+    if (DEBUG) {
+      debug("notifyCFStateChanged for " + aClientId);
+    }
+
+    let provider = this.getItemByServiceId(aClientId);
+    provider.notifyCFStateChanged(aAction, aReason, aNumber, aTimeSeconds,
+                                  aServiceClass);
+  },
+
   /**
    * nsIObserver interface.
    */
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
       case NS_NETWORK_ACTIVE_CHANGED_TOPIC_ID:
         for (let i = 0; i < this.numItems; i++) {
           let provider = this._providers[i];
--- a/dom/mobileconnection/gonk/nsIGonkMobileConnectionService.idl
+++ b/dom/mobileconnection/gonk/nsIGonkMobileConnectionService.idl
@@ -4,17 +4,17 @@
 
 #include "nsIMobileConnectionService.idl"
 
 %{C++
 #define GONK_MOBILECONNECTION_SERVICE_CONTRACTID \
         "@mozilla.org/mobileconnection/gonkmobileconnectionservice;1"
 %}
 
-[scriptable, uuid(e54fa0a4-d357-48ef-9a1e-ffc9705b44b1)]
+[scriptable, uuid(b0310517-e7f6-4fa5-a52e-fa6ff35c8fc1)]
 interface nsIGonkMobileConnectionService : nsIMobileConnectionService
 {
   void notifyNetworkInfoChanged(in unsigned long clientId, in jsval networkInfo);
 
   void notifyVoiceInfoChanged(in unsigned long clientId, in jsval voiceInfo);
 
   void notifyDataInfoChanged(in unsigned long clientId, in jsval dataInfo);
 
@@ -41,9 +41,16 @@ interface nsIGonkMobileConnectionService
 
   void notifyNetworkSelectModeChanged(in unsigned long clientId,
                                       in DOMString mode);
 
   void notifySpnAvailable(in unsigned long clientId);
 
   void notifyLastHomeNetworkChanged(in unsigned long clientId,
                                     in DOMString network);
+
+  void notifyCFStateChanged(in unsigned long clientId,
+                            in unsigned short action,
+                            in unsigned short reason,
+                            in DOMString number,
+                            in unsigned short timeSeconds,
+                            in unsigned short serviceClass);
 };
--- a/dom/mobileconnection/tests/marionette/test_mobile_mmi.js
+++ b/dom/mobileconnection/tests/marionette/test_mobile_mmi.js
@@ -28,17 +28,17 @@ function testInvalidMMICode() {
   let MMI_CODE = "InvalidMMICode";
   return sendMMI(MMI_CODE)
     .then(function resolve() {
       ok(false, MMI_CODE + " should not success");
     }, function reject(aError) {
       ok(true, MMI_CODE + " fail");
       is(aError.name, "emMmiError", "MMI error name");
       is(aError.message, "", "No message");
-      is(aError.serviceCode, "", "No serviceCode");
+      is(aError.serviceCode, "scUssd", "Service code USSD");
       is(aError.additionalInformation, null, "No additional information");
     });
 }
 
 // Start test
 startTestCommon(function() {
    return Promise.resolve()
     .then(() => testGettingIMEI())
--- a/dom/notification/NotificationStorage.js
+++ b/dom/notification/NotificationStorage.js
@@ -81,17 +81,17 @@ NotificationStorage.prototype = {
       lang: lang,
       body: body,
       tag: tag,
       icon: icon,
       alertName: alertName,
       timestamp: new Date().getTime(),
       origin: origin,
       data: data,
-      behavior: behavior
+      mozbehavior: behavior
     };
 
     this._notifications[id] = notification;
     if (tag) {
       if (!this._byTag[origin]) {
         this._byTag[origin] = {};
       }
 
@@ -202,17 +202,17 @@ NotificationStorage.prototype = {
         callback.handle(notification.id,
                         notification.title,
                         notification.dir,
                         notification.lang,
                         notification.body,
                         notification.tag,
                         notification.icon,
                         notification.data,
-                        notification.behavior);
+                        notification.mozbehavior);
       } catch (e) {
         if (DEBUG) { debug("Error calling callback handle: " + e); }
       }
     });
     try {
       callback.done();
     } catch (e) {
       if (DEBUG) { debug("Error calling callback done: " + e); }
--- a/dom/system/gonk/AutoMounter.cpp
+++ b/dom/system/gonk/AutoMounter.cpp
@@ -904,24 +904,26 @@ AutoMounter::UpdateState()
           if (vol->IsMountLocked()) {
             // The volume is currently locked, so leave it in the mounted
             // state.
             LOGW("UpdateState: Mounted volume %s is locked, not sharing or formatting",
                  vol->NameStr());
             break;
           }
 
-          // Mark the volume as if we've started sharing. This will cause
-          // apps which watch device storage notifications to see the volume
-          // go into the shared state, and prompt them to close any open files
-          // that they might have.
+          // Mark the volume as if we've started sharing/formatting/unmmounting.
+          // This will cause apps which watch device storage notifications to see
+          // the volume go into the different state, and prompt them to close any
+          // open files that they might have.
           if (tryToShare && vol->IsSharingEnabled()) {
             vol->SetIsSharing(true);
           } else if (vol->IsFormatRequested()){
             vol->SetIsFormatting(true);
+          } else if (vol->IsUnmountRequested()){
+            vol->SetIsUnmounting(true);
           }
 
           // Check to see if there are any open files on the volume and
           // don't initiate the unmount while there are open files.
           OpenFileFinder::Info fileInfo;
           OpenFileFinder fileFinder(vol->MountPoint());
           if (fileFinder.First(&fileInfo)) {
             LOGW("The following files are open under '%s'",
--- a/dom/system/gonk/Volume.cpp
+++ b/dom/system/gonk/Volume.cpp
@@ -62,16 +62,17 @@ Volume::Volume(const nsCSubstring& aName
     mMountLocked(true),  // Needs to agree with nsVolume::nsVolume
     mSharingEnabled(false),
     mFormatRequested(false),
     mMountRequested(false),
     mUnmountRequested(false),
     mCanBeShared(true),
     mIsSharing(false),
     mIsFormatting(false),
+    mIsUnmounting(false),
     mId(sNextId++)
 {
   DBG("Volume %s: created", NameStr());
 }
 
 void
 Volume::SetIsSharing(bool aIsSharing)
 {
@@ -94,16 +95,28 @@ Volume::SetIsFormatting(bool aIsFormatti
   LOG("Volume %s: IsFormatting set to %d state %s",
       NameStr(), (int)mIsFormatting, StateStr(mState));
   if (mIsFormatting) {
     mEventObserverList.Broadcast(this);
   }
 }
 
 void
+Volume::SetIsUnmounting(bool aIsUnmounting)
+{
+  if (aIsUnmounting == mIsUnmounting) {
+    return;
+  }
+  mIsUnmounting = aIsUnmounting;
+  LOG("Volume %s: IsUnmounting set to %d state %s",
+      NameStr(), (int)mIsUnmounting, StateStr(mState));
+  mEventObserverList.Broadcast(this);
+}
+
+void
 Volume::SetMediaPresent(bool aMediaPresent)
 {
   MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
   MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
 
   // mMediaPresent is slightly redunant to the state, however
   // when media is removed (while Idle), we get the following:
   //    631 Volume sdcard /mnt/sdcard disk removed (179:0)
@@ -196,36 +209,47 @@ Volume::SetState(Volume::STATE aNewState
 
   switch (aNewState) {
      case nsIVolume::STATE_NOMEDIA:
        // Cover the startup case where we don't get insertion/removal events
        mMediaPresent = false;
        mIsSharing = false;
        mUnmountRequested = false;
        mMountRequested = false;
+       mIsUnmounting = false;
        break;
 
      case nsIVolume::STATE_MOUNTED:
        mMountRequested = false;
        mIsFormatting = false;
        mIsSharing = false;
+       mIsUnmounting = false;
        break;
      case nsIVolume::STATE_FORMATTING:
        mFormatRequested = false;
        mIsFormatting = true;
        mIsSharing = false;
+       mIsUnmounting = false;
        break;
 
      case nsIVolume::STATE_SHARED:
      case nsIVolume::STATE_SHAREDMNT:
        // Covers startup cases. Normally, mIsSharing would be set to true
        // when we issue the command to initiate the sharing process, but
        // it's conceivable that a volume could already be in a shared state
        // when b2g starts.
        mIsSharing = true;
+       mIsUnmounting = false;
+       mIsFormatting = false;
+       break;
+
+     case nsIVolume::STATE_UNMOUNTING:
+       mIsUnmounting = true;
+       mIsFormatting = false;
+       mIsSharing = false;
        break;
 
      case nsIVolume::STATE_IDLE:
        break;
      default:
        break;
   }
   mState = aNewState;
--- a/dom/system/gonk/Volume.h
+++ b/dom/system/gonk/Volume.h
@@ -53,16 +53,17 @@ public:
   bool CanBeFormatted() const         { return CanBeShared(); }
   bool CanBeMounted() const           { return CanBeShared(); }
   bool IsSharingEnabled() const       { return mCanBeShared && mSharingEnabled; }
   bool IsFormatRequested() const      { return CanBeFormatted() && mFormatRequested; }
   bool IsMountRequested() const       { return CanBeMounted() && mMountRequested; }
   bool IsUnmountRequested() const     { return CanBeMounted() && mUnmountRequested; }
   bool IsSharing() const              { return mIsSharing; }
   bool IsFormatting() const           { return mIsFormatting; }
+  bool IsUnmounting() const           { return mIsUnmounting; }
 
   void SetSharingEnabled(bool aSharingEnabled);
   void SetFormatRequested(bool aFormatRequested);
   void SetMountRequested(bool aMountRequested);
   void SetUnmountRequested(bool aUnmountRequested);
 
   typedef mozilla::Observer<Volume *>     EventObserver;
   typedef mozilla::ObserverList<Volume *> EventObserverList;
@@ -83,16 +84,17 @@ private:
   void StartMount(VolumeResponseCallback* aCallback);
   void StartUnmount(VolumeResponseCallback* aCallback);
   void StartFormat(VolumeResponseCallback* aCallback);
   void StartShare(VolumeResponseCallback* aCallback);
   void StartUnshare(VolumeResponseCallback* aCallback);
 
   void SetIsSharing(bool aIsSharing);
   void SetIsFormatting(bool aIsFormatting);
+  void SetIsUnmounting(bool aIsUnmounting);
   void SetState(STATE aNewState);
   void SetMediaPresent(bool aMediaPresent);
   void SetMountPoint(const nsCSubstring& aMountPoint);
   void StartCommand(VolumeCommand* aCommand);
 
   void HandleVoldResponse(int aResponseCode, nsCWhitespaceTokenizer& aTokenizer);
 
   static void UpdateMountLock(const nsACString& aVolumeName,
@@ -107,16 +109,17 @@ private:
   bool              mMountLocked;
   bool              mSharingEnabled;
   bool              mFormatRequested;
   bool              mMountRequested;
   bool              mUnmountRequested;
   bool              mCanBeShared;
   bool              mIsSharing;
   bool              mIsFormatting;
+  bool              mIsUnmounting;
   uint32_t          mId;                // Unique ID (used by MTP)
 
   static EventObserverList mEventObserverList;
 };
 
 } // system
 } // mozilla
 
--- a/dom/system/gonk/nsIVolume.idl
+++ b/dom/system/gonk/nsIVolume.idl
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 #include "nsIVolumeStat.idl"
 
-[scriptable, uuid(13caa69c-8f1f-11e3-8e36-10bf48d707fb)]
+[scriptable, uuid(9D0DC356-395D-11E4-9306-A97C1D5D46B0)]
 interface nsIVolume : nsISupports
 {
   // These MUST match the states from android's system/vold/Volume.h header
   const long STATE_INIT        = -1;
   const long STATE_NOMEDIA     = 0;
   const long STATE_IDLE        = 1;
   const long STATE_PENDING     = 2;
   const long STATE_CHECKING    = 3;
@@ -62,16 +62,18 @@ interface nsIVolume : nsISupports
   // transitioning from mounted to sharing and back again.
   readonly attribute boolean isSharing;
 
   // Determines if the volume is currently formatting. This sets true once 
   // mFormatRequest == true and mState == STATE_MOUNTED, and sets false
   // once the volume has been formatted and mounted again.
   readonly attribute boolean isFormatting;
 
+  readonly attribute boolean isUnmounting;
+
   nsIVolumeStat getStats();
 
   // Formats the volume in IO thread, if the volume is ready to be formatted.
   // Automounter will unmount it, format it and then mount it again.
   void format();
 
   // Mounts the volume in IO thread, if the volume is already unmounted.
   // Automounter will mount it. Otherwise Automounter will skip this.
--- a/dom/system/gonk/nsVolume.cpp
+++ b/dom/system/gonk/nsVolume.cpp
@@ -51,17 +51,18 @@ nsVolume::nsVolume(const Volume* aVolume
   : mName(NS_ConvertUTF8toUTF16(aVolume->Name())),
     mMountPoint(NS_ConvertUTF8toUTF16(aVolume->MountPoint())),
     mState(aVolume->State()),
     mMountGeneration(aVolume->MountGeneration()),
     mMountLocked(aVolume->IsMountLocked()),
     mIsFake(false),
     mIsMediaPresent(aVolume->MediaPresent()),
     mIsSharing(aVolume->IsSharing()),
-    mIsFormatting(aVolume->IsFormatting())
+    mIsFormatting(aVolume->IsFormatting()),
+    mIsUnmounting(aVolume->IsUnmounting())
 {
 }
 
 bool nsVolume::Equals(nsIVolume* aVolume)
 {
   nsString volName;
   aVolume->GetName(volName);
   if (!mName.Equals(volName)) {
@@ -105,43 +106,55 @@ bool nsVolume::Equals(nsIVolume* aVolume
   }
 
   bool isFormatting;
   aVolume->GetIsFormatting(&isFormatting);
   if (mIsFormatting != isFormatting) {
     return false;
   }
 
+  bool isUnmounting;
+  aVolume->GetIsUnmounting(&isUnmounting);
+  if (mIsUnmounting != isUnmounting) {
+    return false;
+  }
+
   return true;
 }
 
-NS_IMETHODIMP nsVolume::GetIsMediaPresent(bool *aIsMediaPresent)
+NS_IMETHODIMP nsVolume::GetIsMediaPresent(bool* aIsMediaPresent)
 {
   *aIsMediaPresent = mIsMediaPresent;
   return NS_OK;
 }
 
-NS_IMETHODIMP nsVolume::GetIsMountLocked(bool *aIsMountLocked)
+NS_IMETHODIMP nsVolume::GetIsMountLocked(bool* aIsMountLocked)
 {
   *aIsMountLocked = mMountLocked;
   return NS_OK;
 }
 
-NS_IMETHODIMP nsVolume::GetIsSharing(bool *aIsSharing)
+NS_IMETHODIMP nsVolume::GetIsSharing(bool* aIsSharing)
 {
   *aIsSharing = mIsSharing;
   return NS_OK;
 }
 
-NS_IMETHODIMP nsVolume::GetIsFormatting(bool *aIsFormatting)
+NS_IMETHODIMP nsVolume::GetIsFormatting(bool* aIsFormatting)
 {
   *aIsFormatting = mIsFormatting;
   return NS_OK;
 }
 
+NS_IMETHODIMP nsVolume::GetIsUnmounting(bool* aIsUnmounting)
+{
+  *aIsUnmounting = mIsUnmounting;
+  return NS_OK;
+}
+
 NS_IMETHODIMP nsVolume::GetName(nsAString& aName)
 {
   aName = mName;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsVolume::GetMountGeneration(int32_t* aMountGeneration)
 {
@@ -257,21 +270,21 @@ void nsVolume::UnmountVolumeIOThread(con
   AutoMounterUnmountVolume(aVolume);
 }
 
 void
 nsVolume::LogState() const
 {
   if (mState == nsIVolume::STATE_MOUNTED) {
     LOG("nsVolume: %s state %s @ '%s' gen %d locked %d fake %d "
-        "media %d sharing %d formatting %d",
+        "media %d sharing %d formatting %d unmounting %d",
         NameStr().get(), StateStr(), MountPointStr().get(),
         MountGeneration(), (int)IsMountLocked(), (int)IsFake(),
         (int)IsMediaPresent(), (int)IsSharing(),
-        (int)IsFormatting());
+        (int)IsFormatting(), (int)IsUnmounting());
     return;
   }
 
   LOG("nsVolume: %s state %s", NameStr().get(), StateStr());
 }
 
 void nsVolume::Set(nsIVolume* aVolume)
 {
@@ -279,16 +292,17 @@ void nsVolume::Set(nsIVolume* aVolume)
 
   aVolume->GetName(mName);
   aVolume->GetMountPoint(mMountPoint);
   aVolume->GetState(&mState);
   aVolume->GetIsFake(&mIsFake);
   aVolume->GetIsMediaPresent(&mIsMediaPresent);
   aVolume->GetIsSharing(&mIsSharing);
   aVolume->GetIsFormatting(&mIsFormatting);
+  aVolume->GetIsUnmounting(&mIsUnmounting);
 
   int32_t volMountGeneration;
   aVolume->GetMountGeneration(&volMountGeneration);
 
   if (mState != nsIVolume::STATE_MOUNTED) {
     // Since we're not in the mounted state, we need to
     // forgot whatever mount generation we may have had.
     mMountGeneration = -1;
--- a/dom/system/gonk/nsVolume.h
+++ b/dom/system/gonk/nsVolume.h
@@ -25,40 +25,43 @@ public:
   // This constructor is used by the UpdateVolumeRunnable constructor
   nsVolume(const Volume* aVolume);
 
   // This constructor is used by ContentChild::RecvFileSystemUpdate which is
   // used to update the volume cache maintained in the child process.
   nsVolume(const nsAString& aName, const nsAString& aMountPoint,
            const int32_t& aState, const int32_t& aMountGeneration,
            const bool& aIsMediaPresent, const bool& aIsSharing,
-           const bool& aIsFormatting, const bool& aIsFake)
+           const bool& aIsFormatting, const bool& aIsFake,
+           const bool& aIsUnmounting)
     : mName(aName),
       mMountPoint(aMountPoint),
       mState(aState),
       mMountGeneration(aMountGeneration),
       mMountLocked(false),
       mIsFake(aIsFake),
       mIsMediaPresent(aIsMediaPresent),
       mIsSharing(aIsSharing),
-      mIsFormatting(aIsFormatting)
+      mIsFormatting(aIsFormatting),
+      mIsUnmounting(aIsUnmounting)
   {
   }
 
   // This constructor is used by nsVolumeService::FindAddVolumeByName, and
   // will be followed shortly by a Set call.
   nsVolume(const nsAString& aName)
     : mName(aName),
       mState(STATE_INIT),
       mMountGeneration(-1),
       mMountLocked(true),  // Needs to agree with Volume::Volume
       mIsFake(false),
       mIsMediaPresent(false),
       mIsSharing(false),
-      mIsFormatting(false)
+      mIsFormatting(false),
+      mIsUnmounting(false)
   {
   }
 
   bool Equals(nsIVolume* aVolume);
   void Set(nsIVolume* aVolume);
 
   void LogState() const;
 
@@ -73,16 +76,17 @@ public:
 
   int32_t State() const               { return mState; }
   const char* StateStr() const        { return NS_VolumeStateStr(mState); }
 
   bool IsFake() const                 { return mIsFake; }
   bool IsMediaPresent() const         { return mIsMediaPresent; }
   bool IsSharing() const              { return mIsSharing; }
   bool IsFormatting() const           { return mIsFormatting; }
+  bool IsUnmounting() const           { return mIsUnmounting; }
 
   typedef nsTArray<nsRefPtr<nsVolume> > Array;
 
 private:
   virtual ~nsVolume() {}  // MozExternalRefCountType complains if this is non-virtual
 
   friend class nsVolumeService; // Calls the following XxxMountLock functions
   void UpdateMountLock(const nsAString& aMountLockState);
@@ -98,14 +102,15 @@ private:
   nsString mMountPoint;
   int32_t  mState;
   int32_t  mMountGeneration;
   bool     mMountLocked;
   bool     mIsFake;
   bool     mIsMediaPresent;
   bool     mIsSharing;
   bool     mIsFormatting;
+  bool     mIsUnmounting;
 };
 
 } // system
 } // mozilla
 
 #endif  // mozilla_system_nsvolume_h__
--- a/dom/system/gonk/nsVolumeService.cpp
+++ b/dom/system/gonk/nsVolumeService.cpp
@@ -203,17 +203,18 @@ nsVolumeService::CreateOrGetVolumeByPath
   // In order to support queries by the updater, we will fabricate a volume
   // from the pathname, so that the caller can determine the volume size.
   nsCOMPtr<nsIVolume> vol = new nsVolume(NS_LITERAL_STRING("fake"),
                                          aPath, nsIVolume::STATE_MOUNTED,
                                          -1    /* generation */,
                                          true  /* isMediaPresent*/,
                                          false /* isSharing */,
                                          false /* isFormatting */,
-                                         true  /* isFake */);
+                                         true  /* isFake */,
+                                         false /* isUnmounting*/);
   vol.forget(aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsVolumeService::GetVolumeNames(nsIArray** aVolNames)
 {
   GetVolumesFromParent();
@@ -264,16 +265,17 @@ nsVolumeService::GetVolumesForIPC(nsTArr
     volInfo->name()             = vol->mName;
     volInfo->mountPoint()       = vol->mMountPoint;
     volInfo->volState()         = vol->mState;
     volInfo->mountGeneration()  = vol->mMountGeneration;
     volInfo->isMediaPresent()   = vol->mIsMediaPresent;
     volInfo->isSharing()        = vol->mIsSharing;
     volInfo->isFormatting()     = vol->mIsFormatting;
     volInfo->isFake()           = vol->mIsFake;
+    volInfo->isUnmounting()     = vol->mIsUnmounting;
   }
 }
 
 void
 nsVolumeService::GetVolumesFromParent()
 {
   if (XRE_GetProcessType() == GeckoProcessType_Default) {
     // We are the parent. Therefore our volumes are already correct.
@@ -291,17 +293,18 @@ nsVolumeService::GetVolumesFromParent()
     const VolumeInfo& volInfo(result[i]);
     nsRefPtr<nsVolume> vol = new nsVolume(volInfo.name(),
                                           volInfo.mountPoint(),
                                           volInfo.volState(),
                                           volInfo.mountGeneration(),
                                           volInfo.isMediaPresent(),
                                           volInfo.isSharing(),
                                           volInfo.isFormatting(),
-                                          volInfo.isFake());
+                                          volInfo.isFake(),
+                                          volInfo.isUnmounting());
     UpdateVolume(vol, false);
   }
 }
 
 NS_IMETHODIMP
 nsVolumeService::CreateMountLock(const nsAString& aVolumeName, nsIVolumeMountLock **aResult)
 {
   nsCOMPtr<nsIVolumeMountLock> mountLock = nsVolumeMountLock::Create(aVolumeName);
@@ -415,17 +418,18 @@ NS_IMETHODIMP
 nsVolumeService::CreateFakeVolume(const nsAString& name, const nsAString& path)
 {
   if (XRE_GetProcessType() == GeckoProcessType_Default) {
     nsRefPtr<nsVolume> vol = new nsVolume(name, path, nsIVolume::STATE_INIT,
                                           -1    /* mountGeneration */,
                                           true  /* isMediaPresent */,
                                           false /* isSharing */,
                                           false /* isFormatting */,
-                                          true  /* isFake */);
+                                          true  /* isFake */,
+                                          false /* isUnmounting */);
     vol->LogState();
     UpdateVolume(vol.get());
     return NS_OK;
   }
 
   ContentChild::GetSingleton()->SendCreateFakeVolume(nsString(name), nsString(path));
   return NS_OK;
 }
@@ -465,40 +469,40 @@ public:
   {
     MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
   }
 
   NS_IMETHOD Run()
   {
     MOZ_ASSERT(NS_IsMainThread());
     DBG("UpdateVolumeRunnable::Run '%s' state %s gen %d locked %d "
-        "media %d sharing %d formatting %d",
+        "media %d sharing %d formatting %d unmounting %d",
         mVolume->NameStr().get(), mVolume->StateStr(),
         mVolume->MountGeneration(), (int)mVolume->IsMountLocked(),
         (int)mVolume->IsMediaPresent(), mVolume->IsSharing(),
-        mVolume->IsFormatting());
+        mVolume->IsFormatting(), mVolume->IsUnmounting());
 
     mVolumeService->UpdateVolume(mVolume);
     mVolumeService = nullptr;
     mVolume = nullptr;
     return NS_OK;
   }
 
 private:
   nsRefPtr<nsVolumeService> mVolumeService;
   nsRefPtr<nsVolume>        mVolume;
 };
 
 void
 nsVolumeService::UpdateVolumeIOThread(const Volume* aVolume)
 {
   DBG("UpdateVolumeIOThread: Volume '%s' state %s mount '%s' gen %d locked %d "
-      "media %d sharing %d formatting %d",
+      "media %d sharing %d formatting %d unmounting %d",
       aVolume->NameStr(), aVolume->StateStr(), aVolume->MountPoint().get(),
       aVolume->MountGeneration(), (int)aVolume->IsMountLocked(),
       (int)aVolume->MediaPresent(), (int)aVolume->IsSharing(),
-      (int)aVolume->IsFormatting());
+      (int)aVolume->IsFormatting(), (int)mVolume->IsUnmounting());
   MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
   NS_DispatchToMainThread(new UpdateVolumeRunnable(this, aVolume));
 }
 
 } // namespace system
 } // namespace mozilla
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -52,30 +52,16 @@ if (!this.debug) {
   };
 }
 
 // Timeout value for emergency callback mode.
 const EMERGENCY_CB_MODE_TIMEOUT_MS = 300000;  // 5 mins = 300000 ms.
 
 const ICC_MAX_LINEAR_FIXED_RECORDS = 0xfe;
 
-// MMI match groups
-const MMI_MATCH_GROUP_FULL_MMI = 1;
-const MMI_MATCH_GROUP_PROCEDURE = 2;
-const MMI_MATCH_GROUP_SERVICE_CODE = 3;
-const MMI_MATCH_GROUP_SIA = 4;
-const MMI_MATCH_GROUP_SIB = 5;
-const MMI_MATCH_GROUP_SIC = 6;
-const MMI_MATCH_GROUP_PWD_CONFIRM = 7;
-const MMI_MATCH_GROUP_DIALING_NUMBER = 8;
-
-const MMI_MAX_LENGTH_SHORT_CODE = 2;
-
-const MMI_END_OF_USSD = "#";
-
 const GET_CURRENT_CALLS_RETRY_MAX = 3;
 
 let RILQUIRKS_CALLSTATE_EXTRA_UINT32;
 // This may change at runtime since in RIL v6 and later, we get the version
 // number via the UNSOLICITED_RIL_CONNECTED parcel.
 let RILQUIRKS_V5_LEGACY;
 let RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL;
 let RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS;
@@ -2397,233 +2383,34 @@ RilObject.prototype = {
   /**
    * Get failure casue code for the most recently failed PDP context.
    */
   getFailCauseCode: function(callback) {
     this.context.Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE,
                                    {callback: callback});
   },
 
-  /**
-   * Parse the dial number to extract its mmi code part.
-   *
-   * @param number
-   *        Phone number to be parsed
-   */
-  parseMMIFromDialNumber: function(options) {
-    // We don't have to parse mmi in cdma.
-    if (!this._isCdma) {
-      options.mmi = this._parseMMI(options.number);
-    }
-    this.sendChromeMessage(options);
-  },
-
-  /**
-   * Helper to parse MMI/USSD string. TS.22.030 Figure 3.5.3.2.
-   */
-  _parseMMI: function(mmiString) {
-    if (!mmiString || !mmiString.length) {
-      return null;
-    }
-
-    let matches = this._getMMIRegExp().exec(mmiString);
-    if (matches) {
-      return {
-        fullMMI: matches[MMI_MATCH_GROUP_FULL_MMI],
-        procedure: matches[MMI_MATCH_GROUP_PROCEDURE],
-        serviceCode: matches[MMI_MATCH_GROUP_SERVICE_CODE],
-        sia: matches[MMI_MATCH_GROUP_SIA],
-        sib: matches[MMI_MATCH_GROUP_SIB],
-        sic: matches[MMI_MATCH_GROUP_SIC],
-        pwd: matches[MMI_MATCH_GROUP_PWD_CONFIRM],
-        dialNumber: matches[MMI_MATCH_GROUP_DIALING_NUMBER]
-      };
-    }
-
-    if (this._isPoundString(mmiString) || this._isMMIShortString(mmiString)) {
-      return {
-        fullMMI: mmiString
-      };
-    }
-
-    return null;
-  },
-
-  /**
-   * Build the regex to parse MMI string.
-   *
-   * The resulting groups after matching will be:
-   *    1 = full MMI string that might be used as a USSD request.
-   *    2 = MMI procedure.
-   *    3 = Service code.
-   *    4 = SIA.
-   *    5 = SIB.
-   *    6 = SIC.
-   *    7 = Password registration.
-   *    8 = Dialing number.
-   *
-   * @see TS.22.030 Figure 3.5.3.2.
-   */
-  _buildMMIRegExp: function() {
-    // The general structure of the codes is as follows:
-    //    - Activation (*SC*SI#).
-    //    - Deactivation (#SC*SI#).
-    //    - Interrogation (*#SC*SI#).
-    //    - Registration (**SC*SI#).
-    //    - Erasure (##SC*SI#).
-    //
-    // where SC = Service Code (2 or 3 digits) and SI = Supplementary Info
-    // (variable length).
-
-    // MMI procedure, which could be *, #, *#, **, ##
-    let procedure = "(\\*[*#]?|##?)";
-
-    // MMI Service code, which is a 2 or 3 digits that uniquely specifies the
-    // Supplementary Service associated with the MMI code.
-    let serviceCode = "(\\d{2,3})";
-
-    // MMI Supplementary Information SIA, SIB and SIC. SIA may comprise e.g. a
-    // PIN code or Directory Number, SIB may be used to specify the tele or
-    // bearer service and SIC to specify the value of the "No Reply Condition
-    // Timer". Where a particular service request does not require any SI,
-    // "*SI" is not entered. The use of SIA, SIB and SIC is optional and shall
-    // be entered in any of the following formats:
-    //    - *SIA*SIB*SIC#
-    //    - *SIA*SIB#
-    //    - *SIA**SIC#
-    //    - *SIA#
-    //    - **SIB*SIC#
-    //    - ***SIC#
-    //
-    // Also catch the additional NEW_PASSWORD for the case of a password
-    // registration procedure. Ex:
-    //    - *  03 * ZZ * OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
-    //    - ** 03 * ZZ * OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
-    //    - *  03 **     OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
-    //    - ** 03 **     OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
-    let si = "\\*([^*#]*)";
-    let allSi = "";
-    for (let i = 0; i < 4; ++i) {
-      allSi = "(?:" + si + allSi + ")?";
-    }
-
-    let fullmmi = "(" + procedure + serviceCode + allSi + "#)";
-
-    // dial string after the #.
-    let dialString = "([^#]*)";
-
-    return new RegExp(fullmmi + dialString);
-  },
-
-  /**
-   * Provide the regex to parse MMI string.
-   */
-  _getMMIRegExp: function() {
-    if (!this._mmiRegExp) {
-      this._mmiRegExp = this._buildMMIRegExp();
-    }
-
-    return this._mmiRegExp;
-  },
-
-  /**
-   * Helper to parse # string. TS.22.030 Figure 3.5.3.2.
-   */
-  _isPoundString: function(mmiString) {
-    return (mmiString.charAt(mmiString.length - 1) === MMI_END_OF_USSD);
-  },
-
-  /**
-   * Helper to parse short string. TS.22.030 Figure 3.5.3.2.
-   */
-  _isMMIShortString: function(mmiString) {
-    if (mmiString.length > 2) {
-      return false;
-    }
-
-    // TODO: Should take care of checking if the string is an emergency number
-    // in Bug 889737. See Bug 1023141 for more background.
-
-    // In a call case.
-    if (Object.getOwnPropertyNames(this.currentCalls).length > 0) {
-      return true;
-    }
-
-    // Input string is 2 digits starting with a "1"
-    if ((mmiString.length == 2) && (mmiString.charAt(0) === '1')) {
-      return false;
-    }
-
-    return true;
-  },
-
-  _serviceCodeToKeyString: function(serviceCode) {
-    switch (serviceCode) {
-      case MMI_SC_CFU:
-      case MMI_SC_CF_BUSY:
-      case MMI_SC_CF_NO_REPLY:
-      case MMI_SC_CF_NOT_REACHABLE:
-      case MMI_SC_CF_ALL:
-      case MMI_SC_CF_ALL_CONDITIONAL:
-        return MMI_KS_SC_CALL_FORWARDING;
-      case MMI_SC_PIN:
-        return MMI_KS_SC_PIN;
-      case MMI_SC_PIN2:
-        return MMI_KS_SC_PIN2;
-      case MMI_SC_PUK:
-        return MMI_KS_SC_PUK;
-      case MMI_SC_PUK2:
-        return MMI_KS_SC_PUK2;
-      case MMI_SC_IMEI:
-        return MMI_KS_SC_IMEI;
-      case MMI_SC_CLIP:
-        return MMI_KS_SC_CLIP;
-      case MMI_SC_CLIR:
-        return MMI_KS_SC_CLIR;
-      case MMI_SC_BAOC:
-      case MMI_SC_BAOIC:
-      case MMI_SC_BAOICxH:
-      case MMI_SC_BAIC:
-      case MMI_SC_BAICr:
-      case MMI_SC_BA_ALL:
-      case MMI_SC_BA_MO:
-      case MMI_SC_BA_MT:
-        return MMI_KS_SC_CALL_BARRING;
-      case MMI_SC_CALL_WAITING:
-        return MMI_KS_SC_CALL_WAITING;
-      default:
-        return MMI_KS_SC_USSD;
-    }
-  },
-
   sendMMI: function(options) {
     if (DEBUG) {
       this.context.debug("SendMMI " + JSON.stringify(options));
     }
 
-    let mmi = this._parseMMI(options.mmi);
-    if (DEBUG) {
-      this.context.debug("MMI " + JSON.stringify(mmi));
-    }
-
     let _sendMMIError = (function(errorMsg) {
       options.success = false;
       options.errorMsg = errorMsg;
       this.sendChromeMessage(options);
     }).bind(this);
 
     // It's neither a valid mmi code nor an ongoing ussd.
+    let mmi = options.mmi;
     if (!mmi && !this._ussdSession) {
       _sendMMIError(MMI_ERROR_KS_ERROR);
       return;
     }
 
-    options.mmiServiceCode = mmi ?
-      this._serviceCodeToKeyString(mmi.serviceCode) : MMI_KS_SC_USSD;
-
     function _isValidPINPUKRequest() {
       // The only allowed MMI procedure for ICC PIN, PIN2, PUK and PUK2 handling
       // is "Registration" (**).
       if (mmi.procedure != MMI_PROCEDURE_REGISTRATION ) {
         _sendMMIError(MMI_ERROR_KS_INVALID_ACTION);
         return false;
       }
 
@@ -3571,46 +3358,44 @@ RilObject.prototype = {
       options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
     }
     options.retryCount = length ? this.context.Buf.readInt32List()[0] : -1;
     if (options.rilMessageType != "sendMMI") {
       this.sendChromeMessage(options);
       return;
     }
 
-    let mmiServiceCode = options.mmiServiceCode;
+    let serviceCode = options.mmi.serviceCode;
 
     if (options.success) {
-      switch (mmiServiceCode) {
-        case MMI_KS_SC_PIN:
+      switch (serviceCode) {
+        case MMI_SC_PIN:
           options.statusMessage = MMI_SM_KS_PIN_CHANGED;
           break;
-        case MMI_KS_SC_PIN2:
+        case MMI_SC_PIN2:
           options.statusMessage = MMI_SM_KS_PIN2_CHANGED;
           break;
-        case MMI_KS_SC_PUK:
+        case MMI_SC_PUK:
           options.statusMessage = MMI_SM_KS_PIN_UNBLOCKED;
           break;
-        case MMI_KS_SC_PUK2:
+        case MMI_SC_PUK2:
           options.statusMessage = MMI_SM_KS_PIN2_UNBLOCKED;
           break;
       }
     } else {
       if (options.retryCount <= 0) {
-        if (mmiServiceCode === MMI_KS_SC_PUK) {
+        if (serviceCode === MMI_SC_PUK) {
           options.errorMsg = MMI_ERROR_KS_SIM_BLOCKED;
-        } else if (mmiServiceCode === MMI_KS_SC_PIN) {
+        } else if (serviceCode === MMI_SC_PIN) {
           options.errorMsg = MMI_ERROR_KS_NEEDS_PUK;
         }
       } else {
-        if (mmiServiceCode === MMI_KS_SC_PIN ||
-            mmiServiceCode === MMI_KS_SC_PIN2) {
+        if (serviceCode === MMI_SC_PIN || serviceCode === MMI_SC_PIN2) {
           options.errorMsg = MMI_ERROR_KS_BAD_PIN;
-        } else if (mmiServiceCode === MMI_KS_SC_PUK ||
-                   mmiServiceCode === MMI_KS_SC_PUK2) {
+        } else if (serviceCode === MMI_SC_PUK || serviceCode === MMI_SC_PUK2) {
           options.errorMsg = MMI_ERROR_KS_BAD_PUK;
         }
         if (options.retryCount !== undefined) {
           options.additionalInformation = options.retryCount;
         }
       }
     }
 
@@ -6025,17 +5810,17 @@ RilObject.prototype[REQUEST_QUERY_CALL_F
     // MMI query call forwarding options request returns a set of rules that
     // will be exposed in the form of an array of MozCallForwardingOptions
     // instances.
     options.additionalInformation = rules;
   }
   this.sendChromeMessage(options);
 };
 RilObject.prototype[REQUEST_SET_CALL_FORWARD] =
-  function REQUEST_SET_CALL_FORWARD(length, options) {
+    function REQUEST_SET_CALL_FORWARD(length, options) {
   options.success = (options.rilRequestError === 0);
   if (!options.success) {
     options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
   } else if (options.rilMessageType === "sendMMI") {
     switch (options.action) {
       case CALL_FORWARD_ACTION_ENABLE:
         options.statusMessage = MMI_SM_KS_SERVICE_ENABLED;
         break;
--- a/dom/system/gonk/tests/test_ril_worker_mmi.js
+++ b/dom/system/gonk/tests/test_ril_worker_mmi.js
@@ -2,16 +2,29 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this);
 
 function run_test() {
   run_next_test();
 }
 
+function createMMIOptions(procedure, serviceCode, sia, sib, sic) {
+  let mmi = {
+    fullMMI: Array.slice(arguments).join("*") + "#",
+    procedure: procedure,
+    serviceCode: serviceCode,
+    sia: sia,
+    sib: sib,
+    sic: sic
+  };
+
+  return mmi;
+}
+
 function testSendMMI(mmi, error) {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
 
   do_print("worker.postMessage " + worker.postMessage);
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
@@ -22,30 +35,18 @@ function testSendMMI(mmi, error) {
   do_check_eq(postedMessage.rilMessageType, "sendMMI");
   do_check_eq(postedMessage.errorMsg, error);
 }
 
 /**
  * sendMMI tests.
  */
 
-add_test(function test_sendMMI_empty() {
-  testSendMMI("", MMI_ERROR_KS_ERROR);
-
-  run_next_test();
-});
-
-add_test(function test_sendMMI_undefined() {
-  testSendMMI({}, MMI_ERROR_KS_ERROR);
-
-  run_next_test();
-});
-
-add_test(function test_sendMMI_invalid() {
-  testSendMMI("11", MMI_ERROR_KS_ERROR);
+add_test(function test_sendMMI_null() {
+  testSendMMI(null, MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_short_code() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
@@ -56,213 +57,227 @@ add_test(function test_sendMMI_short_cod
     ussdOptions = options;
     context.RIL[REQUEST_SEND_USSD](0, {
       rilRequestError: ERROR_SUCCESS
     });
 
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "**"});
+  context.RIL.sendMMI({mmi: {fullMMI: "**"}});
 
   let postedMessage = workerhelper.postedMessage;
   do_check_eq(ussdOptions.ussd, "**");
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
   do_check_true(context.RIL._ussdSession);
 
   run_next_test();
 });
 
-add_test(function test_sendMMI_dial_string() {
-  testSendMMI("123", MMI_ERROR_KS_ERROR);
-
-  run_next_test();
-});
-
 add_test(function test_sendMMI_change_PIN() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
 
   context.RIL.changeICCPIN = function fakeChangeICCPIN(options) {
     context.RIL[REQUEST_ENTER_SIM_PIN](0, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "**04*1234*4567*4567#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("**", "04", "1234", "4567",
+                                             "4567")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN_no_new_PIN() {
-  testSendMMI("**04*1234**4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "04", "1234", "", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN_no_old_PIN() {
-  testSendMMI("**04**1234*4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "04", "", "1234", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN_wrong_procedure() {
-  testSendMMI("*04*1234*4567*4567#", MMI_ERROR_KS_INVALID_ACTION);
+  testSendMMI(createMMIOptions("*", "04", "1234", "4567", "4567"),
+              MMI_ERROR_KS_INVALID_ACTION);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN_new_PIN_mismatch() {
-  testSendMMI("**04*4567*1234*4567#", MMI_ERROR_KS_MISMATCH_PIN);
+  testSendMMI(createMMIOptions("**", "04", "4567", "1234", "4567"),
+              MMI_ERROR_KS_MISMATCH_PIN);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN2() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
 
   context.RIL.changeICCPIN2 = function fakeChangeICCPIN2(options){
     context.RIL[REQUEST_ENTER_SIM_PIN2](0, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "**042*1234*4567*4567#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("**", "042", "1234", "4567",
+                                             "4567")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN2_no_new_PIN2() {
-  testSendMMI("**042*1234**4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "042", "1234", "", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN2_no_old_PIN2() {
-  testSendMMI("**042**1234*4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "042", "", "1234", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN2_wrong_procedure() {
-  testSendMMI("*042*1234*4567*4567#", MMI_ERROR_KS_INVALID_ACTION);
+  testSendMMI(createMMIOptions("*", "042", "1234", "4567", "4567"),
+              MMI_ERROR_KS_INVALID_ACTION);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_change_PIN2_new_PIN2_mismatch() {
-  testSendMMI("**042*4567*1234*4567#", MMI_ERROR_KS_MISMATCH_PIN);
+  testSendMMI(createMMIOptions("**", "042", "4567", "1234", "4567"),
+              MMI_ERROR_KS_MISMATCH_PIN);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
 
   context.RIL.enterICCPUK = function fakeEnterICCPUK(options){
     context.RIL[REQUEST_ENTER_SIM_PUK](0, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "**05*1234*4567*4567#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("**", "05", "1234", "4567",
+                                             "4567")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN_no_new_PIN() {
-  testSendMMI("**05*1234**4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "05", "1234", "", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN_no_PUK() {
-  testSendMMI("**05**1234*4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "05", "", "1234", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN_wrong_procedure() {
-  testSendMMI("*05*1234*4567*4567#", MMI_ERROR_KS_INVALID_ACTION);
+  testSendMMI(createMMIOptions("*", "05", "1234", "4567", "4567"),
+              MMI_ERROR_KS_INVALID_ACTION);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN_new_PIN_mismatch() {
-  testSendMMI("**05*4567*1234*4567#", MMI_ERROR_KS_MISMATCH_PIN);
+  testSendMMI(createMMIOptions("**", "05", "4567", "1234", "4567"),
+              MMI_ERROR_KS_MISMATCH_PIN);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN2() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
 
   context.RIL.enterICCPUK2 = function fakeEnterICCPUK2(options){
     context.RIL[REQUEST_ENTER_SIM_PUK2](0, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "**052*1234*4567*4567#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("**", "052", "1234", "4567",
+                                             "4567")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN2_no_new_PIN2() {
-  testSendMMI("**052*1234**4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "052", "1234", "", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN2_no_PUK2() {
-  testSendMMI("**052**1234*4567#", MMI_ERROR_KS_ERROR);
+  testSendMMI(createMMIOptions("**", "052", "", "1234", "4567"),
+              MMI_ERROR_KS_ERROR);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN2_wrong_procedure() {
-  testSendMMI("*052*1234*4567*4567#", MMI_ERROR_KS_INVALID_ACTION);
+  testSendMMI(createMMIOptions("*", "052", "1234", "4567", "4567"),
+              MMI_ERROR_KS_INVALID_ACTION);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_unblock_PIN2_new_PIN_mismatch() {
-  testSendMMI("**052*4567*1234*4567#", MMI_ERROR_KS_MISMATCH_PIN);
+  testSendMMI(createMMIOptions("**", "052", "4567", "1234", "4567"),
+              MMI_ERROR_KS_MISMATCH_PIN);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_get_IMEI() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
@@ -270,17 +285,17 @@ add_test(function test_sendMMI_get_IMEI(
 
   context.RIL.getIMEI = function getIMEI(options){
     mmiOptions = options;
     context.RIL[REQUEST_SEND_USSD](0, {
       rilRequestError: ERROR_SUCCESS,
     });
   };
 
-  context.RIL.sendMMI({mmi: "*#06#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*#", "06")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_neq(mmiOptions.mmi, null);
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
 
   run_next_test();
@@ -294,17 +309,17 @@ add_test(function test_sendMMI_get_IMEI_
 
   context.RIL.getIMEI = function getIMEI(options){
     mmiOptions = options;
     context.RIL[REQUEST_SEND_USSD](0, {
       rilRequestError: ERROR_RADIO_NOT_AVAILABLE,
     });
   };
 
-  context.RIL.sendMMI({mmi: "*#06#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*#", "06")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_neq(mmiOptions.mmi, null);
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_RADIO_NOT_AVAILABLE);
   do_check_false(postedMessage.success);
 
   run_next_test();
@@ -323,17 +338,17 @@ add_test(function test_sendMMI_call_barr
     function fakeQueryICCFacilityLock(options){
       context.RIL[REQUEST_QUERY_FACILITY_LOCK](1, {
         rilMessageType: "sendMMI",
         rilRequestError: ERROR_SUCCESS
       });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "*#33#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*#", "33")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_true(postedMessage.success);
   do_check_true(postedMessage.enabled);
   do_check_eq(postedMessage.statusMessage,  MMI_SM_KS_SERVICE_ENABLED_FOR);
   do_check_true(Array.isArray(postedMessage.additionalInformation));
   do_check_eq(postedMessage.additionalInformation[0], "serviceClassVoice");
@@ -353,17 +368,17 @@ add_test(function test_sendMMI_call_barr
       context.RIL[REQUEST_SET_FACILITY_LOCK](0, {
         rilMessageType: "sendMMI",
         procedure: MMI_PROCEDURE_ACTIVATION,
         rilRequestError: ERROR_SUCCESS
       });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "*33#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*", "33")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq(mmiOptions.procedure, MMI_PROCEDURE_ACTIVATION);
   do_check_true(postedMessage.success);
   do_check_eq(postedMessage.statusMessage,  MMI_SM_KS_SERVICE_ENABLED);
 
   run_next_test();
@@ -381,29 +396,29 @@ add_test(function test_sendMMI_call_barr
       context.RIL[REQUEST_SET_FACILITY_LOCK](0, {
         rilMessageType: "sendMMI",
         procedure: MMI_PROCEDURE_DEACTIVATION,
         rilRequestError: ERROR_SUCCESS
       });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "#33#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("#", "33")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq(mmiOptions.procedure, MMI_PROCEDURE_DEACTIVATION);
   do_check_true(postedMessage.success);
   do_check_eq(postedMessage.statusMessage,  MMI_SM_KS_SERVICE_DISABLED);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_barring_BAIC_procedure_not_supported() {
-  testSendMMI("**33*0000#", MMI_ERROR_KS_NOT_SUPPORTED);
+  testSendMMI(createMMIOptions("**", "33", "0000"), MMI_ERROR_KS_NOT_SUPPORTED);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_USSD() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
@@ -412,21 +427,21 @@ add_test(function test_sendMMI_USSD() {
   context.RIL.sendUSSD = function fakeSendUSSD(options){
     ussdOptions = options;
     context.RIL[REQUEST_SEND_USSD](0, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "*123#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*", "123")});
 
   let postedMessage = workerhelper.postedMessage;
 
-  do_check_eq(ussdOptions.ussd, "*123#");
+  do_check_eq(ussdOptions.ussd, "**123#");
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
   do_check_true(context.RIL._ussdSession);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_USSD_error() {
@@ -438,21 +453,21 @@ add_test(function test_sendMMI_USSD_erro
   context.RIL.sendUSSD = function fakeSendUSSD(options){
     ussdOptions = options;
     context.RIL[REQUEST_SEND_USSD](0, {
       rilRequestError: ERROR_GENERIC_FAILURE
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "*123#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*", "123")});
 
   let postedMessage = workerhelper.postedMessage;
 
-  do_check_eq(ussdOptions.ussd, "*123#");
+  do_check_eq(ussdOptions.ussd, "**123#");
   do_check_eq (postedMessage.errorMsg, GECKO_ERROR_GENERIC_FAILURE);
   do_check_false(postedMessage.success);
   do_check_false(context.RIL._ussdSession);
 
   run_next_test();
 });
 
 function setCallWaitingSuccess(mmi) {
@@ -471,35 +486,35 @@ function setCallWaitingSuccess(mmi) {
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq(postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
 }
 
 add_test(function test_sendMMI_call_waiting_activation() {
-  setCallWaitingSuccess("*43*10#");
+  setCallWaitingSuccess(createMMIOptions("*", "43", "10"));
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_waiting_deactivation() {
-  setCallWaitingSuccess("#43#");
+  setCallWaitingSuccess(createMMIOptions("#", "43"));
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_waiting_registration() {
-  testSendMMI("**43#", MMI_ERROR_KS_NOT_SUPPORTED);
+  testSendMMI(createMMIOptions("**", "43"), MMI_ERROR_KS_NOT_SUPPORTED);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_waiting_erasure() {
-  testSendMMI("##43#", MMI_ERROR_KS_NOT_SUPPORTED);
+  testSendMMI(createMMIOptions("##", "43"), MMI_ERROR_KS_NOT_SUPPORTED);
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_waiting_interrogation() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
@@ -515,17 +530,17 @@ add_test(function test_sendMMI_call_wait
       2    // length
     ];
     context.RIL[REQUEST_QUERY_CALL_WAITING](1, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "*#43#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*#", "43")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq(postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
   do_check_true(postedMessage.success);
   do_check_eq(postedMessage.length, 2);
   do_check_true(postedMessage.enabled);
   run_next_test();
--- a/dom/system/gonk/tests/test_ril_worker_mmi_cf.js
+++ b/dom/system/gonk/tests/test_ril_worker_mmi_cf.js
@@ -2,44 +2,58 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this);
 
 function run_test() {
   run_next_test();
 }
 
-function setCallForwardSuccess(mmi) {
+function createMMIOptions(procedure, serviceCode, sia, sib, sic) {
+  let mmi = {
+    fullMMI: Array.slice(arguments).join("*") + "#",
+    procedure: procedure,
+    serviceCode: serviceCode,
+    sia: sia,
+    sib: sib,
+    sic: sic
+  };
+
+  return mmi;
+}
+
+function setCallForwardSuccess(procedure, serviceCode, sia, sib, sic) {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
 
   context.RIL.setCallForward = function fakeSetCallForward(options) {
     context.RIL[REQUEST_SET_CALL_FORWARD](0, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: mmi});
+  context.RIL.sendMMI({mmi: createMMIOptions(procedure, serviceCode, sia, sib,
+                                             sic)});
 
   let postedMessage = workerhelper.postedMessage;
 
-  do_check_eq(postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
+  do_check_eq(postedMessage.errorMsg, undefined);
   do_check_true(postedMessage.success);
 }
 
 add_test(function test_sendMMI_call_forwarding_activation() {
-  setCallForwardSuccess("*21*12345*99*10#");
+  setCallForwardSuccess("*", "21", "12345", "99", "10");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_deactivation() {
-  setCallForwardSuccess("#21*12345*99*10#");
+  setCallForwardSuccess("#", "21", "12345", "99", "10");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_interrogation() {
   let workerhelper = newInterceptWorker();
   let worker = workerhelper.worker;
   let context = worker.ContextPool._contexts[0];
@@ -62,21 +76,21 @@ add_test(function test_sendMMI_call_forw
       1    // rulesLength
     ];
     context.RIL[REQUEST_QUERY_CALL_FORWARD_STATUS](1, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "*#21#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*#", "21")});
 
   let postedMessage = workerhelper.postedMessage;
 
-  do_check_eq(postedMessage.errorMsg, GECKO_ERROR_SUCCESS);
+  do_check_eq(postedMessage.errorMsg, undefined);
   do_check_true(postedMessage.success);
   do_check_true(Array.isArray(postedMessage.rules));
   do_check_eq(postedMessage.rules.length, 1);
   do_check_true(postedMessage.rules[0].active);
   do_check_eq(postedMessage.rules[0].reason, CALL_FORWARD_REASON_UNCONDITIONAL);
   do_check_eq(postedMessage.rules[0].number, "+34666222333");
   run_next_test();
 });
@@ -92,60 +106,60 @@ add_test(function test_sendMMI_call_forw
 
   context.RIL.queryCallForwardStatus = function fakeQueryCallForward(options) {
     context.RIL[REQUEST_QUERY_CALL_FORWARD_STATUS](1, {
       rilRequestError: ERROR_SUCCESS
     });
   };
 
   context.RIL.radioState = GECKO_RADIOSTATE_ENABLED;
-  context.RIL.sendMMI({mmi: "*#21#"});
+  context.RIL.sendMMI({mmi: createMMIOptions("*#", "21")});
 
   let postedMessage = workerhelper.postedMessage;
 
   do_check_eq(postedMessage.errorMsg, GECKO_ERROR_GENERIC_FAILURE);
   do_check_false(postedMessage.success);
 
   run_next_test();
 });
 
 
 add_test(function test_sendMMI_call_forwarding_registration() {
-  setCallForwardSuccess("**21*12345*99*10#");
+  setCallForwardSuccess("**", "21", "12345", "99", "10");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_erasure() {
-  setCallForwardSuccess("##21*12345*99#");
+  setCallForwardSuccess("##", "21", "12345", "99");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_CFB() {
-  setCallForwardSuccess("*67*12345*99*10#");
+  setCallForwardSuccess("*", "67", "12345", "99", "10");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_CFNRy() {
-  setCallForwardSuccess("*61*12345*99*10#");
+  setCallForwardSuccess("*", "61", "12345", "99", "10");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_CFNRc() {
-  setCallForwardSuccess("*62*12345*99*10#");
+  setCallForwardSuccess("*", "62", "12345", "99", "10");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_CFAll() {
-  setCallForwardSuccess("*004*12345*99*10#");
+  setCallForwardSuccess("*", "004", "12345", "99", "10");
 
   run_next_test();
 });
 
 add_test(function test_sendMMI_call_forwarding_CFAllConditional() {
-  setCallForwardSuccess("*002*12345*99*10#");
+  setCallForwardSuccess("*", "002", "12345", "99", "10");
 
   run_next_test();
 });
deleted file mode 100644
--- a/dom/system/gonk/tests/test_ril_worker_mmi_parseMMI.js
+++ /dev/null
@@ -1,317 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this);
-
-function run_test() {
-  run_next_test();
-}
-
-let worker;
-function parseMMI(mmi) {
-  if (!worker) {
-    worker = newWorker({
-      postRILMessage: function(data) {
-        // Do nothing
-      },
-      postMessage: function(message) {
-        // Do nothing
-      }
-    });
-  }
-
-  let context = worker.ContextPool._contexts[0];
-  return context.RIL._parseMMI(mmi);
-}
-
-add_test(function test_parseMMI_empty() {
-  let mmi = parseMMI("");
-
-  do_check_null(mmi);
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_undefined() {
-  let mmi = parseMMI();
-
-  do_check_null(mmi);
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_one_digit_short_code() {
-  let mmi = parseMMI("1");
-
-  do_check_eq(mmi.fullMMI, "1");
-  do_check_eq(mmi.procedure, undefined);
-  do_check_eq(mmi.serviceCode, undefined);
-  do_check_eq(mmi.sia, undefined);
-  do_check_eq(mmi.sib, undefined);
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, undefined);
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_invalid_short_code() {
-  let mmi = parseMMI("11");
-
-  do_check_null(mmi);
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_short_code() {
-  let mmi = parseMMI("21");
-
-  do_check_eq(mmi.fullMMI, "21");
-  do_check_eq(mmi.procedure, undefined);
-  do_check_eq(mmi.serviceCode, undefined);
-  do_check_eq(mmi.sia, undefined);
-  do_check_eq(mmi.sib, undefined);
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, undefined);
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_dial_string() {
-  let mmi = parseMMI("12345");
-
-  do_check_null(mmi);
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_USSD_without_asterisk_prefix() {
-  let mmi = parseMMI("123#");
-
-  do_check_eq(mmi.fullMMI, "123#");
-  do_check_eq(mmi.procedure, undefined);
-  do_check_eq(mmi.serviceCode, undefined);
-  do_check_eq(mmi.sia, undefined);
-  do_check_eq(mmi.sib, undefined);
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, undefined);
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_USSD() {
-  let mmi = parseMMI("*123#");
-
-  do_check_eq(mmi.fullMMI, "*123#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, undefined);
-  do_check_eq(mmi.sib, undefined);
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_sia() {
-  let mmi = parseMMI("*123*1#");
-
-  do_check_eq(mmi.fullMMI, "*123*1#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, "1");
-  do_check_eq(mmi.sib, undefined);
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_sib() {
-  let mmi = parseMMI("*123**1#");
-
-  do_check_eq(mmi.fullMMI, "*123**1#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, "");
-  do_check_eq(mmi.sib, "1");
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_sic() {
-  let mmi = parseMMI("*123***1#");
-
-  do_check_eq(mmi.fullMMI, "*123***1#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, "");
-  do_check_eq(mmi.sib, "");
-  do_check_eq(mmi.sic, "1");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_sia_sib() {
-  let mmi = parseMMI("*123*1*1#");
-
-  do_check_eq(mmi.fullMMI, "*123*1*1#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, "1");
-  do_check_eq(mmi.sib, "1");
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_sia_sic() {
-  let mmi = parseMMI("*123*1**1#");
-
-  do_check_eq(mmi.fullMMI, "*123*1**1#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, "1");
-  do_check_eq(mmi.sib, "");
-  do_check_eq(mmi.sic, "1");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_sib_sic() {
-  let mmi = parseMMI("*123**1*1#");
-
-  do_check_eq(mmi.fullMMI, "*123**1*1#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, "");
-  do_check_eq(mmi.sib, "1");
-  do_check_eq(mmi.sic, "1");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_pwd() {
-  let mmi = parseMMI("*123****1#");
-
-  do_check_eq(mmi.fullMMI, "*123****1#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, "");
-  do_check_eq(mmi.sib, "");
-  do_check_eq(mmi.sic, "");
-  do_check_eq(mmi.pwd, "1");
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_dial_number() {
-  let mmi = parseMMI("*123#345");
-
-  do_check_eq(mmi.fullMMI, "*123#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "123");
-  do_check_eq(mmi.sia, undefined);
-  do_check_eq(mmi.sib, undefined);
-  do_check_eq(mmi.sic, undefined);
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "345");
-
-  run_next_test();
-});
-
-
-/**
- * MMI procedures tests
- */
-
-add_test(function test_parseMMI_activation() {
-  let mmi = parseMMI("*00*12*34*56#");
-
-  do_check_eq(mmi.fullMMI, "*00*12*34*56#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
-  do_check_eq(mmi.serviceCode, "00");
-  do_check_eq(mmi.sia, "12");
-  do_check_eq(mmi.sib, "34");
-  do_check_eq(mmi.sic, "56");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_deactivation() {
-  let mmi = parseMMI("#00*12*34*56#");
-
-  do_check_eq(mmi.fullMMI, "#00*12*34*56#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_DEACTIVATION);
-  do_check_eq(mmi.serviceCode, "00");
-  do_check_eq(mmi.sia, "12");
-  do_check_eq(mmi.sib, "34");
-  do_check_eq(mmi.sic, "56");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_interrogation() {
-  let mmi = parseMMI("*#00*12*34*56#");
-
-  do_check_eq(mmi.fullMMI, "*#00*12*34*56#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_INTERROGATION);
-  do_check_eq(mmi.serviceCode, "00");
-  do_check_eq(mmi.sia, "12");
-  do_check_eq(mmi.sib, "34");
-  do_check_eq(mmi.sic, "56");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_registration() {
-  let mmi = parseMMI("**00*12*34*56#");
-
-  do_check_eq(mmi.fullMMI, "**00*12*34*56#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_REGISTRATION);
-  do_check_eq(mmi.serviceCode, "00");
-  do_check_eq(mmi.sia, "12");
-  do_check_eq(mmi.sib, "34");
-  do_check_eq(mmi.sic, "56");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
-
-add_test(function test_parseMMI_erasure() {
-  let mmi = parseMMI("##00*12*34*56#");
-
-  do_check_eq(mmi.fullMMI, "##00*12*34*56#");
-  do_check_eq(mmi.procedure, MMI_PROCEDURE_ERASURE);
-  do_check_eq(mmi.serviceCode, "00");
-  do_check_eq(mmi.sia, "12");
-  do_check_eq(mmi.sib, "34");
-  do_check_eq(mmi.sic, "56");
-  do_check_eq(mmi.pwd, undefined);
-  do_check_eq(mmi.dialNumber, "");
-
-  run_next_test();
-});
--- a/dom/system/gonk/tests/xpcshell.ini
+++ b/dom/system/gonk/tests/xpcshell.ini
@@ -19,17 +19,16 @@ tail =
 skip-if = true
 [test_ril_worker_sms_cdma.js]
 [test_ril_worker_sms_cdmapduhelper.js]
 [test_ril_worker_sms_nl_tables.js]
 [test_ril_worker_sms_gsmpduhelper.js]
 [test_ril_worker_sms_segment_info.js]
 [test_ril_worker_mmi.js]
 [test_ril_worker_mmi_cf.js]
-[test_ril_worker_mmi_parseMMI.js]
 [test_ril_worker_cf.js]
 [test_ril_worker_cellbroadcast_config.js]
 [test_ril_worker_cellbroadcast.js]
 [test_ril_worker_ruim.js]
 [test_ril_worker_cw.js]
 [test_ril_worker_clir.js]
 [test_ril_worker_clip.js]
 [test_ril_worker_ssn.js]
--- a/dom/telephony/Telephony.cpp
+++ b/dom/telephony/Telephony.cpp
@@ -1,44 +1,47 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "Telephony.h"
-#include "mozilla/dom/CallEvent.h"
-#include "mozilla/dom/TelephonyBinding.h"
-#include "mozilla/dom/Promise.h"
 
-#include "nsIURI.h"
-#include "nsPIDOMWindow.h"
-#include "nsIPermissionManager.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/CallEvent.h"
+#include "mozilla/dom/MozMobileConnectionBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/TelephonyBinding.h"
+#include "mozilla/dom/UnionTypes.h"
 
-#include "mozilla/dom/UnionTypes.h"
-#include "mozilla/Preferences.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsContentUtils.h"
+#include "nsIPermissionManager.h"
+#include "nsIURI.h"
 #include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
 #include "nsServiceManagerUtils.h"
 #include "nsThreadUtils.h"
 
 #include "CallsList.h"
 #include "TelephonyCall.h"
 #include "TelephonyCallGroup.h"
 #include "TelephonyCallId.h"
+#include "TelephonyCallback.h"
 
 // Service instantiation
 #include "ipc/TelephonyIPCService.h"
 #if defined(MOZ_WIDGET_GONK) && defined(MOZ_B2G_RIL)
 #include "nsIGonkTelephonyService.h"
 #endif
 #include "nsXULAppAPI.h" // For XRE_GetProcessType()
 
 using namespace mozilla::dom;
+using namespace mozilla::dom::telephony;
 using mozilla::ErrorResult;
 
 class Telephony::Listener : public nsITelephonyListener
 {
   Telephony* mTelephony;
 
   virtual ~Listener() {}
 
@@ -55,53 +58,16 @@ public:
   void
   Disconnect()
   {
     MOZ_ASSERT(mTelephony);
     mTelephony = nullptr;
   }
 };
 
-class Telephony::Callback : public nsITelephonyCallback
-{
-  nsRefPtr<Telephony> mTelephony;
-  nsRefPtr<Promise> mPromise;
-  uint32_t mServiceId;
-
-  virtual ~Callback() {}
-
-public:
-  NS_DECL_ISUPPORTS
-
-  Callback(Telephony* aTelephony, Promise* aPromise, uint32_t aServiceId)
-    : mTelephony(aTelephony), mPromise(aPromise), mServiceId(aServiceId)
-  {
-    MOZ_ASSERT(mTelephony);
-  }
-
-  NS_IMETHODIMP
-  NotifyDialError(const nsAString& aError)
-  {
-    mPromise->MaybeRejectBrokenly(aError);
-    return NS_OK;
-  }
-
-  NS_IMETHODIMP
-  NotifyDialSuccess(uint32_t aCallIndex, const nsAString& aNumber)
-  {
-    nsRefPtr<TelephonyCallId> id = mTelephony->CreateCallId(aNumber);
-    nsRefPtr<TelephonyCall> call =
-      mTelephony->CreateCall(id, mServiceId, aCallIndex,
-                             nsITelephonyService::CALL_STATE_DIALING);
-
-    mPromise->MaybeResolve(call);
-    return NS_OK;
-  }
-};
-
 class Telephony::EnumerationAck : public nsRunnable
 {
   nsRefPtr<Telephony> mTelephony;
   nsString mType;
 
 public:
   EnumerationAck(Telephony* aTelephony, const nsAString& aType)
   : mTelephony(aTelephony), mType(aType)
@@ -265,17 +231,18 @@ Telephony::DialInternal(uint32_t aServic
 
   // We only support one outgoing call at a time.
   if (HasDialingCall()) {
     promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
   nsCOMPtr<nsITelephonyCallback> callback =
-    new Callback(this, promise, aServiceId);
+    new TelephonyCallback(GetOwner(), this, promise, aServiceId);
+
   nsresult rv = mService->Dial(aServiceId, aNumber, aEmergency, callback);
   if (NS_FAILED(rv)) {
     promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
   return promise.forget();
 }
@@ -376,17 +343,16 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_
   // Telephony does not expose nsITelephonyListener.  mListener is the exposed
   // nsITelephonyListener and forwards the calls it receives to us.
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(Telephony, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(Telephony, DOMEventTargetHelper)
 
 NS_IMPL_ISUPPORTS(Telephony::Listener, nsITelephonyListener)
-NS_IMPL_ISUPPORTS(Telephony::Callback, nsITelephonyCallback)
 
 // Telephony WebIDL
 
 already_AddRefed<Promise>
 Telephony::Dial(const nsAString& aNumber, const Optional<uint32_t>& aServiceId,
                 ErrorResult& aRv)
 {
   uint32_t serviceId = ProvidedOrDefaultServiceId(aServiceId);
--- a/dom/telephony/Telephony.h
+++ b/dom/telephony/Telephony.h
@@ -16,36 +16,39 @@
 // Need to include TelephonyCall.h because we have inline methods that
 // assume they see the definition of TelephonyCall.
 #include "TelephonyCall.h"
 
 class nsPIDOMWindow;
 
 namespace mozilla {
 namespace dom {
+namespace telephony {
+
+class TelephonyCallback;
+
+} // namespace telephony
 
 class OwningTelephonyCallOrTelephonyCallGroup;
 
 class Telephony MOZ_FINAL : public DOMEventTargetHelper,
                             private nsITelephonyListener
 {
   /**
    * Class Telephony doesn't actually expose nsITelephonyListener.
    * Instead, it owns an nsITelephonyListener derived instance mListener
    * and passes it to nsITelephonyService. The onreceived events are first
    * delivered to mListener and then forwarded to its owner, Telephony. See
    * also bug 775997 comment #51.
    */
   class Listener;
+  class EnumerationAck;
 
-  class Callback;
-  friend class Callback;
-
-  class EnumerationAck;
   friend class EnumerationAck;
+  friend class telephony::TelephonyCallback;
 
   nsCOMPtr<nsITelephonyService> mService;
   nsRefPtr<Listener> mListener;
 
   nsTArray<nsRefPtr<TelephonyCall> > mCalls;
   nsRefPtr<CallsList> mCallsList;
 
   nsRefPtr<TelephonyCallGroup> mGroup;
new file mode 100644
--- /dev/null
+++ b/dom/telephony/TelephonyCallback.cpp
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelephonyCallback.h"
+
+#include "mozilla/dom/DOMMMIError.h"
+#include "nsServiceManagerUtils.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::dom::telephony;
+
+NS_IMPL_ISUPPORTS(TelephonyCallback, nsITelephonyCallback)
+
+TelephonyCallback::TelephonyCallback(nsPIDOMWindow* aWindow,
+                                     Telephony* aTelephony,
+                                     Promise* aPromise,
+                                     uint32_t aServiceId)
+  : mWindow(aWindow), mTelephony(aTelephony), mPromise(aPromise),
+    mServiceId(aServiceId)
+{
+  MOZ_ASSERT(mTelephony);
+}
+
+nsresult
+TelephonyCallback::NotifyDialMMISuccess(const nsAString& aStatusMessage)
+{
+  AutoJSAPI jsapi;
+  if (!NS_WARN_IF(jsapi.Init(mWindow))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  JSContext* cx = jsapi.cx();
+
+  MozMMIResult result;
+
+  result.mServiceCode.Assign(mServiceCode);
+  result.mStatusMessage.Assign(aStatusMessage);
+
+  return NotifyDialMMISuccess(cx, result);
+}
+
+nsresult
+TelephonyCallback::NotifyDialMMISuccess(JSContext* aCx,
+                                        const nsAString& aStatusMessage,
+                                        JS::Handle<JS::Value> aInfo)
+{
+  RootedDictionary<MozMMIResult> result(aCx);
+
+  result.mServiceCode.Assign(mServiceCode);
+  result.mStatusMessage.Assign(aStatusMessage);
+  result.mAdditionalInformation.Construct().SetAsObject() = &aInfo.toObject();
+
+  return NotifyDialMMISuccess(aCx, result);
+}
+
+nsresult
+TelephonyCallback::NotifyDialMMISuccess(JSContext* aCx,
+                                        const MozMMIResult& aResult)
+{
+  JS::Rooted<JS::Value> jsResult(aCx);
+
+  if (!ToJSValue(aCx, aResult, &jsResult)) {
+    JS_ClearPendingException(aCx);
+    return NS_ERROR_TYPE_ERR;
+  }
+
+  return NotifyDialMMISuccess(jsResult);
+}
+
+// nsITelephonyCallback
+
+NS_IMETHODIMP
+TelephonyCallback::NotifyDialMMI(const nsAString& aServiceCode)
+{
+  mMMIRequest = new DOMRequest(mWindow);
+  mServiceCode.Assign(aServiceCode);
+
+  mPromise->MaybeResolve(mMMIRequest);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+TelephonyCallback::NotifyDialError(const nsAString& aError)
+{
+  mPromise->MaybeRejectBrokenly(aError);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+TelephonyCallback::NotifyDialCallSuccess(uint32_t aCallIndex,
+                                         const nsAString& aNumber)
+{
+  nsRefPtr<TelephonyCallId> id = mTelephony->CreateCallId(aNumber);
+  nsRefPtr<TelephonyCall> call =
+      mTelephony->CreateCall(id, mServiceId, aCallIndex,
+                             nsITelephonyService::CALL_STATE_DIALING);
+
+  mPromise->MaybeResolve(call);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+TelephonyCallback::NotifyDialMMISuccess(JS::Handle<JS::Value> aResult)
+{
+  nsCOMPtr<nsIDOMRequestService> rs = do_GetService(DOMREQUEST_SERVICE_CONTRACTID);
+  NS_ENSURE_TRUE(rs, NS_ERROR_FAILURE);
+
+  return rs->FireSuccessAsync(mMMIRequest, aResult);
+}
+
+NS_IMETHODIMP
+TelephonyCallback::NotifyDialMMIError(const nsAString& aError)
+{
+  Nullable<int16_t> info;
+
+  nsRefPtr<DOMError> error =
+      new DOMMMIError(mWindow, aError, EmptyString(), mServiceCode, info);
+
+  nsCOMPtr<nsIDOMRequestService> rs = do_GetService(DOMREQUEST_SERVICE_CONTRACTID);
+  NS_ENSURE_TRUE(rs, NS_ERROR_FAILURE);
+
+  return rs->FireDetailedError(mMMIRequest, error);
+}
+
+NS_IMETHODIMP
+TelephonyCallback::NotifyDialMMIErrorWithInfo(const nsAString& aError,
+                                              uint16_t aInfo)
+{
+  Nullable<int16_t> info(aInfo);
+
+  nsRefPtr<DOMError> error =
+      new DOMMMIError(mWindow, aError, EmptyString(), mServiceCode, info);
+
+  nsCOMPtr<nsIDOMRequestService> rs = do_GetService(DOMREQUEST_SERVICE_CONTRACTID);
+  NS_ENSURE_TRUE(rs, NS_ERROR_FAILURE);
+
+  return rs->FireDetailedError(mMMIRequest, error);
+}
new file mode 100644
--- /dev/null
+++ b/dom/telephony/TelephonyCallback.h
@@ -0,0 +1,81 @@
+/* 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/. */
+
+#ifndef mozilla_dom_TelephonyCallback_h
+#define mozilla_dom_TelephonyCallback_h
+
+#include "Telephony.h"
+#include "mozilla/dom/DOMRequest.h"
+#include "mozilla/dom/MozMobileConnectionBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsAutoPtr.h"
+#include "nsCOMPtr.h"
+#include "nsITelephonyService.h"
+#include "nsJSUtils.h"
+#include "nsString.h"
+
+class nsPIDOMWindow;
+
+namespace mozilla {
+namespace dom {
+namespace telephony {
+
+class TelephonyCallback MOZ_FINAL : public nsITelephonyCallback
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSITELEPHONYCALLBACK
+
+  TelephonyCallback(nsPIDOMWindow* aWindow, Telephony* aTelephony,
+                    Promise* aPromise, uint32_t aServiceId);
+
+  nsresult
+  NotifyDialMMISuccess(const nsAString& aStatusMessage);
+
+  template<typename T>
+  nsresult
+  NotifyDialMMISuccess(const nsAString& aStatusMessage, const T& aInfo)
+  {
+    AutoJSAPI jsapi;
+    if (!NS_WARN_IF(jsapi.Init(mWindow))) {
+      return NS_ERROR_FAILURE;
+    }
+
+    JSContext* cx = jsapi.cx();
+    JS::Rooted<JS::Value> info(cx);
+
+    if (!ToJSValue(cx, aInfo, &info)) {
+      JS_ClearPendingException(cx);
+      return NS_ERROR_TYPE_ERR;
+    }
+
+    return NotifyDialMMISuccess(cx, aStatusMessage, info);
+  }
+
+private:
+  ~TelephonyCallback() {}
+
+  nsresult
+  NotifyDialMMISuccess(JSContext* aCx, const nsAString& aStatusMessage,
+                       JS::Handle<JS::Value> aInfo);
+
+  nsresult
+  NotifyDialMMISuccess(JSContext* aCx, const MozMMIResult& aResult);
+
+
+  nsCOMPtr<nsPIDOMWindow> mWindow;
+  nsRefPtr<Telephony> mTelephony;
+  nsRefPtr<Promise> mPromise;
+  uint32_t mServiceId;
+
+  nsRefPtr<DOMRequest> mMMIRequest;
+  nsString mServiceCode;
+};
+
+} // namespace telephony
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_TelephonyCallback_h
--- a/dom/telephony/gonk/TelephonyService.js
+++ b/dom/telephony/gonk/TelephonyService.js
@@ -49,16 +49,26 @@ const AUDIO_STATE_IN_CALL  = 2;
 const AUDIO_STATE_NAME = [
   "PHONE_STATE_NORMAL",
   "PHONE_STATE_RINGTONE",
   "PHONE_STATE_IN_CALL"
 ];
 
 const DEFAULT_EMERGENCY_NUMBERS = ["112", "911"];
 
+// MMI match groups
+const MMI_MATCH_GROUP_FULL_MMI = 1;
+const MMI_MATCH_GROUP_PROCEDURE = 2;
+const MMI_MATCH_GROUP_SERVICE_CODE = 3;
+const MMI_MATCH_GROUP_SIA = 4;
+const MMI_MATCH_GROUP_SIB = 5;
+const MMI_MATCH_GROUP_SIC = 6;
+const MMI_MATCH_GROUP_PWD_CONFIRM = 7;
+const MMI_MATCH_GROUP_DIALING_NUMBER = 8;
+
 let DEBUG;
 function debug(s) {
   dump("TelephonyService: " + s + "\n");
 }
 
 XPCOMUtils.defineLazyGetter(this, "gAudioManager", function getAudioManager() {
   try {
     return Cc["@mozilla.org/telephony/audiomanager;1"]
@@ -94,25 +104,62 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyServiceGetter(this, "gPowerManagerService",
                                    "@mozilla.org/power/powermanagerservice;1",
                                    "nsIPowerManagerService");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger",
                                    "@mozilla.org/system-message-internal;1",
                                    "nsISystemMessagesInternal");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gGonkMobileConnectionService",
+                                   "@mozilla.org/mobileconnection/mobileconnectionservice;1",
+                                   "nsIGonkMobileConnectionService");
+
 XPCOMUtils.defineLazyGetter(this, "gPhoneNumberUtils", function() {
   let ns = {};
   Cu.import("resource://gre/modules/PhoneNumberUtils.jsm", ns);
   return ns.PhoneNumberUtils;
 });
 
+function MMIResult(aMmiServiceCode, aOptions) {
+  this.serviceCode = aMmiServiceCode;
+  this.statusMessage = aOptions.statusMessage;
+  this.additionalInformation = aOptions.additionalInformation;
+}
+MMIResult.prototype = {
+  __exposedProps__ : {serviceCode: 'r',
+                      statusMessage: 'r',
+                      additionalInformation: 'r'},
+};
+
+function CallForwardingOptions(aOptions) {
+  this.active = aOptions.active;
+  this.action = aOptions.action;
+  this.reason = aOptions.reason;
+  this.number = aOptions.number;
+  this.timeSeconds = aOptions.timeSeconds;
+  this.serviceClass = aOptions.serviceClass;
+}
+CallForwardingOptions.prototype = {
+  __exposedProps__ : {active: 'r',
+                      action: 'r',
+                      reason: 'r',
+                      number: 'r',
+                      timeSeconds: 'r',
+                      serviceClass: 'r'},
+};
+
 function TelephonyService() {
   this._numClients = gRadioInterfaceLayer.numRadioInterfaces;
   this._listeners = [];
+
+  this._mmiRegExp = null;
+
+  this._isDialing = false;
+  this._cachedDialRequest = null;
   this._currentCalls = {};
 
   this._cdmaCallWaitingNumber = null;
 
   // _isActiveCall[clientId][callIndex] shows the active status of the call.
   this._isActiveCall = {};
   this._numActiveCall = 0;
 
@@ -295,16 +342,20 @@ TelephonyService.prototype = {
       default:
         if (DEBUG) {
           debug("Unknown rilSuppSvcNotification: " + aNotification);
         }
         return;
     }
   },
 
+  _rulesToCallForwardingOptions: function(aRules) {
+    return aRules.map(rule => new CallForwardingOptions(rule));
+  },
+
   _updateDebugFlag: function() {
     try {
       DEBUG = RIL.DEBUG_RIL ||
               Services.prefs.getBoolPref(kPrefRilDebuggingEnabled);
     } catch (e) {}
   },
 
   _getDefaultServiceId: function() {
@@ -425,22 +476,23 @@ TelephonyService.prototype = {
                                      call.namePresentation, call.isOutgoing,
                                      call.isEmergency, call.isConference,
                                      call.isSwitchable, call.isMergeable);
       }
     }
     aListener.enumerateCallStateComplete();
   },
 
+  _hasCalls: function(aClientId) {
+    return Object.keys(this._currentCalls[aClientId]).length !== 0;
+  },
+
   _hasCallsOnOtherClient: function(aClientId) {
     for (let cid = 0; cid < this._numClients; ++cid) {
-      if (cid === aClientId) {
-        continue;
-      }
-      if (Object.keys(this._currentCalls[cid]).length !== 0) {
+      if (cid !== aClientId && this._hasCalls(cid)) {
         return true;
       }
     }
     return false;
   },
 
   // All calls in the conference is regarded as one conference call.
   _numCallsOnLine: function(aClientId) {
@@ -494,46 +546,61 @@ TelephonyService.prototype = {
     let parentCall = this._currentCalls[aClientId][childCall.parentId];
     parentCall.childId = CDMA_SECOND_CALL_INDEX;
     parentCall.state = RIL.CALL_STATE_HOLDING;
     parentCall.isSwitchable = false;
     parentCall.isMergeable = true;
     this.notifyCallStateChanged(aClientId, parentCall);
   },
 
-  _composeDialRequest: function(aClientId, aNumber) {
-    return new Promise((resolve, reject) => {
-      this._sendToRilWorker(aClientId, "parseMMIFromDialNumber",
-                            {number: aNumber}, response => {
-        let options = {};
-        let mmi = response.mmi;
-
-        if (!mmi) {
-          resolve({
-            number: aNumber
-          });
-        } else if (this._isTemporaryCLIR(mmi)) {
-          resolve({
-            number: mmi.dialNumber,
-            clirMode: this._getTemporaryCLIRMode(mmi.procedure)
-          });
-        } else {
-          reject(DIAL_ERROR_BAD_NUMBER);
-        }
-      });
-    });
-  },
-
-  cachedDialRequest: null,
-  isDialing: false,
-
   dial: function(aClientId, aNumber, aIsDialEmergency, aCallback) {
     if (DEBUG) debug("Dialing " + (aIsDialEmergency ? "emergency " : "") + aNumber);
 
-    if (this.isDialing) {
+    // We don't try to be too clever here, as the phone is probably in the
+    // locked state. Let's just check if it's a number without normalizing
+    if (!aIsDialEmergency) {
+      aNumber = gPhoneNumberUtils.normalize(aNumber);
+    }
+
+    // Validate the number.
+    // Note: isPlainPhoneNumber also accepts USSD and SS numbers
+    if (!gPhoneNumberUtils.isPlainPhoneNumber(aNumber)) {
+      if (DEBUG) debug("Error: Number '" + aNumber + "' is not viable. Drop.");
+      aCallback.notifyDialError(DIAL_ERROR_BAD_NUMBER);
+      return;
+    }
+
+    let mmi = this._parseMMI(aNumber, this._hasCalls(aClientId));
+    if (!mmi) {
+      this._dialCall(aClientId,
+                     { number: aNumber,
+                       isDialEmergency: aIsDialEmergency }, aCallback);
+    } else if (this._isTemporaryCLIR(mmi)) {
+      this._dialCall(aClientId,
+                     { number: mmi.dialNumber,
+                       clirMode: this._getTemporaryCLIRMode(mmi.procedure),
+                       isDialEmergency: aIsDialEmergency }, aCallback);
+    } else {
+      // Reject MMI code from dialEmergency api.
+      if (aIsDialEmergency) {
+        aCallback.notifyDialError(DIAL_ERROR_BAD_NUMBER);
+        return;
+      }
+
+      this._dialMMI(aClientId, mmi, aCallback);
+    }
+  },
+
+  /**
+   * @param aOptions.number
+   * @param aOptions.clirMode (optional)
+   * @param aOptions.isDialEmergency
+   */
+  _dialCall: function(aClientId, aOptions, aCallback) {
+    if (this._isDialing) {
       if (DEBUG) debug("Error: Already has a dialing call.");
       aCallback.notifyDialError(DIAL_ERROR_INVALID_STATE_ERROR);
       return;
     }
 
     // We can only have at most two calls on the same line (client).
     if (this._numCallsOnLine(aClientId) >= 2) {
       if (DEBUG) debug("Error: Already has more than 2 calls on line.");
@@ -544,92 +611,304 @@ TelephonyService.prototype = {
     // For DSDS, if there is aleady a call on SIM 'aClientId', we cannot place
     // any new call on other SIM.
     if (this._hasCallsOnOtherClient(aClientId)) {
       if (DEBUG) debug("Error: Already has a call on other sim.");
       aCallback.notifyDialError(DIAL_ERROR_OTHER_CONNECTION_IN_USE);
       return;
     }
 
-    // We don't try to be too clever here, as the phone is probably in the
-    // locked state. Let's just check if it's a number without normalizing
-    if (!aIsDialEmergency) {
-      aNumber = gPhoneNumberUtils.normalize(aNumber);
-    }
-
-    // Validate the number.
-    // Note: isPlainPhoneNumber also accepts USSD and SS numbers
-    if (!gPhoneNumberUtils.isPlainPhoneNumber(aNumber)) {
-      if (DEBUG) debug("Error: Number '" + aNumber + "' is not viable. Drop.");
-      aCallback.notifyDialError(DIAL_ERROR_BAD_NUMBER);
-      return;
+    aOptions.isEmergency = this._isEmergencyNumber(aOptions.number);
+    if (aOptions.isEmergency) {
+      // Automatically select a proper clientId for emergency call.
+      aClientId = gRadioInterfaceLayer.getClientIdForEmergencyCall() ;
+      if (aClientId === -1) {
+        if (DEBUG) debug("Error: No client is avaialble for emergency call.");
+        aCallback.notifyDialError(DIAL_ERROR_INVALID_STATE_ERROR);
+        return;
+      }
     }
 
-    this._composeDialRequest(aClientId, aNumber).then(options => {
-      options.isEmergency = this._isEmergencyNumber(options.number);
-      options.isDialEmergency = aIsDialEmergency;
-
-      if (options.isEmergency) {
-        // Automatically select a proper clientId for emergency call.
-        aClientId = gRadioInterfaceLayer.getClientIdForEmergencyCall() ;
-        if (aClientId === -1) {
-          if (DEBUG) debug("Error: No client is avaialble for emergency call.");
-          aCallback.notifyDialError(DIAL_ERROR_INVALID_STATE_ERROR);
-          return;
-        }
-      }
+    // Before we dial, we have to hold the active call first.
+    let activeCall = this._getOneActiveCall(aClientId);
+    if (!activeCall) {
+      this._sendDialCallRequest(aClientId, aOptions, aCallback);
+    } else {
+      if (DEBUG) debug("There is an active call. Hold it first before dial.");
 
-      // Before we dial, we have to hold the active call first.
-      let activeCall = this._getOneActiveCall(aClientId);
-      if (!activeCall) {
-        this._dialInternal(aClientId, options, aCallback);
-      } else {
-        if (DEBUG) debug("There is an active call. Hold it first before dial.");
+      this._cachedDialRequest = {
+        clientId: aClientId,
+        options: aOptions,
+        callback: aCallback
+      };
 
-        this.cachedDialRequest = {
-          clientId: aClientId,
-          options: options,
-          callback: aCallback
-        };
-
-        if (activeCall.isConference) {
-          this.holdConference(aClientId);
-        } else {
-          this.holdCall(aClientId, activeCall.callIndex);
-        }
+      if (activeCall.isConference) {
+        this.holdConference(aClientId);
+      } else {
+        this.holdCall(aClientId, activeCall.callIndex);
       }
-    }, cause => {
-      aCallback.notifyDialError(DIAL_ERROR_BAD_NUMBER);
-    });
+    }
   },
 
-  _dialInternal: function(aClientId, aOptions, aCallback) {
-    this.isDialing = true;
+  _sendDialCallRequest: function(aClientId, aOptions, aCallback) {
+    this._isDialing = true;
 
     this._sendToRilWorker(aClientId, "dial", aOptions, response => {
-      this.isDialing = false;
+      this._isDialing = false;
 
       if (!response.success) {
         aCallback.notifyDialError(response.errorMsg);
         return;
       }
 
       let currentCdmaCallIndex = !response.isCdma ? null :
         Object.keys(this._currentCalls[aClientId])[0];
 
       if (currentCdmaCallIndex == null) {
-        aCallback.notifyDialSuccess(response.callIndex, response.number);
+        aCallback.notifyDialCallSuccess(response.callIndex, response.number);
       } else {
         // RIL doesn't hold the 2nd call. We create one by ourselves.
-        aCallback.notifyDialSuccess(CDMA_SECOND_CALL_INDEX, response.number);
+        aCallback.notifyDialCallSuccess(CDMA_SECOND_CALL_INDEX, response.number);
         this._addCdmaChildCall(aClientId, response.number, currentCdmaCallIndex);
       }
     });
   },
 
+  /**
+   * @param aMmi
+   *        Parsed MMI structure.
+   */
+  _dialMMI: function(aClientId, aMmi, aCallback) {
+    let mmiServiceCode = aMmi ?
+      this._serviceCodeToKeyString(aMmi.serviceCode) : RIL.MMI_KS_SC_USSD;
+
+    aCallback.notifyDialMMI(mmiServiceCode);
+
+    this._sendToRilWorker(aClientId, "sendMMI", { mmi: aMmi }, response => {
+      if (DEBUG) debug("MMI response: " + JSON.stringify(response));
+
+      if (!response.success) {
+        if (response.additionalInformation != null) {
+          aCallback.notifyDialMMIErrorWithInfo(response.errorMsg,
+                                               response.additionalInformation);
+        } else {
+          aCallback.notifyDialMMIError(response.errorMsg);
+        }
+        return;
+      }
+
+      // We expect to have an IMEI at this point if the request was supposed
+      // to query for the IMEI, so getting a successful reply from the RIL
+      // without containing an actual IMEI number is considered an error.
+      if (mmiServiceCode === RIL.MMI_KS_SC_IMEI &&
+          !response.statusMessage) {
+        aCallback.notifyDialMMIError(RIL.GECKO_ERROR_GENERIC_FAILURE);
+        return;
+      }
+
+      // MMI query call forwarding options request returns a set of rules that
+      // will be exposed in the form of an array of MozCallForwardingOptions
+      // instances.
+      if (mmiServiceCode === RIL.MMI_KS_SC_CALL_FORWARDING) {
+        if (response.isSetCallForward) {
+          gGonkMobileConnectionService.notifyCFStateChanged(aClientId,
+                                                            response.action,
+                                                            response.reason,
+                                                            response.number,
+                                                            response.timeSeconds,
+                                                            response.serviceClass);
+        }
+
+        if (response.additionalInformation != null) {
+          response.additionalInformation =
+            this._rulesToCallForwardingOptions(response.additionalInformation);
+        }
+      }
+
+      let result = new MMIResult(mmiServiceCode, response);
+      aCallback.notifyDialMMISuccess(result);
+    });
+  },
+
+  /**
+   * Build the regex to parse MMI string. TS.22.030
+   *
+   * The resulting groups after matching will be:
+   *    1 = full MMI string that might be used as a USSD request.
+   *    2 = MMI procedure.
+   *    3 = Service code.
+   *    4 = SIA.
+   *    5 = SIB.
+   *    6 = SIC.
+   *    7 = Password registration.
+   *    8 = Dialing number.
+   */
+  _buildMMIRegExp: function() {
+    // The general structure of the codes is as follows:
+    //    - Activation (*SC*SI#).
+    //    - Deactivation (#SC*SI#).
+    //    - Interrogation (*#SC*SI#).
+    //    - Registration (**SC*SI#).
+    //    - Erasure (##SC*SI#).
+    //
+    // where SC = Service Code (2 or 3 digits) and SI = Supplementary Info
+    // (variable length).
+
+    // Procedure, which could be *, #, *#, **, ##
+    let procedure = "(\\*[*#]?|##?)";
+
+    // Service code, which is a 2 or 3 digits that uniquely specifies the
+    // Supplementary Service associated with the MMI code.
+    let serviceCode = "(\\d{2,3})";
+
+    // Supplementary Information SIA, SIB and SIC. SIA may comprise e.g. a PIN
+    // code or Directory Number, SIB may be used to specify the tele or bearer
+    // service and SIC to specify the value of the "No Reply Condition  Timer".
+    // Where a particular service request does not require any SI,  "*SI" is
+    // not entered. The use of SIA, SIB and SIC is optional and shall be
+    // entered in any of the following formats:
+    //    - *SIA*SIB*SIC#
+    //    - *SIA*SIB#
+    //    - *SIA**SIC#
+    //    - *SIA#
+    //    - **SIB*SIC#
+    //    - ***SIC#
+    //
+    // Also catch the additional NEW_PASSWORD for the case of a password
+    // registration procedure. Ex:
+    //    - *  03 * ZZ * OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
+    //    - ** 03 * ZZ * OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
+    //    - *  03 **     OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
+    //    - ** 03 **     OLD_PASSWORD * NEW_PASSWORD * NEW_PASSWORD #
+    let si = "\\*([^*#]*)";
+    let allSi = "";
+    for (let i = 0; i < 4; ++i) {
+      allSi = "(?:" + si + allSi + ")?";
+    }
+
+    let fullmmi = "(" + procedure + serviceCode + allSi + "#)";
+
+    // Dial string after the #.
+    let dialString = "([^#]*)";
+
+    return new RegExp(fullmmi + dialString);
+  },
+
+  /**
+   * Provide the regex to parse MMI string.
+   */
+  _getMMIRegExp: function() {
+    if (!this._mmiRegExp) {
+      this._mmiRegExp = this._buildMMIRegExp();
+    }
+
+    return this._mmiRegExp;
+  },
+
+  /**
+   * Helper to parse # string. TS.22.030 Figure 3.5.3.2.
+   */
+  _isPoundString: function(aMmiString) {
+    return (aMmiString.charAt(aMmiString.length - 1) === "#");
+  },
+
+  /**
+   * Helper to parse short string. TS.22.030 Figure 3.5.3.2.
+   */
+  _isShortString: function(aMmiString, hasCalls) {
+    if (aMmiString.length > 2) {
+      return false;
+    }
+
+    if (hasCalls) {
+      return true;
+    }
+
+    // Input string is
+    //   - emergency number or
+    //   - 2 digits starting with a "1"
+    if (this._isEmergencyNumber(aMmiString) ||
+        (aMmiString.length == 2) && (aMmiString.charAt(0) === '1')) {
+      return false;
+    }
+
+    return true;
+  },
+
+  /**
+   * Helper to parse MMI/USSD string. TS.22.030 Figure 3.5.3.2.
+   */
+  _parseMMI: function(aMmiString, hasCalls) {
+    if (!aMmiString) {
+      return null;
+    }
+
+    let matches = this._getMMIRegExp().exec(aMmiString);
+    if (matches) {
+      return {
+        fullMMI: matches[MMI_MATCH_GROUP_FULL_MMI],
+        procedure: matches[MMI_MATCH_GROUP_PROCEDURE],
+        serviceCode: matches[MMI_MATCH_GROUP_SERVICE_CODE],
+        sia: matches[MMI_MATCH_GROUP_SIA],
+        sib: matches[MMI_MATCH_GROUP_SIB],
+        sic: matches[MMI_MATCH_GROUP_SIC],
+        pwd: matches[MMI_MATCH_GROUP_PWD_CONFIRM],
+        dialNumber: matches[MMI_MATCH_GROUP_DIALING_NUMBER]
+      };
+    }
+
+    if (this._isPoundString(aMmiString) ||
+        this._isShortString(aMmiString, hasCalls)) {
+      return {
+        fullMMI: aMmiString
+      };
+    }
+
+    return null;
+  },
+
+  _serviceCodeToKeyString: function(aServiceCode) {
+    switch (aServiceCode) {
+      case RIL.MMI_SC_CFU:
+      case RIL.MMI_SC_CF_BUSY:
+      case RIL.MMI_SC_CF_NO_REPLY:
+      case RIL.MMI_SC_CF_NOT_REACHABLE:
+      case RIL.MMI_SC_CF_ALL:
+      case RIL.MMI_SC_CF_ALL_CONDITIONAL:
+        return RIL.MMI_KS_SC_CALL_FORWARDING;
+      case RIL.MMI_SC_PIN:
+        return RIL.MMI_KS_SC_PIN;
+      case RIL.MMI_SC_PIN2:
+        return RIL.MMI_KS_SC_PIN2;
+      case RIL.MMI_SC_PUK:
+        return RIL.MMI_KS_SC_PUK;
+      case RIL.MMI_SC_PUK2:
+        return RIL.MMI_KS_SC_PUK2;
+      case RIL.MMI_SC_IMEI:
+        return RIL.MMI_KS_SC_IMEI;
+      case RIL.MMI_SC_CLIP:
+        return RIL.MMI_KS_SC_CLIP;
+      case RIL.MMI_SC_CLIR:
+        return RIL.MMI_KS_SC_CLIR;
+      case RIL.MMI_SC_BAOC:
+      case RIL.MMI_SC_BAOIC:
+      case RIL.MMI_SC_BAOICxH:
+      case RIL.MMI_SC_BAIC:
+      case RIL.MMI_SC_BAICr:
+      case RIL.MMI_SC_BA_ALL:
+      case RIL.MMI_SC_BA_MO:
+      case RIL.MMI_SC_BA_MT:
+        return RIL.MMI_KS_SC_CALL_BARRING;
+      case RIL.MMI_SC_CALL_WAITING:
+        return RIL.MMI_KS_SC_CALL_WAITING;
+      default:
+        return RIL.MMI_KS_SC_USSD;
+    }
+  },
+
   hangUp: function(aClientId, aCallIndex) {
     let parentId = this._currentCalls[aClientId][aCallIndex].parentId;
     if (parentId) {
       // Should release both, child and parent, together. Since RIL holds only
       // the parent call, we send 'parentId' to RIL.
       this.hangUp(aClientId, parentId);
     } else {
       this._sendToRilWorker(aClientId, "hangUp", { callIndex: aCallIndex });
@@ -927,22 +1206,22 @@ TelephonyService.prototype = {
       call.name = pick(aCall.name, "");
       call.numberPresentaation = pick(aCall.numberPresentation, nsITelephonyService.CALL_PRESENTATION_ALLOWED);
       call.namePresentaation = pick(aCall.namePresentation, nsITelephonyService.CALL_PRESENTATION_ALLOWED);
 
       this._currentCalls[aClientId][aCall.callIndex] = call;
     }
 
     // Handle cached dial request.
-    if (this.cachedDialRequest && !this._getOneActiveCall()) {
+    if (this._cachedDialRequest && !this._getOneActiveCall()) {
       if (DEBUG) debug("All calls held. Perform the cached dial request.");
 
-      let request = this.cachedDialRequest;
-      this._dialInternal(request.clientId, request.options, request.callback);
-      this.cachedDialRequest = null;
+      let request = this._cachedDialRequest;
+      this._sendDialCallRequest(request.clientId, request.options, request.callback);
+      this._cachedDialRequest = null;
     }
 
     this._notifyAllListeners("callStateChanged", [aClientId,
                                                   call.callIndex,
                                                   call.state,
                                                   call.number,
                                                   call.numberPresentation,
                                                   call.name,
@@ -982,16 +1261,21 @@ TelephonyService.prototype = {
   },
 
   notifyConferenceCallStateChanged: function(aState) {
     if (DEBUG) debug("handleConferenceCallStateChanged: " + aState);
     aState = this._convertRILCallState(aState);
     this._notifyAllListeners("conferenceCallStateChanged", [aState]);
   },
 
+  dialMMI: function(aClientId, aMmiString, aCallback) {
+    let mmi = this._parseMMI(aMmiString, this._hasCalls(aClientId));
+    this._dialMMI(aClientId, mmi, aCallback);
+  },
+
   /**
    * nsIObserver interface.
    */
 
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
       case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
         if (aData === kPrefRilDebuggingEnabled) {
--- a/dom/telephony/ipc/PTelephonyRequest.ipdl
+++ b/dom/telephony/ipc/PTelephonyRequest.ipdl
@@ -11,37 +11,57 @@ namespace mozilla {
 namespace dom {
 namespace telephony {
 
 struct EnumerateCallsResponse
 {
   // empty.
 };
 
-struct DialResponse
+struct DialResponseError
+{
+  nsString name;
+};
+
+struct DialResponseCallSuccess
 {
-  // empty.
+  uint32_t callIndex;
+  nsString number;
+};
+
+struct DialResponseMMISuccess
+{
+  nsString statusMessage;
+  AdditionalInformation additionalInformation;
+};
+
+struct DialResponseMMIError
+{
+  nsString name;
+  AdditionalInformation additionalInformation;
 };
 
 union IPCTelephonyResponse
 {
   EnumerateCallsResponse;
-  DialResponse;
+  // dial
+  DialResponseError;
+  DialResponseCallSuccess;
+  DialResponseMMISuccess;
+  DialResponseMMIError;
 };
 
 protocol PTelephonyRequest
 {
   manager PTelephony;
 
 child:
   NotifyEnumerateCallState(uint32_t aClientId, IPCCallStateData aData);
 
-  NotifyDialError(nsString aError);
-
-  NotifyDialSuccess(uint32_t aCallIndex, nsString aNumber);
+  NotifyDialMMI(nsString aServiceCode);
 
   /**
    * Sent when the asynchronous request has completed.
    */
   __delete__(IPCTelephonyResponse aResponse);
 };
 
 } /* namespace telephony */
--- a/dom/telephony/ipc/TelephonyChild.cpp
+++ b/dom/telephony/ipc/TelephonyChild.cpp
@@ -1,14 +1,16 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "TelephonyChild.h"
+
+#include "mozilla/dom/telephony/TelephonyCallback.h"
 #include "TelephonyIPCService.h"
 
 USING_TELEPHONY_NAMESPACE
 
 /*******************************************************************************
  * TelephonyChild
  ******************************************************************************/
 
@@ -140,19 +142,24 @@ TelephonyRequestChild::ActorDestroy(Acto
 
 bool
 TelephonyRequestChild::Recv__delete__(const IPCTelephonyResponse& aResponse)
 {
   switch (aResponse.type()) {
     case IPCTelephonyResponse::TEnumerateCallsResponse:
       mListener->EnumerateCallStateComplete();
       break;
-    case IPCTelephonyResponse::TDialResponse:
-      // Do nothing.
-      break;
+    case IPCTelephonyResponse::TDialResponseError:
+      return DoResponse(aResponse.get_DialResponseError());
+    case IPCTelephonyResponse::TDialResponseCallSuccess:
+      return DoResponse(aResponse.get_DialResponseCallSuccess());
+    case IPCTelephonyResponse::TDialResponseMMISuccess:
+      return DoResponse(aResponse.get_DialResponseMMISuccess());
+    case IPCTelephonyResponse::TDialResponseMMIError:
+      return DoResponse(aResponse.get_DialResponseMMIError());
     default:
       MOZ_CRASH("Unknown type!");
   }
 
   return true;
 }
 
 bool
@@ -172,25 +179,84 @@ TelephonyRequestChild::RecvNotifyEnumera
                                 aData.isEmergency(),
                                 aData.isConference(),
                                 aData.isSwitchable(),
                                 aData.isMergeable());
   return true;
 }
 
 bool
-TelephonyRequestChild::RecvNotifyDialError(const nsString& aError)
+TelephonyRequestChild::RecvNotifyDialMMI(const nsString& aServiceCode)
 {
   MOZ_ASSERT(mCallback);
 
-  mCallback->NotifyDialError(aError);
+  mCallback->NotifyDialMMI(aServiceCode);
+  return true;
+}
+
+bool
+TelephonyRequestChild::DoResponse(const DialResponseError& aResponse)
+{
+  MOZ_ASSERT(mCallback);
+  mCallback->NotifyDialError(aResponse.name());
+  return true;
+}
+
+bool
+TelephonyRequestChild::DoResponse(const DialResponseCallSuccess& aResponse)
+{
+  MOZ_ASSERT(mCallback);
+  mCallback->NotifyDialCallSuccess(aResponse.callIndex(), aResponse.number());
   return true;
 }
 
 bool
-TelephonyRequestChild::RecvNotifyDialSuccess(const uint32_t& aCallIndex,
-                                             const nsString& aNumber)
+TelephonyRequestChild::DoResponse(const DialResponseMMISuccess& aResponse)
 {
   MOZ_ASSERT(mCallback);
 
-  mCallback->NotifyDialSuccess(aCallIndex, aNumber);
+  // FIXME: Need to overload NotifyDialMMISuccess in the IDL. mCallback is not
+  // necessarily an instance of TelephonyCallback.
+  nsRefPtr<TelephonyCallback> callback = static_cast<TelephonyCallback*>(mCallback.get());
+
+  nsAutoString statusMessage(aResponse.statusMessage());
+  AdditionalInformation info(aResponse.additionalInformation());
+
+  switch (info.type()) {
+    case AdditionalInformation::Tvoid_t:
+      callback->NotifyDialMMISuccess(statusMessage);
+      break;
+    case AdditionalInformation::TArrayOfnsString:
+      callback->NotifyDialMMISuccess(statusMessage, info.get_ArrayOfnsString());
+      break;
+    case AdditionalInformation::TArrayOfMozCallForwardingOptions:
+      callback->NotifyDialMMISuccess(statusMessage, info.get_ArrayOfMozCallForwardingOptions());
+      break;
+    default:
+      MOZ_CRASH("Received invalid type!");
+      break;
+  }
+
   return true;
 }
+
+bool
+TelephonyRequestChild::DoResponse(const DialResponseMMIError& aResponse)
+{
+  MOZ_ASSERT(mCallback);
+
+  nsAutoString name(aResponse.name());
+  AdditionalInformation info(aResponse.additionalInformation());
+
+  switch (info.type()) {
+    case AdditionalInformation::Tvoid_t:
+      mCallback->NotifyDialMMIError(name);
+      break;
+    case AdditionalInformation::Tuint16_t:
+      mCallback->NotifyDialMMIErrorWithInfo(name, info.get_uint16_t());
+      break;
+    default:
+      MOZ_CRASH("Received invalid type!");
+      break;
+  }
+
+  return true;
+}
--- a/dom/telephony/ipc/TelephonyChild.h
+++ b/dom/telephony/ipc/TelephonyChild.h
@@ -75,22 +75,30 @@ protected:
   virtual bool
   Recv__delete__(const IPCTelephonyResponse& aResponse) MOZ_OVERRIDE;
 
   virtual bool
   RecvNotifyEnumerateCallState(const uint32_t& aClientId,
                                const IPCCallStateData& aData) MOZ_OVERRIDE;
 
   virtual bool
-  RecvNotifyDialError(const nsString& aError) MOZ_OVERRIDE;
-
-  virtual bool
-  RecvNotifyDialSuccess(const uint32_t& aCallIndex,
-                        const nsString& aNumber) MOZ_OVERRIDE;
+  RecvNotifyDialMMI(const nsString& aServiceCode) MOZ_OVERRIDE;
 
 private:
+  bool
+  DoResponse(const DialResponseError& aResponse);
+
+  bool
+  DoResponse(const DialResponseCallSuccess& aResponse);
+
+  bool
+  DoResponse(const DialResponseMMISuccess& aResponse);
+
+  bool
+  DoResponse(const DialResponseMMIError& aResponse);
+
   nsCOMPtr<nsITelephonyListener> mListener;
   nsCOMPtr<nsITelephonyCallback> mCallback;
 };
 
 END_TELEPHONY_NAMESPACE
 
 #endif // mozilla_dom_telephony_TelephonyChild_h
--- a/dom/telephony/ipc/TelephonyParent.cpp
+++ b/dom/telephony/ipc/TelephonyParent.cpp
@@ -429,16 +429,24 @@ TelephonyRequestParent::DoRequest(const 
                    aRequest.isEmergency(), this);
   } else {
     return NS_SUCCEEDED(NotifyDialError(NS_LITERAL_STRING("InvalidStateError")));
   }
 
   return true;
 }
 
+nsresult
+TelephonyRequestParent::SendResponse(const IPCTelephonyResponse& aResponse)
+{
+  NS_ENSURE_TRUE(!mActorDestroyed, NS_ERROR_FAILURE);
+
+  return Send__delete__(this, aResponse) ? NS_OK : NS_ERROR_FAILURE;
+}
+
 // nsITelephonyListener
 
 NS_IMETHODIMP
 TelephonyRequestParent::CallStateChanged(uint32_t aClientId,
                                          uint32_t aCallIndex,
                                          uint16_t aCallState,
                                          const nsAString& aNumber,
                                          uint16_t aNumberPresentation,
@@ -522,25 +530,110 @@ TelephonyRequestParent::SupplementarySer
                                                          uint16_t aNotification)
 {
   MOZ_CRASH("Not a TelephonyParent!");
 }
 
 // nsITelephonyCallback
 
 NS_IMETHODIMP
-TelephonyRequestParent::NotifyDialError(const nsAString& aError)
+TelephonyRequestParent::NotifyDialMMI(const nsAString& aServiceCode)
 {
   NS_ENSURE_TRUE(!mActorDestroyed, NS_ERROR_FAILURE);
 
-  return (SendNotifyDialError(nsString(aError)) &&
-          Send__delete__(this, DialResponse())) ? NS_OK : NS_ERROR_FAILURE;
+  return SendNotifyDialMMI(nsAutoString(aServiceCode)) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelephonyRequestParent::NotifyDialError(const nsAString& aError)
+{
+  return SendResponse(DialResponseError(nsAutoString(aError)));
+}
+
+NS_IMETHODIMP
+TelephonyRequestParent::NotifyDialCallSuccess(uint32_t aCallIndex,
+                                              const nsAString& aNumber)
+{
+  return SendResponse(DialResponseCallSuccess(aCallIndex, nsAutoString(aNumber)));
 }
 
 NS_IMETHODIMP
-TelephonyRequestParent::NotifyDialSuccess(uint32_t aCallIndex,
-                                          const nsAString& aNumber)
+TelephonyRequestParent::NotifyDialMMISuccess(JS::Handle<JS::Value> aResult)
 {
-  NS_ENSURE_TRUE(!mActorDestroyed, NS_ERROR_FAILURE);
+  AutoSafeJSContext cx;
+  RootedDictionary<MozMMIResult> result(cx);
+
+  if (!result.Init(cx, aResult)) {
+    return NS_ERROR_TYPE_ERR;
+  }
+
+  // No additionInformation passed
+  if (!result.mAdditionalInformation.WasPassed()) {
+    return SendResponse(DialResponseMMISuccess(result.mStatusMessage,
+                                               AdditionalInformation(mozilla::void_t())));
+  }
+
+  OwningUnsignedShortOrObject& info = result.mAdditionalInformation.Value();
+
+  // Currently, we could only accept the following values for |info|:
+  //   1. array of string
+  //   2. array of MozCallForwardingOptions
+  if (!info.IsObject()) {
+    return NS_ERROR_TYPE_ERR;
+  }
+
+  JS::Rooted<JSObject*> object(cx, info.GetAsObject());
+  JS::Rooted<JS::Value> value(cx);
+  uint32_t length;
+
+  if (!JS_IsArrayObject(cx, object) ||
+      !JS_GetArrayLength(cx, object, &length) || length <= 0 ||
+      // Check first element to decide the format of array.
+      !JS_GetElement(cx, object, 0, &value)) {
+    return NS_ERROR_TYPE_ERR;
+  }
+
+  if (value.isString()) {
+    // String[]
+    nsTArray<nsString> infos;
 
-  return (SendNotifyDialSuccess(aCallIndex, nsString(aNumber)) &&
-          Send__delete__(this, DialResponse())) ? NS_OK : NS_ERROR_FAILURE;
+    for (uint32_t i = 0; i < length; i++) {
+      nsAutoJSString str;
+      if (!JS_GetElement(cx, object, i, &value) || !value.isString() ||
+          !str.init(cx, value.toString())) {
+        return NS_ERROR_TYPE_ERR;
+      }
+      infos.AppendElement(str);
+    }
+
+    return SendResponse(DialResponseMMISuccess(result.mStatusMessage,
+                                               AdditionalInformation(infos)));
+  } else {
+    // IPC::MozCallForwardingOptions[]
+    nsTArray<IPC::MozCallForwardingOptions> infos;
+
+    for (uint32_t i = 0; i < length; i++) {
+      IPC::MozCallForwardingOptions info;
+      if (!JS_GetElement(cx, object, i, &value) || !info.Init(cx, value)) {
+        return NS_ERROR_TYPE_ERR;
+      }
+      infos.AppendElement(info);
+    }
+
+    return SendResponse(DialResponseMMISuccess(result.mStatusMessage,
+                                               AdditionalInformation(infos)));
+  }
 }
+
+NS_IMETHODIMP
+TelephonyRequestParent::NotifyDialMMIError(const nsAString& aError)
+{
+  return SendResponse(DialResponseMMIError(nsAutoString(aError),
+                                           AdditionalInformation(mozilla::void_t())));
+}
+
+NS_IMETHODIMP
+TelephonyRequestParent::NotifyDialMMIErrorWithInfo(const nsAString& aError,
+                                                   uint16_t aInfo)
+{
+  return SendResponse(DialResponseMMIError(nsAutoString(aError),
+                                           AdditionalInformation(aInfo)));
+}
--- a/dom/telephony/ipc/TelephonyParent.h
+++ b/dom/telephony/ipc/TelephonyParent.h
@@ -109,16 +109,19 @@ public:
 
 protected:
   TelephonyRequestParent();
   virtual ~TelephonyRequestParent() {}
 
   virtual void
   ActorDestroy(ActorDestroyReason why);
 
+  nsresult
+  SendResponse(const IPCTelephonyResponse& aResponse);
+
 private:
   bool mActorDestroyed;
 
   bool
   DoRequest(const EnumerateCallsRequest& aRequest);
 
   bool
   DoRequest(const DialRequest& aRequest);
--- a/dom/telephony/ipc/TelephonyTypes.ipdlh
+++ b/dom/telephony/ipc/TelephonyTypes.ipdlh
@@ -1,14 +1,17 @@
 /* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
+using struct mozilla::void_t from "ipc/IPCMessageUtils.h";
+using struct IPC::MozCallForwardingOptions from "mozilla/dom/mobileconnection/MobileConnectionIPCSerializer.h";
+
 namespace mozilla {
 namespace dom {
 namespace telephony {
 
 struct IPCCallStateData
 {
   uint32_t callIndex;
   uint16_t callState;
@@ -26,11 +29,18 @@ struct IPCCallStateData
 struct IPCCdmaWaitingCallData
 {
   nsString number;
   uint16_t numberPresentation;
   nsString name;
   uint16_t namePresentation;
 };
 
+union AdditionalInformation {
+  void_t;
+  uint16_t;
+  nsString[];
+  MozCallForwardingOptions[];
+};
+
 } /* namespace telephony */
 } /* namespace dom */
 } /* namespace mozilla */
--- a/dom/telephony/moz.build
+++ b/dom/telephony/moz.build
@@ -16,37 +16,40 @@ EXPORTS.mozilla.dom += [
     'TelephonyCall.h',
     'TelephonyCallGroup.h',
     'TelephonyCallId.h',
 ]
 
 EXPORTS.mozilla.dom.telephony += [
     'ipc/TelephonyChild.h',
     'ipc/TelephonyParent.h',
+    'TelephonyCallback.h',
     'TelephonyCommon.h',
 ]
 
 UNIFIED_SOURCES += [
     'CallsList.cpp',
     'ipc/TelephonyChild.cpp',
     'ipc/TelephonyIPCService.cpp',
     'ipc/TelephonyParent.cpp',
     'Telephony.cpp',
     'TelephonyCall.cpp',
+    'TelephonyCallback.cpp',
     'TelephonyCallGroup.cpp',
     'TelephonyCallId.cpp',
 ]
 
 IPDL_SOURCES += [
     'ipc/PTelephony.ipdl',
     'ipc/PTelephonyRequest.ipdl',
     'ipc/TelephonyTypes.ipdlh'
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk' and CONFIG['MOZ_B2G_RIL']:
+    XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
     XPIDL_SOURCES += [
         'nsIGonkTelephonyService.idl',
     ]
     EXTRA_COMPONENTS += [
         'gonk/TelephonyService.js',
         'gonk/TelephonyService.manifest',
     ]
 
--- a/dom/telephony/nsIGonkTelephonyService.idl
+++ b/dom/telephony/nsIGonkTelephonyService.idl
@@ -21,9 +21,12 @@ interface nsIGonkTelephonyService : nsIT
                               [optional] in boolean skipStateConversion);
 
   void notifyCdmaCallWaiting(in unsigned long clientId, in jsval waitingCall);
 
   void notifySupplementaryService(in unsigned long clientId, in long callIndex,
                                   in AString notification);
 
   void notifyConferenceCallStateChanged(in short state);
+
+  void dialMMI(in unsigned long clientId, in AString mmiString,
+               in nsITelephonyCallback callback);
 };
--- a/dom/telephony/nsITelephonyService.idl
+++ b/dom/telephony/nsITelephonyService.idl
@@ -171,30 +171,65 @@ interface nsITelephonyListener : nsISupp
    *        Error name. Possible values are addError and removeError.
    * @param message
    *        Detailed error message from RIL.
    */
   void notifyConferenceError(in AString name,
                              in AString message);
 };
 
-[scriptable, uuid(b3b2b0b0-357f-4efb-bc9f-ca2b2d5686a1)]
+/**
+ * A callback interface for handling asynchronous response of Telephony.dial().
+ */
+[scriptable, uuid(67533db3-cd38-475c-a774-8d0bbf9169fb)]
 interface nsITelephonyCallback : nsISupports
 {
   /**
+   * Called when a dial request is treated as an MMI code and it is about to
+   * process the request.
+   *
+   * @param serviceCode
+   *        MMI service code key string that defined in MMI_KS_SC_*
+   */
+  void notifyDialMMI(in AString serviceCode);
+
+  /**
    * Called when a dial request fails.
+   *
    * @param error
    *        Error from RIL.
    */
   void notifyDialError(in AString error);
 
   /**
-   * Called when a dial request succeeds.
+   * Called when a dial request is treated as a call setup and the result
+   * succeeds.
+   *
+   * @param callIndex
+   *        Call index from RIL.
+   * @param number
+   *        Dialed out phone number (ex: Temporary CLIR prefix will be removed)
    */
-  void notifyDialSuccess(in unsigned long callIndex, in AString number);
+  void notifyDialCallSuccess(in unsigned long callIndex, in AString number);
+
+  /**
+   * Called when a MMI code request succeeds.
+   * The function should only be called after notifyDialMMI.
+   *
+   * @param result
+   *        Result of the request. See MozMMIResult.
+   */
+  void notifyDialMMISuccess(in jsval result);
+
+  /**
+   * Called when a MMI code request fails.
+   * The function should only be called after notifyDialMMI.
+   */
+  void notifyDialMMIError(in AString error);
+  void notifyDialMMIErrorWithInfo(in AString error, in unsigned short info);
 };
 
 %{C++
 #define TELEPHONY_SERVICE_CID \
   { 0x9cf8aa52, 0x7c1c, 0x4cde, { 0x97, 0x4e, 0xed, 0x2a, 0xa0, 0xe7, 0x35, 0xfa } }
 #define TELEPHONY_SERVICE_CONTRACTID \
   "@mozilla.org/telephony/telephonyservice;1"
 %}
--- a/dom/telephony/test/marionette/head.js
+++ b/dom/telephony/test/marionette/head.js
@@ -1297,8 +1297,30 @@ function startDSDSTest(test) {
   if (numRIL > 1) {
     startTest(test);
   } else {
     log("Not a DSDS environment. Test is skipped.");
     ok(true);  // We should run at least one test.
     finish();
   }
 }
+
+function sendMMI(aMmi) {
+  let deferred = Promise.defer();
+
+  telephony.dial(aMmi)
+    .then(request => {
+      ok(request instanceof DOMRequest,
+         "request is instanceof " + request.constructor);
+
+      request.addEventListener("success", function(event) {
+        deferred.resolve(request.result);
+      });
+
+      request.addEventListener("error", function(event) {
+        deferred.reject(request.error);
+      });
+    }, cause => {
+      deferred.reject(cause);
+    });
+
+  return deferred.promise;
+}
--- a/dom/telephony/test/marionette/manifest.ini
+++ b/dom/telephony/test/marionette/manifest.ini
@@ -58,10 +58,12 @@ disabled = Bug 821958
 [test_conference_three_hangup_one.js]
 [test_conference_three_remove_one.js]
 [test_conference_add_twice_error.js]
 [test_outgoing_when_two_calls_on_line.js]
 [test_call_presentation.js]
 [test_incomingcall_phonestate_speaker.js]
 [test_temporary_clir.js]
 [test_outgoing_error_state.js]
-[test_mmi_code.js]
 [test_outgoing_auto_hold.js]
+[test_mmi.js]
+[test_mmi_change_pin.js]
+[test_mmi_call_forwarding.js]
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/marionette/test_mmi.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = "head.js";
+
+function testGettingIMEI() {
+  log("Test *#06# ...");
+
+  let MMI_CODE = "*#06#";
+  return sendMMI(MMI_CODE)
+    .then(function resolve(aResult) {
+      ok(true, MMI_CODE + " success");
+      is(aResult.serviceCode, "scImei", "Service code IMEI");
+      // IMEI is hardcoded as "000000000000000".
+      // See it here {B2G_HOME}/external/qemu/telephony/android_modem.c
+      // (The result of +CGSN).
+      is(aResult.statusMessage, "000000000000000", "Emulator IMEI");
+      is(aResult.additionalInformation, undefined, "No additional information");
+    }, function reject() {
+      ok(false, MMI_CODE + " should not fail");
+    });
+}
+
+// Start test
+startTest(function() {
+  Promise.resolve()
+    .then(() => testGettingIMEI())
+    .then(finish);
+});
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/marionette/test_mmi_call_forwarding.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = "head.js";
+
+let connection;
+
+/**
+ * Wait for one named MobileConnection event.
+ *
+ * Resolve if that named event occurs.  Never reject.
+ *
+ * Fulfill params: the DOMEvent passed.
+ *
+ * @param aEventName
+ *        A string event name.
+ *
+ * @return A deferred promise.
+ */
+function waitForManagerEvent(aEventName) {
+  let deferred = Promise.defer();
+
+  connection.addEventListener(aEventName, function onevent(aEvent) {
+    connection.removeEventListener(aEventName, onevent);
+
+    ok(true, "MobileConnection event '" + aEventName + "' got.");
+    deferred.resolve(aEvent);
+  });
+
+  return deferred.promise;
+}
+
+/**
+ * Wrap DOMRequest onsuccess/onerror events to Promise resolve/reject.
+ *
+ * Fulfill params: A DOMEvent.
+ * Reject params: A DOMEvent.
+ *
+ * @param aRequest
+ *        A DOMRequest instance.
+ *
+ * @return A deferred promise.
+ */
+function wrapDomRequestAsPromise(aRequest) {
+  let deferred = Promise.defer();
+
+  ok(aRequest instanceof DOMRequest,
+     "aRequest is instanceof " + aRequest.constructor);
+
+  aRequest.addEventListener("success", function(aEvent) {
+    deferred.resolve(aEvent);
+  });
+  aRequest.addEventListener("error", function(aEvent) {
+    deferred.reject(aEvent);
+  });
+
+  return deferred.promise;
+}
+
+/**
+ * Configures call forward options.
+ *
+ * Fulfill params: (none)
+ * Reject params:
+ *   'RadioNotAvailable', 'RequestNotSupported', 'InvalidParameter', or
+ *   'GenericFailure'.
+ *
+ * @param aOptions
+ *        A MozCallForwardingOptions.
+ *
+ * @return A deferred promise.
+ */
+function setCallForwardingOption(aOptions) {
+  let request = connection.setCallForwardingOption(aOptions);
+  return wrapDomRequestAsPromise(request)
+    .then(null, () => { throw request.error; });
+}
+
+const TEST_DATA = [
+  {
+    reason: MozMobileConnection.CALL_FORWARD_REASON_UNCONDITIONAL,
+    number: "+886912345678",
+    serviceClass: MozMobileConnection.ICC_SERVICE_CLASS_VOICE,
+    timeSeconds: 5
+  }, {
+    reason: MozMobileConnection.CALL_FORWARD_REASON_MOBILE_BUSY,
+    number: "0912345678",
+    serviceClass: MozMobileConnection.ICC_SERVICE_CLASS_VOICE,
+    timeSeconds: 10
+  }, {
+    reason: MozMobileConnection.CALL_FORWARD_REASON_NO_REPLY,
+    number: "+886987654321",
+    serviceClass: MozMobileConnection.ICC_SERVICE_CLASS_VOICE,
+    timeSeconds: 15
+  }, {
+    reason: MozMobileConnection.CALL_FORWARD_REASON_NOT_REACHABLE,
+    number: "+0987654321",
+    serviceClass: MozMobileConnection.ICC_SERVICE_CLASS_VOICE,
+    timeSeconds: 20
+  }
+];
+
+// Please see TS 22.030 Annex B
+const CF_REASON_TO_MMI = {
+  /* CALL_FORWARD_REASON_UNCONDITIONAL */
+  0: "21",
+  /* CALL_FORWARD_REASON_MOBILE_BUSY */
+  1: "67",
+  /* CALL_FORWARD_REASON_NO_REPLY */
+  2: "61",
+  /* CALL_FORWARD_REASON_NOT_REACHABLE */
+  3: "62",
+  /* CALL_FORWARD_REASON_ALL_CALL_FORWARDING */
+  4: "002",
+  /* CALL_FORWARD_REASON_ALL_CONDITIONAL_CALL_FORWARDING */
+  5: "004"
+};
+
+// Please see TS 22.030 Annex C
+const SERVICE_CLASS_TO_MMI = {
+  /* ICC_SERVICE_CLASS_VOICE */
+  1: "11"
+};
+
+function testSetCallForwarding(aData) {
+  // Registration: **SC*SIA*SIB*SIC#
+  let MMI_CODE = "**" + CF_REASON_TO_MMI[aData.reason] + "*" + aData.number +
+                 "*" + SERVICE_CLASS_TO_MMI[aData.serviceClass] +
+                 "*" + aData.timeSeconds + "#";
+  log("Test " + MMI_CODE);
+
+  let promises = [];
+  // Check cfstatechange event.
+  promises.push(waitForManagerEvent("cfstatechange").then(function(aEvent) {
+    is(aEvent.success, true, "check success");
+    is(aEvent.action, MozMobileConnection.CALL_FORWARD_ACTION_REGISTRATION,
+       "check action");
+    is(aEvent.reason, aData.reason, "check reason");
+    is(aEvent.number, aData.number, "check number");
+    is(aEvent.timeSeconds, aData.timeSeconds, "check timeSeconds");
+    is(aEvent.serviceClass, aData.serviceClass, "check serviceClass");
+  }));
+  // Check DOMRequest's result.
+  promises.push(sendMMI(MMI_CODE)
+    .then(function resolve(aResult) {
+      is(aResult.serviceCode, "scCallForwarding", "Check service code");
+      is(aResult.statusMessage, "smServiceRegistered", "Check status message");
+      is(aResult.additionalInformation, undefined, "Check additional information");
+    }, function reject(aError) {
+      ok(false, "got '" + aError.name + "' error");
+    }));
+
+  return Promise.all(promises);
+}
+
+function testGetCallForwarding(aExpectedData) {
+  // Interrogation: *#SC#
+  let MMI_CODE = "*#" + CF_REASON_TO_MMI[aExpectedData.reason] + "#";
+  log("Test " + MMI_CODE);
+
+  return sendMMI(MMI_CODE)
+    .then(function resolve(aResult) {
+      is(aResult.serviceCode, "scCallForwarding", "Check service code");
+      is(aResult.statusMessage, "smServiceInterrogated", "Check status message");
+      is(Array.isArray(aResult.additionalInformation), true,
+         "additionalInformation should be an array");
+
+      for (let i = 0; i < aResult.additionalInformation.length; i++) {
+        let result = aResult.additionalInformation[i];
+
+        // Only need to check the result containing the serviceClass that we are
+        // interested in.
+        if (!(result.serviceClass & aExpectedData.serviceClass)) {
+          continue;
+        }
+
+        is(result.active, true, "check active");
+        is(result.reason, aExpectedData.reason, "check reason");
+        is(result.number, aExpectedData.number, "check number");
+        is(result.timeSeconds, aExpectedData.timeSeconds, "check timeSeconds");
+      }
+    }, function reject(aError) {
+      ok(false, MMI_CODE + " got error: " + aError.name);
+    });
+}
+
+function clearAllCallForwardingSettings() {
+  log("Clear all call forwarding settings");
+
+  let promise = Promise.resolve();
+  for (let reason = MozMobileConnection.CALL_FORWARD_REASON_UNCONDITIONAL;
+       reason <= MozMobileConnection.CALL_FORWARD_REASON_ALL_CONDITIONAL_CALL_FORWARDING;
+       reason++) {
+    let options = {
+      reason: reason,
+      action: MozMobileConnection.CALL_FORWARD_ACTION_ERASURE
+    };
+    // Emulator doesn't support CALL_FORWARD_REASON_ALL_* yet, we catch the
+    // reject here in order to avoid impact the test result.
+    promise =
+      promise.then(() => setCallForwardingOption(options).then(null, () => {}));
+  }
+  return promise;
+}
+
+// Start tests
+startTestWithPermissions(['mobileconnection'], function() {
+  connection = navigator.mozMobileConnections[0];
+
+  let promise = Promise.resolve();
+  for (let i = 0; i < TEST_DATA.length; i++) {
+    let data = TEST_DATA[i];
+    promise = promise.then(() => testSetCallForwarding(data))
+                     .then(() => testGetCallForwarding(data));
+  }
+  // reset call forwarding settings.
+  return promise.then(null, () => { ok(false, "promise reject during test"); })
+    .then(() => clearAllCallForwardingSettings())
+    .then(finish);
+});
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/marionette/test_mmi_change_pin.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = "head.js";
+
+// PIN is hardcoded as "0000" by default.
+// See it here {B2G_HOME}/external/qemu/telephony/sim_card.c,
+// in asimcard_create().
+const TEST_DATA = [
+  // Test passing no pin.
+  {
+    pin: "",
+    newPin: "0000",
+    newPinAgain: "1111",
+    expectedError: {
+      name: "emMmiError",
+      additionalInformation: null
+    }
+  },
+  // Test passing no newPin.
+  {
+    pin: "0000",
+    newPin: "",
+    newPinAgain: "",
+    expectedError: {
+      name: "emMmiError",
+      additionalInformation: null
+    }
+  },
+  // Test passing mismatched newPin.
+  {
+    pin: "0000",
+    newPin: "0000",
+    newPinAgain: "1111",
+    expectedError: {
+      name: "emMmiErrorMismatchPin",
+      additionalInformation: null
+    }
+  },
+  // Test passing invalid pin (< 4 digit).
+  {
+    pin: "000",
+    newPin: "0000",
+    newPinAgain: "0000",
+    expectedError: {
+      name: "emMmiErrorInvalidPin",
+      additionalInformation: null
+    }
+  },
+  // Test passing invalid newPin (> 8 digit).
+  {
+    pin: "0000",
+    newPin: "000000000",
+    newPinAgain: "000000000",
+    expectedError: {
+      name: "emMmiErrorInvalidPin",
+      additionalInformation: null
+    }
+  },
+  // Test passing incorrect pin.
+  {
+    pin: "1234",
+    newPin: "0000",
+    newPinAgain: "0000",
+    expectedError: {
+      name: "emMmiErrorBadPin",
+      // The default pin retries is 3, failed once becomes to 2
+      additionalInformation: 2
+    }
+  },
+  // Test changing pin successfully (Reset the retries).
+  {
+    pin: "0000",
+    newPin: "0000",
+    newPinAgain: "0000"
+  }
+];
+
+function testChangePin(aPin, aNewPin, aNewPinAgain, aExpectedError) {
+  let MMI_CODE = "**04*" + aPin + "*" + aNewPin + "*" + aNewPinAgain + "#";
+  log("Test " + MMI_CODE);
+
+  return sendMMI(MMI_CODE)
+    .then(function resolve(aResult) {
+      ok(!aExpectedError, MMI_CODE + " success");
+      is(aResult.serviceCode, "scPin", "Check service code");
+      is(aResult.statusMessage, "smPinChanged", "Check status message");
+      is(aResult.additionalInformation, undefined, "Check additional information");
+    }, function reject(aError) {
+      ok(aExpectedError, MMI_CODE + " fail");
+      is(aError.name, aExpectedError.name, "Check name");
+      is(aError.message, "", "Check message");
+      is(aError.serviceCode, "scPin", "Check service code");
+      is(aError.additionalInformation, aExpectedError.additionalInformation,
+         "Check additional information");
+    });
+}
+
+// Start test
+startTest(function() {
+  let promise = Promise.resolve();
+  for (let i = 0; i < TEST_DATA.length; i++) {
+    let data = TEST_DATA[i];
+    promise = promise.then(() => testChangePin(data.pin,
+                                               data.newPin,
+                                               data.newPinAgain,
+                                               data.expectedError));
+  }
+  return promise.then(finish);
+});
deleted file mode 100644
--- a/dom/telephony/test/marionette/test_mmi_code.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-MARIONETTE_TIMEOUT = 60000;
-MARIONETTE_HEAD_JS = 'head.js';
-
-let number = "";
-
-function dialMMI() {
-  telephony.dial("*#06#").then(null, cause => {
-    log("Received promise 'reject'");
-
-    is(telephony.active, null);
-    is(telephony.calls.length, 0);
-    is(cause, "BadNumberError");
-
-    finish();
-  });
-}
-
-startTest(function() {
-  dialMMI();
-});
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/xpcshell/header_helpers.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ *    http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+let subscriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+                        .getService(Ci.mozIJSSubScriptLoader);
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/xpcshell/test_parseMMI.js
@@ -0,0 +1,308 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this);
+
+let NS = {};
+subscriptLoader.loadSubScript("resource://gre/components/TelephonyService.js",
+                              NS);
+
+function run_test() {
+  run_next_test();
+}
+
+function parseMMI(mmiString) {
+  return NS.TelephonyService.prototype._parseMMI(mmiString, false);
+}
+
+add_test(function test_parseMMI_empty() {
+  let mmi = parseMMI("");
+
+  equal(mmi, null);
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_undefined() {
+  let mmi = parseMMI();
+
+  equal(mmi, null);
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_one_digit_short_code() {
+  let mmi = parseMMI("1");
+
+  equal(mmi.fullMMI, "1");
+  equal(mmi.procedure, undefined);
+  equal(mmi.serviceCode, undefined);
+  equal(mmi.sia, undefined);
+  equal(mmi.sib, undefined);
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, undefined);
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_invalid_short_code() {
+  let mmi = parseMMI("11");
+
+  equal(mmi, null);
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_short_code() {
+  let mmi = parseMMI("21");
+
+  equal(mmi.fullMMI, "21");
+  equal(mmi.procedure, undefined);
+  equal(mmi.serviceCode, undefined);
+  equal(mmi.sia, undefined);
+  equal(mmi.sib, undefined);
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, undefined);
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_dial_string() {
+  let mmi = parseMMI("12345");
+
+  equal(mmi, null);
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_USSD_without_asterisk_prefix() {
+  let mmi = parseMMI("123#");
+
+  equal(mmi.fullMMI, "123#");
+  equal(mmi.procedure, undefined);
+  equal(mmi.serviceCode, undefined);
+  equal(mmi.sia, undefined);
+  equal(mmi.sib, undefined);
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, undefined);
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_USSD() {
+  let mmi = parseMMI("*123#");
+
+  equal(mmi.fullMMI, "*123#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, undefined);
+  equal(mmi.sib, undefined);
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_sia() {
+  let mmi = parseMMI("*123*1#");
+
+  equal(mmi.fullMMI, "*123*1#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, "1");
+  equal(mmi.sib, undefined);
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_sib() {
+  let mmi = parseMMI("*123**1#");
+
+  equal(mmi.fullMMI, "*123**1#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, "");
+  equal(mmi.sib, "1");
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_sic() {
+  let mmi = parseMMI("*123***1#");
+
+  equal(mmi.fullMMI, "*123***1#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, "");
+  equal(mmi.sib, "");
+  equal(mmi.sic, "1");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_sia_sib() {
+  let mmi = parseMMI("*123*1*1#");
+
+  equal(mmi.fullMMI, "*123*1*1#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, "1");
+  equal(mmi.sib, "1");
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_sia_sic() {
+  let mmi = parseMMI("*123*1**1#");
+
+  equal(mmi.fullMMI, "*123*1**1#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, "1");
+  equal(mmi.sib, "");
+  equal(mmi.sic, "1");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_sib_sic() {
+  let mmi = parseMMI("*123**1*1#");
+
+  equal(mmi.fullMMI, "*123**1*1#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, "");
+  equal(mmi.sib, "1");
+  equal(mmi.sic, "1");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_pwd() {
+  let mmi = parseMMI("*123****1#");
+
+  equal(mmi.fullMMI, "*123****1#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, "");
+  equal(mmi.sib, "");
+  equal(mmi.sic, "");
+  equal(mmi.pwd, "1");
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_dial_number() {
+  let mmi = parseMMI("*123#345");
+
+  equal(mmi.fullMMI, "*123#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "123");
+  equal(mmi.sia, undefined);
+  equal(mmi.sib, undefined);
+  equal(mmi.sic, undefined);
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "345");
+
+  run_next_test();
+});
+
+
+/**
+ * MMI procedures tests
+ */
+
+add_test(function test_parseMMI_activation() {
+  let mmi = parseMMI("*00*12*34*56#");
+
+  equal(mmi.fullMMI, "*00*12*34*56#");
+  equal(mmi.procedure, MMI_PROCEDURE_ACTIVATION);
+  equal(mmi.serviceCode, "00");
+  equal(mmi.sia, "12");
+  equal(mmi.sib, "34");
+  equal(mmi.sic, "56");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_deactivation() {
+  let mmi = parseMMI("#00*12*34*56#");
+
+  equal(mmi.fullMMI, "#00*12*34*56#");
+  equal(mmi.procedure, MMI_PROCEDURE_DEACTIVATION);
+  equal(mmi.serviceCode, "00");
+  equal(mmi.sia, "12");
+  equal(mmi.sib, "34");
+  equal(mmi.sic, "56");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_interrogation() {
+  let mmi = parseMMI("*#00*12*34*56#");
+
+  equal(mmi.fullMMI, "*#00*12*34*56#");
+  equal(mmi.procedure, MMI_PROCEDURE_INTERROGATION);
+  equal(mmi.serviceCode, "00");
+  equal(mmi.sia, "12");
+  equal(mmi.sib, "34");
+  equal(mmi.sic, "56");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_registration() {
+  let mmi = parseMMI("**00*12*34*56#");
+
+  equal(mmi.fullMMI, "**00*12*34*56#");
+  equal(mmi.procedure, MMI_PROCEDURE_REGISTRATION);
+  equal(mmi.serviceCode, "00");
+  equal(mmi.sia, "12");
+  equal(mmi.sib, "34");
+  equal(mmi.sic, "56");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
+
+add_test(function test_parseMMI_erasure() {
+  let mmi = parseMMI("##00*12*34*56#");
+
+  equal(mmi.fullMMI, "##00*12*34*56#");
+  equal(mmi.procedure, MMI_PROCEDURE_ERASURE);
+  equal(mmi.serviceCode, "00");
+  equal(mmi.sia, "12");
+  equal(mmi.sib, "34");
+  equal(mmi.sic, "56");
+  equal(mmi.pwd, undefined);
+  equal(mmi.dialNumber, "");
+
+  run_next_test();
+});
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = header_helpers.js
+tail =
+
+[test_parseMMI.js]
--- a/dom/webidl/Telephony.webidl
+++ b/dom/webidl/Telephony.webidl
@@ -11,18 +11,24 @@ interface Telephony : EventTarget {
    * |serviceId| to indicate the target telephony service. If not specified,
    * the implementation MUST use the default service.
    *
    * Possible values of |serviceId| are 0 ~ (number of services - 1), which is
    * simply the index of a service. Get number of services by acquiring
    * |navigator.mozMobileConnections.length|.
    */
 
+  /**
+   * Make a phone call or send the mmi code depending on the number provided.
+   *
+   * TelephonyCall - for call setup
+   * DOMRequest - for MMI code
+   */
   [Throws]
-  Promise<TelephonyCall> dial(DOMString number, optional unsigned long serviceId);
+  Promise<(TelephonyCall or DOMRequest)> dial(DOMString number, optional unsigned long serviceId);
 
   [Throws]
   Promise<TelephonyCall> dialEmergency(DOMString number, optional unsigned long serviceId);
 
   [Throws]
   void startTone(DOMString tone, optional unsigned long serviceId);
 
   [Throws]
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -198,16 +198,19 @@ XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_NOT_SIGN
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY      , "An entry in the JAR has been modified after the JAR was signed.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY      , "An entry in the JAR has not been signed.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_MISSING       , "An entry is missing from the JAR file.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_WRONG_SIGNATURE     , "The JAR's signature is wrong.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE     , "An entry in the JAR is too large.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_INVALID       , "An entry in the JAR is invalid.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID    , "The JAR's manifest or signature file is invalid.")
 
+/* Codes related to signed manifests */
+XPC_MSG_DEF(NS_ERROR_SIGNED_APP_MANIFEST_INVALID   , "The signed app manifest or signature file is invalid.")
+
 /* Codes for printing-related errors. */
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE , "No printers available.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_NAME_NOT_FOUND       , "The selected printer could not be found.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_COULD_NOT_OPEN_FILE  , "Failed to open output file for print to file.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_STARTDOC             , "Printing failed while starting the print job.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_ENDDOC               , "Printing failed while completing the print job.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_STARTPAGE            , "Printing failed while starting a new page.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_DOC_IS_BUSY          , "Cannot print this document yet, it is still being loaded.")
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -278,16 +278,23 @@ pref("browser.search.noCurrentEngine", t
 
 #ifdef MOZ_OFFICIAL_BRANDING
 // {moz:official} expands to "official"
 pref("browser.search.official", true);
 #endif
 
 // Control media casting feature
 pref("browser.casting.enabled", true);
+#ifdef RELEASE_BUILD
+pref("browser.mirroring.enabled", false);
+pref("browser.mirroring.enabled.roku", false);
+#else
+pref("browser.mirroring.enabled", true);
+pref("browser.mirroring.enabled.roku", true);
+#endif
 
 // Enable sparse localization by setting a few package locale overrides
 pref("chrome.override_package.global", "browser");
 pref("chrome.override_package.mozapps", "browser");
 pref("chrome.override_package.passwordmgr", "browser");
 
 // enable xul error pages
 pref("browser.xul.error_pages.enabled", true);
--- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
+++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
@@ -21,17 +21,17 @@
 
         <View android:layout_width="match_parent"
               android:layout_height="2dp"
               android:layout_alignParentBottom="true"
               android:background="#1A000000"/>
 
     </RelativeLayout>
 
-    <view class="org.mozilla.gecko.tabs.TabsPanel$TabsListContainer"
+    <view class="org.mozilla.gecko.tabs.TabsPanel$PanelViewContainer"
           android:id="@+id/tabs_container"
           android:layout_width="match_parent"
           android:layout_height="0dip"
           android:layout_weight="1.0">
 
         <view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
               android:id="@+id/normal_tabs"
               style="@style/TabsList"
--- a/mobile/android/base/resources/layout/tabs_panel.xml
+++ b/mobile/android/base/resources/layout/tabs_panel.xml
@@ -21,17 +21,17 @@
 
         <View android:layout_width="match_parent"
               android:layout_height="2dp"
               android:layout_alignParentBottom="true"
               android:background="#1A000000"/>
 
     </RelativeLayout>
 
-    <view class="org.mozilla.gecko.tabs.TabsPanel$TabsListContainer"
+    <view class="org.mozilla.gecko.tabs.TabsPanel$PanelViewContainer"
           android:id="@+id/tabs_container"
           android:layout_width="match_parent"
           android:layout_height="wrap_content">
 
         <view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
               android:id="@+id/normal_tabs"
               style="@style/TabsList"
               android:layout_width="match_parent"
--- a/mobile/android/base/resources/values-land/layout.xml
+++ b/mobile/android/base/resources/values-land/layout.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <resources>
-    <item type="layout" name="tabs_row">@layout/tabs_item_cell</item>
+    <item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
 </resources>
--- a/mobile/android/base/resources/values-large-v11/layout.xml
+++ b/mobile/android/base/resources/values-large-v11/layout.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <resources>
-    <item type="layout" name="tabs_row">@layout/tabs_item_cell</item>
+    <item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
 </resources>
--- a/mobile/android/base/resources/values/layout.xml
+++ b/mobile/android/base/resources/values/layout.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <resources>
-    <item type="layout" name="tabs_row">@layout/tabs_item_row</item>
+    <item type="layout" name="tabs_layout_item_view">@layout/tabs_item_row</item>
 </resources>
--- a/mobile/android/base/tabs/TabsLayoutAdapter.java
+++ b/mobile/android/base/tabs/TabsLayoutAdapter.java
@@ -76,17 +76,17 @@ public class TabsLayoutAdapter extends B
             view = convertView;
         }
         final Tab tab = mTabs.get(position);
         bindView(view, tab);
         return view;
     }
 
     View newView(int position, ViewGroup parent) {
-        final View view = mInflater.inflate(R.layout.tabs_row, parent, false);
+        final View view = mInflater.inflate(R.layout.tabs_layout_item_view, parent, false);
         final TabsLayoutItemView item = new TabsLayoutItemView(view);
         view.setTag(item);
         return view;
     }
 
     void bindView(View view, Tab tab) {
         TabsLayoutItemView item = (TabsLayoutItemView) view.getTag();
         item.assignValues(tab);
--- a/mobile/android/base/tabs/TabsPanel.java
+++ b/mobile/android/base/tabs/TabsPanel.java
@@ -72,17 +72,17 @@ public class TabsPanel extends LinearLay
     public static interface TabsLayoutChangeListener {
         public void onTabsLayoutChange(int width, int height);
     }
 
     private Context mContext;
     private final GeckoApp mActivity;
     private final LightweightTheme mTheme;
     private RelativeLayout mHeader;
-    private TabsListContainer mTabsContainer;
+    private PanelViewContainer mPanelsContainer;
     private PanelView mPanel;
     private PanelView mPanelNormal;
     private PanelView mPanelPrivate;
     private PanelView mPanelRemote;
     private RelativeLayout mFooter;
     private TabsLayoutChangeListener mLayoutChangeListener;
     private AppStateListener mAppStateListener;
 
@@ -132,17 +132,17 @@ public class TabsPanel extends LinearLay
 
             @Override
             public void onPause() {}
         };
     }
 
     private void initialize() {
         mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header);
-        mTabsContainer = (TabsListContainer) findViewById(R.id.tabs_container);
+        mPanelsContainer = (PanelViewContainer) findViewById(R.id.tabs_container);
 
         mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
         mPanelNormal.setTabsPanel(this);
 
         mPanelPrivate = (PanelView) findViewById(R.id.private_tabs_panel);
         mPanelPrivate.setTabsPanel(this);
 
         mPanelRemote = (PanelView) findViewById(R.id.remote_tabs);
@@ -259,29 +259,29 @@ public class TabsPanel extends LinearLay
 
         if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) {
             hide();
         }
 
         return mActivity.onOptionsItemSelected(item);
     }
 
-    private static int getTabContainerHeight(TabsListContainer listContainer) {
-        Resources resources = listContainer.getContext().getResources();
+    private static int getPanelsContainerHeight(PanelViewContainer panelsContainer) {
+        Resources resources = panelsContainer.getContext().getResources();
 
-        PanelView panelView = listContainer.getCurrentPanelView();
+        PanelView panelView = panelsContainer.getCurrentPanelView();
         if (panelView != null && !panelView.shouldExpand()) {
             return resources.getDimensionPixelSize(R.dimen.tabs_tray_horizontal_height);
         }
 
         int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
         int screenHeight = resources.getDisplayMetrics().heightPixels;
 
         Rect windowRect = new Rect();
-        listContainer.getWindowVisibleDisplayFrame(windowRect);
+        panelsContainer.getWindowVisibleDisplayFrame(windowRect);
         int windowHeight = windowRect.bottom - windowRect.top;
 
         // The web content area should have at least 1.5x the height of the action bar.
         // The tabs panel shouldn't take less than 50% of the screen height and can take
         // up to 80% of the window height.
         return (int) Math.max(screenHeight * 0.5f,
                               Math.min(windowHeight - 2.5f * actionBarHeight, windowHeight * 0.8f) - actionBarHeight);
     }
@@ -318,19 +318,19 @@ public class TabsPanel extends LinearLay
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         onLightweightThemeChanged();
     }
 
-    // Tabs List Container holds the ListView
-    static class TabsListContainer extends FrameLayout {
-        public TabsListContainer(Context context, AttributeSet attrs) {
+    // Panel View Container holds the ListView
+    static class PanelViewContainer extends FrameLayout {
+        public PanelViewContainer(Context context, AttributeSet attrs) {
             super(context, attrs);
         }
 
         public PanelView getCurrentPanelView() {
             final int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
                 View child = getChildAt(i);
                 if (!(child instanceof PanelView))
@@ -341,17 +341,17 @@ public class TabsPanel extends LinearLay
             }
 
             return null;
         }
 
         @Override
         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
             if (!GeckoAppShell.getGeckoInterface().hasTabsSideBar()) {
-                int heightSpec = MeasureSpec.makeMeasureSpec(getTabContainerHeight(TabsListContainer.this), MeasureSpec.EXACTLY);
+                int heightSpec = MeasureSpec.makeMeasureSpec(getPanelsContainerHeight(PanelViewContainer.this), MeasureSpec.EXACTLY);
                 super.onMeasure(widthMeasureSpec, heightSpec);
             } else {
                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
             }
         }
     }
 
     // Tabs Panel Toolbar contains the Buttons
@@ -463,17 +463,17 @@ public class TabsPanel extends LinearLay
             }
         }
 
         if (isSideBar()) {
             if (showAnimation)
                 dispatchLayoutChange(getWidth(), getHeight());
         } else {
             int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
-            int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
+            int height = actionBarHeight + getPanelsContainerHeight(mPanelsContainer);
             dispatchLayoutChange(getWidth(), height);
         }
         mHeaderVisible = true;
     }
 
     public void hide() {
         mHeaderVisible = false;
 
@@ -521,51 +521,51 @@ public class TabsPanel extends LinearLay
         if (Versions.preHC) {
             return;
         }
 
         if (mIsSideBar) {
             final int tabsPanelWidth = getWidth();
             if (mVisible) {
                 ViewHelper.setTranslationX(mHeader, -tabsPanelWidth);
-                ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth);
+                ViewHelper.setTranslationX(mPanelsContainer, -tabsPanelWidth);
 
                 // The footer view is only present on the sidebar, v11+.
                 ViewHelper.setTranslationX(mFooter, -tabsPanelWidth);
             }
             final int translationX = (mVisible ? 0 : -tabsPanelWidth);
-            animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
+            animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
             animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_X, translationX);
             animator.attach(mFooter, PropertyAnimator.Property.TRANSLATION_X, translationX);
 
         } else if (!mHeaderVisible) {
             final Resources resources = getContext().getResources();
             final int toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
             final int translationY = (mVisible ? 0 : -toolbarHeight);
             if (mVisible) {
                 ViewHelper.setTranslationY(mHeader, -toolbarHeight);
-                ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight);
-                ViewHelper.setAlpha(mTabsContainer, 0.0f);
+                ViewHelper.setTranslationY(mPanelsContainer, -toolbarHeight);
+                ViewHelper.setAlpha(mPanelsContainer, 0.0f);
             }
-            animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
-            animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
+            animator.attach(mPanelsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
+            animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
             animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY);
         }
 
         mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-        mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        mPanelsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
     }
 
     public void finishTabsAnimation() {
         if (Versions.preHC) {
             return;
         }
 
         mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
-        mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+        mPanelsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
 
         // If the tray is now hidden, call hide() on current panel and unset it as the current panel
         // to avoid hide() being called again when the tray is opened next.
         if (!mVisible && mPanel != null) {
             mPanel.hide();
             mPanel = null;
         }
     }
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -11,16 +11,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 // JSM files, but we left them here to allow for better lazy JSM loading.
 var rokuDevice = {
   id: "roku:ecp",
   target: "roku:ecp",
   factory: function(aService) {
     Cu.import("resource://gre/modules/RokuApp.jsm");
     return new RokuApp(aService);
   },
+  mirror: Services.prefs.getBoolPref("browser.mirroring.enabled.roku"),
   types: ["video/mp4"],
   extensions: ["mp4"]
 };
 
 var fireflyDevice = {
   id: "firefly:dial",
   target: "urn:dial-multiscreen-org:service:dial:1",
   filters: {
@@ -47,17 +48,17 @@ var mediaPlayerDevice = {
 };
 
 var CastingApps = {
   _castMenuId: -1,
   mirrorStartMenuId: -1,
   mirrorStopMenuId: -1,
 
   init: function ca_init() {
-    if (!this.isEnabled()) {
+    if (!this.isCastingEnabled()) {
       return;
     }
 
     // Register targets
     SimpleServiceDiscovery.registerDevice(rokuDevice);
     SimpleServiceDiscovery.registerDevice(fireflyDevice);
     SimpleServiceDiscovery.registerDevice(mediaPlayerDevice);
 
@@ -94,50 +95,58 @@ var CastingApps = {
     Services.obs.removeObserver(this, "Casting:Stop");
     Services.obs.removeObserver(this, "Casting:Mirror");
     Services.obs.removeObserver(this, "ssdp-service-found");
     Services.obs.removeObserver(this, "ssdp-service-lost");
 
     NativeWindow.contextmenus.remove(this._castMenuId);
   },
 
+  _mirrorStarted: function(stopMirrorCallback) {
+    this.stopMirrorCallback = stopMirrorCallback;
+    NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false });
+    NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true });
+  },
+
   serviceAdded: function(aService) {
-    if (aService.mirror && this.mirrorStartMenuId == -1) {
+    if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) {
       this.mirrorStartMenuId = NativeWindow.menu.add({
         name: Strings.browser.GetStringFromName("casting.mirrorTab"),
         callback: function() {
-          function callbackFunc(aService) {
+          let callbackFunc = function(aService) {
             let app = SimpleServiceDiscovery.findAppForService(aService);
-            if (app)
-              app.mirror(function() {
-              });
-          }
+            if (app) {
+              app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this));
+            }
+          }.bind(this);
 
-          function filterFunc(aService) {
-            return aService.mirror == true;
-          }
-          this.prompt(callbackFunc, filterFunc);
+          this.prompt(callbackFunc, aService => aService.mirror);
         }.bind(this),
         parent: NativeWindow.menu.toolsMenuID
       });
 
       this.mirrorStopMenuId = NativeWindow.menu.add({
         name: Strings.browser.GetStringFromName("casting.mirrorTabStop"),
         callback: function() {
           if (this.tabMirror) {
             this.tabMirror.stop();
             this.tabMirror = null;
+          } else if (this.stopMirrorCallback) {
+            this.stopMirrorCallback();
+            this.stopMirrorCallback = null;
           }
           NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true });
           NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
         }.bind(this),
         parent: NativeWindow.menu.toolsMenuID
       });
     }
-    NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+    if (this.mirrorStartMenuId != -1) {
+      NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+    }
   },
 
   serviceLost: function(aService) {
     if (aService.mirror && this.mirrorStartMenuId != -1) {
       let haveMirror = false;
       SimpleServiceDiscovery.services.forEach(function(service) {
         if (service.mirror) {
           haveMirror = true;
@@ -145,20 +154,24 @@ var CastingApps = {
       });
       if (!haveMirror) {
         NativeWindow.menu.remove(this.mirrorStartMenuId);
         this.mirrorStartMenuId = -1;
       }
     }
   },
 
-  isEnabled: function isEnabled() {
+  isCastingEnabled: function isCastingEnabled() {
     return Services.prefs.getBoolPref("browser.casting.enabled");
   },
 
+  isMirroringEnabled: function isMirroringEnabled() {
+    return Services.prefs.getBoolPref("browser.mirroring.enabled");
+  },
+
   observe: function (aSubject, aTopic, aData) {
     switch (aTopic) {
       case "Casting:Play":
         if (this.session && this.session.remoteMedia.status == "paused") {
           this.session.remoteMedia.play();
         }
         break;
       case "Casting:Pause":
--- a/mobile/android/chrome/content/WebrtcUI.js
+++ b/mobile/android/chrome/content/WebrtcUI.js
@@ -1,22 +1,17 @@
 /* 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";
 
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "contentPrefs",
-                                   "@mozilla.org/content-pref/service;1",
-                                   "nsIContentPrefService2");
 
 var WebrtcUI = {
   _notificationId: null,
-  VIDEO_SOURCE: "videoSource",
-  AUDIO_SOURCE: "audioDevice",
 
   observe: function(aSubject, aTopic, aData) {
     if (aTopic === "getUserMedia:request") {
       this.handleRequest(aSubject, aTopic, aData);
     } else if (aTopic === "recording-device-events") {
       switch (aData) {
         case "shutdown":
         case "starting":
@@ -78,203 +73,144 @@ var WebrtcUI = {
   },
 
   handleRequest: function handleRequest(aSubject, aTopic, aData) {
     let constraints = aSubject.getConstraints();
     let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
 
     contentWindow.navigator.mozGetUserMediaDevices(
       constraints,
-      function (aDevices) {
-        WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio, constraints.video, aDevices);
+      function (devices) {
+        WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio,
+                        constraints.video, devices);
       },
-      Cu.reportError, aSubject.innerWindowID);
+      function (error) {
+        Cu.reportError(error);
+      },
+      aSubject.innerWindowID);
   },
 
-  getDeviceButtons: function(aAudioDevices, aVideoDevices, aCallID, aHost) {
+  getDeviceButtons: function(audioDevices, videoDevices, aCallID) {
     return [{
       label: Strings.browser.GetStringFromName("getUserMedia.denyRequest.label"),
-      callback: () => {
+      callback: function() {
         Services.obs.notifyObservers(null, "getUserMedia:response:deny", aCallID);
       }
-    }, {
+    },
+    {
       label: Strings.browser.GetStringFromName("getUserMedia.shareRequest.label"),
-      callback: (checked /* ignored */, inputs) => {
+      callback: function(checked /* ignored */, inputs) {
         let allowedDevices = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
 
         let audioId = 0;
-        if (inputs && inputs[this.AUDIO_SOURCE] != undefined) {
-          audioId = inputs[this.AUDIO_SOURCE];
-        }
-
-        if (aAudioDevices[audioId]) {
-          allowedDevices.AppendElement(aAudioDevices[audioId]);
-          this.setDefaultDevice(this.AUDIO_SOURCE, aAudioDevices[audioId].name, aHost);
-        }
+        if (inputs && inputs.audioDevice != undefined)
+          audioId = inputs.audioDevice;
+        if (audioDevices[audioId])
+          allowedDevices.AppendElement(audioDevices[audioId]);
 
         let videoId = 0;
-        if (inputs && inputs[this.VIDEO_SOURCE] != undefined) {
-          videoId = inputs[this.VIDEO_SOURCE];
-        }
-
-        if (aVideoDevices[videoId]) {
-          allowedDevices.AppendElement(aVideoDevices[videoId]);
-          this.setDefaultDevice(this.VIDEO_SOURCE, aVideoDevices[videoId].name, aHost);
-        }
+        if (inputs && inputs.videoSource != undefined)
+          videoId = inputs.videoSource;
+        if (videoDevices[videoId])
+          allowedDevices.AppendElement(videoDevices[videoId]);
 
         Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
       }
     }];
   },
 
   // Get a list of string names for devices. Ensures that none of the strings are blank
   _getList: function(aDevices, aType) {
     let defaultCount = 0;
     return aDevices.map(function(device) {
-      let name = device.name;
-      // if this is a Camera input, convert the name to something readable
-      let res = /Camera\ \d+,\ Facing (front|back)/.exec(name);
-      if (res) {
-        return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
-      }
+        // if this is a Camera input, convert the name to something readable
+        let res = /Camera\ \d+,\ Facing (front|back)/.exec(device.name);
+        if (res)
+          return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
 
-      if (name.startsWith("&") && name.endsWith(";")) {
-        return Strings.browser.GetStringFromName(name.substring(1, name.length -1));
-      }
+        if (device.name.startsWith("&") && device.name.endsWith(";"))
+          return Strings.browser.GetStringFromName(device.name.substring(1, device.name.length -1));
 
-      if (name.trim() == "") {
-        defaultCount++;
-        return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
-      }
-
-      return name;
-    }, this);
+        if (device.name.trim() == "") {
+          defaultCount++;
+          return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
+        }
+        return device.name
+      }, this);
   },
 
-  _addDevicesToOptions: function(aDevices, aType, aOptions, aHost, aContext) {
-    if (aDevices.length == 0) {
-      return Promise.resolve(aOptions);
-    }
+  _addDevicesToOptions: function(aDevices, aType, aOptions, extraOptions) {
+    if (aDevices.length) {
 
-    let updateOptions = () => {
       // Filter out empty items from the list
       let list = this._getList(aDevices, aType);
+      if (extraOptions)
+        list = list.concat(extraOptions);
+
       if (list.length > 0) {
         aOptions.inputs.push({
           id: aType,
           type: "menulist",
           label: Strings.browser.GetStringFromName("getUserMedia." + aType + ".prompt"),
           values: list
         });
+
       }
-
-      return aOptions;
-    }
-
-    return this.getDefaultDevice(aType, aHost, aContext).then((defaultDevice) => {
-      aDevices.sort((a, b) => {
-        if (b.name === defaultDevice) return 1;
-        return 0;
-      });
-      return updateOptions();
-    }).catch(updateOptions);
-  },
-
-  // Sets the default for a aHost. If no aHost is specified, sets the browser wide default.
-  // Saving is async, but this doesn't wait for a result.
-  setDefaultDevice: function(aType, aValue, aHost, aContext) {
-    if (aHost) {
-      contentPrefs.set(aHost, "webrtc." + aType, aValue, aContext);
-    } else {
-      contentPrefs.setGlobal("webrtc." + aType, aValue, aContext);
     }
   },
 
-  _checkContentPref(aHost, aType, aContext) {
-    return new Promise((resolve, reject) => {
-      let result = null;
-      let handler = {
-        handleResult: (aResult) => result = aResult,
-        handleCompletion: function(aReason) {
-          if (aReason == Components.interfaces.nsIContentPrefCallback2.COMPLETE_OK &&
-              result instanceof Components.interfaces.nsIContentPref) {
-            resolve(result.value);
-          } else {
-            reject(result);
-          }
-        }
-      };
-
-      if (aHost) {
-        contentPrefs.getByDomainAndName(aHost, "webrtc." + aType, aContext, handler);
-      } else {
-        contentPrefs.getGlobal("webrtc." + aType, aContext, handler);
-      }
-    });
-  },
-
-  // Returns the default device for this aHost. If no aHost is specified, returns a browser wide default
-  getDefaultDevice: function(aType, aHost, aContext) {
-    return this._checkContentPref(aHost, aType, aContext).catch(() => {
-      // If we found nothing for the initial pref, try looking for a global one
-      return this._checkContentPref(null, aType, aContext);
-    });
-  },
-
-  prompt: function (aWindow, aCallID, aAudioRequested, aVideoRequested, aDevices) {
+  prompt: function prompt(aContentWindow, aCallID, aAudioRequested,
+                          aVideoRequested, aDevices) {
     let audioDevices = [];
     let videoDevices = [];
-
-    // Split up all the available aDevices into audio and video categories
     for (let device of aDevices) {
       device = device.QueryInterface(Ci.nsIMediaDevice);
       switch (device.type) {
       case "audio":
-        if (aAudioRequested) {
+        if (aAudioRequested)
           audioDevices.push(device);
-        }
         break;
       case "video":
-        if (aVideoRequested) {
+        if (aVideoRequested)
           videoDevices.push(device);
-        }
         break;
       }
     }
 
-    // Bsaed on the aTypes available, setup the prompt and icon text
     let requestType;
-    if (audioDevices.length && videoDevices.length) {
+    if (audioDevices.length && videoDevices.length)
       requestType = "CameraAndMicrophone";
-    } else if (audioDevices.length) {
+    else if (audioDevices.length)
       requestType = "Microphone";
-    } else if (videoDevices.length) {
+    else if (videoDevices.length)
       requestType = "Camera";
-    } else {
+    else
       return;
-    }
 
-    let host = aWindow.document.documentURIObject.host;
-    // Show the app name if this is a WebRT app, otherwise show the host.
+    let host = aContentWindow.document.documentURIObject.host;
     let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name  + "'" : host;
     let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1);
 
     let options = { inputs: [] };
     // if the users only option would be to select "No Audio" or "No Video"
     // i.e. we're only showing audio or only video and there is only one device for that type
     // don't bother showing a menulist to select from
-    if (videoDevices.length > 0 && audioDevices.length > 0) {
-      videoDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.videoSource.none") });
-      audioDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") });
+    var extraItems = null;
+    if (videoDevices.length > 1 || audioDevices.length > 0) {
+      // Only show the No Video option if there are also Audio devices to choose from
+      if (audioDevices.length > 0)
+        extraItems = [ Strings.browser.GetStringFromName("getUserMedia.videoSource.none") ];
+      // videoSource is both the string used for l10n lookup and the object that will be returned
+      this._addDevicesToOptions(videoDevices, "videoSource", options, extraItems);
     }
 
-    let loadContext = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIWebNavigation)
-                             .QueryInterface(Ci.nsILoadContext);
-    // videoSource is both the string used for l10n lookup and the object that will be returned
-    this._addDevicesToOptions(videoDevices, this.VIDEO_SOURCE, options, host, loadContext).then((aOptions) => {
-      return this._addDevicesToOptions(audioDevices, this.AUDIO_SOURCE, aOptions, host, loadContext);
-    }).catch(Cu.reportError).then((aOptions) => {
-      let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, host);
-      NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, aOptions);
-    });
+    if (audioDevices.length > 1 || videoDevices.length > 0) {
+      // Only show the No Audio option if there are also Video devices to choose from
+      if (videoDevices.length > 0)
+        extraItems = [ Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") ];
+      this._addDevicesToOptions(audioDevices, "audioDevice", options, extraItems);
+    }
+
+    let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID);
+
+    NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, options);
   }
 }
--- a/mobile/android/modules/RokuApp.jsm
+++ b/mobile/android/modules/RokuApp.jsm
@@ -6,16 +6,20 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["RokuApp"];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
+const WEBRTC_PLAYER_NAME = "WebRTC Player";
+const MIRROR_PORT = 8011;
+const JSON_MESSAGE_TERMINATOR = "\r\n";
+
 function log(msg) {
   //Services.console.logStringMessage(msg);
 }
 
 const PROTOCOL_VERSION = 1;
 
 /* RokuApp is a wrapper for interacting with a Roku channel.
  * The basic interactions all use a REST API.
@@ -24,36 +28,39 @@ const PROTOCOL_VERSION = 1;
 function RokuApp(service) {
   this.service = service;
   this.resourceURL = this.service.location;
 #ifdef RELEASE_BUILD
   this.app = "Firefox";
 #else
   this.app = "Firefox Nightly";
 #endif
-  this.appID = -1;
+  this.mediaAppID = -1;
+  this.mirrorAppID = -1;
 }
 
 RokuApp.prototype = {
   status: function status(callback) {
     // We have no way to know if the app is running, so just return "unknown"
-    // but we use this call to fetch the appID for the given app name
+    // but we use this call to fetch the mediaAppID for the given app name
     let url = this.resourceURL + "query/apps";
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("GET", url, true);
     xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
     xhr.overrideMimeType("text/xml");
 
     xhr.addEventListener("load", (function() {
       if (xhr.status == 200) {
         let doc = xhr.responseXML;
         let apps = doc.querySelectorAll("app");
         for (let app of apps) {
           if (app.textContent == this.app) {
-            this.appID = app.id;
+            this.mediaAppID = app.id;
+          } else if (app.textContent == WEBRTC_PLAYER_NAME) {
+            this.mirrorAppID = app.id
           }
         }
       }
 
       // Since ECP has no way of telling us if an app is running, we always return "unknown"
       if (callback) {
         callback({ state: "unknown" });
       }
@@ -64,33 +71,33 @@ RokuApp.prototype = {
         callback({ state: "unknown" });
       }
     }).bind(this), false);
 
     xhr.send(null);
   },
 
   start: function start(callback) {
-    // We need to make sure we have cached the appID
-    if (this.appID == -1) {
+    // We need to make sure we have cached the mediaAppID
+    if (this.mediaAppID == -1) {
       this.status(function() {
-        // If we found the appID, use it to make a new start call
-        if (this.appID != -1) {
+        // If we found the mediaAppID, use it to make a new start call
+        if (this.mediaAppID != -1) {
           this.start(callback);
         } else {
           // We failed to start the app, so let the caller know
           callback(false);
         }
       }.bind(this));
       return;
     }
 
     // Start a given app with any extra query data. Each app uses it's own data scheme.
     // NOTE: Roku will also pass "source=external-control" as a param
-    let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION);
+    let url = this.resourceURL + "launch/" + this.mediaAppID + "?version=" + parseInt(PROTOCOL_VERSION);
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("POST", url, true);
     xhr.overrideMimeType("text/plain");
 
     xhr.addEventListener("load", (function() {
       if (callback) {
         callback(xhr.status === 200);
       }
@@ -124,25 +131,63 @@ RokuApp.prototype = {
         callback(false);
       }
     }).bind(this), false);
 
     xhr.send(null);
   },
 
   remoteMedia: function remoteMedia(callback, listener) {
-    if (this.appID != -1) {
+    if (this.mediaAppID != -1) {
       if (callback) {
         callback(new RemoteMedia(this.resourceURL, listener));
       }
     } else {
       if (callback) {
         callback();
       }
     }
+  },
+
+  mirror: function(callback, win, viewport, mirrorStartedCallback) {
+    if (this.mirrorAppID == -1) {
+      // The status function may not have been called yet if mirrorAppID is -1
+      this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback));
+    } else {
+      this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback);
+    }
+  },
+
+  _createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback) {
+    if (this.mirrorAppID == -1) {
+      // TODO: Inform user to install Roku WebRTC Player Channel.
+      log("RokuApp: Failed to find Mirror App ID.");
+    } else {
+      let url = this.resourceURL + "launch/" + this.mirrorAppID;
+      let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+      xhr.open("POST", url, true);
+      xhr.overrideMimeType("text/plain");
+
+      xhr.addEventListener("load", (function() {
+        // 204 seems to be returned if the channel is already running
+        if ((xhr.status == 200) || (xhr.status == 204)) {
+          this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback);
+        }
+      }).bind(this), false);
+
+      xhr.addEventListener("error", function() {
+        log("RokuApp: XHR Failed to launch application: " + WEBRTC_PLAYER_NAME);
+      }, false);
+
+      xhr.send(null);
+    }
+
+    if (callback) {
+      callback();
+    }
   }
 }
 
 /* RemoteMedia provides a wrapper for using TCP socket to control Roku apps.
  * The server implementation must be built into the Roku receiver app.
  */
 function RemoteMedia(url, listener) {
   this._url = url;
@@ -220,16 +265,158 @@ RemoteMedia.prototype = {
     // TODO: add position support
     this._sendMsg({ type: "PLAY" });
   },
 
   pause: function pause() {
     this._sendMsg({ type: "STOP" });
   },
 
-  load: function load(aData) {
-    this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster });
+  load: function load(data) {
+    this._sendMsg({ type: "LOAD", title: data.title, source: data.source, poster: data.poster });
   },
 
   get status() {
     return this._status;
   }
 }
+
+function RemoteMirror(url, win, viewport, mirrorStartedCallback) {
+  this._serverURI = Services.io.newURI(url , null, null);
+  this._window = win;
+  this._iceCandidates = [];
+  this.mirrorStarted = mirrorStartedCallback;
+
+  // This code insures the generated tab mirror is not wider than 800 nor taller than 600
+  // Better dimensions should be chosen after the Roku Channel is working.
+  let windowId = win.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+  let cWidth =  Math.max(viewport.cssWidth, viewport.width);
+  let cHeight = Math.max(viewport.cssHeight, viewport.height);
+
+  const MAX_WIDTH = 800;
+  const MAX_HEIGHT = 600;
+
+  let tWidth = 0;
+  let tHeight = 0;
+
+  if ((cWidth / MAX_WIDTH) > (cHeight / MAX_HEIGHT)) {
+    tHeight = Math.ceil((MAX_WIDTH / cWidth) * cHeight);
+    tWidth = MAX_WIDTH;
+  } else {
+    tWidth = Math.ceil((MAX_HEIGHT / cHeight) * cWidth);
+    tHeight = MAX_HEIGHT;
+  }
+
+  let constraints = {
+    video: {
+      mediaSource: "browser",
+      browserWindow: windowId,
+      scrollWithPage: true,
+      advanced: [
+        {
+          width: { min: tWidth, max: tWidth },
+          height: { min: tHeight, max: tHeight }
+        },
+        { aspectRatio: cWidth / cHeight }
+      ]
+    }
+  };
+
+  this._window.navigator.mozGetUserMedia(constraints, this._onReceiveGUMStream.bind(this), function() {});
+}
+
+RemoteMirror.prototype = {
+  _sendOffer: function(offer) {
+    if (!this._baseSocket) {
+      this._baseSocket = Cc["@mozilla.org/tcp-socket;1"].createInstance(Ci.nsIDOMTCPSocket);
+    }
+    this._jsonOffer = JSON.stringify(offer);
+    this._socket = this._baseSocket.open(this._serverURI.host, MIRROR_PORT, { useSecureTransport: false, binaryType: "string" });
+    this._socket.onopen = this._onSocketOpen.bind(this);
+    this._socket.ondata = this._onSocketData.bind(this);
+    this._socket.onerror = this._onSocketError.bind(this);
+  },
+
+  _onReceiveGUMStream: function(stream) {
+    this._pc = new this._window.mozRTCPeerConnection;
+    this._pc.addStream(stream);
+    this._pc.onicecandidate = (evt => {
+      // Usually the last candidate is null, expected?
+      if (!evt.candidate) {
+        return;
+      }
+      let jsonCandidate = JSON.stringify(evt.candidate);
+      this._iceCandidates.push(jsonCandidate);
+      this._sendIceCandidates();
+    });
+
+    this._pc.createOffer(offer => {
+      this._pc.setLocalDescription(
+        new this._window.mozRTCSessionDescription(offer),
+        () => this._sendOffer(offer),
+        () => log("RemoteMirror: Failed to set local description."));
+    },
+    () => log("RemoteMirror: Failed to create offer."));
+  },
+
+  _stopMirror: function() {
+    if (this._socket) {
+      this._socket.close();
+      this._socket = null;
+    }
+    if (this._pc) {
+      this._pc.close();
+      this._pc = null;
+    }
+    this._jsonOffer = null;
+    this._iceCandidates = [];
+  },
+
+  _onSocketData: function(response) {
+    if (response.type == "data") {
+      response.data.split(JSON_MESSAGE_TERMINATOR).forEach(data => {
+        if (data) {
+          let parsedData = JSON.parse(data);
+          if (parsedData.type == "answer") {
+            this._pc.setRemoteDescription(
+              new this._window.mozRTCSessionDescription(parsedData),
+              () => this.mirrorStarted(this._stopMirror.bind(this)),
+              () => log("RemoteMirror: Failed to set remote description."));
+          } else {
+            this._pc.addIceCandidate(new this._window.mozRTCIceCandidate(parsedData))
+          }
+        } else {
+          log("RemoteMirror: data is null");
+        }
+      });
+    } else if (response.type == "error") {
+      log("RemoteMirror: Got socket error.");
+      this._stopMirror();
+    } else {
+      log("RemoteMirror: Got unhandled socket event: " + response.type);
+    }
+  },
+
+  _onSocketError: function(err) {
+    log("RemoteMirror: Error socket.onerror: " + (err.data ? err.data : "NO DATA"));
+    this._stopMirror();
+  },
+
+  _onSocketOpen: function() {
+    this._open = true;
+    if (this._jsonOffer) {
+      let jsonOffer = this._jsonOffer + JSON_MESSAGE_TERMINATOR;
+      this._socket.send(jsonOffer, jsonOffer.length);
+      this._jsonOffer = null;
+      this._sendIceCandidates();
+    }
+  },
+
+  _sendIceCandidates: function() {
+    if (this._socket && this._open) {
+      this._iceCandidates.forEach(value => {
+        value = value + JSON_MESSAGE_TERMINATOR;
+        this._socket.send(value, value.length);
+      });
+      this._iceCandidates = [];
+    }
+  }
+};
--- a/mobile/android/modules/SimpleServiceDiscovery.jsm
+++ b/mobile/android/modules/SimpleServiceDiscovery.jsm
@@ -404,16 +404,20 @@ var SimpleServiceDiscovery = {
   _addService: function(service) {
     // Filter out services that do not match the device filter
     if (!this._filterService(service)) {
       return;
     }
 
     // Only add and notify if we don't already know about this service
     if (!this._services.has(service.uuid)) {
+      let device = this._devices.get(service.target);
+      if (device && device.mirror) {
+        service.mirror = true;
+      }
       this._services.set(service.uuid, service);
       Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid);
     }
 
     // Make sure we remember this service is not stale
     this._services.get(service.uuid).lastPing = this._searchTimestamp;
   }
 }
--- a/security/apps/AppSignatureVerification.cpp
+++ b/security/apps/AppSignatureVerification.cpp
@@ -7,34 +7,39 @@
 #ifdef MOZ_LOGGING
 #define FORCE_PR_LOG 1
 #endif
 
 #include "nsNSSCertificateDB.h"
 
 #include "pkix/pkix.h"
 #include "pkix/pkixnss.h"
+#include "pkix/ScopedPtr.h"
 #include "mozilla/RefPtr.h"
 #include "CryptoTask.h"
 #include "AppTrustDomain.h"
 #include "nsComponentManagerUtils.h"
 #include "nsCOMPtr.h"
 #include "nsDataSignatureVerifier.h"
 #include "nsHashKeys.h"
 #include "nsIFile.h"
+#include "nsIFileStreams.h"
 #include "nsIInputStream.h"
 #include "nsIStringEnumerator.h"
 #include "nsIZipReader.h"
+#include "nsNetUtil.h"
 #include "nsNSSCertificate.h"
 #include "nsProxyRelease.h"
+#include "NSSCertDBTrustDomain.h"
 #include "nsString.h"
 #include "nsTHashtable.h"
 
 #include "base64.h"
 #include "certdb.h"
+#include "nssb64.h"
 #include "secmime.h"
 #include "plstr.h"
 #include "prlog.h"
 
 using namespace mozilla::pkix;
 using namespace mozilla;
 using namespace mozilla::psm;
 
@@ -763,16 +768,92 @@ OpenSignedAppFile(AppTrustedRoot aTruste
       nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert);
     NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY);
     signerCert.forget(aSignerCert);
   }
 
   return NS_OK;
 }
 
+nsresult
+VerifySignedManifest(AppTrustedRoot aTrustedRoot,
+                     nsIInputStream* aManifestStream,
+                     nsIInputStream* aSignatureStream,
+                     /*out, optional */ nsIX509Cert** aSignerCert)
+{
+  NS_ENSURE_ARG(aManifestStream);
+  NS_ENSURE_ARG(aSignatureStream);
+
+  if (aSignerCert) {
+    *aSignerCert = nullptr;
+  }
+
+  // Load signature file in buffer
+  ScopedAutoSECItem signatureBuffer;
+  nsresult rv = ReadStream(aSignatureStream, signatureBuffer);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+  signatureBuffer.type = siBuffer;
+
+  // Load manifest file in buffer
+  ScopedAutoSECItem manifestBuffer;
+  rv = ReadStream(aManifestStream, manifestBuffer);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  // Calculate SHA1 digest of the manifest buffer
+  Digest manifestCalculatedDigest;
+  rv = manifestCalculatedDigest.DigestBuf(SEC_OID_SHA1,
+                                          manifestBuffer.data,
+                                          manifestBuffer.len - 1); // buffer is null terminated
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Get base64 encoded string from manifest buffer digest
+  ScopedPtr<char, PORT_Free_string> base64EncDigest(NSSBase64_EncodeItem(nullptr,
+    nullptr, 0, const_cast<SECItem*>(&manifestCalculatedDigest.get())));
+  if (NS_WARN_IF(!base64EncDigest)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  // Calculate SHA1 digest of the base64 encoded string
+  Digest doubleDigest;
+  rv = doubleDigest.DigestBuf(SEC_OID_SHA1,
+                              reinterpret_cast<uint8_t*>(base64EncDigest.get()),
+                              strlen(base64EncDigest.get()));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Verify the manifest signature (signed digest of the base64 encoded string)
+  ScopedCERTCertList builtChain;
+  rv = VerifySignature(aTrustedRoot, signatureBuffer,
+                       doubleDigest.get(), builtChain);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  // Return the signer's certificate to the reader if they want it.
+  if (aSignerCert) {
+    MOZ_ASSERT(CERT_LIST_HEAD(builtChain));
+    nsCOMPtr<nsIX509Cert> signerCert =
+      nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert);
+    if (NS_WARN_IF(!signerCert)) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    signerCert.forget(aSignerCert);
+  }
+
+  return NS_OK;
+}
+
 class OpenSignedAppFileTask MOZ_FINAL : public CryptoTask
 {
 public:
   OpenSignedAppFileTask(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
                         nsIOpenSignedAppFileCallback* aCallback)
     : mTrustedRoot(aTrustedRoot)
     , mJarFile(aJarFile)
     , mCallback(new nsMainThreadPtrHolder<nsIOpenSignedAppFileCallback>(aCallback))
@@ -798,22 +879,75 @@ private:
 
   const AppTrustedRoot mTrustedRoot;
   const nsCOMPtr<nsIFile> mJarFile;
   nsMainThreadPtrHandle<nsIOpenSignedAppFileCallback> mCallback;
   nsCOMPtr<nsIZipReader> mZipReader; // out
   nsCOMPtr<nsIX509Cert> mSignerCert; // out
 };
 
+class VerifySignedmanifestTask MOZ_FINAL : public CryptoTask
+{
+public:
+  VerifySignedmanifestTask(AppTrustedRoot aTrustedRoot,
+                           nsIInputStream* aManifestStream,
+                           nsIInputStream* aSignatureStream,
+                           nsIVerifySignedManifestCallback* aCallback)
+    : mTrustedRoot(aTrustedRoot)
+    , mManifestStream(aManifestStream)
+    , mSignatureStream(aSignatureStream)
+    , mCallback(
+      new nsMainThreadPtrHolder<nsIVerifySignedManifestCallback>(aCallback))
+  {
+  }
+
+private:
+  virtual nsresult CalculateResult() MOZ_OVERRIDE
+  {
+    return VerifySignedManifest(mTrustedRoot, mManifestStream,
+                                mSignatureStream, getter_AddRefs(mSignerCert));
+  }
+
+  // nsNSSCertificate implements nsNSSShutdownObject, so there's nothing that
+  // needs to be released
+  virtual void ReleaseNSSResources() { }
+
+  virtual void CallCallback(nsresult rv)
+  {
+    (void) mCallback->VerifySignedManifestFinished(rv, mSignerCert);
+  }
+
+  const AppTrustedRoot mTrustedRoot;
+  const nsCOMPtr<nsIInputStream> mManifestStream;
+  const nsCOMPtr<nsIInputStream> mSignatureStream;
+  nsMainThreadPtrHandle<nsIVerifySignedManifestCallback> mCallback;
+  nsCOMPtr<nsIX509Cert> mSignerCert; // out
+};
+
 } // unnamed namespace
 
 NS_IMETHODIMP
 nsNSSCertificateDB::OpenSignedAppFileAsync(
   AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
   nsIOpenSignedAppFileCallback* aCallback)
 {
   NS_ENSURE_ARG_POINTER(aJarFile);
   NS_ENSURE_ARG_POINTER(aCallback);
   RefPtr<OpenSignedAppFileTask> task(new OpenSignedAppFileTask(aTrustedRoot,
                                                                aJarFile,
                                                                aCallback));
   return task->Dispatch("SignedJAR");
 }
+
+NS_IMETHODIMP
+nsNSSCertificateDB::VerifySignedManifestAsync(
+  AppTrustedRoot aTrustedRoot, nsIInputStream* aManifestStream,
+  nsIInputStream* aSignatureStream, nsIVerifySignedManifestCallback* aCallback)
+{
+  NS_ENSURE_ARG_POINTER(aManifestStream);
+  NS_ENSURE_ARG_POINTER(aSignatureStream);
+  NS_ENSURE_ARG_POINTER(aCallback);
+
+  RefPtr<VerifySignedmanifestTask> task(
+    new VerifySignedmanifestTask(aTrustedRoot, aManifestStream,
+                                 aSignatureStream, aCallback));
+  return task->Dispatch("SignedManifest");
+}
--- a/security/apps/AppTrustDomain.cpp
+++ b/security/apps/AppTrustDomain.cpp
@@ -19,16 +19,19 @@
 
 // Generated in Makefile.in
 #include "marketplace-prod-public.inc"
 #include "marketplace-prod-reviewers.inc"
 #include "marketplace-dev-public.inc"
 #include "marketplace-dev-reviewers.inc"
 #include "marketplace-stage.inc"
 #include "xpcshell.inc"
+// Trusted Hosted Apps Certificates
+#include "manifest-signing-root.inc"
+#include "manifest-signing-test-root.inc"
 
 using namespace mozilla::pkix;
 
 #ifdef PR_LOGGING
 extern PRLogModuleInfo* gPIPNSSLog;
 #endif
 
 namespace mozilla { namespace psm {
@@ -74,16 +77,26 @@ AppTrustDomain::SetTrustedRoot(AppTruste
       trustedDER.len = mozilla::ArrayLength(marketplaceStageRoot);
       break;
 
     case nsIX509CertDB::AppXPCShellRoot:
       trustedDER.data = const_cast<uint8_t*>(xpcshellRoot);
       trustedDER.len = mozilla::ArrayLength(xpcshellRoot);
       break;
 
+    case nsIX509CertDB::TrustedHostedAppPublicRoot:
+      trustedDER.data = const_cast<uint8_t*>(trustedAppPublicRoot);
+      trustedDER.len = mozilla::ArrayLength(trustedAppPublicRoot);
+      break;
+
+    case nsIX509CertDB::TrustedHostedAppTestRoot:
+      trustedDER.data = const_cast<uint8_t*>(trustedAppTestRoot);
+      trustedDER.len = mozilla::ArrayLength(trustedAppTestRoot);
+      break;
+
     default:
       PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
       return SECFailure;
   }
 
   mTrustedRoot = CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
                                          &trustedDER, nullptr, false, true);
   if (!mTrustedRoot) {
--- a/security/apps/Makefile.in
+++ b/security/apps/Makefile.in
@@ -1,33 +1,48 @@
 #
 # 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/.
 
 GEN_CERT_HEADER = $(srcdir)/gen_cert_header.py
+TEST_SSL_PATH = $(srcdir)/../manager/ssl/tests/unit/test_signed_manifest/
 
 marketplace-prod-public.inc: marketplace-prod-public.crt $(GEN_CERT_HEADER)
 	$(PYTHON) $(GEN_CERT_HEADER) marketplaceProdPublicRoot $< > $@
 
 marketplace-prod-reviewers.inc: marketplace-prod-reviewers.crt $(GEN_CERT_HEADER)
 	$(PYTHON) $(GEN_CERT_HEADER) marketplaceProdReviewersRoot $< > $@
 
 marketplace-dev-public.inc: marketplace-dev-public.crt $(GEN_CERT_HEADER)
 	$(PYTHON) $(GEN_CERT_HEADER) marketplaceDevPublicRoot $< > $@
 
 marketplace-dev-reviewers.inc: marketplace-dev-reviewers.crt $(GEN_CERT_HEADER)
 	$(PYTHON) $(GEN_CERT_HEADER) marketplaceDevReviewersRoot $< > $@
 
 marketplace-stage.inc: marketplace-stage.crt $(GEN_CERT_HEADER)
 	$(PYTHON) $(GEN_CERT_HEADER) marketplaceStageRoot $< > $@
 
+ifeq ($(shell test -s trusted-app-public.der; echo $$?),0)
+TRUSTED_APP_PUBLIC=trusted-app-public.der
+else
+TRUSTED_APP_PUBLIC=
+endif
+
+manifest-signing-root.inc: $(TRUSTED_APP_PUBLIC) $(GEN_CERT_HEADER)
+	$(PYTHON) $(GEN_CERT_HEADER) trustedAppPublicRoot $(TRUSTED_APP_PUBLIC) > $@
+
+manifest-signing-test-root.inc: $(TEST_SSL_PATH)trusted_ca1.der $(GEN_CERT_HEADER)
+	$(PYTHON) $(GEN_CERT_HEADER) trustedAppTestRoot $< > $@
+
 xpcshell.inc: $(srcdir)/../manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der $(GEN_CERT_HEADER)
 	$(PYTHON) $(GEN_CERT_HEADER) xpcshellRoot $< > $@
 
 export:: \
   marketplace-prod-public.inc \
   marketplace-prod-reviewers.inc \
   marketplace-dev-public.inc \
   marketplace-dev-reviewers.inc \
   marketplace-stage.inc \
+  manifest-signing-root.inc \
+  manifest-signing-test-root.inc \
   xpcshell.inc \
   $(NULL)
--- a/security/apps/gen_cert_header.py
+++ b/security/apps/gen_cert_header.py
@@ -17,13 +17,23 @@ def file_byte_generator(filename, block_
 
 def create_header(array_name, in_filename):
   hexified = ["0x" + binascii.hexlify(byte) for byte in file_byte_generator(in_filename)]
   print "const uint8_t " + array_name + "[] = {"
   print ", ".join(hexified)
   print "};"
   return 0
 
+def create_empty_header(array_name):
+  # mfbt/ArrayUtils.h will not be able to pick up the
+  # correct specialization for ArrayLength(const array[0])
+  # so add a value of 0 which will fail cert verification
+  # just the same as an empty array
+  print "const uint8_t " + array_name + "[] = { 0x0 };"
+  return 0
+
 if __name__ == '__main__':
-  if len(sys.argv) < 3:
+  if len(sys.argv) < 2:
     print 'ERROR: usage: gen_cert_header.py array_name in_filename'
     sys.exit(1);
+  if len(sys.argv) == 2:
+    sys.exit(create_empty_header(sys.argv[1]))
   sys.exit(create_header(sys.argv[1], sys.argv[2]))
--- a/security/manager/ssl/public/nsIX509CertDB.idl
+++ b/security/manager/ssl/public/nsIX509CertDB.idl
@@ -7,36 +7,44 @@
 #include "nsISupports.idl"
 
 interface nsIArray;
 interface nsIX509Cert;
 interface nsIFile;
 interface nsIInterfaceRequestor;
 interface nsIZipReader;
 interface nsIX509CertList;
+interface nsIInputStream;
 
 %{C++
 #define NS_X509CERTDB_CONTRACTID "@mozilla.org/security/x509certdb;1"
 %}
 
 typedef uint32_t AppTrustedRoot;
 
 [scriptable, function, uuid(fc2b60e5-9a07-47c2-a2cd-b83b68a660ac)]
 interface nsIOpenSignedAppFileCallback : nsISupports
 {
   void openSignedAppFileFinished(in nsresult rv,
                                  in nsIZipReader aZipReader,
                                  in nsIX509Cert aSignerCert);
 };
 
+[scriptable, function, uuid(3d6a9c87-5c5f-46fc-9410-96da6092f0f2)]
+interface nsIVerifySignedManifestCallback : nsISupports
+{
+  void verifySignedManifestFinished(in nsresult rv,
+                                    in nsIX509Cert aSignerCert);
+};
+
 /**
  * This represents a service to access and manipulate
  * X.509 certificates stored in a database.
  */
-[scriptable, uuid(dd6e4af8-23bb-41d9-a1e3-9ce925429f2f)]
+[scriptable, uuid(8b01c2af-3a44-44d3-8ea5-51c2455e6c4b)]
 interface nsIX509CertDB : nsISupports {
 
   /**
    *  Constants that define which usages a certificate
    *  is trusted for.
    */
   const unsigned long UNTRUSTED       =      0;
   const unsigned long TRUSTED_SSL     = 1 << 0;
@@ -296,20 +304,38 @@ interface nsIX509CertDB : nsISupports {
    *  first step in opening the JAR.
    */
   const AppTrustedRoot AppMarketplaceProdPublicRoot = 1;
   const AppTrustedRoot AppMarketplaceProdReviewersRoot = 2;
   const AppTrustedRoot AppMarketplaceDevPublicRoot = 3;
   const AppTrustedRoot AppMarketplaceDevReviewersRoot = 4;
   const AppTrustedRoot AppMarketplaceStageRoot = 5;
   const AppTrustedRoot AppXPCShellRoot = 6;
+  const AppTrustedRoot TrustedHostedAppPublicRoot = 7;
+  const AppTrustedRoot TrustedHostedAppTestRoot = 8;
   void openSignedAppFileAsync(in AppTrustedRoot trustedRoot,
                               in nsIFile aJarFile,
                               in nsIOpenSignedAppFileCallback callback);
 
+  /**
+   * Given streams containing a signature and a manifest file, verifies
+   * that the signature is valid for the manifest. The signature must
+   * come from a certificate that is trusted for code signing and that
+   * was issued by the given trusted root.
+   *
+   *  On success, NS_OK and the trusted certificate that signed the
+   *  Manifest are returned.
+   *
+   *  On failure, an error code is returned.
+   */
+  void verifySignedManifestAsync(in AppTrustedRoot trustedRoot,
+                                 in nsIInputStream aManifestStream,
+                                 in nsIInputStream aSignatureStream,
+                                 in nsIVerifySignedManifestCallback callback);
+
   /*
    * Add a cert to a cert DB from a binary string.
    *
    * @param certDER The raw DER encoding of a certificate.
    * @param aTrust decoded by CERT_DecodeTrustString. 3 comma separated characters,
    *                indicating SSL, Email, and Obj signing trust
    * @param aName name of the cert for display purposes.
    */
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_signed_manifest/README.md
@@ -0,0 +1,17 @@
+This folder contains the scripts needed to generate signed manifest files
+to verify the Trusted Hosted Apps concept.
+
+Prerequisites:
+
+* NSS 3.4 or higher.
+* Python 2.7 (should work with 2.6 also)
+* Bash
+* OpenSSL
+
+Usage:
+
+Run
+  I) For usage info execute ./create_test_files.sh --help
+
+ II) Upload the signed manifest.webapp and manifest.sig to the
+     application hosting server.