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 id27532
push userkwierso@gmail.com
push dateTue, 23 Sep 2014 01:57:26 +0000
treeherdermozilla-central@790f41c631cc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;
<