Merge m-c to inbound a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 07 Oct 2014 18:43:16 -0700
changeset 232467 fd9219cb431c6016646cf33fb8c930c2cb788853
parent 232466 a86612eb044a4d5da1d0522bff175107a7340eeb (current diff)
parent 232440 e4cfacb76830902057b6f698a5ca9f78aea25d06 (diff)
child 232468 ebcbf94ef701951a8d3718c9bf3534eb5c414989
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound a=merge
--- 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",