Merge m-c to inbound on a CLOSED TREE. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 05 Aug 2014 16:01:50 -0400
changeset 198022 3dbc718058d2a7a3b92256455d794211e2af0f7e
parent 198021 b2ba34d2805b843e9d848802fa0d3b771fbfaf16 (current diff)
parent 197908 191e834ff32ba45bf323edef57849c0c872922e6 (diff)
child 198023 6cbdd4d523a7cb521ccbe993e995e6f97dcfa5ae
push id1036
push userrnewman@mozilla.com
push dateWed, 06 Aug 2014 02:14:58 +0000
treeherderservices-central@3fd543e150c8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone34.0a1
Merge m-c to inbound on a CLOSED TREE. a=merge
browser/devtools/webide/locales/en-US/webide.dtd
browser/devtools/webide/locales/en-US/webide.properties
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,22 +14,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
   <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="227354333a185180b85471f2cc6abfb029e44718"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,19 +12,19 @@
   <!--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="3aa6abd313f965a84aa86c6b213dc154e4875139">
     <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="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,21 +10,21 @@
   <!--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="999e945b85c578c503ad445c2285940f16aacdae">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <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="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,22 +14,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
   <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="227354333a185180b85471f2cc6abfb029e44718"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,19 +12,19 @@
   <!--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="3aa6abd313f965a84aa86c6b213dc154e4875139">
     <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="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "f47342e0a4e802b69ec1da8b641f24ae35016d9d", 
+    "revision": "cf6d955a1703c6b7a947d0f01cb60955e7e87b0d", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,21 +12,21 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
   <project name="platform/development" path="development" revision="2460485184bc8535440bb63876d4e63ec1b4770c"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,21 +10,21 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="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"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
   <project name="platform/development" path="development" revision="b1025ec93beeb480caaf3049d171283c3846461d"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,19 +12,19 @@
   <!--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="3aa6abd313f965a84aa86c6b213dc154e4875139">
     <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="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,21 +12,21 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e93780f9da8b34f370a4113abd4df9780d58e443"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d85bbae28dd9ab9679b42d8d37c84810059e097c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2249037c79fda1d2eede2d085716577d5357e796"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="e0a9ac010df3afaa47ba107192c05ac8b5516435"/>
   <project name="platform/development" path="development" revision="a384622f5fcb1d2bebb9102591ff7ae91fe8ed2d"/>
   <project name="device/common" path="device/common" revision="7c65ea240157763b8ded6154a17d3c033167afb7"/>
--- a/browser/components/loop/README.txt
+++ b/browser/components/loop/README.txt
@@ -8,21 +8,45 @@ modern browser that supports WebRTC.
 The standalone client is a set of web pages intended to be hosted on a
 standalone server referenced by the loop-server.
 
 The standalone client exists in standalone/ but shares items
 (from content/shared/) with the desktop implementation. See the README.md
 file in the standalone/ directory for how to run the server locally.
 
 
-Working with JSX
-================
+Hacking
+=======
+Please be sure to execute
+
+  browser/components/loop/run-all-loop-tests.sh
+
+from the top level before requesting review on a patch.
+
 
-You need to install the JSX compiler using npm in order to compile the .jsx
-files into regular .js ones:
+Functional Tests
+================
+These are currently a work in progress, but it's already possible to run a test
+if you have a [loop-server](https://github.com/mozilla-services/loop-server)
+install that is properly configured.  From the top-level gecko directory,
+execute:
+
+  export LOOP_SERVER=/Users/larry/src/loop-server
+  ./mach marionette-test browser/components/loop/test/functional/manifest.ini
+
+Once the automation is complete, we'll include this in run-all-loop-tests.sh
+as well.
+
+
+Working with React JSX files
+============================
+
+Our views use [React](http://facebook.github.io/react/) written in JSX files
+and transpiled to JS before we commit. You need to install the JSX compiler
+using npm in order to compile the .jsx files into regular .js ones:
 
     npm install -g react-tools
 
 Once installed, run build-jsx with the --watch option from
 browser/components/loop, eg.:
 
     cd browser/components/loop
     ./build-jsx --watch
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -153,39 +153,52 @@ loop.conversation = (function(OT, mozL10
      * Incoming call route.
      *
      * @param {String} loopVersion The version from the push notification, set
      *                             by the router from the URL.
      */
     incoming: function(loopVersion) {
       navigator.mozLoop.startAlerting();
       this._conversation.set({loopVersion: loopVersion});
-      this._conversation.once("accept", function() {
+      this._conversation.once("accept", () => {
         this.navigate("call/accept", {trigger: true});
-      }.bind(this));
-      this._conversation.once("decline", function() {
+      });
+      this._conversation.once("decline", () => {
         this.navigate("call/decline", {trigger: true});
-      }.bind(this));
-      this._conversation.once("declineAndBlock", function() {
+      });
+      this._conversation.once("declineAndBlock", () => {
         this.navigate("call/declineAndBlock", {trigger: true});
-      }.bind(this));
-      this.loadReactComponent(loop.conversation.IncomingCallView({
-        model: this._conversation
-      }));
+      });
+      this._conversation.once("call:incoming", this.startCall, this);
+      this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
+        if (err) {
+          console.error("Failed to get the sessionData", err);
+          // XXX Not the ideal response, but bug 1047410 will be replacing
+          //this by better "call failed" UI.
+          this._notifier.errorL10n("cannot_start_call_session_not_ready");
+          return;
+        }
+        // XXX For incoming calls we might have more than one call queued.
+        // For now, we'll just assume the first call is the right information.
+        // We'll probably really want to be getting this data from the
+        // background worker on the desktop client.
+        // Bug 1032700 should fix this.
+        this._conversation.setSessionData(sessionData[0]);
+        this.loadReactComponent(loop.conversation.IncomingCallView({
+          model: this._conversation
+        }));
+      });
     },
 
     /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
-      this._conversation.initiate({
-        client: new loop.Client(),
-        outgoing: false
-      });
+      this._conversation.incoming();
     },
 
     /**
      * Declines an incoming call.
      */
     decline: function() {
       navigator.mozLoop.stopAlerting();
       // XXX For now, we just close the window
@@ -196,20 +209,20 @@ loop.conversation = (function(OT, mozL10
      * Decline and block an incoming call
      * @note:
      * - loopToken is the callUrl identifier. It gets set in the panel
      *   after a callUrl is received
      */
     declineAndBlock: function() {
       navigator.mozLoop.stopAlerting();
       var token = navigator.mozLoop.getLoopCharPref('loopToken');
-      var client = new loop.Client();
-      client.deleteCallUrl(token, function(error) {
+      this._client.deleteCallUrl(token, function(error) {
         // XXX The conversation window will be closed when this cb is triggered
         // figure out if there is a better way to report the error to the user
+        // (bug 1048909).
         console.log(error);
       });
       window.close();
     },
 
     /**
      * conversation is the route when the conversation is active. The start
      * route should be navigated to first.
@@ -249,18 +262,22 @@ loop.conversation = (function(OT, mozL10
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     document.title = mozL10n.get("incoming_call_title");
 
+    var client = new loop.Client();
     router = new ConversationRouter({
-      conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
+      client: client,
+      conversation: new loop.shared.models.ConversationModel(
+        {},         // Model attributes
+        {sdk: OT}), // Model dependencies
       notifier: new sharedViews.NotificationListView({el: "#messages"})
     });
     Backbone.history.start();
   }
 
   return {
     ConversationRouter: ConversationRouter,
     IncomingCallView: IncomingCallView,
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -153,39 +153,52 @@ loop.conversation = (function(OT, mozL10
      * Incoming call route.
      *
      * @param {String} loopVersion The version from the push notification, set
      *                             by the router from the URL.
      */
     incoming: function(loopVersion) {
       navigator.mozLoop.startAlerting();
       this._conversation.set({loopVersion: loopVersion});
-      this._conversation.once("accept", function() {
+      this._conversation.once("accept", () => {
         this.navigate("call/accept", {trigger: true});
-      }.bind(this));
-      this._conversation.once("decline", function() {
+      });
+      this._conversation.once("decline", () => {
         this.navigate("call/decline", {trigger: true});
-      }.bind(this));
-      this._conversation.once("declineAndBlock", function() {
+      });
+      this._conversation.once("declineAndBlock", () => {
         this.navigate("call/declineAndBlock", {trigger: true});
-      }.bind(this));
-      this.loadReactComponent(loop.conversation.IncomingCallView({
-        model: this._conversation
-      }));
+      });
+      this._conversation.once("call:incoming", this.startCall, this);
+      this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
+        if (err) {
+          console.error("Failed to get the sessionData", err);
+          // XXX Not the ideal response, but bug 1047410 will be replacing
+          //this by better "call failed" UI.
+          this._notifier.errorL10n("cannot_start_call_session_not_ready");
+          return;
+        }
+        // XXX For incoming calls we might have more than one call queued.
+        // For now, we'll just assume the first call is the right information.
+        // We'll probably really want to be getting this data from the
+        // background worker on the desktop client.
+        // Bug 1032700 should fix this.
+        this._conversation.setSessionData(sessionData[0]);
+        this.loadReactComponent(loop.conversation.IncomingCallView({
+          model: this._conversation
+        }));
+      });
     },
 
     /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
-      this._conversation.initiate({
-        client: new loop.Client(),
-        outgoing: false
-      });
+      this._conversation.incoming();
     },
 
     /**
      * Declines an incoming call.
      */
     decline: function() {
       navigator.mozLoop.stopAlerting();
       // XXX For now, we just close the window
@@ -196,20 +209,20 @@ loop.conversation = (function(OT, mozL10
      * Decline and block an incoming call
      * @note:
      * - loopToken is the callUrl identifier. It gets set in the panel
      *   after a callUrl is received
      */
     declineAndBlock: function() {
       navigator.mozLoop.stopAlerting();
       var token = navigator.mozLoop.getLoopCharPref('loopToken');
-      var client = new loop.Client();
-      client.deleteCallUrl(token, function(error) {
+      this._client.deleteCallUrl(token, function(error) {
         // XXX The conversation window will be closed when this cb is triggered
         // figure out if there is a better way to report the error to the user
+        // (bug 1048909).
         console.log(error);
       });
       window.close();
     },
 
     /**
      * conversation is the route when the conversation is active. The start
      * route should be navigated to first.
@@ -249,18 +262,22 @@ loop.conversation = (function(OT, mozL10
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     document.title = mozL10n.get("incoming_call_title");
 
+    var client = new loop.Client();
     router = new ConversationRouter({
-      conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
+      client: client,
+      conversation: new loop.shared.models.ConversationModel(
+        {},         // Model attributes
+        {sdk: OT}), // Model dependencies
       notifier: new sharedViews.NotificationListView({el: "#messages"})
     });
     Backbone.history.start();
   }
 
   return {
     ConversationRouter: ConversationRouter,
     IncomingCallView: IncomingCallView,
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -204,21 +204,25 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
+      var inputCSSClass = {
+        "pending": this.state.pending,
+        "callUrl": !this.state.pending
+      };
       return (
         PanelLayout({summary: __("share_link_header_text")}, 
           React.DOM.div({className: "invite"}, 
             React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", 
-                   className: cx({'pending': this.state.pending})})
+                   className: cx(inputCSSClass)})
           )
         )
       );
     }
   });
 
   /**
    * Panel view.
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -204,21 +204,25 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
+      var inputCSSClass = {
+        "pending": this.state.pending,
+        "callUrl": !this.state.pending
+      };
       return (
         <PanelLayout summary={__("share_link_header_text")}>
           <div className="invite">
             <input type="url" value={this.state.callUrl} readOnly="true"
-                   className={cx({'pending': this.state.pending})} />
+                   className={cx(inputCSSClass)} />
           </div>
         </PanelLayout>
       );
     }
   });
 
   /**
    * Panel view.
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -74,106 +74,76 @@ loop.shared.models = (function() {
       this.sdk = options.sdk;
       this.pendingCallTimeout = options.pendingCallTimeout || 20000;
 
       // Ensure that any pending call timer is cleared on disconnect/error
       this.on("session:ended session:error", this._clearPendingCallTimer, this);
     },
 
     /**
-     * Initiates a conversation, requesting call session information to the Loop
-     * server and updates appropriately the current model attributes with the
-     * data.
-     *
-     * Available options:
+     * Starts an incoming conversation.
+     */
+    incoming: function() {
+      this.trigger("call:incoming");
+    },
+
+    /**
+     * Used to indicate that an outgoing call should start any necessary
+     * set-up.
+     */
+    setupOutgoingCall: function() {
+      this.trigger("call:outgoing:setup");
+    },
+
+    /**
+     * Starts an outgoing conversation.
      *
-     * - {Boolean} outgoing Set to true if this model represents the
-     *                            outgoing call.
-     * - {Boolean} callType Only valid for outgoing calls. The type of media in
-     *                      the call, e.g. "audio" or "audio-video"
-     * - {loop.shared.Client} client  A client object to request call information
-     *                                from. Expects requestCallInfo for outgoing
-     *                                calls, requestCallsInfo for incoming calls.
-     *
-     * Triggered events:
-     *
-     * - `session:ready` when the session information have been successfully
-     *   retrieved from the server;
-     * - `session:error` when the request failed.
-     *
-     * @param {Object} options Options object
+     * @param {Object} sessionData The session data received from the
+     *                             server for the outgoing call.
      */
-    initiate: function(options) {
-      options = options || {};
+    outgoing: function(sessionData) {
+      this._clearPendingCallTimer();
 
       // Outgoing call has never reached destination, closing - see bug 1020448
       function handleOutgoingCallTimeout() {
         /*jshint validthis:true */
         if (!this.get("ongoing")) {
           this.trigger("timeout").endSession();
         }
       }
 
-      function handleResult(err, sessionData) {
-        /*jshint validthis:true */
-        this._clearPendingCallTimer();
-
-        if (err) {
-          this._handleServerError(err);
-          return;
-        }
+      // Setup pending call timeout.
+      this._pendingCallTimer = setTimeout(
+        handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
 
-        if (options.outgoing) {
-          // Setup pending call timeout.
-          this._pendingCallTimer = setTimeout(
-            handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
-        } else {
-          // XXX For incoming calls we might have more than one call queued.
-          // For now, we'll just assume the first call is the right information.
-          // We'll probably really want to be getting this data from the
-          // background worker on the desktop client.
-          // Bug 990714 should fix this.
-          sessionData = sessionData[0];
-        }
-
-        this.setReady(sessionData);
-      }
-
-      if (options.outgoing) {
-        options.client.requestCallInfo(this.get("loopToken"), options.callType,
-          handleResult.bind(this));
-      }
-      else {
-        options.client.requestCallsInfo(this.get("loopVersion"),
-          handleResult.bind(this));
-      }
+      this.setSessionData(sessionData);
+      this.trigger("call:outgoing");
     },
 
     /**
      * Checks that the session is ready.
      *
      * @return {Boolean}
      */
     isSessionReady: function() {
       return !!this.get("sessionId");
     },
 
     /**
-     * Sets session information and triggers the `session:ready` event.
+     * Sets session information.
      *
      * @param {Object} sessionData Conversation session information.
      */
-    setReady: function(sessionData) {
+    setSessionData: function(sessionData) {
       // Explicit property assignment to prevent later "surprises"
       this.set({
         sessionId:    sessionData.sessionId,
         sessionToken: sessionData.sessionToken,
         apiKey:       sessionData.apiKey
-      }).trigger("session:ready", this);
-      return this;
+      });
     },
 
     /**
      * Starts a SDK session and subscribe to call events.
      */
     startSession: function() {
       if (!this.isSessionReady()) {
         throw new Error("Can't start session as it's not ready");
--- a/browser/components/loop/content/shared/js/router.js
+++ b/browser/components/loop/content/shared/js/router.js
@@ -116,19 +116,22 @@ loop.shared.router = (function(l10n) {
      *
      * @param {Object} options Options object.
      */
     constructor: function(options) {
       options = options || {};
       if (!options.conversation) {
         throw new Error("missing required conversation");
       }
+      if (!options.client) {
+        throw new Error("missing required client");
+      }
       this._conversation = options.conversation;
+      this._client = options.client;
 
-      this.listenTo(this._conversation, "session:ready", this._onSessionReady);
       this.listenTo(this._conversation, "session:ended", this._onSessionEnded);
       this.listenTo(this._conversation, "session:peer-hungup",
                                         this._onPeerHungup);
       this.listenTo(this._conversation, "session:network-disconnected",
                                         this._onNetworkDisconnected);
       this.listenTo(this._conversation, "session:connection-error",
                     this._notifyError);
 
@@ -141,33 +144,21 @@ loop.shared.router = (function(l10n) {
      */
     _notifyError: function(error) {
       console.log(error);
       this._notifier.errorL10n("connection_error_see_console_notification");
       this.endCall();
     },
 
     /**
-     * Starts the call. This method should be overriden.
-     */
-    startCall: function() {},
-
-    /**
      * Ends the call. This method should be overriden.
      */
     endCall: function() {},
 
     /**
-     * Session is ready.
-     */
-    _onSessionReady: function() {
-      this.startCall();
-    },
-
-    /**
      * Session has ended. Notifies the user and ends the call.
      */
     _onSessionEnded: function() {
       this.endCall();
     },
 
     /**
      * Peer hung up. Notifies the user and ends the call.
--- a/browser/components/loop/standalone/Makefile
+++ b/browser/components/loop/standalone/Makefile
@@ -10,18 +10,18 @@ install:
 	@npm install
 
 test:
 	@echo "Not implemented yet."
 
 lint:
 	@$(NODE_LOCAL_BIN)/jshint *.js content test
 
-runserver: config
-	@node server.js
+runserver: remove_old_config
+	node server.js
 
 frontend:
 	@echo "Not implemented yet."
 
 # Try hg first, if not fall back to git.
 SOURCE_STAMP := $(shell hg parent --template '{node|short}\n' 2> /dev/null)
 ifndef SOURCE_STAMP
 SOURCE_STAMP := $(shell git describe --always --tag)
@@ -31,13 +31,26 @@ SOURCE_DATE := $(shell hg parent --templ
 ifndef SOURCE_DATE
 SOURCE_DATE := $(shell git log -1 --format="%H%n%aD")
 endif
 
 version:
 	@echo $(SOURCE_STAMP) > content/VERSION.txt
 	@echo $(SOURCE_DATE) >> content/VERSION.txt
 
+
+# The local node server used for client dev (server.js) used to use a static
+# content/config.js.  Now that information is server up dynamically.  This
+# target is depended on by runserver, and removes any copies of that to avoid
+# confusion.
+remove_old_config:
+	@rm -f content/config.js
+
+
+# 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.pendingCallTimeout = `echo $(LOOP_PENDING_CALL_TIMEOUT)`;" >> content/config.js
--- a/browser/components/loop/standalone/README.md
+++ b/browser/components/loop/standalone/README.md
@@ -9,17 +9,18 @@ NodeJS and npm installed.
 Installation
 ------------
 
     $ make install
 
 Configuration
 -------------
 
-You will need to generate a configuration file, you can do so with:
+If you need a static config.js file for deployment (most people wont; only
+folks deploying the development server will!), you can generate one like this:
 
     $ 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`).
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -126,17 +126,17 @@ loop.webapp = (function($, _, OT, webL10
       );
     }
   });
 
   /**
    * Conversation launcher view. A ConversationModel is associated and attached
    * as a `model` property.
    */
-  var ConversationFormView = React.createClass({displayName: 'ConversationFormView',
+  var StartConversationView = React.createClass({displayName: 'StartConversationView',
     /**
      * Constructor.
      *
      * Required options:
      * - {loop.shared.model.ConversationModel}    model    Conversation model.
      * - {loop.shared.views.NotificationListView} notifier Notifier component.
      *
      */
@@ -169,29 +169,19 @@ loop.webapp = (function($, _, OT, webL10
     _onSessionError: function(error) {
       console.error(error);
       this.props.notifier.errorL10n("unable_retrieve_call_info");
     },
 
     /**
      * Initiates the call.
      */
-    _initiate: function() {
-      this.props.model.initiate({
-        client: new loop.StandaloneClient({
-          baseServerUrl: baseServerUrl
-        }),
-        outgoing: true,
-        // For now, we assume both audio and video as there is no
-        // other option to select.
-        callType: "audio-video",
-        loopServer: loop.config.serverUrl
-      });
-
+    _initiateOutgoingCall: function() {
       this.setState({disableCallButton: true});
+      this.props.model.setupOutgoingCall();
     },
 
     _setConversationTimestamp: function(err, callUrlInfo) {
       if (err) {
         this.props.notifier.errorL10n("unable_retrieve_call_info");
       } else {
         var date = (new Date(callUrlInfo.urlCreationDate * 1000));
         var options = {year: "numeric", month: "long", day: "numeric"};
@@ -226,17 +216,17 @@ loop.webapp = (function($, _, OT, webL10
             React.DOM.p({className: "large-font light-weight-font"}, 
               __("initiate_call_button_label")
             ), 
 
             React.DOM.div({id: "messages"}), 
 
             React.DOM.div({className: "button-group"}, 
               React.DOM.div({className: "flex-padding-1"}), 
-              React.DOM.button({ref: "submitButton", onClick: this._initiate, 
+              React.DOM.button({ref: "submitButton", onClick: this._initiateOutgoingCall, 
                 className: callButtonClasses, 
                 disabled: this.state.disableCallButton}, 
                 __("initiate_call_button"), 
                 React.DOM.i({className: "icon icon-video"})
               ), 
               React.DOM.div({className: "flex-padding-1"})
             ), 
 
@@ -262,40 +252,75 @@ loop.webapp = (function($, _, OT, webL10
       "call/expired":        "expired",
       "call/ongoing/:token": "loadConversation",
       "call/:token":         "initiate"
     },
 
     initialize: function(options) {
       this.helper = options.helper;
       if (!this.helper) {
-        throw new Error("WebappRouter requires an helper object");
+        throw new Error("WebappRouter requires a helper object");
       }
 
       // Load default view
       this.loadView(new HomeView());
 
       this.listenTo(this._conversation, "timeout", this._onTimeout);
-      this.listenTo(this._conversation, "session:expired",
-                    this._onSessionExpired);
     },
 
     _onSessionExpired: function() {
       this.navigate("/call/expired", {trigger: true});
     },
 
     /**
-     * @override {loop.shared.router.BaseConversationRouter.startCall}
+     * Starts the set up of a call, obtaining the required information from the
+     * server.
      */
-    startCall: function() {
-      if (!this._conversation.get("loopToken")) {
+    setupOutgoingCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
         this._notifier.errorL10n("missing_conversation_info");
         this.navigate("home", {trigger: true});
       } else {
-        this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
+        this._conversation.once("call:outgoing", this.startCall, this);
+
+        // XXX For now, we assume both audio and video as there is no
+        // other option to select (bug 1048333)
+        this._client.requestCallInfo(this._conversation.get("loopToken"), "audio-video",
+                                     (err, sessionData) => {
+          if (err) {
+            switch (err.errno) {
+              // loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
+              // missing OR expired; we treat this information as if the url is always
+              // expired.
+              case 105:
+                this._onSessionExpired();
+                break;
+              default:
+                this._notifier.errorL10n("missing_conversation_info");
+                this.navigate("home", {trigger: true});
+                break;
+            }
+            return;
+          }
+          this._conversation.outgoing(sessionData);
+        });
+      }
+    },
+
+    /**
+     * Actually starts the call.
+     */
+    startCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
+        this._notifier.errorL10n("missing_conversation_info");
+        this.navigate("home", {trigger: true});
+      } else {
+        this.navigate("call/ongoing/" + loopToken, {
           trigger: true
         });
       }
     },
 
     /**
      * @override {loop.shared.router.BaseConversationRouter.endCall}
      */
@@ -338,23 +363,24 @@ loop.webapp = (function($, _, OT, webL10
      * @param  {String} loopToken Loop conversation token.
      */
     initiate: function(loopToken) {
       // Check if a session is ongoing; if so, terminate it
       if (this._conversation.get("ongoing")) {
         this._conversation.endSession();
       }
       this._conversation.set("loopToken", loopToken);
-      this.loadReactComponent(ConversationFormView({
+
+      var startView = StartConversationView({
         model: this._conversation,
         notifier: this._notifier,
-        client: new loop.StandaloneClient({
-          baseServerUrl: loop.config.serverUrl
-        })
-      }));
+        client: this._client
+      });
+      this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
+      this.loadReactComponent(startView);
     },
 
     /**
      * Loads conversation establishment view.
      *
      */
     loadConversation: function(loopToken) {
       if (!this._conversation.isSessionReady()) {
@@ -385,19 +411,23 @@ loop.webapp = (function($, _, OT, webL10
     }
   };
 
   /**
    * App initialization.
    */
   function init() {
     var helper = new WebappHelper();
+    var client = new loop.StandaloneClient({
+      baseServerUrl: baseServerUrl
+    }),
     router = new WebappRouter({
       helper: helper,
       notifier: new sharedViews.NotificationListView({el: "#messages"}),
+      client: client,
       conversation: new sharedModels.ConversationModel({}, {
         sdk: OT,
         pendingCallTimeout: loop.config.pendingCallTimeout
       })
     });
     Backbone.history.start();
     if (helper.isIOS(navigator.platform)) {
       router.navigate("unsupportedDevice", {trigger: true});
@@ -407,16 +437,16 @@ loop.webapp = (function($, _, OT, webL10
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = document.webL10n.getLanguage();
     document.documentElement.dir = document.webL10n.getDirection();
   }
 
   return {
     baseServerUrl: baseServerUrl,
     CallUrlExpiredView: CallUrlExpiredView,
-    ConversationFormView: ConversationFormView,
+    StartConversationView: StartConversationView,
     HomeView: HomeView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRouter: WebappRouter
   };
 })(jQuery, _, window.OT, document.webL10n);
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -126,17 +126,17 @@ loop.webapp = (function($, _, OT, webL10
       );
     }
   });
 
   /**
    * Conversation launcher view. A ConversationModel is associated and attached
    * as a `model` property.
    */
-  var ConversationFormView = React.createClass({
+  var StartConversationView = React.createClass({
     /**
      * Constructor.
      *
      * Required options:
      * - {loop.shared.model.ConversationModel}    model    Conversation model.
      * - {loop.shared.views.NotificationListView} notifier Notifier component.
      *
      */
@@ -169,29 +169,19 @@ loop.webapp = (function($, _, OT, webL10
     _onSessionError: function(error) {
       console.error(error);
       this.props.notifier.errorL10n("unable_retrieve_call_info");
     },
 
     /**
      * Initiates the call.
      */
-    _initiate: function() {
-      this.props.model.initiate({
-        client: new loop.StandaloneClient({
-          baseServerUrl: baseServerUrl
-        }),
-        outgoing: true,
-        // For now, we assume both audio and video as there is no
-        // other option to select.
-        callType: "audio-video",
-        loopServer: loop.config.serverUrl
-      });
-
+    _initiateOutgoingCall: function() {
       this.setState({disableCallButton: true});
+      this.props.model.setupOutgoingCall();
     },
 
     _setConversationTimestamp: function(err, callUrlInfo) {
       if (err) {
         this.props.notifier.errorL10n("unable_retrieve_call_info");
       } else {
         var date = (new Date(callUrlInfo.urlCreationDate * 1000));
         var options = {year: "numeric", month: "long", day: "numeric"};
@@ -226,17 +216,17 @@ loop.webapp = (function($, _, OT, webL10
             <p className="large-font light-weight-font">
               {__("initiate_call_button_label")}
             </p>
 
             <div id="messages"></div>
 
             <div className="button-group">
               <div className="flex-padding-1"></div>
-              <button ref="submitButton" onClick={this._initiate}
+              <button ref="submitButton" onClick={this._initiateOutgoingCall}
                 className={callButtonClasses}
                 disabled={this.state.disableCallButton}>
                 {__("initiate_call_button")}
                 <i className="icon icon-video"></i>
               </button>
               <div className="flex-padding-1"></div>
             </div>
 
@@ -262,40 +252,75 @@ loop.webapp = (function($, _, OT, webL10
       "call/expired":        "expired",
       "call/ongoing/:token": "loadConversation",
       "call/:token":         "initiate"
     },
 
     initialize: function(options) {
       this.helper = options.helper;
       if (!this.helper) {
-        throw new Error("WebappRouter requires an helper object");
+        throw new Error("WebappRouter requires a helper object");
       }
 
       // Load default view
       this.loadView(new HomeView());
 
       this.listenTo(this._conversation, "timeout", this._onTimeout);
-      this.listenTo(this._conversation, "session:expired",
-                    this._onSessionExpired);
     },
 
     _onSessionExpired: function() {
       this.navigate("/call/expired", {trigger: true});
     },
 
     /**
-     * @override {loop.shared.router.BaseConversationRouter.startCall}
+     * Starts the set up of a call, obtaining the required information from the
+     * server.
      */
-    startCall: function() {
-      if (!this._conversation.get("loopToken")) {
+    setupOutgoingCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
         this._notifier.errorL10n("missing_conversation_info");
         this.navigate("home", {trigger: true});
       } else {
-        this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
+        this._conversation.once("call:outgoing", this.startCall, this);
+
+        // XXX For now, we assume both audio and video as there is no
+        // other option to select (bug 1048333)
+        this._client.requestCallInfo(this._conversation.get("loopToken"), "audio-video",
+                                     (err, sessionData) => {
+          if (err) {
+            switch (err.errno) {
+              // loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
+              // missing OR expired; we treat this information as if the url is always
+              // expired.
+              case 105:
+                this._onSessionExpired();
+                break;
+              default:
+                this._notifier.errorL10n("missing_conversation_info");
+                this.navigate("home", {trigger: true});
+                break;
+            }
+            return;
+          }
+          this._conversation.outgoing(sessionData);
+        });
+      }
+    },
+
+    /**
+     * Actually starts the call.
+     */
+    startCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
+        this._notifier.errorL10n("missing_conversation_info");
+        this.navigate("home", {trigger: true});
+      } else {
+        this.navigate("call/ongoing/" + loopToken, {
           trigger: true
         });
       }
     },
 
     /**
      * @override {loop.shared.router.BaseConversationRouter.endCall}
      */
@@ -338,23 +363,24 @@ loop.webapp = (function($, _, OT, webL10
      * @param  {String} loopToken Loop conversation token.
      */
     initiate: function(loopToken) {
       // Check if a session is ongoing; if so, terminate it
       if (this._conversation.get("ongoing")) {
         this._conversation.endSession();
       }
       this._conversation.set("loopToken", loopToken);
-      this.loadReactComponent(ConversationFormView({
+
+      var startView = StartConversationView({
         model: this._conversation,
         notifier: this._notifier,
-        client: new loop.StandaloneClient({
-          baseServerUrl: loop.config.serverUrl
-        })
-      }));
+        client: this._client
+      });
+      this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
+      this.loadReactComponent(startView);
     },
 
     /**
      * Loads conversation establishment view.
      *
      */
     loadConversation: function(loopToken) {
       if (!this._conversation.isSessionReady()) {
@@ -385,19 +411,23 @@ loop.webapp = (function($, _, OT, webL10
     }
   };
 
   /**
    * App initialization.
    */
   function init() {
     var helper = new WebappHelper();
+    var client = new loop.StandaloneClient({
+      baseServerUrl: baseServerUrl
+    }),
     router = new WebappRouter({
       helper: helper,
       notifier: new sharedViews.NotificationListView({el: "#messages"}),
+      client: client,
       conversation: new sharedModels.ConversationModel({}, {
         sdk: OT,
         pendingCallTimeout: loop.config.pendingCallTimeout
       })
     });
     Backbone.history.start();
     if (helper.isIOS(navigator.platform)) {
       router.navigate("unsupportedDevice", {trigger: true});
@@ -407,16 +437,16 @@ loop.webapp = (function($, _, OT, webL10
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = document.webL10n.getLanguage();
     document.documentElement.dir = document.webL10n.getDirection();
   }
 
   return {
     baseServerUrl: baseServerUrl,
     CallUrlExpiredView: CallUrlExpiredView,
-    ConversationFormView: ConversationFormView,
+    StartConversationView: StartConversationView,
     HomeView: HomeView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRouter: WebappRouter
   };
 })(jQuery, _, window.OT, document.webL10n);
--- a/browser/components/loop/standalone/server.js
+++ b/browser/components/loop/standalone/server.js
@@ -1,17 +1,55 @@
 /* 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/. */
 
 var express = require('express');
 var app = express();
 
+var port = process.env.PORT || 3000;
+var loopServerPort = process.env.LOOP_SERVER_PORT || 5000;
+
+app.get('/content/config.js', function (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;"
+  );
+
+});
+
 // This lets /test/ be mapped to the right place for running tests
-app.use(express.static(__dirname + '/../'));
-// This lets /content/ be mappy right for the static contents.
-app.use(express.static(__dirname + '/'));
+app.use('/', express.static(__dirname + '/../'));
+// This lets /content/ be mapped right for the static contents.
+app.use('/', express.static(__dirname + '/'));
+
+var server = app.listen(port);
 
-app.listen(3000);
-console.log("Serving repository root over HTTP at http://localhost:3000/");
-console.log("Static contents are available at http://localhost:3000/content/");
-console.log("Tests are viewable at http://localhost:3000/test/");
+var baseUrl = "http://localhost:" + port + "/";
+
+console.log("Serving repository root over HTTP at " + baseUrl);
+console.log("Static contents are available at " + baseUrl + "content/");
+console.log("Tests are viewable at " + baseUrl + "test/");
 console.log("Use this for development only.");
+
+// Handle SIGTERM signal.
+function shutdown(cb) {
+  "use strict";
+
+  try {
+    server.close(function () {
+      process.exit(0);
+      if (cb !== undefined) {
+        cb();
+      }
+    });
+
+  } catch (ex) {
+    console.log(ex + " while calling server.close)");
+  }
+}
+
+process.on('SIGTERM', shutdown);
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -98,35 +98,39 @@ describe("loop.conversation", function()
     it("should start Backbone history", function() {
       loop.conversation.init();
 
       sinon.assert.calledOnce(Backbone.history.start);
     });
   });
 
   describe("ConversationRouter", function() {
-    var conversation;
+    var conversation, client;
 
     beforeEach(function() {
+      client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {},
-        pendingCallTimeout: 1000
+        pendingCallTimeout: 1000,
       });
-      sandbox.stub(conversation, "initiate");
+      sandbox.stub(client, "requestCallsInfo");
+      sandbox.stub(conversation, "setSessionData");
     });
 
     describe("Routes", function() {
       var router;
 
       beforeEach(function() {
         router = new ConversationRouter({
+          client: client,
           conversation: conversation,
           notifier: notifier
         });
         sandbox.stub(router, "loadView");
+        sandbox.stub(conversation, "incoming");
       });
 
       describe("#incoming", function() {
 
         // XXX refactor to Just Work with "sandbox.stubComponent" or else
         // just pass in the sandbox and put somewhere generally usable
 
         function stubComponent(obj, component, mockTagName){
@@ -139,56 +143,88 @@ describe("loop.conversation", function()
           return sandbox.stub(obj, component, reactClass);
         }
 
         beforeEach(function() {
           sandbox.stub(router, "loadReactComponent");
           stubComponent(loop.conversation, "IncomingCallView");
         });
 
+        it("should start alerting", function() {
+          sandbox.stub(navigator.mozLoop, "startAlerting");
+          router.incoming("fakeVersion");
+
+          sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
+        });
+
         it("should set the loopVersion on the conversation model", function() {
           router.incoming("fakeVersion");
 
           expect(conversation.get("loopVersion")).to.equal("fakeVersion");
         });
 
-        it("should display the incoming call view", function() {
+        it("should call requestCallsInfo on the client",
+          function() {
+            router.incoming(42);
+
+            sinon.assert.calledOnce(client.requestCallsInfo);
+            sinon.assert.calledWith(client.requestCallsInfo, 42);
+          });
+
+        it("should display an error if requestCallsInfo returns an error",
+          function(){
+            client.requestCallsInfo.callsArgWith(1, "failed");
+
+            router.incoming(42);
+
+            sinon.assert.calledOnce(notifier.errorL10n);
+          });
+
+        describe("requestCallsInfo successful", function() {
+          var fakeSessionData;
+
+          beforeEach(function() {
+            fakeSessionData  = {
+              sessionId:    "sessionId",
+              sessionToken: "sessionToken",
+              apiKey:       "apiKey"
+            };
+
+            client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
+          });
+
+          it("should store the session data", function() {
+            router.incoming(42);
+
+            sinon.assert.calledOnce(conversation.setSessionData);
+            sinon.assert.calledWithExactly(conversation.setSessionData,
+                                           fakeSessionData);
+          });
+
+          it("should display the incoming call view", function() {
             router.incoming("fakeVersion");
 
             sinon.assert.calledOnce(loop.conversation.IncomingCallView);
             sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
                                            {model: conversation});
             sinon.assert.calledOnce(router.loadReactComponent);
             sinon.assert.calledWith(router.loadReactComponent,
               sinon.match(function(value) {
                 return TestUtils.isDescriptorOfType(value,
                   loop.conversation.IncomingCallView);
               }));
-        });
-
-        it("should start alerting", function() {
-          sandbox.stub(navigator.mozLoop, "startAlerting");
-          router.incoming("fakeVersion");
-
-          sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
+          });
         });
       });
 
       describe("#accept", function() {
         it("should initiate the conversation", function() {
           router.accept();
 
-          sinon.assert.calledOnce(conversation.initiate);
-          sinon.assert.calledWithMatch(conversation.initiate, {
-            client: {
-              mozLoop: navigator.mozLoop,
-              settings: {}
-            },
-            outgoing: false
-          });
+          sinon.assert.calledOnce(conversation.incoming);
         });
 
         it("should stop alerting", function() {
           sandbox.stub(navigator.mozLoop, "stopAlerting");
           router.accept();
 
           sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
         });
@@ -332,24 +368,27 @@ describe("loop.conversation", function()
           sessionId:    "sessionId",
           sessionToken: "sessionToken",
           apiKey:       "apiKey"
         };
         sandbox.stub(loop.conversation.ConversationRouter.prototype,
                      "navigate");
         conversation.set("loopToken", "fakeToken");
         router = new loop.conversation.ConversationRouter({
+          client: client,
           conversation: conversation,
           notifier: notifier
         });
       });
 
-      it("should navigate to call/ongoing once the call session is ready",
+      it("should navigate to call/ongoing once the call is ready",
         function() {
-          conversation.setReady(fakeSessionData);
+          router.incoming(42);
+
+          conversation.incoming();
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWith(router.navigate, "call/ongoing");
         });
 
       it("should navigate to call/feedback when the call session ends",
         function() {
           conversation.trigger("session:ended");
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/functional/hanging_threads.py
@@ -0,0 +1,27 @@
+import sys
+import threading
+import time
+
+# XXX We want to convince ourselves that the race condition this file works
+# around, tracked by bug 1046873, is gone, and get rid of this file entirely
+# before we hook this up to Tbpl, so that we don't introduce an intermittent
+# failure.
+#
+# reduced down from <https://gist.github.com/niccokunzmann/6038331>; importing
+# this class just so happens to allow mach to exit rather than hanging after
+# our tests are run.
+
+
+def monitor():
+    while 1:
+        time.sleep(1.)
+        _ = sys._current_frames()
+
+
+def start_monitoring():
+    thread = threading.Thread(target=monitor)
+    thread.daemon = True
+    thread.start()
+    return thread
+
+start_monitoring()
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/functional/manifest.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+b2g = false
+browser = true
+qemu = false
+
+[test_get_url.py]
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/functional/test_get_url.py
@@ -0,0 +1,145 @@
+from marionette_test import MarionetteTestCase
+from marionette.errors import NoSuchElementException
+from marionette.errors import StaleElementException
+from by import By
+# noinspection PyUnresolvedReferences
+from marionette.wait import Wait
+import os
+
+# noinspection PyUnresolvedReferences
+from mozprocess import processhandler
+import urlparse
+
+# XXX We want to convince ourselves that the race condition that importing
+# hanging_threads.py works around, tracked by bug 1046873, is gone, and get
+# rid of this inclusion entirely before we hook our functional tests up to
+# Tbpl, so that we don't introduce an intermittent failure.
+import sys
+sys.path.append(os.path.dirname(__file__))
+import hanging_threads
+
+CONTENT_SERVER_PORT = 3001
+LOOP_SERVER_PORT = 5001
+
+CONTENT_SERVER_COMMAND = ["make", "runserver"]
+CONTENT_SERVER_ENV = os.environ.copy()
+# Set PORT so that it does not interfere with any other
+# development server that might be running
+CONTENT_SERVER_ENV.update({"PORT": str(CONTENT_SERVER_PORT),
+                           "LOOP_SERVER_PORT": str(LOOP_SERVER_PORT)})
+
+WEB_APP_URL = "http://localhost:" + str(CONTENT_SERVER_PORT) + \
+              "/content/#call/{token}"
+
+LOOP_SERVER_COMMAND = ["make", "runserver"]
+LOOP_SERVER_ENV = os.environ.copy()
+# Set PORT so that it does not interfere with any other
+# development server that might be running
+LOOP_SERVER_ENV.update({"NODE_ENV": "dev", "PORT": str(LOOP_SERVER_PORT),
+                        "WEB_APP_URL": WEB_APP_URL})
+
+
+class LoopTestServers:
+    def __init__(self):
+        self.loop_server = self.start_loop_server()
+        self.content_server = self.start_content_server()
+
+    @staticmethod
+    def start_loop_server():
+        loop_server_location = os.environ.get('LOOP_SERVER')
+        if loop_server_location is None:
+            raise Exception('LOOP_SERVER variable not set')
+
+        os.chdir(loop_server_location)
+
+        p = processhandler.ProcessHandler(LOOP_SERVER_COMMAND,
+                                          env=LOOP_SERVER_ENV)
+        p.run()
+        return p
+
+    @staticmethod
+    def start_content_server():
+        content_server_location = os.path.join(os.path.dirname(__file__),
+                                               "../../standalone")
+        os.chdir(content_server_location)
+
+        p = processhandler.ProcessHandler(CONTENT_SERVER_COMMAND,
+                                          env=CONTENT_SERVER_ENV)
+        p.run()
+        return p
+
+    def shutdown(self):
+        self.content_server.kill()
+        self.loop_server.kill()
+
+
+class TestGetUrl(MarionetteTestCase):
+    # XXX Move this to setup class so it doesn't restart the server
+    # after every test.  This can happen when we write our second test,
+    # expected to be in bug 976116.
+    def setUp(self):
+        # start server
+        self.loop_test_servers = LoopTestServers()
+
+        MarionetteTestCase.setUp(self)
+
+        # Unfortunately, enforcing preferences currently comes with the side
+        # effect of launching and restarting the browser before running the
+        # real functional tests.  Bug 1048554 has been filed to track this.
+        preferences = {"loop.server": "http://localhost:" + str(LOOP_SERVER_PORT)}
+        self.marionette.enforce_gecko_prefs(preferences)
+
+        # this is browser chrome, kids, not the content window just yet
+        self.marionette.set_context("chrome")
+
+    def switch_to_panel(self):
+        button = self.marionette.find_element(By.ID, "loop-call-button")
+
+        # click the element
+        button.click()
+
+        # switch to the frame
+        frame = self.marionette.find_element(By.ID, "loop")
+        self.marionette.switch_to_frame(frame)
+
+    # taken from https://github.com/mozilla-b2g/gaia/blob/master/tests/python/gaia-ui-tests/gaiatest/gaia_test.py#L858
+    # XXX factor out into utility object for use by other tests, this can
+    # when we write our second test, in bug 976116
+    def wait_for_element_displayed(self, by, locator, timeout=None):
+        Wait(self.marionette, timeout,
+             ignored_exceptions=[NoSuchElementException, StaleElementException])\
+            .until(lambda m: m.find_element(by, locator).is_displayed())
+        return self.marionette.find_element(by, locator)
+
+    def test_get_url(self):
+        self.switch_to_panel()
+
+        # get and check for a call url
+        url_input_element = self.wait_for_element_displayed(By.TAG_NAME,
+                                                            "input")
+
+        # wait for pending state to finish
+        self.assertEqual(url_input_element.get_attribute("class"), "pending",
+                         "expect the input to be pending")
+
+        # get and check the input (the "callUrl" class is only added after
+        # the pending class is removed and the URL has arrived).
+        #
+        # XXX should investigate getting rid of the fragile and otherwise
+        # unnecessary callUrl class and replacing this with a By.CSS_SELECTOR
+        # and some possible combination of :not and/or an attribute selector
+        # once bug 1048551 is fixed.
+        url_input_element = self.wait_for_element_displayed(By.CLASS_NAME,
+                                                            "callUrl")
+        call_url = url_input_element.get_attribute("value")
+        self.assertNotEqual(call_url, u'',
+                            "input is populated with call URL after pending"
+                            " is finished")
+
+        self.assertIn(urlparse.urlparse(call_url).scheme, ['http', 'https'],
+                      "call URL returned by server " + call_url +
+                      " has invalid scheme")
+
+    def tearDown(self):
+        self.loop_test_servers.shutdown()
+        MarionetteTestCase.tearDown(self)
--- a/browser/components/loop/test/shared/models_test.js
+++ b/browser/components/loop/test/shared/models_test.js
@@ -73,161 +73,84 @@ describe("loop.shared.models", function(
         fakeClient = {
           requestCallInfo: sandbox.stub(),
           requestCallsInfo: sandbox.stub()
         };
         requestCallInfoStub = fakeClient.requestCallInfo;
         requestCallsInfoStub = fakeClient.requestCallsInfo;
       });
 
-      describe("#initiate", function() {
-        beforeEach(function() {
-          sandbox.stub(conversation, "endSession");
-        });
-
-        it("call requestCallInfo on the client for outgoing calls",
-          function() {
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true,
-              callType: "audio"
-            });
-
-            sinon.assert.calledOnce(requestCallInfoStub);
-            sinon.assert.calledWith(requestCallInfoStub, "fakeToken", "audio");
+      describe("#incoming", function() {
+        it("should trigger a `call:incoming` event", function(done) {
+          conversation.once("call:incoming", function() {
+            done();
           });
 
-        it("should not call requestCallsInfo on the client for outgoing calls",
-          function() {
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true,
-              callType: "audio"
-            });
-
-            sinon.assert.notCalled(requestCallsInfoStub);
-          });
+          conversation.incoming();
+        });
+      });
 
-        it("call requestCallsInfo on the client for incoming calls",
-          function() {
-            conversation.set("loopVersion", 42);
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: false
-            });
-
-            sinon.assert.calledOnce(requestCallsInfoStub);
-            sinon.assert.calledWith(requestCallsInfoStub, 42);
-          });
-
-        it("should not call requestCallInfo on the client for incoming calls",
-          function() {
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: false
-            });
-
-            sinon.assert.notCalled(requestCallInfoStub);
+      describe("#setupOutgoingCall", function() {
+        it("should trigger a `call:outgoing:setup` event", function(done) {
+          conversation.once("call:outgoing:setup", function() {
+            done();
           });
 
-        it("should update conversation session information from server data",
-          function() {
-            sandbox.stub(conversation, "setReady");
-            requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
+          conversation.setupOutgoingCall();
+        });
+      });
 
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true
-            });
+      describe("#outgoing", function() {
+        beforeEach(function() {
+          sandbox.stub(conversation, "endSession");
+          sandbox.stub(conversation, "setSessionData");
+        });
 
-            sinon.assert.calledOnce(conversation.setReady);
-            sinon.assert.calledWith(conversation.setReady, fakeSessionData);
-          });
+        it("should save the sessionData", function() {
+          conversation.outgoing(fakeSessionData);
 
-        it("should trigger a `session:error` event errno is undefined",
-          function(done) {
-            var errMsg = "HTTP 500 Server Error; fake";
-            var err = new Error(errMsg);
-            requestCallInfoStub.callsArgWith(2, err);
+          sinon.assert.calledOnce(conversation.setSessionData);
+        });
 
-            conversation.on("session:error", function(err) {
-              expect(err.message).eql(errMsg);
-              done();
-            }).initiate({ client: fakeClient, outgoing: true });
+        it("should trigger a `call:outgoing` event", function(done) {
+          conversation.once("call:outgoing", function() {
+            done();
           });
 
-        it("should trigger a `session:error` event when errno is not 105",
-          function(done) {
-            var errMsg = "HTTP 400 Bad Request; fake";
-            var err = new Error(errMsg);
-            err.errno = 101;
-            requestCallInfoStub.callsArgWith(2, err);
-
-            conversation.on("session:error", function(err) {
-              expect(err.message).eql(errMsg);
-              done();
-            }).initiate({ client: fakeClient, outgoing: true });
-          });
-
-        it("should trigger a `session:expired` event when errno is 105",
-          function(done) {
-            var err = new Error("HTTP 404 Not Found; fake");
-            err.errno = 105;
-            requestCallInfoStub.callsArgWith(2, err);
-
-            conversation.on("session:expired", function(err2) {
-              expect(err2).eql(err);
-              done();
-            }).initiate({ client: fakeClient, outgoing: true });
-          });
+          conversation.outgoing();
+        });
 
         it("should end the session on outgoing call timeout", function() {
-          requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
-
-          conversation.initiate({
-            client: fakeClient,
-            outgoing: true
-          });
+          conversation.outgoing();
 
           sandbox.clock.tick(1001);
 
           sinon.assert.calledOnce(conversation.endSession);
         });
 
         it("should trigger a `timeout` event on outgoing call timeout",
           function(done) {
-            requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
-
             conversation.once("timeout", function() {
               done();
             });
 
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true
-            });
+            conversation.outgoing();
 
             sandbox.clock.tick(1001);
           });
       });
 
-      describe("#setReady", function() {
+      describe("#setSessionData", function() {
         it("should update conversation session information", function() {
-          conversation.setReady(fakeSessionData);
+          conversation.setSessionData(fakeSessionData);
 
           expect(conversation.get("sessionId")).eql("sessionId");
           expect(conversation.get("sessionToken")).eql("sessionToken");
           expect(conversation.get("apiKey")).eql("apiKey");
         });
-
-        it("should trigger a `session:ready` event", function(done) {
-          conversation.on("session:ready", function() {
-            done();
-          }).setReady(fakeSessionData);
-        });
       });
 
       describe("#startSession", function() {
         var model;
 
         beforeEach(function() {
           sandbox.stub(sharedModels.ConversationModel.prototype,
                        "_clearPendingCallTimer");
--- a/browser/components/loop/test/shared/router_test.js
+++ b/browser/components/loop/test/shared/router_test.js
@@ -92,47 +92,52 @@ describe("loop.shared.router", function(
     });
   });
 
   describe("BaseConversationRouter", function() {
     var conversation, TestRouter;
 
     beforeEach(function() {
       TestRouter = loop.shared.router.BaseConversationRouter.extend({
-        startCall: sandbox.spy(),
         endCall: sandbox.spy()
       });
       conversation = new loop.shared.models.ConversationModel({
         loopToken: "fakeToken"
       }, {
         sdk: {},
         pendingCallTimeout: 1000
       });
     });
 
     describe("#constructor", function() {
       it("should require a ConversationModel instance", function() {
         expect(function() {
-          new TestRouter();
+          new TestRouter({ client: {} });
         }).to.Throw(Error, /missing required conversation/);
       });
+      it("should require a Client instance", function() {
+        expect(function() {
+          new TestRouter({ conversation: {} });
+        }).to.Throw(Error, /missing required client/);
+      });
     });
 
     describe("Events", function() {
       var router, fakeSessionData;
 
       beforeEach(function() {
         fakeSessionData = {
           sessionId:    "sessionId",
           sessionToken: "sessionToken",
           apiKey:       "apiKey"
         };
         router = new TestRouter({
           conversation: conversation,
-          notifier: notifier
+          notifier: notifier,
+          client: {}
         });
       });
 
       describe("session:connection-error", function() {
 
         it("should warn the user when .connect() call fails", function() {
           conversation.trigger("session:connection-error");
 
@@ -144,22 +149,16 @@ describe("loop.shared.router", function(
           conversation.trigger("session:connection-error");
 
           sinon.assert.calledOnce(router.endCall);
           sinon.assert.calledWithExactly(router.endCall);
         });
 
       });
 
-      it("should call startCall() once the call session is ready", function() {
-        conversation.trigger("session:ready");
-
-        sinon.assert.calledOnce(router.startCall);
-      });
-
       it("should call endCall() when conversation ended", function() {
         conversation.trigger("session:ended");
 
         sinon.assert.calledOnce(router.endCall);
       });
 
       it("should warn the user when peer hangs up", function() {
         conversation.trigger("session:peer-hungup");
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -12,16 +12,19 @@ describe("loop.webapp", function() {
 
   var sharedModels = loop.shared.models,
       sharedViews = loop.shared.views,
       sandbox,
       notifier;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+    // conversation#outgoing sets timers, so we need to use fake ones
+    // to prevent random failures.
+    sandbox.useFakeTimers();
     notifier = {
       notify: sandbox.spy(),
       warn: sandbox.spy(),
       warnL10n: sandbox.spy(),
       error: sandbox.spy(),
       errorL10n: sandbox.spy(),
     };
     loop.config.pendingCallTimeout = 1000;
@@ -64,26 +67,31 @@ describe("loop.webapp", function() {
 
       sinon.assert.calledOnce(WebappRouter.prototype.navigate);
       sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
                                      "unsupportedBrowser", {trigger: true});
     });
   });
 
   describe("WebappRouter", function() {
-    var router, conversation;
+    var router, conversation, client;
 
     beforeEach(function() {
+      client = new loop.StandaloneClient({
+        baseServerUrl: "http://fake.example.com"
+      });
+      sandbox.stub(client, "requestCallInfo");
       conversation = new sharedModels.ConversationModel({}, {
         sdk: {},
         pendingCallTimeout: 1000
       });
       sandbox.stub(loop.webapp.WebappRouter.prototype, "loadReactComponent");
       router = new loop.webapp.WebappRouter({
         helper: {},
+        client: client,
         conversation: conversation,
         notifier: notifier
       });
       sandbox.stub(router, "loadView");
       sandbox.stub(router, "navigate");
     });
 
     describe("#startCall", function() {
@@ -157,24 +165,24 @@ describe("loop.webapp", function() {
 
       describe("#initiate", function() {
         it("should set the token on the conversation model", function() {
           router.initiate("fakeToken");
 
           expect(conversation.get("loopToken")).eql("fakeToken");
         });
 
-        it("should load the ConversationFormView", function() {
+        it("should load the StartConversationView", function() {
           router.initiate("fakeToken");
 
           sinon.assert.calledOnce(router.loadReactComponent);
           sinon.assert.calledWithExactly(router.loadReactComponent,
             sinon.match(function(value) {
               return React.addons.TestUtils.isDescriptorOfType(
-                value, loop.webapp.ConversationFormView);
+                value, loop.webapp.StartConversationView);
             }));
         });
 
         // https://bugzilla.mozilla.org/show_bug.cgi?id=991118
         it("should terminate any ongoing call session", function() {
           sinon.stub(conversation, "endSession");
           conversation.set("ongoing", true);
 
@@ -237,17 +245,18 @@ describe("loop.webapp", function() {
           sessionToken: "sessionToken",
           apiKey:       "apiKey"
         };
         conversation.set("loopToken", "fakeToken");
       });
 
       it("should navigate to call/ongoing/:token once call session is ready",
         function() {
-          conversation.trigger("session:ready");
+          router.setupOutgoingCall();
+          conversation.outgoing(fakeSessionData);
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
         });
 
       it("should navigate to call/{token} when conversation ended", function() {
         conversation.trigger("session:ended");
 
@@ -265,27 +274,91 @@ describe("loop.webapp", function() {
       it("should navigate to call/{token} when network disconnects",
         function() {
           conversation.trigger("session:network-disconnected");
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
         });
 
-      it("should navigate to call/expired when a session:expired event is " +
-         "received", function() {
-        conversation.trigger("session:expired");
+      describe("#setupOutgoingCall", function() {
+        beforeEach(function() {
+          router.initiate();
+        });
+
+        describe("No loop token", function() {
+          it("should navigate to home", function() {
+            conversation.setupOutgoingCall();
+
+            sinon.assert.calledOnce(router.navigate);
+            sinon.assert.calledWithMatch(router.navigate, "home");
+          });
+
+          it("should display an error", function() {
+            conversation.setupOutgoingCall();
+
+            sinon.assert.calledOnce(notifier.errorL10n);
+          });
+        });
+
+        describe("Has loop token", function() {
+          beforeEach(function() {
+            conversation.set("loopToken", "fakeToken");
+            sandbox.stub(conversation, "outgoing");
+          });
+
+          it("should call requestCallInfo on the client",
+            function() {
+              conversation.setupOutgoingCall();
+
+              sinon.assert.calledOnce(client.requestCallInfo);
+              sinon.assert.calledWith(client.requestCallInfo, "fakeToken",
+                                      "audio-video");
+            });
 
-        sinon.assert.calledOnce(router.navigate);
-        sinon.assert.calledWith(router.navigate, "/call/expired");
+          describe("requestCallInfo response handling", function() {
+            it("should navigate to call/expired when a session has expired",
+               function() {
+                client.requestCallInfo.callsArgWith(2, {errno: 105});
+                conversation.setupOutgoingCall();
+
+                sinon.assert.calledOnce(router.navigate);
+                sinon.assert.calledWith(router.navigate, "/call/expired");
+              });
+
+            it("should navigate to home on any other error", function() {
+              client.requestCallInfo.callsArgWith(2, {errno: 104});
+              conversation.setupOutgoingCall();
+
+              sinon.assert.calledOnce(router.navigate);
+              sinon.assert.calledWith(router.navigate, "home");
+              });
+
+            it("should notify the user on any other error", function() {
+              client.requestCallInfo.callsArgWith(2, {errno: 104});
+              conversation.setupOutgoingCall();
+
+              sinon.assert.calledOnce(notifier.errorL10n);
+            });
+
+            it("should call outgoing on the conversation model when details " +
+               "are successfully received", function() {
+                client.requestCallInfo.callsArgWith(2, null, fakeSessionData);
+                conversation.setupOutgoingCall();
+
+                sinon.assert.calledOnce(conversation.outgoing);
+                sinon.assert.calledWithExactly(conversation.outgoing, fakeSessionData);
+              });
+          });
+        });
       });
     });
   });
 
-  describe("ConversationFormView", function() {
+  describe("StartConversationView", function() {
     var conversation;
 
     beforeEach(function() {
       conversation = new sharedModels.ConversationModel({}, {
         sdk: {},
         pendingCallTimeout: 1000});
     });
 
@@ -293,52 +366,49 @@ describe("loop.webapp", function() {
       it("should require a conversation option", function() {
         expect(function() {
           new loop.webapp.WebappRouter();
         }).to.Throw(Error, /missing required conversation/);
       });
     });
 
     describe("#initiate", function() {
-      var conversation, initiate, view, fakeSubmitEvent, requestCallUrlInfo;
+      var conversation, setupOutgoingCall, view, fakeSubmitEvent,
+          requestCallUrlInfo;
 
       beforeEach(function() {
         conversation = new sharedModels.ConversationModel({}, {
           sdk: {},
           pendingCallTimeout: 1000
         });
 
         fakeSubmitEvent = {preventDefault: sinon.spy()};
-        initiate = sinon.stub(conversation, "initiate");
+        setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
 
         var standaloneClientStub = {
           requestCallUrlInfo: function(token, cb) {
             cb(null, {urlCreationDate: 0});
           },
           settings: {baseServerUrl: loop.webapp.baseServerUrl}
-        }
+        };
 
         view = React.addons.TestUtils.renderIntoDocument(
-            loop.webapp.ConversationFormView({
+            loop.webapp.StartConversationView({
               model: conversation,
               notifier: notifier,
               client: standaloneClientStub
             })
         );
       });
 
       it("should start the conversation establishment process", function() {
         var button = view.getDOMNode().querySelector("button");
         React.addons.TestUtils.Simulate.click(button);
 
-        sinon.assert.calledOnce(initiate);
-        sinon.assert.calledWith(initiate, sinon.match(function (value) {
-          return !!value.outgoing &&
-            (value.client.settings.baseServerUrl === loop.webapp.baseServerUrl)
-        }, "outgoing: true && correct baseServerUrl"));
+        sinon.assert.calledOnce(setupOutgoingCall);
       });
 
       it("should disable current form once session is initiated", function() {
         conversation.set("loopToken", "fake");
 
         var button = view.getDOMNode().querySelector("button");
         React.addons.TestUtils.Simulate.click(button);
 
@@ -368,17 +438,17 @@ describe("loop.webapp", function() {
           sdk: {},
           pendingCallTimeout: 1000
         });
 
         sandbox.spy(conversation, "listenTo");
         requestCallUrlInfo = sandbox.stub();
 
         view = React.addons.TestUtils.renderIntoDocument(
-            loop.webapp.ConversationFormView({
+            loop.webapp.StartConversationView({
               model: conversation,
               notifier: notifier,
               client: {requestCallUrlInfo: requestCallUrlInfo}
             })
           );
       });
 
       it("should call requestCallUrlInfo", function() {
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -11,17 +11,17 @@
     <link rel="stylesheet" type="text/css" href="../content/shared/css/panel.css">
     <link rel="stylesheet" type="text/css" href="ui-showcase.css">
  </head>
   <body>
     <div id="main"></div>
     <script src="fake-mozLoop.js"></script>
     <script src="fake-l10n.js"></script>
     <script src="../content/libs/sdk.js"></script>
-    <script src="../content/shared/libs/react-0.10.0.js"></script>
+    <script src="../content/shared/libs/react-0.11.1.js"></script>
     <script src="../content/shared/libs/jquery-2.1.0.js"></script>
     <script src="../content/shared/libs/lodash-2.4.1.js"></script>
     <script src="../content/shared/libs/backbone-1.1.2.js"></script>
     <script src="../content/shared/js/feedbackApiClient.js"></script>
     <script src="../content/shared/js/utils.js"></script>
     <script src="../content/shared/js/models.js"></script>
     <script src="../content/shared/js/router.js"></script>
     <script src="../content/shared/js/views.js"></script>
--- a/browser/devtools/webide/content/addons.js
+++ b/browser/devtools/webide/content/addons.js
@@ -1,17 +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/. */
 
 const Cu = Components.utils;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
 const {GetAvailableAddons, ForgetAddonsList} = require("devtools/webide/addons");
-const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
+const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   document.querySelector("#aboutaddons").onclick = function() {
     window.parent.UI.openInBrowser("about:addons");
   }
   document.querySelector("#close").onclick = CloseUI;
   GetAvailableAddons().then(BuildUI, (e) => {
--- a/browser/devtools/webide/content/addons.xhtml
+++ b/browser/devtools/webide/content/addons.xhtml
@@ -1,16 +1,16 @@
 <?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/. -->
 
 <!DOCTYPE html [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta charset="utf8"/>
     <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://webide/skin/addons.css" type="text/css"/>
--- a/browser/devtools/webide/content/details.xhtml
+++ b/browser/devtools/webide/content/details.xhtml
@@ -1,16 +1,16 @@
 <?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/. -->
 
 <!DOCTYPE html [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta charset="utf8"/>
     <link rel="stylesheet" href="chrome://webide/skin/details.css" type="text/css"/>
     <script type="application/javascript;version=1.8" src="chrome://webide/content/details.js"></script>
--- a/browser/devtools/webide/content/jar.mn
+++ b/browser/devtools/webide/content/jar.mn
@@ -15,14 +15,8 @@ webide.jar:
     content/permissionstable.js       (permissionstable.js)
     content/permissionstable.xhtml    (permissionstable.xhtml)
     content/runtimedetails.js         (runtimedetails.js)
     content/runtimedetails.xhtml      (runtimedetails.xhtml)
     content/prefs.js                  (prefs.js)
     content/prefs.xhtml               (prefs.xhtml)
     content/monitor.xhtml             (monitor.xhtml)
     content/monitor.js                (monitor.js)
-
-# Temporarily include locales in content, until we're ready
-# to localize webide
-
-    content/webide.dtd                (../locales/en-US/webide.dtd)
-    content/webide.properties         (../locales/en-US/webide.properties)
--- a/browser/devtools/webide/content/monitor.xhtml
+++ b/browser/devtools/webide/content/monitor.xhtml
@@ -1,16 +1,16 @@
 <?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/. -->
 
 <!DOCTYPE html [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta charset="utf8"/>
     <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
--- a/browser/devtools/webide/content/newapp.xul
+++ b/browser/devtools/webide/content/newapp.xul
@@ -1,16 +1,16 @@
 <?xml version="1.0"?>
 
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!DOCTYPE window [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 <?xml-stylesheet href="chrome://global/skin/global.css"?>
 <?xml-stylesheet href="chrome://webide/skin/newapp.css"?>
 
 <dialog id="webide:newapp" title="&newAppWindowTitle;"
   width="600" height="400"
--- a/browser/devtools/webide/content/permissionstable.xhtml
+++ b/browser/devtools/webide/content/permissionstable.xhtml
@@ -1,16 +1,16 @@
 <?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/. -->
 
 <!DOCTYPE html [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta charset="utf8"/>
     <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://webide/skin/permissionstable.css" type="text/css"/>
--- a/browser/devtools/webide/content/prefs.xhtml
+++ b/browser/devtools/webide/content/prefs.xhtml
@@ -1,16 +1,16 @@
 <?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/. -->
 
 <!DOCTYPE html [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta charset="utf8"/>
     <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://webide/skin/prefs.css" type="text/css"/>
--- a/browser/devtools/webide/content/runtimedetails.js
+++ b/browser/devtools/webide/content/runtimedetails.js
@@ -4,17 +4,17 @@
 
 const Cu = Components.utils;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
 const {AppManager} = require("devtools/webide/app-manager");
 const {Connection} = require("devtools/client/connection-manager");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {USBRuntime} = require("devtools/webide/runtimes");
-const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
+const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   document.querySelector("#close").onclick = CloseUI;
   document.querySelector("#certified-check button").onclick = EnableCertApps;
   document.querySelector("#adb-check button").onclick = RootADB;
   AppManager.on("app-manager-update", OnAppManagerUpdate);
   BuildUI();
--- a/browser/devtools/webide/content/runtimedetails.xhtml
+++ b/browser/devtools/webide/content/runtimedetails.xhtml
@@ -1,16 +1,16 @@
 <?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/. -->
 
 <!DOCTYPE html [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta charset="utf8"/>
     <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://webide/skin/runtimedetails.css" type="text/css"/>
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -17,17 +17,17 @@ const {AppProjects} = require("devtools/
 const {Connection} = require("devtools/client/connection-manager");
 const {AppManager} = require("devtools/webide/app-manager");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const ProjectEditor = require("projecteditor/projecteditor");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {GetAvailableAddons} = require("devtools/webide/addons");
 const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
 
-const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
+const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 const HTML = "http://www.w3.org/1999/xhtml";
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
 
 // download template index early
 GetTemplatesJSON(true);
 
 // See bug 989619
--- a/browser/devtools/webide/content/webide.xul
+++ b/browser/devtools/webide/content/webide.xul
@@ -1,16 +1,16 @@
 <?xml version="1.0"?>
 
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!DOCTYPE window [
-  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  <!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
   %webideDTD;
 ]>
 
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 
 <?xml-stylesheet href="chrome://global/skin/global.css"?>
 <?xml-stylesheet href="chrome://webide/skin/webide.css"?>
 
deleted file mode 100644
--- a/browser/devtools/webide/locales/en-US/webide.dtd
+++ /dev/null
@@ -1,138 +0,0 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!ENTITY windowTitle "Firefox WebIDE">
-
-<!ENTITY projectMenu_label "Project">
-<!ENTITY projectMenu_accesskey "P">
-<!ENTITY projectMenu_newApp_label "New App…">
-<!ENTITY projectMenu_newApp_accesskey "N">
-<!ENTITY projectMenu_importPackagedApp_label "Open Packaged App…">
-<!ENTITY projectMenu_importPackagedApp_accesskey "P">
-<!ENTITY projectMenu_importHostedApp_label "Open Hosted App…">
-<!ENTITY projectMenu_importHostedApp_accesskey "H">
-<!ENTITY projectMenu_selectApp_label "Open App…">
-<!ENTITY projectMenu_selectApp_accessley "S">
-<!ENTITY projectMenu_play_label "Install and Run">
-<!ENTITY projectMenu_play_accesskey "I">
-<!ENTITY projectMenu_stop_label "Stop App">
-<!ENTITY projectMenu_stop_accesskey "S">
-<!ENTITY projectMenu_debug_label "Debug App">
-<!ENTITY projectMenu_debug_accesskey "D">
-<!ENTITY projectMenu_remove_label "Remove Project">
-<!ENTITY projectMenu_remove_accesskey "R">
-<!ENTITY projectMenu_showPrefs_label "Preferences">
-<!ENTITY projectMenu_showPrefs_accesskey "e">
-
-<!ENTITY runtimeMenu_label "Runtime">
-<!ENTITY runtimeMenu_accesskey "R">
-<!ENTITY runtimeMenu_disconnect_label "Disconnect">
-<!ENTITY runtimeMenu_disconnect_accesskey "D">
-<!ENTITY runtimeMenu_showPermissionTable_label "Permissions Table">
-<!ENTITY runtimeMenu_showPermissionTable_accesskey "P">
-<!ENTITY runtimeMenu_takeScreenshot_label "Screenshot">
-<!ENTITY runtimeMenu_takeScreenshot_accesskey "S">
-<!ENTITY runtimeMenu_showDetails_label "Runtime Info">
-<!ENTITY runtimeMenu_showDetails_accesskey "E">
-<!ENTITY runtimeMenu_showMonitor_label "Monitor">
-<!ENTITY runtimeMenu_showMonitor_accesskey "M">
-
-<!ENTITY viewMenu_label "View">
-<!ENTITY viewMenu_accesskey "V">
-<!ENTITY viewMenu_toggleEditor_label "Toggle Editor">
-<!ENTITY viewMenu_toggleEditor_accesskey "E">
-<!ENTITY viewMenu_showAddons_label "Manage Simulators">
-<!ENTITY viewMenu_showAddons_accesskey "M">
-
-<!ENTITY projectButton_label "Open App">
-<!ENTITY runtimeButton_label "Select Runtime">
-
-<!-- We try to repicate Firefox' bindings: -->
-<!-- quit app -->
-<!ENTITY key_quit "W">
-<!-- open menu -->
-<!ENTITY key_showProjectPanel "O">
-<!-- reload app -->
-<!ENTITY key_play "R">
-<!-- show toolbox -->
-<!ENTITY key_toggleToolbox "VK_F12">
-<!-- toggle sidebar -->
-<!ENTITY key_toggleEditor "B">
-
-<!ENTITY projectPanel_myProjects "My Projects">
-<!ENTITY projectPanel_runtimeApps "Runtime Apps">
-<!ENTITY runtimePanel_USBDevices "USB Devices">
-<!ENTITY runtimePanel_WiFiDevices "WiFi Devices">
-<!ENTITY runtimePanel_simulators "Simulators">
-<!ENTITY runtimePanel_custom "Custom">
-<!ENTITY runtimePanel_installsimulator "Install Simulator">
-<!ENTITY runtimePanel_noadbhelper "Install ADB Helper">
-<!ENTITY runtimePanel_nousbdevice "Can't see your device?">
-
-<!-- Lense -->
-<!ENTITY details_valid_header "valid">
-<!ENTITY details_warning_header "warnings">
-<!ENTITY details_error_header "errors">
-<!ENTITY details_description "Description">
-<!ENTITY details_location "Location">
-<!ENTITY details_manifestURL "App ID">
-<!ENTITY details_removeProject_button "Remove Project">
-
-<!-- New App -->
-<!ENTITY newAppWindowTitle "New App">
-<!ENTITY newAppHeader "Select template">
-<!ENTITY newAppLoadingTemplate "Loading templates…">
-<!ENTITY newAppProjectName "Project Name:">
-
-
-<!-- Decks -->
-
-<!ENTITY deck_close "Close">
-
-<!-- Addons -->
-<!ENTITY addons_title "Extra Components">
-<!ENTITY addons_aboutaddons "Open Add-ons Manager">
-
-<!-- Prefs -->
-<!ENTITY prefs_title "Preferences">
-<!ENTITY prefs_editor_title "Editor">
-<!ENTITY prefs_general_title "General">
-<!ENTITY prefs_restore "Restore Defaults">
-<!ENTITY prefs_simulators "Manage Simulators">
-<!ENTITY prefs_options_enablelocalruntime "Enable local runtime">
-<!ENTITY prefs_options_enablelocalruntime_tooltip "Allow WebIDE to connect to its own runtime (running browser instance)">
-<!ENTITY prefs_options_rememberlastproject "Remember last project">
-<!ENTITY prefs_options_rememberlastproject_tooltip "Restore previous project when WebIDE starts">
-<!ENTITY prefs_options_templatesurl "Templates URL">
-<!ENTITY prefs_options_templatesurl_tooltip "Index of available templates">
-<!ENTITY prefs_options_showeditor "Show editor">
-<!ENTITY prefs_options_showeditor_tooltip "Show internal editor">
-<!ENTITY prefs_options_tabsize "Tab size">
-<!ENTITY prefs_options_expandtab "Soft tabs">
-<!ENTITY prefs_options_expandtab_tooltip "Use spaces instead of the tab character">
-<!ENTITY prefs_options_detectindentation "Autoindent">
-<!ENTITY prefs_options_detectindentation_tooltip "Guess indentation based on source content">
-<!ENTITY prefs_options_autocomplete "Autocomplete">
-<!ENTITY prefs_options_autocomplete_tooltip "Enable code autocompletion">
-<!ENTITY prefs_options_autoclosebrackets "Autoclose brackets">
-<!ENTITY prefs_options_autoclosebrackets_tooltip "Automatically insert closing brackets">
-<!ENTITY prefs_options_keybindings "Keybindings">
-<!ENTITY prefs_options_keybindings_default "Default">
-
-<!-- Permissions Table -->
-<!ENTITY permissionstable_title "Permissions Table">
-<!ENTITY permissionstable_name_header "Name">
-
-<!-- Runtime Details -->
-<!ENTITY runtimedetails_title "Runtime Info">
-<!ENTITY runtimedetails_adbIsRoot "ADB is root: ">
-<!ENTITY runtimedetails_summonADBRoot "root device">
-<!ENTITY runtimedetails_ADBRootWarning "(requires unlocked bootloader)">
-<!ENTITY runtimedetails_restrictedPrivileges "DevTools restricted privileges: ">
-<!ENTITY runtimedetails_requestPrivileges "request higher privileges">
-<!ENTITY runtimedetails_privilegesWarning "(Will reboot device. Requires root access.)">
-
-<!-- Monitor -->
-<!ENTITY monitor_title "Monitor">
-<!ENTITY monitor_help "Help">
deleted file mode 100644
--- a/browser/devtools/webide/locales/en-US/webide.properties
+++ /dev/null
@@ -1,50 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-title_noApp=Firefox WebIDE
-title_app=Firefox WebIDE: %S
-
-runtimeButton_label=Select Runtime
-projectButton_label=Open App
-
-mainProcess_label=Main Process
-
-local_runtime=Local Runtime
-remote_runtime=Remote Runtime
-remote_runtime_promptTitle=Remote Runtime
-remote_runtime_promptMessage=hostname:port
-
-importPackagedApp_title=Select Directory
-importHostedApp_title=Open Hosted App
-importHostedApp_header=Enter Manifest URL
-
-notification_showTroubleShooting_label=troubleshooting
-notification_showTroubleShooting_accesskey=t
-
-error_operationTimeout=Operation timed out: %1$S
-error_operationFail=Operation failed: %1$S
-error_listRunningApps=Can't get app list from device
-error_cantConnectToApp=Can't connect to app: %1$S
-error_cantInstallNotFullyConnected=Can't install project. Not fully connected.
-error_cantInstallValidationErrors=Can't install project. Validation errors.
-error_cantFetchAddonsJSON=Can't fetch the add-on list: %S
-
-addons_stable=stable
-addons_unstable=unstable
-addons_simulator_label=Firefox OS %1$S Simulator (%2$S)
-addons_install_button=install
-addons_uninstall_button=uninstall
-addons_adb_label=ADB Helper Add-on
-addons_adb_warning=USB devices won't be detected without this add-on
-addons_status_unknown=?
-addons_status_installed=Installed
-addons_status_uninstalled=Not Installed
-addons_status_preparing=preparing
-addons_status_downloading=downloading
-addons_status_installing=installing
-
-runtimedetails_checkno=no
-runtimedetails_checkyes=yes
-runtimedetails_checkunkown=unknown
-runtimedetails_notUSBDevice=Not a USB device
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -21,17 +21,17 @@ const AppActorFront = require("devtools/
 const {getDeviceFront} = require("devtools/server/actors/device");
 const {getPreferenceFront} = require("devtools/server/actors/preference");
 const {setTimeout} = require("sdk/timers");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const {USBRuntime, WiFiRuntime, SimulatorRuntime,
        gLocalRuntime, gRemoteRuntime} = require("devtools/webide/runtimes");
 const discovery = require("devtools/toolkit/discovery/discovery");
 
-const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
+const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 const WIFI_SCANNING_PREF = "devtools.remote.wifi.scan";
 
 exports.AppManager = AppManager = {
 
   // FIXME: will break when devtools/app-manager will be removed:
   DEFAULT_PROJECT_ICON: "chrome://browser/skin/devtools/app-manager/default-app-icon.png",
   DEFAULT_PROJECT_NAME: "--",
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -6,17 +6,17 @@ const {Cu} = require("chrome");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 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://webide/content/webide.properties");
+const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 function USBRuntime(id) {
   this.id = id;
 }
 
 USBRuntime.prototype = {
   connect: function(connection) {
     let device = Devices.getByName(this.id);
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/webide.dtd
@@ -0,0 +1,138 @@
+<!-- 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/. -->
+
+<!ENTITY windowTitle "Firefox WebIDE">
+
+<!ENTITY projectMenu_label "Project">
+<!ENTITY projectMenu_accesskey "P">
+<!ENTITY projectMenu_newApp_label "New App…">
+<!ENTITY projectMenu_newApp_accesskey "N">
+<!ENTITY projectMenu_importPackagedApp_label "Open Packaged App…">
+<!ENTITY projectMenu_importPackagedApp_accesskey "P">
+<!ENTITY projectMenu_importHostedApp_label "Open Hosted App…">
+<!ENTITY projectMenu_importHostedApp_accesskey "H">
+<!ENTITY projectMenu_selectApp_label "Open App…">
+<!ENTITY projectMenu_selectApp_accessley "S">
+<!ENTITY projectMenu_play_label "Install and Run">
+<!ENTITY projectMenu_play_accesskey "I">
+<!ENTITY projectMenu_stop_label "Stop App">
+<!ENTITY projectMenu_stop_accesskey "S">
+<!ENTITY projectMenu_debug_label "Debug App">
+<!ENTITY projectMenu_debug_accesskey "D">
+<!ENTITY projectMenu_remove_label "Remove Project">
+<!ENTITY projectMenu_remove_accesskey "R">
+<!ENTITY projectMenu_showPrefs_label "Preferences">
+<!ENTITY projectMenu_showPrefs_accesskey "e">
+
+<!ENTITY runtimeMenu_label "Runtime">
+<!ENTITY runtimeMenu_accesskey "R">
+<!ENTITY runtimeMenu_disconnect_label "Disconnect">
+<!ENTITY runtimeMenu_disconnect_accesskey "D">
+<!ENTITY runtimeMenu_showPermissionTable_label "Permissions Table">
+<!ENTITY runtimeMenu_showPermissionTable_accesskey "P">
+<!ENTITY runtimeMenu_takeScreenshot_label "Screenshot">
+<!ENTITY runtimeMenu_takeScreenshot_accesskey "S">
+<!ENTITY runtimeMenu_showDetails_label "Runtime Info">
+<!ENTITY runtimeMenu_showDetails_accesskey "E">
+<!ENTITY runtimeMenu_showMonitor_label "Monitor">
+<!ENTITY runtimeMenu_showMonitor_accesskey "M">
+
+<!ENTITY viewMenu_label "View">
+<!ENTITY viewMenu_accesskey "V">
+<!ENTITY viewMenu_toggleEditor_label "Toggle Editor">
+<!ENTITY viewMenu_toggleEditor_accesskey "E">
+<!ENTITY viewMenu_showAddons_label "Manage Simulators">
+<!ENTITY viewMenu_showAddons_accesskey "M">
+
+<!ENTITY projectButton_label "Open App">
+<!ENTITY runtimeButton_label "Select Runtime">
+
+<!-- We try to repicate Firefox' bindings: -->
+<!-- quit app -->
+<!ENTITY key_quit "W">
+<!-- open menu -->
+<!ENTITY key_showProjectPanel "O">
+<!-- reload app -->
+<!ENTITY key_play "R">
+<!-- show toolbox -->
+<!ENTITY key_toggleToolbox "VK_F12">
+<!-- toggle sidebar -->
+<!ENTITY key_toggleEditor "B">
+
+<!ENTITY projectPanel_myProjects "My Projects">
+<!ENTITY projectPanel_runtimeApps "Runtime Apps">
+<!ENTITY runtimePanel_USBDevices "USB Devices">
+<!ENTITY runtimePanel_WiFiDevices "Wi-Fi Devices">
+<!ENTITY runtimePanel_simulators "Simulators">
+<!ENTITY runtimePanel_custom "Custom">
+<!ENTITY runtimePanel_installsimulator "Install Simulator">
+<!ENTITY runtimePanel_noadbhelper "Install ADB Helper">
+<!ENTITY runtimePanel_nousbdevice "Can't see your device?">
+
+<!-- Lense -->
+<!ENTITY details_valid_header "valid">
+<!ENTITY details_warning_header "warnings">
+<!ENTITY details_error_header "errors">
+<!ENTITY details_description "Description">
+<!ENTITY details_location "Location">
+<!ENTITY details_manifestURL "App ID">
+<!ENTITY details_removeProject_button "Remove Project">
+
+<!-- New App -->
+<!ENTITY newAppWindowTitle "New App">
+<!ENTITY newAppHeader "Select template">
+<!ENTITY newAppLoadingTemplate "Loading templates…">
+<!ENTITY newAppProjectName "Project Name:">
+
+
+<!-- Decks -->
+
+<!ENTITY deck_close "Close">
+
+<!-- Addons -->
+<!ENTITY addons_title "Extra Components">
+<!ENTITY addons_aboutaddons "Open Add-ons Manager">
+
+<!-- Prefs -->
+<!ENTITY prefs_title "Preferences">
+<!ENTITY prefs_editor_title "Editor">
+<!ENTITY prefs_general_title "General">
+<!ENTITY prefs_restore "Restore Defaults">
+<!ENTITY prefs_simulators "Manage Simulators">
+<!ENTITY prefs_options_enablelocalruntime "Enable local runtime">
+<!ENTITY prefs_options_enablelocalruntime_tooltip "Allow WebIDE to connect to its own runtime (running browser instance)">
+<!ENTITY prefs_options_rememberlastproject "Remember last project">
+<!ENTITY prefs_options_rememberlastproject_tooltip "Restore previous project when WebIDE starts">
+<!ENTITY prefs_options_templatesurl "Templates URL">
+<!ENTITY prefs_options_templatesurl_tooltip "Index of available templates">
+<!ENTITY prefs_options_showeditor "Show editor">
+<!ENTITY prefs_options_showeditor_tooltip "Show internal editor">
+<!ENTITY prefs_options_tabsize "Tab size">
+<!ENTITY prefs_options_expandtab "Soft tabs">
+<!ENTITY prefs_options_expandtab_tooltip "Use spaces instead of the tab character">
+<!ENTITY prefs_options_detectindentation "Autoindent">
+<!ENTITY prefs_options_detectindentation_tooltip "Guess indentation based on source content">
+<!ENTITY prefs_options_autocomplete "Autocomplete">
+<!ENTITY prefs_options_autocomplete_tooltip "Enable code autocompletion">
+<!ENTITY prefs_options_autoclosebrackets "Autoclose brackets">
+<!ENTITY prefs_options_autoclosebrackets_tooltip "Automatically insert closing brackets">
+<!ENTITY prefs_options_keybindings "Keybindings">
+<!ENTITY prefs_options_keybindings_default "Default">
+
+<!-- Permissions Table -->
+<!ENTITY permissionstable_title "Permissions Table">
+<!ENTITY permissionstable_name_header "Name">
+
+<!-- Runtime Details -->
+<!ENTITY runtimedetails_title "Runtime Info">
+<!ENTITY runtimedetails_adbIsRoot "ADB is root: ">
+<!ENTITY runtimedetails_summonADBRoot "root device">
+<!ENTITY runtimedetails_ADBRootWarning "(requires unlocked bootloader)">
+<!ENTITY runtimedetails_restrictedPrivileges "DevTools restricted privileges: ">
+<!ENTITY runtimedetails_requestPrivileges "request higher privileges">
+<!ENTITY runtimedetails_privilegesWarning "(Will reboot device. Requires root access.)">
+
+<!-- Monitor -->
+<!ENTITY monitor_title "Monitor">
+<!ENTITY monitor_help "Help">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/webide.properties
@@ -0,0 +1,58 @@
+# 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/.
+
+title_noApp=Firefox WebIDE
+title_app=Firefox WebIDE: %S
+
+runtimeButton_label=Select Runtime
+projectButton_label=Open App
+
+mainProcess_label=Main Process
+
+local_runtime=Local Runtime
+remote_runtime=Remote Runtime
+remote_runtime_promptTitle=Remote Runtime
+remote_runtime_promptMessage=hostname:port
+
+importPackagedApp_title=Select Directory
+importHostedApp_title=Open Hosted App
+importHostedApp_header=Enter Manifest URL
+
+notification_showTroubleShooting_label=troubleshooting
+notification_showTroubleShooting_accesskey=t
+
+# These messages appear in a notification box when an error occur.
+
+error_cantInstallNotFullyConnected=Can't install project. Not fully connected.
+error_cantInstallValidationErrors=Can't install project. Validation errors.
+error_listRunningApps=Can't get app list from device
+
+# Variable: name of the operation (in english)
+error_operationTimeout=Operation timed out: %1$S
+error_operationFail=Operation failed: %1$S
+
+# Variable: app name
+error_cantConnectToApp=Can't connect to app: %1$S
+
+# Variable: error message (in english)
+error_cantFetchAddonsJSON=Can't fetch the add-on list: %S
+
+addons_stable=stable
+addons_unstable=unstable
+addons_simulator_label=Firefox OS %1$S Simulator (%2$S)
+addons_install_button=install
+addons_uninstall_button=uninstall
+addons_adb_label=ADB Helper Add-on
+addons_adb_warning=USB devices won't be detected without this add-on
+addons_status_unknown=?
+addons_status_installed=Installed
+addons_status_uninstalled=Not Installed
+addons_status_preparing=preparing
+addons_status_downloading=downloading
+addons_status_installing=installing
+
+runtimedetails_checkno=no
+runtimedetails_checkyes=yes
+runtimedetails_checkunkown=unknown
+runtimedetails_notUSBDevice=Not a USB device
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -59,16 +59,18 @@
     locale/browser/devtools/inspector.dtd          (%chrome/browser/devtools/inspector.dtd)
     locale/browser/devtools/projecteditor.properties     (%chrome/browser/devtools/projecteditor.properties)
     locale/browser/devtools/eyedropper.properties     (%chrome/browser/devtools/eyedropper.properties)
     locale/browser/devtools/connection-screen.dtd  (%chrome/browser/devtools/connection-screen.dtd)
     locale/browser/devtools/connection-screen.properties (%chrome/browser/devtools/connection-screen.properties)
     locale/browser/devtools/font-inspector.dtd     (%chrome/browser/devtools/font-inspector.dtd)
     locale/browser/devtools/app-manager.dtd        (%chrome/browser/devtools/app-manager.dtd)
     locale/browser/devtools/app-manager.properties (%chrome/browser/devtools/app-manager.properties)
+    locale/browser/devtools/webide.dtd             (%chrome/browser/devtools/webide.dtd)
+    locale/browser/devtools/webide.properties      (%chrome/browser/devtools/webide.properties)
     locale/browser/loop/loop.properties            (%chrome/browser/loop/loop.properties)
     locale/browser/newTab.dtd                      (%chrome/browser/newTab.dtd)
     locale/browser/newTab.properties               (%chrome/browser/newTab.properties)
     locale/browser/pageInfo.dtd                    (%chrome/browser/pageInfo.dtd)
     locale/browser/pageInfo.properties             (%chrome/browser/pageInfo.properties)
     locale/browser/quitDialog.properties           (%chrome/browser/quitDialog.properties)
     locale/browser/safeMode.dtd                    (%chrome/browser/safeMode.dtd)
     locale/browser/sanitize.dtd                    (%chrome/browser/sanitize.dtd)
index dc0329419f3d0f9d9cd19fb4de36f73dc9471258..87e54cd7f80bfb1c4dbbca0f1d62defc1889cc32
GIT binary patch
literal 3995
zc$@*74`lF(P)<h;3K|Lk000e1NJLTq004df000sQ1^@s6ofT@S000kSNkl<ZcmeI1
zd301&g2uml@4IiSy^=~o7M75OKtK|dMIi(<ZVauq*g8$C=x8G@ZL7FoOOG;W_Z+od
zPLDm!%-GJHo|chu5a?qgEu?Lw+ocg@F>D%^V5kI=RHe4JPfaBjAVpGxf6n~QsZ%+1
zZvE=3`@MVbE5QHre-nZnl#Z#JB?xRW7o|CZz(-I*G|RU48iu~XvaA;YbYmDy!w?A}
zw+Niyz&USVl=(mqu`GMLWg4r~o?ijbkq3c9Y?e&vVjp93T#RP&7`C&w-?BDnj+0NG
z3`c|3EGCh~vPYOBDs%*)&@98*tH<pPrf%o$VJ4j=%5<^Y$LGi{mi7?MOnZ|3<_2A}
z^3QX~KZ`JFF%vy=C|5^-P|dN8y|y0T;8=Rz9*sM*h^Q<kYG4kL+#{T{2bQtdNk%rn
zNagepf5Uuyh1z#OrIfC6x!emqULV|EFBC-ur4+VpLrW&n-ye;~<Nb>P{5#G<UvKYZ
z&iU(#>Z()}#c-*v9kML<TDDbSnC3*&v;-}gIG9K#z71eI&VqEjOBD%ORVs=LD-{Xl
zq6ojhAQC$8Ii`trO^<(Wm_z3IM2)LVAgesZd|{wMLZD27D=0t^Dbj@_(PyIfh#rp|
zv4-@-!<s6HWL2nCSy)=>Ldj?a{vsKoL||w(BFA-n*`A0W`&=8+vtt`q2^CgxSII)r
zQvu0a0YNDMVItCn!%X!+?>`c^V~2<IM6}~7Vd5%MFmfR+ssSsi2Jw~vT!v$&fM^%&
zu03(qz5fgkDV}>90D!e`<%|iNe1Sl7C{zTWKLAOVAqYHEyyG~qZ40SX3cWoipeGV{
z0{8{KM*G1p8YD@2FMZ+gva-@&{OG|S{@X<}rbYp10L-`EZVkM&>Xkd9vG^UDmNF!P
z%>nQMzDCdPom{4LQ>`L5H@aOID@&jP>yG(+uUih_jYJj%2q{e24t6CH*u{jLc{b^t
zT#)Fd!ZC7l=>!!8V`Qj)0ckmyK;SrlW!OkYEQI$Z5IvCFb7a?XF3EILMXjf~c8VVr
zHEwtdM92yQ<C*!%Ttv|x17GZn;P6f>r>FII2~L^3N%B`VtD$<NpCQNrP(cQv0+5+!
z16HaR$)0@>llyY#IrbSRqPmHdPH1M8Gmw4;@f3lGE`V{su`?GD`nzEs`54~Lk52QD
zss)b%08-o6tw@)>d_-9}f(4;W>4XsE!Z37nhr?!Rac~-dk1@#GZ*P%BQEc=20;RK?
zF8cn9Pp|wFfF1z70BoFuE?9W$)hGJ;SKFr9^{sg=697bT3cq>!*%j&1mp6GmsCTJw
zQ;H*m?9?x~;IWP`I}_TTJx@2v(&P~$N78oiVJvPQudY5dPljLZ-MXT%QeHmh0uPF7
z6evCh$_UQD0knjTqdVfJKVEZ&=e^&5uwrDbd->$qg{Z9e!W$HGdK?Ex#Z0t)*k@W@
z<!5+a`R6?=<iObF?$T+HgX6$t9|&Q{705OcNOo;E>x!dic(%O1Wd$pnxSZFufQ_00
z>Mj7NfLwu`R6nc(@0yM5i>!ytd*DTUvi2W?imG;$Rg6-KN0er^_e|+SkV+;H={>Rj
z&DVc@-B)LIMOFPh>6QLsO-=QaZ>)ahUjZBikic1(KKt@VWS9FVLBIEj|N70#PYg^y
zFuA-kSS<36xqhEA!{bH~7hpIJ_Bq&TPMx;&Pal2MYlORgIoIoM7^f%@DZz2m#5=aL
ze%U))vUx@~R0iFJykqPPuQGD78}0%Dgc1xzyw}9BzhM1;tT^czH?<}hES5T^TpUvB
zX9VFdl0b*?^c~l*`wyx0&p)ss>j_oP2nuSb!&5#}ag{WJD}?}|7z#7l1Jd`w`fY0;
z&Uyk9<^-v~v_qOWPvNz*LA@maWf%%O8iu<6ckA1Jz9{1ne*4TlsCeU-^IUF^;tvF&
zs48eiGBlE+g2nn<cDL;<25<xbIOo@^E_dJ0pMC080LL?>4?$OV*Uv(PJRB8-`2d~=
z&<6mFka?w|sMN?Z3b+792rK~9FxCD@pSv&8Z*tpmS<b-9ARq`ZhEZCw<>1a;**uhy
zdA=f135{0Z4sb9=FdQzQ0A)n7Wl#I*p29Lksi^V5S0I5gisA4EBovNLwH!F~S=Ix}
z=85hSMGjPgs|A^@eK-Wg4=rBS@@d<ltcRpMLa;&+O6$NpArLlfUzr^t=^xwj$^N4m
zkC6TYjInyv<;s*U2psu9DTSh_%BIcl)ZKUY_d5VMTofmHeV+Hq%Suz35{Kc7&py`2
zPq}D|tSFZO@O<?iPDp(b7ZBhKoRI9#4^D|7Fh)g@bDpF!C&=zh0wE}1RQdF`Z`NJ4
z?4e8^CXo8{iig`TfODFW3<nhmTz;l(+4g4LoeKs$T%`4ZVkvW`3lam#!y||U-a=7%
zf7hnE@6G#R#zQHu7nBgV90X+|@(Dzk2r37aH{RY+_qX@V&v=L+)-!(zxVHd|^L#V_
z72tS_l>hwmzPgn+&FK&(SC@e?7La5GoQs)4^F`ns{qeXD00*F_Jswq2`v4>XILHIj
zFv6B;xd2EQM4}|%X95&TK`{Uu7@#J5dJACal1hn7A_Vl5>Enc=KN`vA5kwkLy#geK
zWmbE>h$2Hmk7hkwq5-#$Lsl6mL*57?Maqn2J%mXCF8jbl6_g6d7bZxE#-b-ZTnY$k
zAnjK&M^3&tTtp;gWjq2hkT4|_j0vEW<u7u8Q5EIK0nh;r?Y1nd1b~&l1SJ4YWF488
z5GI6x5t3celn^My3gTF}*=yM;zreh*z_MeU!4D0M9(^(o5T=wM^B^SOJa8-r<)M=v
z$4Pq_0cF`ddCiTT_LLM>oR)`B215SlL6LtT8<isqvmOE`^Sv0QU?l&t9Lqq}h(N|8
z45*#0rfC3#fU^9Gw=4^eZHK>m-PK=yRkUqeTQyz3><{n$*#{tuVYu;@+eNp>+boLw
z{ZlU%+riejZNYL*u4dvm03lJ?=RV@IZNnRn$7MqNl#t9A-F6TI!t-yrIh)6^vDHY}
zAojo{<%6l)uu?R9ZSzg1c$}@NxCPrn-Yd+|Z0K<(eEB8c&3XWPt8F9z;(+A2z#K~l
zagyQNzccrwCq2*5VqlI5Mv<S#(7}voc*zwF8P7oRw%wXcBsvUTD|fry$Ok=@g2dTc
z&ba}=007H0Up7ta>VNp<%S+b$`kx*EU=MSjaIYjufpIm}YXKxNh~YS`$8@73Vw&Y4
zE`R_7E1J-Vr+nmg!8({q#i9|@ar_v#AP}=G^m)B&1yRiAu}!BncHHRDVph4YPz1>E
ziz}J5L1NyuTsY}54X5=)r`8dV8s&jv8H5bK+LLh;sb1Gwf$^-zvCUR9(bZw={pHkE
zgnR-SS^|C^T`O{y^$^Qwb^1CwNTR1)@E0MUK&BQ!$Ox|$g#k~d`1U=YX!TQOE{*i{
zuJ-u+5JeF~VHi4+iFo+#+rIZN0HPRV|IUv#Ol`VsOC%=WwP^9Z8v(q9v(Vbwda3Gi
zKk4y!T3>qZC-0p4JN|>i2ef(B6PE5zYOBjc5iT~crMn!f_~?!ucbU<CQ>C;RgC~5_
za?oK~;aLwpa9W;y@9)>9Tv5OD_`%fbV2K1tWkZ&(YZm%Dt?<nk-*=j4#~<6Yrf)Ya
zJ+d#hI#i)TaS20~t|v`&AJN0}r`>&;C%kuyHmc#erK!I5)sm+iOmq!dx@{z3B|F29
zF23+IPuKR1TE*-eml~aWR!fCxkL($;bX$*u^c)N?zj{*ELmbD!S2SIA^_np?bqoA~
zASk6d91aX!M>NtGJ%8f3MfWbb^-}-`PhHuylcxtu%PNkR6o+1T_2riyA5+&L1L!;D
zhzO?L_p2_~BVMns?a9X<z2)NBGmio2z}NV}dFQYBmd~@GQkF8s8@7!TripIDgg^+2
zIR~Qzhjk+=U2^fFmLETMYMv}tE}6DwQj32<Xrz4Be{|8}7J53gXsvI~qT8-qmeaHF
zq1kI@UQ@K7tjZ0R%VRr8#w>ImOhilQg^Rw^bbn4y-6apL@m2oS0@*(%_m=P2u#7nL
z=;7$44SdmxyP9))YW{k~8qc)r7DywiL8y@1lZwIU`aF6?xv^;F4O6q8(?&-lk;+se
z4$I?(ojE&CxpA^g=$eL9GQMr@%xTMRS$JJLfTLdzW9a(mR5Hnefr1mWF1zNfbh%Bo
z<Lq&4`-}7=L>X%kC7F7>-dCUh=@ZXPo;0BsKqqq0nMhQ2t1^nYki9u`QwC8a&>aU6
z(?nQc+sYPPyKLqU?#`7bLvJ`y8S8Pu7ZO1Re*zMkJ<~H~#lmFUv@zE#yJ60fvwFHa
zW0k#KJ_JkTfwP`q;6ic+&h&WHKu?FgZG6!c%jQk{o3nZn(Qu`jJOR;N3_>UdLeaq+
zOAV&p51Q)Sc71d4viokFepXNF#L-Gu<QRy*c<=%uAe6#^1IN%{C8D5_!`l{4^e?-A
z{?yYv+2Tjno;N*E5Ngyl9sQBsHn&UJ7>^~YEZYeYN>YrVy{dB5<{v%q#7BeYaX8oi
zt`U&Mlres&<LFbW>RMM23_fF7<|JJ=gMf3)?RGueGPh~-9k<?iXmES)&h@1lE5=T*
zlI6xe!@@q(ZZiuDHYTI7DrOoXN?9r;i|vKu$8Wy=*}oq$Pxbj#(~Cx{jj5=Ko_4cM
za)KM<$!L{Btq>(7<>*rT=;Cpk|L)eOhV)Fhuy%TRwWl#2G10MCYh#9QV<OpKMFcBE
zKvR|`w3h|PZ+>w8kB9UW*ECI60%IF3Jqok`KwF{9+!&3gs!WT8C?zS0YVA|T1~)(V
z@Kr;5Ce59$l-4vliC#FzcDEI9eZWJ65G6=ScB;L;%)j}$yP9))GR2RoJ&y{4u#$`X
zwe#w0*FFBoeSZO<0q6%H4vupM-x$k?axi+_<SDYOT;%t8pT7O(8`fQQ<)vMN<8=TM
z01Cj$xU;XHNkhe0DigAjbKx~#)68|({_N><%2PF^igJmr6w{vC!VA|uc+*eM$uoXN
zEfr;!$y4q+f8CPHSDcflxVni_K}vhX*Je-jtb5_HdFSSt)J!Q?(tQc9%^t<qy>REO
z|FtK3##c|8O84*huynAPzRcirLk3VX7$O3|#5pi)R5g9~(Ee|nr@m<%-Sx*k-`F#y
zWdhyy``zEzQ##>F`sMC7zp<xm>U?_i!wu)``ELn?$#42DcFX_(002ovPDHLkV1oBp
BmDm6P
--- a/content/media/RtspMediaResource.h
+++ b/content/media/RtspMediaResource.h
@@ -217,17 +217,21 @@ public:
   private:
     nsRefPtr<RtspMediaResource> mResource;
   };
   friend class Listener;
 
 protected:
   // Main thread access only.
   // These are called on the main thread by Listener.
-  NS_DECL_NSISTREAMINGPROTOCOLLISTENER
+  nsresult OnMediaDataAvailable(uint8_t aIndex, const nsACString& aData,
+                                uint32_t aLength, uint32_t aOffset,
+                                nsIStreamingProtocolMetaData* aMeta);
+  nsresult OnConnected(uint8_t aIndex, nsIStreamingProtocolMetaData* aMeta);
+  nsresult OnDisconnected(uint8_t aIndex, nsresult aReason);
 
   nsRefPtr<Listener> mListener;
 
 private:
   // Notify mDecoder the rtsp stream is suspend. Main thread only.
   void NotifySuspend(bool aIsSuspend);
   bool IsVideoEnabled();
   bool IsVideo(uint8_t tracks, nsIStreamingProtocolMetaData *meta);
--- a/gfx/layers/apz/src/GestureEventListener.cpp
+++ b/gfx/layers/apz/src/GestureEventListener.cpp
@@ -49,17 +49,19 @@ float GetCurrentSpan(const MultiTouchInp
   return float(NS_hypot(delta.x, delta.y));
 }
 
 GestureEventListener::GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController)
   : mAsyncPanZoomController(aAsyncPanZoomController),
     mState(GESTURE_NONE),
     mSpanChange(0.0f),
     mPreviousSpan(0.0f),
-    mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0)
+    mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0),
+    mLongTapTimeoutTask(nullptr),
+    mMaxTapTimeoutTask(nullptr)
 {
 }
 
 GestureEventListener::~GestureEventListener()
 {
 }
 
 nsEventStatus GestureEventListener::HandleInputEvent(const MultiTouchInput& aEvent)
@@ -378,16 +380,18 @@ nsEventStatus GestureEventListener::Hand
   }
 
   return rv;
 }
 
 nsEventStatus GestureEventListener::HandleInputTouchCancel()
 {
   SetState(GESTURE_NONE);
+  CancelMaxTapTimeoutTask();
+  CancelLongTapTimeoutTask();
   return nsEventStatus_eIgnore;
 }
 
 void GestureEventListener::HandleInputTimeoutLongTap()
 {
   GEL_LOG("Running long-tap timeout task in state %d\n", mState);
 
   mLongTapTimeoutTask = nullptr;
--- a/mobile/android/base/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/favicons/LoadFaviconTask.java
@@ -135,35 +135,52 @@ public class LoadFaviconTask extends UiA
             // Was the response a failure?
             int status = response.getStatusLine().getStatusCode();
 
             // Handle HTTP status codes requesting a redirect.
             if (status >= 300 && status < 400) {
                 Header header = response.getFirstHeader("Location");
 
                 // Handle mad webservers.
-                if (header == null) {
-                    return null;
+                final String newURI;
+                try {
+                    if (header == null) {
+                        return null;
+                    }
+
+                    newURI = header.getValue();
+                    if (newURI == null || newURI.equals(faviconURI.toString())) {
+                        return null;
+                    }
+
+                    if (visited.contains(newURI)) {
+                        // Already been redirected here - abort.
+                        return null;
+                    }
+
+                    visited.add(newURI);
+                } finally {
+                    // Consume the entity before recurse or exit.
+                    try {
+                        response.getEntity().consumeContent();
+                    } catch (Exception e) {
+                        // Doesn't matter.
+                    }
                 }
 
-                String newURI = header.getValue();
-                if (newURI == null || newURI.equals(faviconURI.toString())) {
-                    return null;
-                }
-
-                if (visited.contains(newURI)) {
-                    // Already been redirected here - abort.
-                    return null;
-                }
-
-                visited.add(newURI);
                 return tryDownloadRecurse(new URI(newURI), visited);
             }
 
             if (status >= 400) {
+                // Consume the entity and exit.
+                try {
+                    response.getEntity().consumeContent();
+                } catch (Exception e) {
+                    // Doesn't matter.
+                }
                 return null;
             }
         }
         return response;
     }
 
     /**
      * Retrieve the specified favicon from the JAR, returning null if it's not
--- a/mozglue/build/Nuwa.cpp
+++ b/mozglue/build/Nuwa.cpp
@@ -135,17 +135,31 @@ struct thread_info : public mozilla::Lin
 
   // The thread specific function to recreate the new thread. It's executed
   // after the thread is recreated.
   void (*recrFunc)(void *arg);
   void *recrArg;
 
   TLSInfoList tlsInfo;
 
-  pthread_mutex_t *reacquireMutex;
+  /**
+   * We must ensure that the recreated thread has entered pthread_cond_wait() or
+   * similar functions before proceeding to recreate the next one. Otherwise, if
+   * the next thread depends on the same mutex, it may be used in an incorrect
+   * state.  To do this, the main thread must unconditionally acquire the mutex.
+   * The mutex is unconditionally released when the recreated thread enters
+   * pthread_cond_wait().  The recreated thread may have locked the mutex itself
+   * (if the pthread_mutex_trylock succeeded) or another thread may have already
+   * held the lock.  If the recreated thread did lock the mutex we must balance
+   * that with another unlock on the main thread, which is signaled by
+   * condMutexNeedsBalancing.
+   */
+  pthread_mutex_t *condMutex;
+  bool condMutexNeedsBalancing;
+
   void *stk;
 
   pid_t origNativeThreadID;
   pid_t recreatedNativeThreadID;
   char nativeThreadName[NATIVE_THREAD_NAME_LENGTH];
 };
 
 typedef struct thread_info thread_info_t;
@@ -496,17 +510,18 @@ static thread_info_t *
 thread_info_new(void) {
   /* link tinfo to sAllThreads */
   thread_info_t *tinfo = new thread_info_t();
   tinfo->flags = 0;
   tinfo->recrFunc = nullptr;
   tinfo->recrArg = nullptr;
   tinfo->recreatedThreadID = 0;
   tinfo->recreatedNativeThreadID = 0;
-  tinfo->reacquireMutex = nullptr;
+  tinfo->condMutex = nullptr;
+  tinfo->condMutexNeedsBalancing = false;
   tinfo->stk = MozTaggedAnonymousMmap(nullptr,
                                       NUWA_STACK_SIZE + getPageSize(),
                                       PROT_READ | PROT_WRITE,
                                       MAP_PRIVATE | MAP_ANONYMOUS,
                                       /* fd */ -1,
                                       /* offset */ 0,
                                       "nuwa-thread-stack");
 
@@ -1011,29 +1026,42 @@ extern "C" MFBT_API int
   THREAD_FREEZE_POINT1_VIP();
   if (freezePoint2) {
     RECREATE_CONTINUE();
     RECREATE_PASS_VIP();
     RECREATE_GATE_VIP();
     return rv;
   }
   if (recreated && mtx) {
-    if (!freezePoint1 && pthread_mutex_trylock(mtx)) {
+    if (!freezePoint1) {
+      tinfo->condMutex = mtx;
       // The thread was frozen in pthread_cond_wait() after releasing mtx in the
       // Nuwa process. In recreating this thread, We failed to reacquire mtx
       // with the pthread_mutex_trylock() call, that is, mtx was acquired by
       // another thread. Because of this, we need the main thread's help to
       // reacquire mtx so that it will be in a valid state.
-      tinfo->reacquireMutex = mtx;
+      if (!pthread_mutex_trylock(mtx)) {
+        tinfo->condMutexNeedsBalancing = true;
+      }
     }
     RECREATE_CONTINUE();
     RECREATE_PASS_VIP();
   }
   rv = REAL(pthread_cond_wait)(cond, mtx);
   if (recreated && mtx) {
+    // We have reacquired mtx. The main thread also wants to acquire mtx to
+    // synchronize with us. If the main thread didn't get a chance to acquire
+    // mtx let it do that now. The main thread clears condMutex after acquiring
+    // it to signal us.
+    if (tinfo->condMutex) {
+      // We need mtx to end up locked, so tell the main thread not to unlock
+      // mtx after it locks it.
+      tinfo->condMutexNeedsBalancing = false;
+      pthread_mutex_unlock(mtx);
+    }
     // We still need to be gated as not to acquire another mutex associated with
     // another VIP thread and interfere with it.
     RECREATE_GATE_VIP();
   }
   THREAD_FREEZE_POINT2_VIP();
 
   return rv;
 }
@@ -1047,24 +1075,31 @@ extern "C" MFBT_API int
   THREAD_FREEZE_POINT1_VIP();
   if (freezePoint2) {
     RECREATE_CONTINUE();
     RECREATE_PASS_VIP();
     RECREATE_GATE_VIP();
     return rv;
   }
   if (recreated && mtx) {
-    if (!freezePoint1 && pthread_mutex_trylock(mtx)) {
-      tinfo->reacquireMutex = mtx;
+    if (!freezePoint1) {
+      tinfo->condMutex = mtx;
+      if (!pthread_mutex_trylock(mtx)) {
+        tinfo->condMutexNeedsBalancing = true;
+      }
     }
     RECREATE_CONTINUE();
     RECREATE_PASS_VIP();
   }
   rv = REAL(pthread_cond_timedwait)(cond, mtx, abstime);
   if (recreated && mtx) {
+    if (tinfo->condMutex) {
+      tinfo->condMutexNeedsBalancing = false;
+      pthread_mutex_unlock(mtx);
+    }
     RECREATE_GATE_VIP();
   }
   THREAD_FREEZE_POINT2_VIP();
 
   return rv;
 }
 
 extern "C" int __pthread_cond_timedwait(pthread_cond_t *cond,
@@ -1082,24 +1117,31 @@ extern "C" MFBT_API int
   THREAD_FREEZE_POINT1_VIP();
   if (freezePoint2) {
     RECREATE_CONTINUE();
     RECREATE_PASS_VIP();
     RECREATE_GATE_VIP();
     return rv;
   }
   if (recreated && mtx) {
-    if (!freezePoint1 && pthread_mutex_trylock(mtx)) {
-      tinfo->reacquireMutex = mtx;
+    if (!freezePoint1) {
+      tinfo->condMutex = mtx;
+      if (!pthread_mutex_trylock(mtx)) {
+        tinfo->condMutexNeedsBalancing = true;
+      }
     }
     RECREATE_CONTINUE();
     RECREATE_PASS_VIP();
   }
   rv = REAL(__pthread_cond_timedwait)(cond, mtx, abstime, clock);
   if (recreated && mtx) {
+    if (tinfo->condMutex) {
+      tinfo->condMutexNeedsBalancing = false;
+      pthread_mutex_unlock(mtx);
+    }
     RECREATE_GATE_VIP();
   }
   THREAD_FREEZE_POINT2_VIP();
 
   return rv;
 }
 
 extern "C" MFBT_API int
@@ -1398,18 +1440,27 @@ RecreateThreads() {
   pthread_mutex_unlock(&sThreadCountLock);
 
   RECREATE_START();
   while (tinfo != nullptr) {
     if (tinfo->flags & TINFO_FLAG_NUWA_SUPPORT) {
       RECREATE_BEFORE(tinfo);
       thread_recreate(tinfo);
       RECREATE_WAIT();
-      if (tinfo->reacquireMutex) {
-        REAL(pthread_mutex_lock)(tinfo->reacquireMutex);
+      if (tinfo->condMutex) {
+        // Synchronize with the recreated thread in pthread_cond_wait().
+        REAL(pthread_mutex_lock)(tinfo->condMutex);
+        // Tell the other thread that we have successfully locked the mutex.
+        // NB: condMutex can only be touched while it is held, so we must clear
+        // it here and store the mutex locally.
+        pthread_mutex_t *mtx = tinfo->condMutex;
+        tinfo->condMutex = nullptr;
+        if (tinfo->condMutexNeedsBalancing) {
+          pthread_mutex_unlock(mtx);
+        }
       }
     } else if(!(tinfo->flags & TINFO_FLAG_NUWA_SKIP)) {
       // An unmarked thread is found other than the main thread.
 
       // All threads should be marked as one of SUPPORT or SKIP, or
       // abort the process to make sure all threads in the Nuwa
       // process are Nuwa-aware.
       abort();