Merge m-c to inbound a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 07 Oct 2014 18:43:16 -0700
changeset 209252 fd9219cb431c6016646cf33fb8c930c2cb788853
parent 209251 a86612eb044a4d5da1d0522bff175107a7340eeb (current diff)
parent 209225 e4cfacb76830902057b6f698a5ca9f78aea25d06 (diff)
child 209253 ebcbf94ef701951a8d3718c9bf3534eb5c414989
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmerge
milestone35.0a1
Merge m-c to inbound a=merge
--- a/addon-sdk/source/test/jetpack-package.ini
+++ b/addon-sdk/source/test/jetpack-package.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 support-files =
   buffers/**
   commonjs-test-adapter/**
   event/**
   fixtures/**
   loader/**
+  libs/**
   modules/**
   private-browsing/**
   sidebar/**
   tabs/**
   traits/**
   windows/**
   zip/**
   fixtures.js
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "c98710d8aeb492eef503879c8542e90818a3839c", 
+    "revision": "8cad8de0f7b98963537f267151275caed62d5d30", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="9050edcda308b65d86577c8ed0eedc5c568d8e44"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="0bc74ce502672cf0265b24cf3a25d117c3de5e71"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f9bc14c28aed7b2571e641bfeeca81876ec48ec"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -1377,16 +1377,17 @@ CustomizeMode.prototype = {
     let footer = doc.getElementById("customization-lwtheme-menu-footer");
     let recommendedLabel = doc.getElementById("customization-lwtheme-menu-recommended");
     for (let element of [footer, recommendedLabel]) {
       while (element.previousSibling &&
              element.previousSibling.localName == "toolbarbutton") {
         element.previousSibling.remove();
       }
     }
+    aEvent.target.removeAttribute("height");
   },
 
   _onUIChange: function() {
     this._changed = true;
     if (!this.resetting) {
       this._updateResetButton();
       this._updateUndoResetButton();
       this._updateEmptyPaletteNotice();
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -273,16 +273,43 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function(num, str) {
         return PluralForm.get(num, str);
       }
     },
 
     /**
+     * Displays a confirmation dialog using the specified strings.
+     *
+     * Callback parameters:
+     * - err null on success, non-null on unexpected failure to show the prompt.
+     * - {Boolean} True if the user chose the OK button.
+     */
+    confirm: {
+      enumerable: true,
+      writable: true,
+      value: function(bodyMessage, okButtonMessage, cancelButtonMessage, callback) {
+        try {
+          let buttonFlags =
+            (Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING) +
+            (Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING);
+
+          let chosenButton = Services.prompt.confirmEx(null, "",
+            bodyMessage, buttonFlags, okButtonMessage, cancelButtonMessage,
+            null, null, {});
+
+          callback(null, chosenButton == 0);
+        } catch (ex) {
+          callback(cloneValueInto(ex, targetWindow));
+        }
+      }
+    },
+
+    /**
      * Call to ensure that any necessary registrations for the Loop Service
      * have taken place.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      *
      * @param {Function} callback Will be called once registration is complete,
      *                            or straight away if registration has already
@@ -587,21 +614,36 @@ function injectLoopAPI(targetWindow) {
      */
     generateUUID: {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.generateUUID();
       }
     },
+
+    /**
+     * Starts a direct call to the contact addresses.
+     *
+     * @param {Object} contact The contact to call
+     * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
+     * @return true if the call is opened, false if it is not opened (i.e. busy)
+     */
+    startDirectCall: {
+      enumerable: true,
+      writable: true,
+      value: function(contact, callType) {
+        MozLoopService.startDirectCall(contact, callType);
+      }
+    },
   };
 
   function onStatusChanged(aSubject, aTopic, aData) {
     let event = new targetWindow.CustomEvent("LoopStatusChanged");
-    targetWindow.dispatchEvent(event)
+    targetWindow.dispatchEvent(event);
   };
 
   function onDOMWindowDestroyed(aSubject, aTopic, aData) {
     if (targetWindow && aSubject != targetWindow)
       return;
     Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed");
     Services.obs.removeObserver(onStatusChanged, "loop-status-changed");
   };
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -714,35 +714,68 @@ let MozLoopServiceInternal = {
    */
 
   _processCalls: function(response, sessionType) {
     try {
       let respData = JSON.parse(response.body);
       if (respData.calls && Array.isArray(respData.calls)) {
         respData.calls.forEach((callData) => {
           if (!this.callsData.inUse) {
-            this.callsData.inUse = true;
             callData.sessionType = sessionType;
-            this.callsData.data = callData;
-            this.openChatWindow(
-              null,
-              this.localizedStrings["incoming_call_title2"].textContent,
-              "about:loopconversation#incoming/" + callData.callId);
+            this._startCall(callData, "incoming");
           } else {
             this._returnBusy(callData);
           }
         });
       } else {
         log.warn("Error: missing calls[] in response");
       }
     } catch (err) {
       log.warn("Error parsing calls info", err);
     }
   },
 
+  /**
+   * Starts a call, saves the call data, and opens a chat window.
+   *
+   * @param {Object} callData The data associated with the call including an id.
+   * @param {Boolean} conversationType Whether or not the call is "incoming"
+   *                                   or "outgoing"
+   */
+  _startCall: function(callData, conversationType) {
+    this.callsData.inUse = true;
+    this.callsData.data = callData;
+    this.openChatWindow(
+      null,
+      // No title, let the page set that, to avoid flickering.
+      "",
+      "about:loopconversation#" + conversationType + "/" + callData.callId);
+  },
+
+  /**
+   * Starts a direct call to the contact addresses.
+   *
+   * @param {Object} contact The contact to call
+   * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
+   * @return true if the call is opened, false if it is not opened (i.e. busy)
+   */
+  startDirectCall: function(contact, callType) {
+    if (this.callsData.inUse)
+      return false;
+
+    var callData = {
+      contact: contact,
+      callType: callType,
+      callId: Math.floor((Math.random() * 10))
+    };
+
+    this._startCall(callData, "outgoing");
+    return true;
+  },
+
    /**
    * Open call progress websocket and terminate with a reason of busy
    * the server.
    *
    * @param {callData} Must contain the progressURL, callId and websocketToken
    *                   returned by the LoopService.
    */
   _returnBusy: function(callData) {
@@ -1500,9 +1533,20 @@ this.MozLoopService = {
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
   hawkRequest: function(sessionType, path, method, payloadObj) {
     return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj).catch(
       error => {MozLoopServiceInternal._hawkRequestError(error);});
   },
+
+    /**
+     * Starts a direct call to the contact addresses.
+     *
+     * @param {Object} contact The contact to call
+     * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
+     * @return true if the call is opened, false if it is not opened (i.e. busy)
+     */
+  startDirectCall: function(contact, callType) {
+    MozLoopServiceInternal.startDirectCall(contact, callType);
+  },
 };
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -8,16 +8,17 @@
 /*global loop:true, React */
 
 var loop = loop || {};
 loop.contacts = (function(_, mozL10n) {
   "use strict";
 
   const Button = loop.shared.views.Button;
   const ButtonGroup = loop.shared.views.ButtonGroup;
+  const CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
   // At least this number of contacts should be present for the filter to appear.
   const MIN_CONTACTS_FOR_FILTERING = 7;
 
   let getContactNames = function(contact) {
@@ -80,24 +81,22 @@ loop.contacts = (function(_, mozL10n) {
 
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         React.DOM.ul({className: cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}, 
-          React.DOM.li({className: cx({ "dropdown-menu-item": true,
-                              "disabled": true }), 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true }), 
               onClick: this.onItemClick, 'data-action': "video-call"}, 
             React.DOM.i({className: "icon icon-video-call"}), 
             mozL10n.get("video_call_menu_button")
           ), 
-          React.DOM.li({className: cx({ "dropdown-menu-item": true,
-                              "disabled": true }), 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true }), 
               onClick: this.onItemClick, 'data-action': "audio-call"}, 
             React.DOM.i({className: "icon icon-audio-call"}), 
             mozL10n.get("audio_call_menu_button")
           ), 
           React.DOM.li({className: cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit }), 
               onClick: this.onItemClick, 'data-action': "edit"}, 
             React.DOM.i({className: "icon icon-edit"}), 
@@ -155,17 +154,18 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     componentShouldUpdate: function(nextProps, nextState) {
       let currContact = this.props.contact;
       let nextContact = nextProps.contact;
       return (
         currContact.name[0] !== nextContact.name[0] ||
         currContact.blocked !== nextContact.blocked ||
-        getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value
+        getPreferredEmail(currContact).value !==
+          getPreferredEmail(nextContact).value
       );
     },
 
     handleAction: function(actionName) {
       if (this.props.handleContactAction) {
         this.props.handleContactAction(this.props.contact, actionName);
       }
     },
@@ -302,25 +302,51 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleContactAction: function(contact, actionName) {
       switch (actionName) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
+          navigator.mozLoop.confirm(
+            mozL10n.get("confirm_delete_contact_alert"),
+            mozL10n.get("confirm_delete_contact_remove_button"),
+            mozL10n.get("confirm_delete_contact_cancel_button"),
+            (err, result) => {
+              if (err) {
+                throw err;
+              }
+
+              if (!result) {
+                return;
+              }
+
+              navigator.mozLoop.contacts.remove(contact._guid, err => {
+                if (err) {
+                  throw err;
+                }
+              });
+            });
+          break;
         case "block":
         case "unblock":
           // Invoke the API named like the action.
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
+        case "video-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          break;
+        case "audio-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -8,16 +8,17 @@
 /*global loop:true, React */
 
 var loop = loop || {};
 loop.contacts = (function(_, mozL10n) {
   "use strict";
 
   const Button = loop.shared.views.Button;
   const ButtonGroup = loop.shared.views.ButtonGroup;
+  const CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
   // At least this number of contacts should be present for the filter to appear.
   const MIN_CONTACTS_FOR_FILTERING = 7;
 
   let getContactNames = function(contact) {
@@ -80,24 +81,22 @@ loop.contacts = (function(_, mozL10n) {
 
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         <ul className={cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}>
-          <li className={cx({ "dropdown-menu-item": true,
-                              "disabled": true })}
+          <li className={cx({ "dropdown-menu-item": true })}
               onClick={this.onItemClick} data-action="video-call">
             <i className="icon icon-video-call" />
             {mozL10n.get("video_call_menu_button")}
           </li>
-          <li className={cx({ "dropdown-menu-item": true,
-                              "disabled": true })}
+          <li className={cx({ "dropdown-menu-item": true })}
               onClick={this.onItemClick} data-action="audio-call">
             <i className="icon icon-audio-call" />
             {mozL10n.get("audio_call_menu_button")}
           </li>
           <li className={cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit })}
               onClick={this.onItemClick} data-action="edit">
             <i className="icon icon-edit" />
@@ -155,17 +154,18 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     componentShouldUpdate: function(nextProps, nextState) {
       let currContact = this.props.contact;
       let nextContact = nextProps.contact;
       return (
         currContact.name[0] !== nextContact.name[0] ||
         currContact.blocked !== nextContact.blocked ||
-        getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value
+        getPreferredEmail(currContact).value !==
+          getPreferredEmail(nextContact).value
       );
     },
 
     handleAction: function(actionName) {
       if (this.props.handleContactAction) {
         this.props.handleContactAction(this.props.contact, actionName);
       }
     },
@@ -302,25 +302,51 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleContactAction: function(contact, actionName) {
       switch (actionName) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
+          navigator.mozLoop.confirm(
+            mozL10n.get("confirm_delete_contact_alert"),
+            mozL10n.get("confirm_delete_contact_remove_button"),
+            mozL10n.get("confirm_delete_contact_cancel_button"),
+            (err, result) => {
+              if (err) {
+                throw err;
+              }
+
+              if (!result) {
+                return;
+              }
+
+              navigator.mozLoop.contacts.remove(contact._guid, err => {
+                if (err) {
+                  throw err;
+                }
+              });
+            });
+          break;
         case "block":
         case "unblock":
           // Invoke the API named like the action.
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
+        case "video-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          break;
+        case "audio-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -546,54 +546,61 @@ loop.conversation = (function(mozL10n) {
     });
 
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
       dispatcher: dispatcher,
       sdkDriver: sdkDriver
     });
 
-    // XXX For now key this on the pref, but this should really be
-    // set by the information from the mozLoop API when we can get it (bug 1072323).
-    var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
-
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the callId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationHash();
     var callId;
-    if (locationHash) {
-      callId = locationHash.match(/\#incoming\/(.*)/)[1]
-      conversation.set("callId", callId);
+    var outgoing;
+
+    var hash = locationHash.match(/\#incoming\/(.*)/);
+    if (hash) {
+      callId = hash[1];
+      outgoing = false;
+    } else {
+      hash = locationHash.match(/\#outgoing\/(.*)/);
+      if (hash) {
+        callId = hash[1];
+        outgoing = true;
+      }
     }
 
+    conversation.set({callId: callId});
+
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
-      navigator.mozLoop.releaseCallData(conversation.get("callId"));
+      navigator.mozLoop.releaseCallData(callId);
     });
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
     React.renderComponent(ConversationControllerView({
       store: conversationStore, 
       client: client, 
       conversation: conversation, 
       dispatcher: dispatcher, 
       sdk: window.OT}
     ), document.querySelector('#main'));
 
     dispatcher.dispatch(new loop.shared.actions.GatherCallData({
       callId: callId,
-      calleeId: outgoingEmail
+      outgoing: outgoing
     }));
   }
 
   return {
     ConversationControllerView: ConversationControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
     init: init
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -546,54 +546,61 @@ loop.conversation = (function(mozL10n) {
     });
 
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
       dispatcher: dispatcher,
       sdkDriver: sdkDriver
     });
 
-    // XXX For now key this on the pref, but this should really be
-    // set by the information from the mozLoop API when we can get it (bug 1072323).
-    var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
-
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the callId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationHash();
     var callId;
-    if (locationHash) {
-      callId = locationHash.match(/\#incoming\/(.*)/)[1]
-      conversation.set("callId", callId);
+    var outgoing;
+
+    var hash = locationHash.match(/\#incoming\/(.*)/);
+    if (hash) {
+      callId = hash[1];
+      outgoing = false;
+    } else {
+      hash = locationHash.match(/\#outgoing\/(.*)/);
+      if (hash) {
+        callId = hash[1];
+        outgoing = true;
+      }
     }
 
+    conversation.set({callId: callId});
+
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
-      navigator.mozLoop.releaseCallData(conversation.get("callId"));
+      navigator.mozLoop.releaseCallData(callId);
     });
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
     React.renderComponent(<ConversationControllerView
       store={conversationStore}
       client={client}
       conversation={conversation}
       dispatcher={dispatcher}
       sdk={window.OT}
     />, document.querySelector('#main'));
 
     dispatcher.dispatch(new loop.shared.actions.GatherCallData({
       callId: callId,
-      calleeId: outgoingEmail
+      outgoing: outgoing
     }));
   }
 
   return {
     ConversationControllerView: ConversationControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
     init: init
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -18,40 +18,60 @@ loop.conversationViews = (function(mozL1
    * Displays details of the incoming/outgoing conversation
    * (name, link, audio/video type etc).
    *
    * Allows the view to be extended with different buttons and progress
    * via children properties.
    */
   var ConversationDetailView = React.createClass({displayName: 'ConversationDetailView',
     propTypes: {
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object
+    },
+
+    // This duplicates a similar function in contacts.jsx that isn't used in the
+    // conversation window. If we get too many of these, we might want to consider
+    // finding a logical place for them to be shared.
+    _getPreferredEmail: function(contact) {
+      // A contact may not contain email addresses, but only a phone number.
+      if (!contact.email || contact.email.length == 0) {
+        return { value: "" };
+      }
+      return contact.email.find(e => e.pref) || contact.email[0];
     },
 
     render: function() {
-      document.title = this.props.calleeId;
+      var contactName;
+
+      if (this.props.contact.name &&
+          this.props.contact.name[0]) {
+        contactName = this.props.contact.name[0];
+      } else {
+        contactName = this._getPreferredEmail(this.props.contact).value;
+      }
+
+      document.title = contactName;
 
       return (
         React.DOM.div({className: "call-window"}, 
-          React.DOM.h2(null, this.props.calleeId), 
+          React.DOM.h2(null, contactName), 
           React.DOM.div(null, this.props.children)
         )
       );
     }
   });
 
   /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       callState: React.PropTypes.string,
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object,
       enableCancelButton: React.PropTypes.bool
     },
 
     getDefaultProps: function() {
       return {
         enableCancelButton: false
       };
     },
@@ -71,17 +91,17 @@ loop.conversationViews = (function(mozL1
 
       var btnCancelStyles = cx({
         "btn": true,
         "btn-cancel": true,
         "disabled": !this.props.enableCancelButton
       });
 
       return (
-        ConversationDetailView({calleeId: this.props.calleeId}, 
+        ConversationDetailView({contact: this.props.contact}, 
 
           React.DOM.p({className: "btn-label"}, pendingStateString), 
 
           React.DOM.div({className: "btn-group call-action-group"}, 
             React.DOM.div({className: "fx-embedded-call-button-spacer"}), 
               React.DOM.button({className: btnCancelStyles, 
                       onClick: this.cancelCall}, 
                 mozL10n.get("initiate_call_cancel_button")
@@ -335,29 +355,29 @@ loop.conversationViews = (function(mozL1
         case CALL_STATES.TERMINATED: {
           return (CallFailedView({
             dispatcher: this.props.dispatcher}
           ));
         }
         case CALL_STATES.ONGOING: {
           return (OngoingConversationView({
             dispatcher: this.props.dispatcher, 
-            video: {enabled: this.state.videoMuted}, 
-            audio: {enabled: this.state.audioMuted}}
+            video: {enabled: !this.state.videoMuted}, 
+            audio: {enabled: !this.state.audioMuted}}
             )
           );
         }
         case CALL_STATES.FINISHED: {
           return this._renderFeedbackView();
         }
         default: {
           return (PendingConversationView({
             dispatcher: this.props.dispatcher, 
             callState: this.state.callState, 
-            calleeId: this.state.calleeId, 
+            contact: this.state.contact, 
             enableCancelButton: this._isCancellable()}
           ))
         }
       }
     },
   });
 
   return {
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -18,40 +18,60 @@ loop.conversationViews = (function(mozL1
    * Displays details of the incoming/outgoing conversation
    * (name, link, audio/video type etc).
    *
    * Allows the view to be extended with different buttons and progress
    * via children properties.
    */
   var ConversationDetailView = React.createClass({
     propTypes: {
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object
+    },
+
+    // This duplicates a similar function in contacts.jsx that isn't used in the
+    // conversation window. If we get too many of these, we might want to consider
+    // finding a logical place for them to be shared.
+    _getPreferredEmail: function(contact) {
+      // A contact may not contain email addresses, but only a phone number.
+      if (!contact.email || contact.email.length == 0) {
+        return { value: "" };
+      }
+      return contact.email.find(e => e.pref) || contact.email[0];
     },
 
     render: function() {
-      document.title = this.props.calleeId;
+      var contactName;
+
+      if (this.props.contact.name &&
+          this.props.contact.name[0]) {
+        contactName = this.props.contact.name[0];
+      } else {
+        contactName = this._getPreferredEmail(this.props.contact).value;
+      }
+
+      document.title = contactName;
 
       return (
         <div className="call-window">
-          <h2>{this.props.calleeId}</h2>
+          <h2>{contactName}</h2>
           <div>{this.props.children}</div>
         </div>
       );
     }
   });
 
   /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       callState: React.PropTypes.string,
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object,
       enableCancelButton: React.PropTypes.bool
     },
 
     getDefaultProps: function() {
       return {
         enableCancelButton: false
       };
     },
@@ -71,17 +91,17 @@ loop.conversationViews = (function(mozL1
 
       var btnCancelStyles = cx({
         "btn": true,
         "btn-cancel": true,
         "disabled": !this.props.enableCancelButton
       });
 
       return (
-        <ConversationDetailView calleeId={this.props.calleeId}>
+        <ConversationDetailView contact={this.props.contact}>
 
           <p className="btn-label">{pendingStateString}</p>
 
           <div className="btn-group call-action-group">
             <div className="fx-embedded-call-button-spacer"></div>
               <button className={btnCancelStyles}
                       onClick={this.cancelCall}>
                 {mozL10n.get("initiate_call_cancel_button")}
@@ -335,29 +355,29 @@ loop.conversationViews = (function(mozL1
         case CALL_STATES.TERMINATED: {
           return (<CallFailedView
             dispatcher={this.props.dispatcher}
           />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             dispatcher={this.props.dispatcher}
-            video={{enabled: this.state.videoMuted}}
-            audio={{enabled: this.state.audioMuted}}
+            video={{enabled: !this.state.videoMuted}}
+            audio={{enabled: !this.state.audioMuted}}
             />
           );
         }
         case CALL_STATES.FINISHED: {
           return this._renderFeedbackView();
         }
         default: {
           return (<PendingConversationView
             dispatcher={this.props.dispatcher}
             callState={this.state.callState}
-            calleeId={this.state.calleeId}
+            contact={this.state.contact}
             enableCancelButton={this._isCancellable()}
           />)
         }
       }
     },
   });
 
   return {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -358,17 +358,21 @@ loop.panel = (function(_, mozL10n) {
       this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
     handleLinkExfiltration: function(event) {
-      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      try {
+        navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
+      } catch (err) {
+        console.error("Error recording telemetry", err);
+      }
       if (this.state.callUrlExpiry) {
         navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
       }
     },
 
     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.
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -358,17 +358,21 @@ loop.panel = (function(_, mozL10n) {
       this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
     handleLinkExfiltration: function(event) {
-      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      try {
+        navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
+      } catch (err) {
+        console.error("Error recording telemetry", err);
+      }
       if (this.state.callUrlExpiry) {
         navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
       }
     },
 
     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.
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -8,17 +8,17 @@
  * "Fixes" the Box Model.
  * @see http://www.paulirish.com/2012/box-sizing-border-box-ftw/
  */
 *, *:before, *:after {
   box-sizing: border-box;
 }
 
 body {
-  font-family: "Lucida Grande", sans-serif;
+  font: message-box;
   font-size: 12px;
   background: #fbfbfb;
 }
 
 img {
   border: none;
 }
 
@@ -360,33 +360,16 @@ p {
 }
 
 .mac p,
 .windows p,
 .linux p {
   line-height: 16px;
 }
 
-/* Using star selector to override
- * the specificity of other selectors
- * if performance is an issue we could
- * explicitely list all the elements */
-.windows * {
-  font-family: 'Segoe';
-}
-
-.mac * {
-  font-family: 'Lucida Grande';
-}
-
-.linux * {
-  /* XXX requires fallbacks */
-  font-family: 'Ubuntu', sans-serif;
-}
-
 /* Web panel */
 
 .info-panel {
   border-radius: 4px;
   background: #fff;
   padding: 20px 0;
   border: 1px solid #e7e7e7;
   box-shadow: 0 2px 0 rgba(0, 0, 0, .03);
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -29,21 +29,19 @@ loop.shared.actions = (function() {
     return Action.bind(null, name, schema);
   };
 
   return {
     /**
      * Used to trigger gathering of initial call data.
      */
     GatherCallData: Action.define("gatherCallData", {
-      // XXX This may change when bug 1072323 is implemented.
-      // Optional: Specify the calleeId for an outgoing call
-      calleeId: [String, null],
       // Specify the callId for an incoming call.
-      callId: [String, null]
+      callId: [String, null],
+      outgoing: Boolean
     }),
 
     /**
      * Used to cancel call setup.
      */
     CancelCall: Action.define("cancelCall", {
     }),
 
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -58,18 +58,18 @@ loop.store = (function() {
       // The current state of the call
       callState: CALL_STATES.INIT,
       // The reason if a call was terminated
       callStateReason: undefined,
       // The error information, if there was a failure
       error: undefined,
       // True if the call is outgoing, false if not, undefined if unknown
       outgoing: undefined,
-      // The id of the person being called for outgoing calls
-      calleeId: undefined,
+      // The contact being called for outgoing calls
+      contact: undefined,
       // The call type for the call.
       // XXX Don't hard-code, this comes from the data in bug 1072323
       callType: CALL_TYPES.AUDIO_VIDEO,
 
       // Call Connection information
       // The call id from the loop-server
       callId: undefined,
       // The connection progress url to connect the websocket
@@ -78,19 +78,19 @@ loop.store = (function() {
       websocketToken: undefined,
       // SDK API key
       apiKey: undefined,
       // SDK session ID
       sessionId: undefined,
       // SDK session token
       sessionToken: undefined,
       // If the audio is muted
-      audioMuted: true,
+      audioMuted: false,
       // If the video is muted
-      videoMuted: true
+      videoMuted: false
     },
 
     /**
      * Constructor
      *
      * Options:
      * - {loop.Dispatcher} dispatcher The dispatcher for dispatching actions and
      *                                registering to consume actions.
@@ -187,24 +187,39 @@ loop.store = (function() {
 
     /**
      * Handles the gather call data action, setting the state
      * and starting to get the appropriate data for the type of call.
      *
      * @param {sharedActions.GatherCallData} actionData The action data.
      */
     gatherCallData: function(actionData) {
+      if (!actionData.outgoing) {
+        // XXX Other types aren't supported yet, but set the state for the
+        // view selection.
+        this.set({outgoing: false});
+        return;
+      }
+
+      var callData = navigator.mozLoop.getCallData(actionData.callId);
+      if (!callData) {
+        console.error("Failed to get the call data");
+        this.set({callState: CALL_STATES.TERMINATED});
+        return;
+      }
+
       this.set({
-        calleeId: actionData.calleeId,
-        outgoing: !!actionData.calleeId,
+        contact: callData.contact,
+        outgoing: actionData.outgoing,
         callId: actionData.callId,
+        callType: callData.callType,
         callState: CALL_STATES.GATHER
       });
 
-      this.videoMuted = this.get("callType") !== CALL_TYPES.AUDIO_VIDEO;
+      this.set({videoMuted: this.get("callType") === CALL_TYPES.AUDIO_ONLY});
 
       if (this.get("outgoing")) {
         this._setupOutgoingCall();
       } // XXX Else, other types aren't supported yet.
     },
 
     /**
      * Handles the connect call action, this saves the appropriate
@@ -280,26 +295,31 @@ loop.store = (function() {
 
     /**
      * Records the mute state for the stream.
      *
      * @param {sharedActions.setMute} actionData The mute state for the stream type.
      */
     setMute: function(actionData) {
       var muteType = actionData.type + "Muted";
-      this.set(muteType, actionData.enabled);
+      this.set(muteType, !actionData.enabled);
     },
 
     /**
      * Obtains the outgoing call data from the server and handles the
      * result.
      */
     _setupOutgoingCall: function() {
-      // XXX For now, we only have one calleeId, so just wrap that in an array.
-      this.client.setupOutgoingCall([this.get("calleeId")],
+      var contactAddresses = [];
+
+      this.get("contact").email.forEach(function(address) {
+        contactAddresses.push(address.value);
+      });
+
+      this.client.setupOutgoingCall(contactAddresses,
         this.get("callType"),
         function(err, result) {
           if (err) {
             console.error("Failed to get outgoing call data", err);
             this.dispatcher.dispatch(
               new sharedActions.ConnectionFailure({reason: "setup"}));
             return;
           }
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -1,123 +1,143 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var expect = chai.expect;
 
 describe("loop.conversationViews", function () {
-  var sandbox, oldTitle, view, dispatcher;
+  var sandbox, oldTitle, view, dispatcher, contact;
 
   var CALL_STATES = loop.store.CALL_STATES;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
     oldTitle = document.title;
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
+
+    contact = {
+      name: [ "mrsmith" ],
+      email: [{
+        type: "home",
+        value: "fakeEmail",
+        pref: true
+      }]
+    };
   });
 
   afterEach(function() {
     document.title = oldTitle;
     view = undefined;
     sandbox.restore();
   });
 
   describe("ConversationDetailView", function() {
     function mountTestComponent(props) {
       return TestUtils.renderIntoDocument(
         loop.conversationViews.ConversationDetailView(props));
     }
 
     it("should set the document title to the calledId", function() {
-      mountTestComponent({calleeId: "mrsmith"});
+      mountTestComponent({contact: contact});
 
       expect(document.title).eql("mrsmith");
     });
 
     it("should set display the calledId", function() {
-      view = mountTestComponent({calleeId: "mrsmith"});
+      view = mountTestComponent({contact: contact});
 
       expect(TestUtils.findRenderedDOMComponentWithTag(
         view, "h2").props.children).eql("mrsmith");
     });
+
+    it("should fallback to the email if the contact name is not defined",
+      function() {
+        delete contact.name;
+
+        view = mountTestComponent({contact: contact});
+
+        expect(TestUtils.findRenderedDOMComponentWithTag(
+          view, "h2").props.children).eql("fakeEmail");
+      }
+    );
   });
 
   describe("PendingConversationView", function() {
     function mountTestComponent(props) {
       return TestUtils.renderIntoDocument(
         loop.conversationViews.PendingConversationView(props));
     }
 
     it("should set display connecting string when the state is not alerting",
       function() {
         view = mountTestComponent({
           callState: CALL_STATES.CONNECTING,
-          calleeId: "mrsmith",
+          contact: contact,
           dispatcher: dispatcher
         });
 
         var label = TestUtils.findRenderedDOMComponentWithClass(
           view, "btn-label").props.children;
 
         expect(label).to.have.string("connecting");
     });
 
     it("should set display ringing string when the state is alerting",
       function() {
         view = mountTestComponent({
           callState: CALL_STATES.ALERTING,
-          calleeId: "mrsmith",
+          contact: contact,
           dispatcher: dispatcher
         });
 
         var label = TestUtils.findRenderedDOMComponentWithClass(
           view, "btn-label").props.children;
 
         expect(label).to.have.string("ringing");
     });
 
     it("should disable the cancel button if enableCancelButton is false",
       function() {
         view = mountTestComponent({
           callState: CALL_STATES.CONNECTING,
-          calleeId: "mrsmith",
+          contact: contact,
           dispatcher: dispatcher,
           enableCancelButton: false
         });
 
         var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
 
         expect(cancelBtn.classList.contains("disabled")).eql(true);
       });
 
     it("should enable the cancel button if enableCancelButton is false",
       function() {
         view = mountTestComponent({
           callState: CALL_STATES.CONNECTING,
-          calleeId: "mrsmith",
+          contact: contact,
           dispatcher: dispatcher,
           enableCancelButton: true
         });
 
         var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
 
         expect(cancelBtn.classList.contains("disabled")).eql(false);
       });
 
     it("should dispatch a cancelCall action when the cancel button is pressed",
       function() {
         view = mountTestComponent({
           callState: CALL_STATES.CONNECTING,
-          calleeId: "mrsmith",
+          contact: contact,
           dispatcher: dispatcher
         });
 
         var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
 
         React.addons.TestUtils.Simulate.click(cancelBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
@@ -288,17 +308,20 @@ describe("loop.conversationViews", funct
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.CallFailedView);
     });
 
     it("should render the PendingConversationView when the call state is 'init'",
       function() {
-        store.set({callState: CALL_STATES.INIT});
+        store.set({
+          callState: CALL_STATES.INIT,
+          contact: contact
+        });
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.PendingConversationView);
     });
 
     it("should render the OngoingConversationView when the call state is 'ongoing'",
@@ -318,17 +341,20 @@ describe("loop.conversationViews", funct
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.shared.views.FeedbackView);
     });
 
     it("should update the rendered views when the state is changed.",
       function() {
-        store.set({callState: CALL_STATES.INIT});
+        store.set({
+          callState: CALL_STATES.INIT,
+          contact: contact
+        });
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.PendingConversationView);
 
         store.set({callState: CALL_STATES.TERMINATED});
 
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -110,20 +110,34 @@ describe("loop.conversation", function()
     });
 
     it("should trigger a gatherCallData action", function() {
       loop.conversation.init();
 
       sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
       sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
         new loop.shared.actions.GatherCallData({
-          calleeId: null,
-          callId: "42"
+          callId: "42",
+          outgoing: false
         }));
     });
+
+    it("should trigger an outgoing gatherCallData action for outgoing calls",
+      function() {
+        loop.shared.utils.Helper.prototype.locationHash.returns("#outgoing/24");
+
+        loop.conversation.init();
+
+        sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
+        sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
+          new loop.shared.actions.GatherCallData({
+            callId: "24",
+            outgoing: true
+          }));
+      });
   });
 
   describe("ConversationControllerView", function() {
     var store, conversation, client, ccView, oldTitle, dispatcher;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.conversation.ConversationControllerView({
@@ -136,17 +150,26 @@ describe("loop.conversation", function()
 
     beforeEach(function() {
       oldTitle = document.title;
       client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {}
       });
       dispatcher = new loop.Dispatcher();
-      store = new loop.store.ConversationStore({}, {
+      store = new loop.store.ConversationStore({
+        contact: {
+          name: [ "Mr Smith" ],
+          email: [{
+            type: "home",
+            value: "fakeEmail",
+            pref: true
+          }]
+        }
+      }, {
         client: client,
         dispatcher: dispatcher,
         sdkDriver: {}
       });
     });
 
     afterEach(function() {
       ccView = undefined;
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -32,16 +32,17 @@ describe("loop.panel", function() {
       get locale() {
         return "en-US";
       },
       setLoopCharPref: sandbox.stub(),
       getLoopCharPref: sandbox.stub().returns("unseen"),
       copyString: sandbox.stub(),
       noteCallUrlExpiry: sinon.spy(),
       composeEmail: sinon.spy(),
+      telemetryAdd: sinon.spy(),
       contacts: {
         getAll: function(callback) {
           callback(null, []);
         },
         on: sandbox.stub()
       }
     };
 
@@ -422,16 +423,37 @@ describe("loop.panel", function() {
 
           TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
 
           sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
           sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
             6000);
         });
 
+      it("should call mozLoop.telemetryAdd when the url is copied via button",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifications: notifications,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
+
+          sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
+          sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
+                                  "LOOP_CLIENT_CALL_URL_SHARED",
+                                  true);
+        });
+
       it("should note the call url expiry when the url is emailed",
         function() {
           var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
             notifications: notifications,
             client: fakeClient
           }));
           view.setState({
             pending: false,
@@ -442,16 +464,37 @@ describe("loop.panel", function() {
 
           TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
 
           sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
           sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
             6000);
         });
 
+      it("should call mozLoop.telemetryAdd when the url is emailed",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifications: notifications,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
+
+          sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
+          sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
+                                  "LOOP_CLIENT_CALL_URL_SHARED",
+                                  true);
+        });
+
       it("should note the call url expiry when the url is copied manually",
         function() {
           var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
             notifications: notifications,
             client: fakeClient
           }));
           view.setState({
             pending: false,
@@ -463,16 +506,38 @@ describe("loop.panel", function() {
           var urlField = view.getDOMNode().querySelector("input[type='url']");
           TestUtils.Simulate.copy(urlField);
 
           sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
           sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
             6000);
         });
 
+      it("should call mozLoop.telemetryAdd when the url is copied manually",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifications: notifications,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          var urlField = view.getDOMNode().querySelector("input[type='url']");
+          TestUtils.Simulate.copy(urlField);
+
+          sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
+          sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
+                                  "LOOP_CLIENT_CALL_URL_SHARED",
+                                  true);
+        });
+
       it("should notify the user when the operation failed", function() {
         fakeClient.requestCallUrl = function(_, cb) {
           cb("fake error");
         };
         sandbox.stub(notifications, "errorL10n");
         var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
           notifications: notifications,
           client: fakeClient
--- a/browser/components/loop/test/mochitest/browser_mozLoop_softStart.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_softStart.js
@@ -51,26 +51,27 @@ let runCheck = function(expectError) {
       } else {
         resolve(error);
       }
     })
   });
 }
 
 add_task(function* test_mozLoop_softStart() {
+  let orig_throttled = Services.prefs.getBoolPref("loop.throttled");
+
   // Set associated variables to proper values
   Services.prefs.setBoolPref("loop.throttled", true);
   Services.prefs.setCharPref("loop.soft_start_hostname", SOFT_START_HOSTNAME);
   Services.prefs.setIntPref("loop.soft_start_ticket_number", -1);
 
   registerCleanupFunction(function () {
-    Services.prefs.clearUserPref("loop.throttled");
-    Services.prefs.clearUserPref("loop.soft_start");
+    Services.prefs.setBoolPref("loop.throttled", orig_throttled);
+    Services.prefs.clearUserPref("loop.soft_start_ticket_number");
     Services.prefs.clearUserPref("loop.soft_start_hostname");
-    Services.prefs.clearUserPref("loop.soft_start_ticket_number");
   });
 
   let throttled;
   let ticket;
 
   info("Ensure that we pick a valid ticket number.");
   yield runCheck();
   throttled = Services.prefs.getBoolPref("loop.throttled");
@@ -79,17 +80,16 @@ add_task(function* test_mozLoop_softStar
   Assert.equal(throttled, true, "Feature should still be throttled");
   Assert.notEqual(ticket, -1, "Ticket should be changed");
   Assert.ok((ticket < 16777214 && ticket > 0), "Ticket should be in range");
 
   // Try some "interesting" ticket numbers
   for (ticket of [1, 256, 65535, 10000000, 16777214]) {
     MockButton.hidden = true;
     Services.prefs.setBoolPref("loop.throttled", true);
-    Services.prefs.setBoolPref("loop.soft_start", true);
     Services.prefs.setIntPref("loop.soft_start_ticket_number", ticket);
 
     info("Ensure that we don't activate when the now serving " +
          "number is less than our value.");
     MockDNSService.nowServing = ticket - 1;
     yield runCheck();
     throttled = Services.prefs.getBoolPref("loop.throttled");
     Assert.equal(MockButton.hidden, true, "Button should still be hidden");
@@ -111,27 +111,25 @@ add_task(function* test_mozLoop_softStar
     Assert.equal(MockButton.hidden, false, "Button should not be hidden");
     Assert.equal(throttled, false, "Feature should be unthrottled");
   }
 
   info("Check DNS error behavior");
   MockDNSService.nowServing = 0;
   MockDNSService.resultCode = 0x80000000;
   Services.prefs.setBoolPref("loop.throttled", true);
-  Services.prefs.setBoolPref("loop.soft_start", true);
   MockButton.hidden = true;
   yield runCheck(true);
   throttled = Services.prefs.getBoolPref("loop.throttled");
   Assert.equal(MockButton.hidden, true, "Button should be hidden");
   Assert.equal(throttled, true, "Feature should be throttled");
 
   info("Check DNS misconfiguration behavior");
   MockDNSService.nowServing = ticket + 1;
   MockDNSService.resultCode = 0;
   MockDNSService.ipFirstOctet = 6;
   Services.prefs.setBoolPref("loop.throttled", true);
-  Services.prefs.setBoolPref("loop.soft_start", true);
   MockButton.hidden = true;
   yield runCheck(true);
   throttled = Services.prefs.getBoolPref("loop.throttled");
   Assert.equal(MockButton.hidden, true, "Button should be hidden");
   Assert.equal(throttled, true, "Feature should be throttled");
 });
--- a/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
@@ -3,34 +3,38 @@
 
 /*
  * This file contains tests for the mozLoop telemetry API.
  */
 
 add_task(loadLoopPanel);
 
 /**
+ * Enable local telemetry recording for the duration of the tests.
+ */
+add_task(function* test_initialize() {
+  let oldCanRecord = Services.telemetry.canRecord;
+  Services.telemetry.canRecord = true;
+  registerCleanupFunction(function () {
+    Services.telemetry.canRecord = oldCanRecord;
+  });
+});
+
+/**
  * Tests that boolean histograms exist and can be updated.
  */
 add_task(function* test_mozLoop_telemetryAdd_boolean() {
   for (let histogramId of [
     "LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS",
+    "LOOP_CLIENT_CALL_URL_SHARED",
   ]) {
-    let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
+    let histogram = Services.telemetry.getHistogramById(histogramId);
 
-    let initialFalseCount = snapshot.counts[0];
-    let initialTrueCount = snapshot.counts[1];
-
+    histogram.clear();
     for (let value of [false, false, true]) {
       gMozLoopAPI.telemetryAdd(histogramId, value);
     }
 
-    // The telemetry service updates histograms asynchronously, so we need to
-    // poll for the final values and time out otherwise.
-    info("Waiting for update of " + histogramId);
-    do {
-      yield new Promise(resolve => setTimeout(resolve, 50));
-      snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
-    } while (snapshot.counts[0] == initialFalseCount + 2 &&
-             snapshot.counts[1] == initialTrueCount + 1);
-    ok(true, "Correctly updated " + histogramId);
+    let snapshot = histogram.snapshot();
+    is(snapshot.counts[0], 2, "snapshot.counts[0] == 2");
+    is(snapshot.counts[1], 1, "snapshot.counts[1] == 1");
   }
 });
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -6,31 +6,41 @@ var expect = chai.expect;
 describe("loop.ConversationStore", function () {
   "use strict";
 
   var CALL_STATES = loop.store.CALL_STATES;
   var WS_STATES = loop.store.WS_STATES;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
+  var contact;
   var connectPromise, resolveConnectPromise, rejectConnectPromise;
   var wsCancelSpy, wsCloseSpy, wsMediaUpSpy, fakeWebsocket;
 
   function checkFailures(done, f) {
     try {
       f();
       done();
     } catch (err) {
       done(err);
     }
   }
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
+    contact = {
+      name: [ "Mr Smith" ],
+      email: [{
+        type: "home",
+        value: "fakeEmail",
+        pref: true
+      }]
+    };
+
     dispatcher = new loop.Dispatcher();
     client = {
       setupOutgoingCall: sinon.stub()
     };
     sdkDriver = {
       connectSession: sinon.stub(),
       disconnectSession: sinon.stub()
     };
@@ -194,57 +204,80 @@ describe("loop.ConversationStore", funct
         });
       });
     });
   });
 
   describe("#gatherCallData", function() {
     beforeEach(function() {
       store.set({callState: CALL_STATES.INIT});
+
+      navigator.mozLoop = {
+        getCallData: function() {
+          return {
+            contact: contact,
+            callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
+          };
+        }
+      };
+    });
+
+    afterEach(function() {
+      delete navigator.mozLoop;
     });
 
     it("should set the state to 'gather'", function() {
       dispatcher.dispatch(
         new sharedActions.GatherCallData({
-          calleeId: "",
-          callId: "76543218"
+          callId: "76543218",
+          outgoing: true
         }));
 
       expect(store.get("callState")).eql(CALL_STATES.GATHER);
     });
 
     it("should save the basic call information", function() {
       dispatcher.dispatch(
         new sharedActions.GatherCallData({
-          calleeId: "fake",
-          callId: "123456"
+          callId: "123456",
+          outgoing: true
         }));
 
-      expect(store.get("calleeId")).eql("fake");
       expect(store.get("callId")).eql("123456");
       expect(store.get("outgoing")).eql(true);
     });
 
+    it("should save the basic information from the mozLoop api", function() {
+      dispatcher.dispatch(
+        new sharedActions.GatherCallData({
+          callId: "123456",
+          outgoing: true
+        }));
+
+      expect(store.get("contact")).eql(contact);
+      expect(store.get("callType")).eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
+    });
+
     describe("outgoing calls", function() {
       var outgoingCallData;
 
       beforeEach(function() {
         outgoingCallData = {
-          calleeId: "fake",
-          callId: "135246"
+          callId: "123456",
+          outgoing: true
         };
       });
 
       it("should request the outgoing call data", function() {
         dispatcher.dispatch(
           new sharedActions.GatherCallData(outgoingCallData));
 
         sinon.assert.calledOnce(client.setupOutgoingCall);
         sinon.assert.calledWith(client.setupOutgoingCall,
-          ["fake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
+          ["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
       });
 
       describe("server response handling", function() {
         beforeEach(function() {
           sandbox.stub(dispatcher, "dispatch");
         });
 
         it("should dispatch a connect call action on success", function() {
@@ -483,24 +516,24 @@ describe("loop.ConversationStore", funct
       expect(store.get("callState")).eql(CALL_STATES.GATHER);
     });
 
     it("should request the outgoing call data", function() {
       store.set({
         callState: CALL_STATES.TERMINATED,
         outgoing: true,
         callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO,
-        calleeId: "fake"
+        contact: contact
       });
 
       dispatcher.dispatch(new sharedActions.RetryCall());
 
       sinon.assert.calledOnce(client.setupOutgoingCall);
       sinon.assert.calledWith(client.setupOutgoingCall,
-        ["fake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
+        ["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
     });
   });
 
   describe("#mediaConnected", function() {
     it("should send mediaUp via the websocket", function() {
       store._websocket = fakeWebsocket;
 
       dispatcher.dispatch(new sharedActions.MediaConnected());
@@ -513,28 +546,28 @@ describe("loop.ConversationStore", funct
     it("should save the mute state for the audio stream", function() {
       store.set({"audioMuted": false});
 
       dispatcher.dispatch(new sharedActions.SetMute({
         type: "audio",
         enabled: true
       }));
 
-      expect(store.get("audioMuted")).eql(true);
+      expect(store.get("audioMuted")).eql(false);
     });
 
     it("should save the mute state for the video stream", function() {
       store.set({"videoMuted": true});
 
       dispatcher.dispatch(new sharedActions.SetMute({
         type: "video",
         enabled: false
       }));
 
-      expect(store.get("videoMuted")).eql(false);
+      expect(store.get("videoMuted")).eql(true);
     });
   });
 
   describe("Events", function() {
     describe("Websocket progress", function() {
       beforeEach(function() {
         dispatcher.dispatch(
           new sharedActions.ConnectCall({sessionData: fakeSessionData}));
--- a/browser/components/loop/test/shared/dispatcher_test.js
+++ b/browser/components/loop/test/shared/dispatcher_test.js
@@ -41,17 +41,17 @@ describe("loop.Dispatcher", function () 
 
   describe("#dispatch", function() {
     var gatherStore1, gatherStore2, cancelStore1, connectStore1;
     var gatherAction, cancelAction, connectAction, resolveCancelStore1;
 
     beforeEach(function() {
       gatherAction = new sharedActions.GatherCallData({
         callId: "42",
-        calleeId: null
+        outgoing: false
       });
 
       cancelAction = new sharedActions.CancelCall();
       connectAction = new sharedActions.ConnectCall({
         sessionData: {}
       });
 
       gatherStore1 = {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopservice_directcall.js
@@ -0,0 +1,59 @@
+/* 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/. */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Chat",
+                                  "resource:///modules/Chat.jsm");
+let openChatOrig = Chat.open;
+
+const contact = {
+  name: [ "Mr Smith" ],
+  email: [{
+    type: "home",
+    value: "fakeEmail",
+    pref: true
+  }]
+};
+
+add_task(function test_startDirectCall_opens_window() {
+  let openedUrl;
+  Chat.open = function(contentWindow, origin, title, url) {
+    openedUrl = url;
+  };
+
+  MozLoopService.startDirectCall(contact, "audio-video");
+
+  do_check_true(!!openedUrl, "should open a chat window");
+
+  // Stop the busy kicking in for following tests.
+  let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
+  MozLoopService.releaseCallData(callId);
+});
+
+add_task(function test_startDirectCall_getCallData() {
+  let openedUrl;
+  Chat.open = function(contentWindow, origin, title, url) {
+    openedUrl = url;
+  };
+
+  MozLoopService.startDirectCall(contact, "audio-video");
+
+  let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
+
+  let callData = MozLoopService.getCallData(callId);
+
+  do_check_eq(callData.callType, "audio-video", "should have the correct call type");
+  do_check_eq(callData.contact, contact, "should have the contact details");
+
+  // Stop the busy kicking in for following tests.
+  MozLoopService.releaseCallData(callId);
+});
+
+function run_test() {
+  do_register_cleanup(function() {
+    // Revert original Chat.open implementation
+    Chat.open = openChatOrig;
+  });
+
+  run_next_test();
+}
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -1,20 +1,21 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 
 [test_loopapi_hawk_request.js]
 [test_looppush_initialize.js]
+[test_loopservice_directcall.js]
 [test_loopservice_dnd.js]
 [test_loopservice_expiry.js]
 [test_loopservice_hawk_errors.js]
 [test_loopservice_loop_prefs.js]
 [test_loopservice_initialize.js]
 [test_loopservice_locales.js]
 [test_loopservice_notification.js]
 [test_loopservice_registration.js]
 [test_loopservice_token_invalid.js]
 [test_loopservice_token_save.js]
 [test_loopservice_token_send.js]
 [test_loopservice_token_validation.js]
-[test_loopservice_busy.js]
\ No newline at end of file
+[test_loopservice_busy.js]
--- a/chrome/RegistryMessageUtils.h
+++ b/chrome/RegistryMessageUtils.h
@@ -38,16 +38,22 @@ struct ChromePackage
            flags == rhs.flags;
   }
 };
 
 struct ResourceMapping
 {
   nsCString resource;
   SerializedURI resolvedURI;
+
+  bool operator ==(const ResourceMapping& rhs) const
+  {
+    return resource.Equals(rhs.resource) &&
+           resolvedURI == rhs.resolvedURI;
+  }
 };
 
 struct OverrideMapping
 {
   SerializedURI originalURI;
   SerializedURI overrideURI;
 
   bool operator==(const OverrideMapping& rhs) const
--- a/chrome/nsChromeRegistryChrome.cpp
+++ b/chrome/nsChromeRegistryChrome.cpp
@@ -448,27 +448,31 @@ nsChromeRegistryChrome::SendRegisteredCh
   InfallibleTArray<ResourceMapping> resources;
   InfallibleTArray<OverrideMapping> overrides;
 
   EnumerationArgs args = {
     packages, mSelectedLocale, mSelectedSkin
   };
   mPackagesHash.EnumerateRead(CollectPackages, &args);
 
-  nsCOMPtr<nsIIOService> io (do_GetIOService());
-  NS_ENSURE_TRUE_VOID(io);
+  // If we were passed a parent then a new child process has been created and
+  // has requested all of the chrome so send it the resources too. Otherwise
+  // resource mappings are sent by the resource protocol handler dynamically.
+  if (aParent) {
+    nsCOMPtr<nsIIOService> io (do_GetIOService());
+    NS_ENSURE_TRUE_VOID(io);
 
-  nsCOMPtr<nsIProtocolHandler> ph;
-  nsresult rv = io->GetProtocolHandler("resource", getter_AddRefs(ph));
-  NS_ENSURE_SUCCESS_VOID(rv);
+    nsCOMPtr<nsIProtocolHandler> ph;
+    nsresult rv = io->GetProtocolHandler("resource", getter_AddRefs(ph));
+    NS_ENSURE_SUCCESS_VOID(rv);
 
-  //FIXME: Some substitutions are set up lazily and might not exist yet
-  nsCOMPtr<nsIResProtocolHandler> irph (do_QueryInterface(ph));
-  nsResProtocolHandler* rph = static_cast<nsResProtocolHandler*>(irph.get());
-  rph->CollectSubstitutions(resources);
+    nsCOMPtr<nsIResProtocolHandler> irph (do_QueryInterface(ph));
+    nsResProtocolHandler* rph = static_cast<nsResProtocolHandler*>(irph.get());
+    rph->CollectSubstitutions(resources);
+  }
 
   mOverrideTable.EnumerateRead(&EnumerateOverride, &overrides);
 
   if (aParent) {
     bool success = aParent->SendRegisterChrome(packages, resources, overrides,
                                                mSelectedLocale, false);
     NS_ENSURE_TRUE_VOID(success);
   } else {
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1569,16 +1569,20 @@ ContentChild::RecvRegisterChromeItem(con
         case ChromeRegistryItem::TChromePackage:
             chromeRegistry->RegisterPackage(item.get_ChromePackage());
             break;
 
         case ChromeRegistryItem::TOverrideMapping:
             chromeRegistry->RegisterOverride(item.get_OverrideMapping());
             break;
 
+        case ChromeRegistryItem::TResourceMapping:
+            chromeRegistry->RegisterResource(item.get_ResourceMapping());
+            break;
+
         default:
             MOZ_ASSERT(false, "bad chrome item");
             return false;
     }
 
     return true;
 }
 
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -66,16 +66,17 @@ using mozilla::dom::NativeThreadId from 
 using mozilla::dom::quota::PersistenceType from "mozilla/dom/quota/PersistenceType.h";
 using mozilla::hal::ProcessPriority from "mozilla/HalTypes.h";
 using gfxIntSize from "nsSize.h";
 
 union ChromeRegistryItem
 {
     ChromePackage;
     OverrideMapping;
+    ResourceMapping;
 };
 
 namespace mozilla {
 namespace dom {
 
 struct FontListEntry {
     nsString  familyName;
     nsString  faceName;
--- a/mobile/android/base/tests/testAddonManager.java
+++ b/mobile/android/base/tests/testAddonManager.java
@@ -1,22 +1,24 @@
 package org.mozilla.gecko.tests;
 
 import org.json.JSONObject;
 import org.mozilla.gecko.Actions;
 
 import android.util.DisplayMetrics;
 
+/**
+ * This test performs the following steps to check the behavior of the Add-on Manager:
+ *
+ * 1) Open the Add-on Manager from the Add-ons menu item, and then close it.
+ * 2) Open the Add-on Manager by visiting about:addons in the URL bar.
+ * 3) Open a new tab, select the Add-ons menu item, then verify that the existing
+ *    Add-on Manager tab was selected, instead of opening a new tab.
+ */
 public class testAddonManager extends PixelTest  {
-    /* This test will check the behavior of the Addons Manager:
-    First the test will open the Addons Manager from the Menu and then close it
-    Then the test will open the Addons Manager by visiting about:addons
-    The test will tap/click on the addons.mozilla.org icon to open the AMO page in a new tab
-    With the Addons Manager open the test will verify that when it is opened again from the menu no new tab will be opened*/
-
     public void testAddonManager() {
         Actions.EventExpecter tabEventExpecter;
         Actions.EventExpecter contentEventExpecter;
         String url = StringHelper.ABOUT_ADDONS_URL;
 
         blockForGeckoReady();
 
         // Use the menu to open the Addon Manger
@@ -38,44 +40,22 @@ public class testAddonManager extends Pi
 
         // Close the Add-on Manager
         mActions.sendSpecialKey(Actions.SpecialKey.BACK);
 
         // Load the about:addons page and verify it was loaded
         loadAndPaint(url);
         verifyPageTitle(StringHelper.ADDONS_LABEL);
 
-        // Change the AMO URL so we do not try to navigate to a live webpage
-        JSONObject jsonPref = new JSONObject();
-        try {
-            jsonPref.put("name", "extensions.getAddons.browseAddons");
-            jsonPref.put("type", "string");
-            jsonPref.put("value", getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL));
-            setPreferenceAndWaitForChange(jsonPref);
-
-        } catch (Exception ex) { 
-            mAsserter.ok(false, "exception in testAddonManager", ex.toString());
-        }
-
-        // Load AMO page by clicking the AMO icon
-        DisplayMetrics dm = new DisplayMetrics();
-        getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
-
-        /* Setup the tap to top value + 25px and right value - 25px.
-        Since the AMO icon is 50x50px this values should set the tap
-        in the middle of the icon */
-        float top = mDriver.getGeckoTop() + 25 * dm.density;;
-        float right = mDriver.getGeckoLeft() + mDriver.getGeckoWidth() - 25 * dm.density;;
-
         // Setup wait for tab to spawn and load
         tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
         contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
 
-        // Tap on the AMO icon
-        mSolo.clickOnScreen(right, top);
+        // Open a new tab
+        addTab(getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL));
 
         // Wait for the new tab and page to load
         tabEventExpecter.blockForEvent();
         contentEventExpecter.blockForEvent();
 
         tabEventExpecter.unregisterListener();
         contentEventExpecter.unregisterListener();
 
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -88,17 +88,16 @@ function init() {
   window.addEventListener("popstate", onPopState, false);
 
   AddonManager.addInstallListener(Addons);
   AddonManager.addAddonListener(Addons);
   Addons.init();
   showList();
   ContextMenus.init();
 
-  document.getElementById("header-button").addEventListener("click", openLink, false);
 }
 
 
 function uninit() {
   AddonManager.removeInstallListener(Addons);
   AddonManager.removeAddonListener(Addons);
 }
 
--- a/mobile/android/chrome/content/aboutAddons.xhtml
+++ b/mobile/android/chrome/content/aboutAddons.xhtml
@@ -27,17 +27,16 @@
   <menu type="context" id="addonmenu">
     <menuitem id="contextmenu-enable" label="&addonAction.enable;"></menuitem>
     <menuitem id="contextmenu-disable" label="&addonAction.disable;" ></menuitem>
     <menuitem id="contextmenu-uninstall" label="&addonAction.uninstall;" ></menuitem>
   </menu>
 
   <div id="addons-header" class="header">
     <div>&aboutAddons.header2;</div>
-    <div id="header-button" role="button" aria-label="&aboutAddons.browseAll;" pref="extensions.getAddons.browseAddons" />
   </div>
   <div id="addons-list" class="list">
   </div>
 
   <div id="addons-details" class="list">
     <div class="addon-item list-item">
       <img class="icon"/>
       <div class="inner">
--- a/mobile/android/locales/en-US/chrome/aboutAddons.dtd
+++ b/mobile/android/locales/en-US/chrome/aboutAddons.dtd
@@ -1,13 +1,12 @@
 <!-- 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 aboutAddons.title2                     "Add-ons">
 <!ENTITY aboutAddons.header2                    "Your Add-ons">
 <!ENTITY aboutAddons.options                    "Options">
-<!ENTITY aboutAddons.browseAll                  "Browse all Firefox Add-ons">
 
 <!ENTITY addonAction.enable                     "Enable">
 <!ENTITY addonAction.disable                    "Disable">
 <!ENTITY addonAction.uninstall                  "Uninstall">
 <!ENTITY addonAction.undo                       "Undo">
--- a/mobile/android/themes/core/aboutAddons.css
+++ b/mobile/android/themes/core/aboutAddons.css
@@ -13,22 +13,16 @@
   display: inline;
 }
 
 .version {
   /* title is not localized, so keep the margin on the left side */
   margin-left: .67em;
 }
 
-#header-button {
-  background-image: url("chrome://browser/skin/images/amo-logo.png"), url("chrome://browser/skin/images/chevron.png");
-  background-size: 20px 20px, 8px 20px;
-  background-position: left, right 3px center;
-}
-
 .description {
   width: 100%;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
 }
 
 .status {
--- a/netwerk/protocol/res/nsResProtocolHandler.cpp
+++ b/netwerk/protocol/res/nsResProtocolHandler.cpp
@@ -1,24 +1,29 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/chrome/RegistryMessageUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/unused.h"
 
 #include "nsResProtocolHandler.h"
 #include "nsIIOService.h"
 #include "nsIFile.h"
 #include "nsNetUtil.h"
 #include "nsURLHelper.h"
 #include "nsEscape.h"
 
 #include "mozilla/Omnijar.h"
 
+using mozilla::dom::ContentParent;
+using mozilla::unused;
+
 static NS_DEFINE_CID(kResURLCID, NS_RESURL_CID);
 
 static nsResProtocolHandler *gResHandler = nullptr;
 
 #if defined(PR_LOGGING)
 //
 // Log module for Resource Protocol logging...
 //
@@ -299,44 +304,72 @@ nsResProtocolHandler::AllowPort(int32_t 
     *_retval = false;
     return NS_OK;
 }
 
 //----------------------------------------------------------------------------
 // nsResProtocolHandler::nsIResProtocolHandler
 //----------------------------------------------------------------------------
 
+static void
+SendResourceSubstitution(const nsACString& root, nsIURI* baseURI)
+{
+    if (GeckoProcessType_Content == XRE_GetProcessType()) {
+        return;
+    }
+
+    ResourceMapping resourceMapping;
+    resourceMapping.resource = root;
+    if (baseURI) {
+        baseURI->GetSpec(resourceMapping.resolvedURI.spec);
+        baseURI->GetOriginCharset(resourceMapping.resolvedURI.charset);
+    }
+
+    nsTArray<ContentParent*> parents;
+    ContentParent::GetAll(parents);
+    if (!parents.Length()) {
+        return;
+    }
+
+    for (uint32_t i = 0; i < parents.Length(); i++) {
+        unused << parents[i]->SendRegisterChromeItem(resourceMapping);
+    }
+}
+
 NS_IMETHODIMP
 nsResProtocolHandler::SetSubstitution(const nsACString& root, nsIURI *baseURI)
 {
     if (!baseURI) {
         mSubstitutions.Remove(root);
+        SendResourceSubstitution(root, baseURI);
         return NS_OK;
     }
 
     // If baseURI isn't a resource URI, we can set the substitution immediately.
     nsAutoCString scheme;
     nsresult rv = baseURI->GetScheme(scheme);
     NS_ENSURE_SUCCESS(rv, rv);
     if (!scheme.EqualsLiteral("resource")) {
         mSubstitutions.Put(root, baseURI);
+        SendResourceSubstitution(root, baseURI);
         return NS_OK;
     }
 
     // baseURI is a resource URI, let's resolve it first.
     nsAutoCString newBase;
     rv = ResolveURI(baseURI, newBase);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsIURI> newBaseURI;
     rv = mIOService->NewURI(newBase, nullptr, nullptr,
                             getter_AddRefs(newBaseURI));
     NS_ENSURE_SUCCESS(rv, rv);
 
     mSubstitutions.Put(root, newBaseURI);
+    SendResourceSubstitution(root, newBaseURI);
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsResProtocolHandler::GetSubstitution(const nsACString& root, nsIURI **result)
 {
     NS_ENSURE_ARG_POINTER(result);
 
--- a/netwerk/test/browser/browser.ini
+++ b/netwerk/test/browser/browser.ini
@@ -1,3 +1,6 @@
 [DEFAULT]
+support-files =
+  dummy.html
 
 [browser_NetUtil.js]
+[browser_child_resource.js]
new file mode 100644
--- /dev/null
+++ b/netwerk/test/browser/browser_child_resource.js
@@ -0,0 +1,256 @@
+/*
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This must be loaded in the remote process for this test to be useful
+const TEST_URL = "http://example.com/browser/netwerk/test/browser/dummy.html";
+
+const expectedRemote = gMultiProcessBrowser ? "true" : "";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+const resProtocol = Cc["@mozilla.org/network/protocol;1?name=resource"]
+                        .getService(Ci.nsIResProtocolHandler);
+
+function getMinidumpDirectory() {
+  var dir = Services.dirsvc.get('ProfD', Ci.nsIFile);
+  dir.append("minidumps");
+  return dir;
+}
+
+// This observer is needed so we can clean up all evidence of the crash so
+// the testrunner thinks things are peachy.
+let CrashObserver = {
+  observe: function(subject, topic, data) {
+    is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
+    ok(subject instanceof Ci.nsIPropertyBag2,
+       'Subject implements nsIPropertyBag2.');
+    // we might see this called as the process terminates due to previous tests.
+    // We are only looking for "abnormal" exits...
+    if (!subject.hasKey("abnormal")) {
+      info("This is a normal termination and isn't the one we are looking for...");
+      return;
+    }
+
+    var dumpID;
+    if ('nsICrashReporter' in Ci) {
+      dumpID = subject.getPropertyAsAString('dumpID');
+      ok(dumpID, "dumpID is present and not an empty string");
+    }
+
+    if (dumpID) {
+      var minidumpDirectory = getMinidumpDirectory();
+      let file = minidumpDirectory.clone();
+      file.append(dumpID + '.dmp');
+      file.remove(true);
+      file = minidumpDirectory.clone();
+      file.append(dumpID + '.extra');
+      file.remove(true);
+    }
+  }
+}
+Services.obs.addObserver(CrashObserver, 'ipc:content-shutdown', false);
+
+registerCleanupFunction(() => {
+  Services.obs.removeObserver(CrashObserver, 'ipc:content-shutdown');
+});
+
+function frameScript() {
+  Components.utils.import("resource://gre/modules/Services.jsm");
+  let resProtocol = Components.classes["@mozilla.org/network/protocol;1?name=resource"]
+                              .getService(Components.interfaces.nsIResProtocolHandler);
+
+  addMessageListener("Test:ResolveURI", function({ data: uri }) {
+    uri = Services.io.newURI(uri, null, null);
+    try {
+      let resolved = resProtocol.resolveURI(uri);
+      sendAsyncMessage("Test:ResolvedURI", resolved);
+    }
+    catch (e) {
+      sendAsyncMessage("Test:ResolvedURI", null);
+    }
+  });
+
+  addMessageListener("Test:Crash", function() {
+    dump("Crashing\n");
+    privateNoteIntentionalCrash();
+    Components.utils.import("resource://gre/modules/ctypes.jsm");
+    let zero = new ctypes.intptr_t(8);
+    let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+    badptr.contents
+  });
+}
+
+function waitForEvent(obj, name, capturing, chromeEvent) {
+  info("Waiting for " + name);
+  return new Promise((resolve) => {
+    function listener(event) {
+      info("Saw " + name);
+      obj.removeEventListener(name, listener, capturing, chromeEvent);
+      resolve(event);
+    }
+
+    obj.addEventListener(name, listener, capturing, chromeEvent);
+  });
+}
+
+function resolveURI(uri) {
+  uri = Services.io.newURI(uri, null, null);
+  try {
+    return resProtocol.resolveURI(uri);
+  }
+  catch (e) {
+    return null;
+  }
+}
+
+function remoteResolveURI(uri) {
+  return new Promise((resolve) => {
+    let manager = gBrowser.selectedBrowser.messageManager;
+
+    function listener({ data: resolved }) {
+      manager.removeMessageListener("Test:ResolvedURI", listener);
+      resolve(resolved);
+    }
+
+    manager.addMessageListener("Test:ResolvedURI", listener);
+    manager.sendAsyncMessage("Test:ResolveURI", uri);
+  });
+}
+
+let loadTestTab = Task.async(function*() {
+  gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
+  let browser = gBrowser.selectedBrowser;
+  yield waitForEvent(browser, "load", true);
+  browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
+  return browser;
+});
+
+// Restarts the child process by crashing it then reloading the tab
+let restart = Task.async(function*() {
+  let browser = gBrowser.selectedBrowser;
+  // If the tab isn't remote this would crash the main process so skip it
+  if (browser.getAttribute("remote") != "true")
+    return browser;
+
+  browser.messageManager.sendAsyncMessage("Test:Crash");
+  yield waitForEvent(browser, "AboutTabCrashedLoad", false, true);
+
+  browser.reload();
+
+  yield waitForEvent(browser, "load", true);
+  is(browser.getAttribute("remote"), expectedRemote, "Browser should be in the right process");
+  browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
+  return browser;
+});
+
+// Sanity check that this test is going to be useful
+add_task(function*() {
+  let browser = yield loadTestTab();
+
+  // This must be loaded in the remote process for this test to be useful
+  is(browser.getAttribute("remote"), expectedRemote, "Browser should be in the right process");
+
+  let local = resolveURI("resource://gre/modules/Services.jsm");
+  let remote = yield remoteResolveURI("resource://gre/modules/Services.jsm");
+  is(local, remote, "Services.jsm should resolve in both processes");
+
+  gBrowser.removeCurrentTab();
+});
+
+// Add a mapping, update it then remove it
+add_task(function*() {
+  let browser = yield loadTestTab();
+
+  info("Set");
+  resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null));
+  let local = resolveURI("resource://testing/test.js");
+  let remote = yield remoteResolveURI("resource://testing/test.js");
+  is(local, "chrome://global/content/test.js", "Should resolve in main process");
+  is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+  info("Change");
+  resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/skin", null, null));
+  local = resolveURI("resource://testing/test.js");
+  remote = yield remoteResolveURI("resource://testing/test.js");
+  is(local, "chrome://global/skin/test.js", "Should resolve in main process");
+  is(remote, "chrome://global/skin/test.js", "Should resolve in child process");
+
+  info("Clear");
+  resProtocol.setSubstitution("testing", null);
+  local = resolveURI("resource://testing/test.js");
+  remote = yield remoteResolveURI("resource://testing/test.js");
+  is(local, null, "Shouldn't resolve in main process");
+  is(remote, null, "Shouldn't resolve in child process");
+
+  gBrowser.removeCurrentTab();
+});
+
+// Add a mapping, restart the child process then check it is still there
+add_task(function*() {
+  let browser = yield loadTestTab();
+
+  info("Set");
+  resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null));
+  let local = resolveURI("resource://testing/test.js");
+  let remote = yield remoteResolveURI("resource://testing/test.js");
+  is(local, "chrome://global/content/test.js", "Should resolve in main process");
+  is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+  yield restart();
+
+  local = resolveURI("resource://testing/test.js");
+  remote = yield remoteResolveURI("resource://testing/test.js");
+  is(local, "chrome://global/content/test.js", "Should resolve in main process");
+  is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+  info("Change");
+  resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/skin", null, null));
+
+  yield restart();
+
+  local = resolveURI("resource://testing/test.js");
+  remote = yield remoteResolveURI("resource://testing/test.js");
+  is(local, "chrome://global/skin/test.js", "Should resolve in main process");
+  is(remote, "chrome://global/skin/test.js", "Should resolve in child process");
+
+  info("Clear");
+  resProtocol.setSubstitution("testing", null);
+
+  yield restart();
+
+  local = resolveURI("resource://testing/test.js");
+  remote = yield remoteResolveURI("resource://testing/test.js");
+  is(local, null, "Shouldn't resolve in main process");
+  is(remote, null, "Shouldn't resolve in child process");
+
+  gBrowser.removeCurrentTab();
+});
+
+// Adding a mapping to a resource URI should work
+add_task(function*() {
+  let browser = yield loadTestTab();
+
+  info("Set");
+  resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null));
+  resProtocol.setSubstitution("testing2", Services.io.newURI("resource://testing", null, null));
+  let local = resolveURI("resource://testing2/test.js");
+  let remote = yield remoteResolveURI("resource://testing2/test.js");
+  is(local, "chrome://global/content/test.js", "Should resolve in main process");
+  is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+  info("Clear");
+  resProtocol.setSubstitution("testing", null);
+  local = resolveURI("resource://testing2/test.js");
+  remote = yield remoteResolveURI("resource://testing2/test.js");
+  is(local, "chrome://global/content/test.js", "Should resolve in main process");
+  is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+  resProtocol.setSubstitution("testing2", null);
+  local = resolveURI("resource://testing2/test.js");
+  remote = yield remoteResolveURI("resource://testing2/test.js");
+  is(local, null, "Shouldn't resolve in main process");
+  is(remote, null, "Shouldn't resolve in child process");
+
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/netwerk/test/browser/dummy.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+  <p>Dummy Page</p>
+</body>
+</html>
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6748,16 +6748,21 @@
     "n_values": 3,
     "description": "Doorhanger shown = 0, Disable = 1, Enable = 2"
   },
   "LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Stores 1 if generating a call URL succeeded, and 0 if it failed."
   },
+  "LOOP_CLIENT_CALL_URL_SHARED": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Stores 1 every time the URL is copied or shared."
+  },
   "E10S_AUTOSTART": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Whether a session is set to autostart e10s windows"
   },
   "E10S_WINDOW": {
     "expires_in_version": "never",
     "kind": "boolean",