Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 26 Jun 2015 13:41:17 +0200
changeset 268508 460768b65147880bec72531c90ae4f5321e8049f
parent 268507 b24f2c2f49c5cb6c83765137b6c3b9e64261a1ea (current diff)
parent 268408 56e207dbb3bda446e30c8a9fcf16050f35a58fb9 (diff)
child 268509 96a1f71786c4820c7e334565cce815e085a4eb9b
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-esr52@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone41.0a1
Merge mozilla-central to mozilla-inbound
--- a/addon-sdk/mach_commands.py
+++ b/addon-sdk/mach_commands.py
@@ -1,17 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # Integrates the xpcshell test runner with mach.
 
+from __future__ import absolute_import
+
 import os
-import re
-import sys
 
 import mozpack.path as mozpath
 
 from mozbuild.base import (
     MachCommandBase,
     MozbuildObject,
 )
 
--- a/b2g/components/AboutServiceWorkers.jsm
+++ b/b2g/components/AboutServiceWorkers.jsm
@@ -14,35 +14,34 @@ Cu.import("resource://gre/modules/XPCOMU
 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
                                   "resource://gre/modules/SystemAppProxy.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gServiceWorkerManager",
                                   "@mozilla.org/serviceworkers/manager;1",
                                   "nsIServiceWorkerManager");
 
 function debug(aMsg) {
-  //dump("AboutServiceWorkers - " + aMsg + "\n");
+  // dump("AboutServiceWorkers - " + aMsg + "\n");
 }
 
 function serializeServiceWorkerInfo(aServiceWorkerInfo) {
   if (!aServiceWorkerInfo) {
     throw new Error("Invalid service worker information");
   }
 
   let result = {};
 
   Object.keys(aServiceWorkerInfo).forEach(property => {
     if (typeof aServiceWorkerInfo[property] == "function") {
       return;
     }
     if (property === "principal") {
       result.principal = {
         origin: aServiceWorkerInfo.principal.origin,
-        appId: aServiceWorkerInfo.principal.appId,
-        isInBrowser: aServiceWorkerInfo.principal.isInBrowser
+        originAttributes: aServiceWorkerInfo.principal.originAttributes
       };
       return;
     }
     result[property] = aServiceWorkerInfo[property];
   });
 
   return result;
 }
@@ -128,42 +127,46 @@ this.AboutServiceWorkers = {
       case "update":
         if (!message.scope) {
           self.sendError(message.id, "MissingScope");
           return;
         }
 
         if (!message.principal ||
             !message.principal.originAttributes) {
-          // XXX This will always error until bug 1171915 is fixed.
           self.sendError(message.id, "MissingOriginAttributes");
           return;
         }
 
-        gServiceWorkerManager.propagateSoftUpdate({},
-                                                  message.scope);
+        gServiceWorkerManager.propagateSoftUpdate(
+          message.principal.originAttributes,
+          message.scope
+        );
+
         self.sendResult(message.id, true);
         break;
 
       case "unregister":
         if (!message.principal ||
             !message.principal.origin ||
-            !message.principal.appId) {
-          self.sendError("MissingPrincipal");
+            !message.principal.originAttributes ||
+            !message.principal.originAttributes.appId ||
+            !message.principal.originAttributes.isInBrowser) {
+          self.sendError(message.id, "MissingPrincipal");
           return;
         }
 
         let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
           Services.io.newURI(message.principal.origin, null, null),
-          message.principal.appId,
-          message.principal.isInBrowser
+          message.principal.originAttributes.appId,
+          message.principal.originAttributes.isInBrowser
         );
 
         if (!message.scope) {
-          self.sendError("MissingScope");
+          self.sendError(message.id, "MissingScope");
           return;
         }
 
         let serviceWorkerUnregisterCallback = {
           unregisterSucceeded: function() {
             self.sendResult(message.id, true);
           },
 
--- a/b2g/config/aries/sources.xml
+++ b/b2g/config/aries/sources.xml
@@ -10,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <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="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="a32003194f707f66a2d8cdb913ed1869f1926c5d"/>
   <project name="device/common" path="device/common" revision="96d4d2006c4fcb2f19a3fa47ab10cb409faa017b"/>
@@ -134,17 +134,17 @@
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="5b71e40213f650459e95d35b6f14af7e88d8ab62"/>
   <project name="platform_frameworks_av" path="frameworks/av" remote="b2g" revision="a920312eb6299b6cc11f7136254c4b0ba7a663be"/>
   <project name="platform/frameworks/base" path="frameworks/base" revision="da8e6bc53c8bc669da0bb627904d08aa293f2497"/>
   <project name="platform/frameworks/native" path="frameworks/native" revision="a46a9f1ac0ed5662d614c277cbb14eb3f332f365"/>
   <project name="platform/hardware/libhardware" path="hardware/libhardware" revision="7196881a0e9dd7bfbbcf0af64c8064e70f0fa094"/>
   <project name="platform/hardware/broadcom/wlan" path="hardware/broadcom/wlan" revision="15a9b66de9b7d84c7ea63df3a834f095bca9e493"/>
   <project name="platform/hardware/qcom/audio" path="hardware/qcom/audio" revision="8d7676dfb68ee0cd069affedd5d1e97316a184ba"/>
   <project name="platform/hardware/qcom/camera" path="hardware/qcom/camera" revision="2a1ded216a91bf62a72b1640cf01ab4998f37028"/>
-  <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="a74adcf8d88320d936daa8d20ce88ca0107fb916"/>
+  <project name="hardware_qcom_display" path="hardware/qcom/display" remote="b2g" revision="37293646f6f0cbfd4fb631536984e056bef0fd2a"/>
   <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="9883ea57b0668d8f60dba025d4522dfa69a1fbfa"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="a558dc844bf5144fc38603fd8f4df8d9557052a5"/>
   <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="57ee1320ed7b4a1a1274d8f3f6c177cd6b9becb2"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="12b1977cc704b35f2e9db2bb423fa405348bc2f3"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="985bf15264d865fe7b9c5b45f61c451cbaafa43d"/>
   <project name="platform/system/core" path="system/core" revision="42839aedcf70bf6bc92a3b7ea4a5cc9bf9aef3f9"/>
   <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="5f4b68c799927b6e078f987b12722c3a6ccd4a45"/>
   <project name="platform/system/qcom" path="system/qcom" revision="63e3f6f176caad587d42bba4c16b66d953fb23c2"/>
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <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="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="83760d213fb3bec7b4117d266fcfbf6fe2ba14ab"/>
   <project name="device/common" path="device/common" revision="6a2995683de147791e516aae2ccb31fdfbe2ad30"/>
--- 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="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <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="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="87a2d8ab9248540910e56921654367b78a587095"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- 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,20 +12,20 @@
   <!--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="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <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="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="51da9b1981be481b92a59a826d4d78dc73d0989a"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,25 +10,25 @@
   <!--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="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <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="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="337e0ef5e40f02a1ae59b90db0548976c70a7226"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" revision="8af5ff6f5dced9eb5a8127459df6c75d24342204"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="30915518fa7ea07166efedc191a4f40aef516fe7"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" revision="96eee58e3389fb05a835310d6a06a6ba4486097a"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" revision="7c8a46698171aa2e0be09edb43d15a6acf832770"/>
   <project groups="pdk,linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" path="prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" revision="24b2038be8a636fd4a5d21f0abae1e466b07bcf7"/>
--- 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="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <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="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="87a2d8ab9248540910e56921654367b78a587095"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- 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,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <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="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="a32003194f707f66a2d8cdb913ed1869f1926c5d"/>
   <project name="device/common" path="device/common" revision="96d4d2006c4fcb2f19a3fa47ab10cb409faa017b"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "038e917076271d304b906a41b4de670e505c67ae", 
+        "git_revision": "8a1e4ae522c121c5cacd39b20a5386ec9055db82", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "7c48140d54ceee5131147e4960d860bb44ac6bb1", 
+    "revision": "155bda717bccdcab21d76d66aeebe400d887fb39", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,20 +12,20 @@
   <!--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="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,25 +10,25 @@
   <!--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="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="038e917076271d304b906a41b4de670e505c67ae"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8a1e4ae522c121c5cacd39b20a5386ec9055db82"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <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="5642f6578ec8ebcc73e1b9aa3826d37010a5b7ce"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0a749b6475889d1f7db8da4385c77aadfe5b06d5"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="337e0ef5e40f02a1ae59b90db0548976c70a7226"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" revision="8af5ff6f5dced9eb5a8127459df6c75d24342204"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="30915518fa7ea07166efedc191a4f40aef516fe7"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" revision="96eee58e3389fb05a835310d6a06a6ba4486097a"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" revision="7c8a46698171aa2e0be09edb43d15a6acf832770"/>
   <project groups="pdk,linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" path="prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" revision="24b2038be8a636fd4a5d21f0abae1e466b07bcf7"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -335,25 +335,30 @@ pref("browser.urlbar.delay", 50);
 // The special characters below can be typed into the urlbar to either restrict
 // the search to visited history, bookmarked, tagged pages; or force a match on
 // just the title text or url.
 pref("browser.urlbar.restrict.history", "^");
 pref("browser.urlbar.restrict.bookmark", "*");
 pref("browser.urlbar.restrict.tag", "+");
 pref("browser.urlbar.restrict.openpage", "%");
 pref("browser.urlbar.restrict.typed", "~");
+pref("browser.urlbar.restrict.searches", "$");
 pref("browser.urlbar.match.title", "#");
 pref("browser.urlbar.match.url", "@");
 
 // The default behavior for the urlbar can be configured to use any combination
 // of the match filters with each additional filter adding more results (union).
 pref("browser.urlbar.suggest.history",              true);
 pref("browser.urlbar.suggest.bookmark",             true);
 pref("browser.urlbar.suggest.openpage",             true);
+#ifdef NIGHTLY_BUILD
 pref("browser.urlbar.suggest.searches",             true);
+#else
+pref("browser.urlbar.suggest.searches",             false);
+#endif
 
 // Restrictions to current suggestions can also be applied (intersection).
 // Typed suggestion works only if history is set to true.
 pref("browser.urlbar.suggest.history.onlyTyped",    false);
 
 pref("browser.urlbar.formatting.enabled", true);
 pref("browser.urlbar.trimURLs", true);
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -285,23 +285,20 @@ toolbar[customizing] > .overflow-button 
 %ifdef XP_WIN
 #main-window[sizemode="maximized"] #titlebar-buttonbox {
   -moz-appearance: -moz-window-button-box-maximized;
 }
 %endif
 
 %endif
 
-#main-window[inDOMFullscreen] #navigator-toolbox,
-#main-window[inDOMFullscreen] #fullscr-toggler,
-#main-window[inDOMFullscreen] #sidebar-box,
-#main-window[inDOMFullscreen] #sidebar-splitter {
-  visibility: collapse;
-}
-
+#main-window[inFullscreen][inDOMFullscreen] #navigator-toolbox,
+#main-window[inFullscreen][inDOMFullscreen] #fullscr-toggler,
+#main-window[inFullscreen][inDOMFullscreen] #sidebar-box,
+#main-window[inFullscreen][inDOMFullscreen] #sidebar-splitter,
 #main-window[inFullscreen]:not([OSXLionFullscreen]) toolbar:not([fullscreentoolbar=true]),
 #main-window[inFullscreen] #global-notificationbox,
 #main-window[inFullscreen] #high-priority-global-notificationbox {
   visibility: collapse;
 }
 
 #navigator-toolbox[fullscreenShouldAnimate] {
   transition: 1.5s margin-top ease-out;
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -1471,72 +1471,226 @@ html[dir="rtl"] .standalone .room-conver
   display: flex;
   flex-flow: column nowrap;
 }
 
 .fx-embedded .text-chat-entries {
   flex: 1 1 auto;
   max-height: 120px;
   min-height: 60px;
-  padding: .7em .5em 0;
 }
 
 .text-chat-box {
   flex: 0 0 auto;
   max-height: 40px;
   min-height: 40px;
   width: 100%;
 }
 
 .text-chat-entries {
   overflow: auto;
 }
 
 .text-chat-entry {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: .5em;
   text-align: end;
-  margin-bottom: 1.5em;
+  flex-wrap: nowrap;
+  justify-content: flex-start;
+  align-content: stretch;
+  align-items: flex-start;
 }
 
 .text-chat-entry > p {
-  border-width: 1px;
-  border-style: solid;
-  border-color: #0095dd;
-  border-radius: 10000px;
-  padding: .5em 1em;
+  position: relative;
+  z-index: 10;
   /* Drop the default margins from the 'p' element. */
   margin: 0;
-  /* inline-block stops the elements taking 100% of the text-chat-view width */
-  display: inline-block;
-  /* Split really long strings with no spaces appropriately, whilst limiting the
-     width to 100%. */
-  max-width: 100%;
+  padding: .7em;
+  /* leave some room for the chat bubble arrow */
+  max-width: 80%;
+  border-width: 1px;
+  border-style: solid;
+  border-color: #2ea4ff;
+  background: #fff;
+  word-wrap: break-word;
   word-wrap: break-word;
+  flex: 0 1 auto;
+  align-self: auto;
+}
+
+.text-chat-entry.sent > p,
+.text-chat-entry.received > p {
+  background: #fff;
+}
+
+.text-chat-entry.sent > p {
+  border-radius: 15px;
+  border-bottom-right-radius: 0;
+}
+
+.text-chat-entry.received > p {
+  border-radius: 15px;
+  border-top-left-radius: 0;
+}
+
+html[dir="rtl"] .text-chat-entry.sent > p {
+  border-radius: 15px;
+  border-bottom-left-radius: 0;
+}
+
+html[dir="rtl"] .text-chat-entry.received > p {
+  border-radius: 15px;
+  border-top-right-radius: 0;
+}
+
+.text-chat-entry.received > p {
+  order: 1;
+}
+
+.text-chat-entry.sent > p {
+  order: 1;
 }
 
 .text-chat-entry.received {
   text-align: start;
 }
 
 .text-chat-entry.received > p {
   border-color: #d8d8d8;
 }
 
-.text-chat-entry.special > p {
-  border: none;
+/* Text chat entry timestamp */
+.text-chat-entry-timestamp {
+  margin: 0 .2em;
+  color: #aaa;
+  font-style: italic;
+  font-size: .8em;
+  order: 0;
+  flex: 0 1 auto;
+  align-self: center;
+}
+
+/* Sent text chat entries should be on the right */
+.text-chat-entry.sent {
+  justify-content: flex-end;
+}
+
+.received > .text-chat-entry-timestamp {
+  order: 2;
+}
+
+.sent > .text-chat-entry-timestamp {
+  order: 0;
+}
+
+/* Pseudo element used to cover part between chat bubble and chat arrow. */
+.text-chat-entry > p:after {
+  position: absolute;
+  background: #fff;
+  content: "";
+}
+
+.text-chat-entry.sent > p:after {
+  right: -2px;
+  bottom: 0;
+  width: 15px;
+  height: 9px;
+  border-top-left-radius: 15px;
+  border-top-right-radius: 22px;
+}
+
+.text-chat-entry.received > p:after {
+  top: 0;
+  left: -2px;
+  width: 15px;
+  height: 9px;
+  border-bottom-left-radius: 22px;
+  border-bottom-right-radius: 15px;
+}
+
+html[dir="rtl"] .text-chat-entry.sent > p:after {
+  /* Reset */
+  right: auto;
+  left: -1px;
+  bottom: 0;
+  width: 15px;
+  height: 9px;
+}
+
+html[dir="rtl"] .text-chat-entry.received > p:after {
+  /* Reset */
+  left: auto;
+  top: 0;
+  right: -1px;
+  width: 15px;
+  height: 6px;
+}
+
+/* Text chat entry arrow */
+.text-chat-arrow {
+  width: 18px;
+  background-repeat: no-repeat;
+  flex: 0 1 auto;
+  position: relative;
+  z-index: 5;
+}
+
+.text-chat-entry.sent .text-chat-arrow {
+  margin-bottom: -1px;
+  margin-left: -11px;
+  height: 10px;
+  background-image: url("../img/chatbubble-arrow-right.svg");
+  order: 2;
+  align-self: flex-end;
+}
+
+.text-chat-entry.received .text-chat-arrow {
+  margin-left: 0;
+  margin-right: -9px;
+  height: 10px;
+  background-image: url("../img/chatbubble-arrow-left.svg");
+  order: 0;
+  align-self: auto;
+}
+
+html[dir="rtl"] .text-chat-arrow {
+  transform: scaleX(-1);
+}
+
+html[dir="rtl"] .text-chat-entry.sent .text-chat-arrow {
+  /* Reset margin. */
+  margin-left: 0;
+  margin-right: -11px;
+}
+
+html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
+  /* Reset margin. */
+  margin-right: 0;
+  margin-left: -10px;
 }
 
 .text-chat-entry.special.room-name {
   color: black;
   font-weight: bold;
   text-align: start;
   background-color: #E8F6FE;
   padding-bottom: 0;
   margin-bottom: 0;
 }
 
+.text-chat-entry.special.room-name p {
+  background: #E8F6FE;
+}
+
+.text-chat-entry.special > p {
+  border: none;
+}
+
 .text-chat-box {
   margin: auto;
 }
 
 .text-chat-box > form > input {
   width: 100%;
   height: 40px;
   padding: 0 .5em .5em;
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/chatbubble-arrow-left.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<svg width="20" height="8" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <title>chatbubble-arrow</title>
+ <desc>Created with Sketch.</desc>
+ <g>
+  <title>Layer 1</title>
+  <g transform="rotate(180 6.2844319343566895,3.8364052772521973) " id="svg_1" fill="none">
+   <path id="svg_2" fill="#d8d8d8" d="m12.061934,7.656905l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967001 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.201999,5.664001 8.377999,6.637 8.439999,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352001"/>
+  </g>
+  <line id="svg_13" y2="0.529488" x2="13.851821" y1="0.529488" x1="17.916953" stroke="#d8d8d8" fill="none"/>
+  <line id="svg_26" y2="0.529488" x2="9.79687" y1="0.529488" x1="13.862002" stroke="#d8d8d8" fill="none"/>
+  <line id="svg_27" y2="0.529488" x2="15.908413" y1="0.529488" x1="19.973545" stroke="#d8d8d8" fill="none"/>
+ </g>
+</svg>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/chatbubble-arrow-right.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<svg width="20" height="9" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <title>chatbubble-arrow</title>
+ <desc>Created with Sketch.</desc>
+ <g>
+  <title>Layer 1</title>
+  <g id="svg_1" fill="none">
+   <path id="svg_2" fill="#2EA4FF" d="m19.505243,8.972466l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.202,5.664 8.377999,6.637 8.44,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352"/>
+  </g>
+  <line id="svg_13" y2="8.474788" x2="6.200791" y1="8.474788" x1="10.265923" stroke="#22a4ff" fill="none"/>
+  <line id="svg_26" y2="8.474788" x2="2.14584" y1="8.474788" x1="6.210972" stroke="#22a4ff" fill="none"/>
+  <line id="svg_27" y2="8.474788" x2="0.000501" y1="8.474788" x1="4.065633" stroke="#22a4ff" fill="none"/>
+ </g>
+</svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -172,25 +172,28 @@ loop.shared.actions = (function() {
       available: Boolean
     }),
 
     /**
      * Used to send a message to the other peer.
      */
     SendTextChatMessage: Action.define("sendTextChatMessage", {
       contentType: String,
-      message: String
+      message: String,
+      sentTimestamp: String
     }),
 
     /**
      * Notifies that a message has been received from the other peer.
      */
     ReceivedTextChatMessage: Action.define("receivedTextChatMessage", {
       contentType: String,
-      message: String
+      message: String,
+      receivedTimestamp: String
+      // sentTimestamp: String (optional)
     }),
 
     /**
      * Used by the ongoing views to notify stores about the elements
      * required for the sdk.
      */
     SetupStreamElements: Action.define("setupStreamElements", {
       // The configuration for the publisher/subscribe options
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -693,18 +693,22 @@ loop.OTSdkDriver = (function() {
         if (err) {
           console.error(err);
           return;
         }
 
         channel.on({
           message: function(ev) {
             try {
+              var message = JSON.parse(ev.data);
+              /* Append the timestamp. This is the time that gets shown. */
+              message.receivedTimestamp = (new Date()).toISOString();
+
               this.dispatcher.dispatch(
-                new sharedActions.ReceivedTextChatMessage(JSON.parse(ev.data)));
+                new sharedActions.ReceivedTextChatMessage(message));
             } catch (ex) {
               console.error("Failed to process incoming chat message", ex);
             }
           }.bind(this),
 
           close: function(e) {
             // XXX We probably want to dispatch and handle this somehow.
             console.log("Subscribed data channel closed!");
--- a/browser/components/loop/content/shared/js/textChatStore.js
+++ b/browser/components/loop/content/shared/js/textChatStore.js
@@ -91,17 +91,19 @@ loop.store.TextChatStore = (function() {
      */
     _appendTextChatMessage: function(type, messageData) {
       // We create a new list to avoid updating the store's state directly,
       // which confuses the views.
       var message = {
         type: type,
         contentType: messageData.contentType,
         message: messageData.message,
-        extraData: messageData.extraData
+        extraData: messageData.extraData,
+        sentTimestamp: messageData.sentTimestamp,
+        receivedTimestamp: messageData.receivedTimestamp
       };
       var newList = this._storeState.messageList.concat(message);
       this.setStoreState({ messageList: newList });
 
       // Notify MozLoopService if appropriate that a message has been appended
       // and it should therefore check if we need a different sized window or not.
       if (type != CHAT_MESSAGE_TYPES.SPECIAL) {
         window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -16,30 +16,54 @@ loop.shared.views.chat = (function(mozL1
    * Renders an individual entry for the text chat entries view.
    */
   var TextChatEntry = React.createClass({displayName: "TextChatEntry",
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
       contentType: React.PropTypes.string.isRequired,
       message: React.PropTypes.string.isRequired,
+      showTimestamp: React.PropTypes.bool.isRequired,
+      timestamp: React.PropTypes.string.isRequired,
       type: React.PropTypes.string.isRequired
     },
 
+    /**
+     * Pretty print timestamp. From time in milliseconds to HH:MM
+     * (or L10N equivalent).
+     *
+     */
+    _renderTimestamp: function() {
+      var date = new Date(this.props.timestamp);
+      var language = mozL10n.language ? mozL10n.language.code
+                                      : mozL10n.getLanguage();
+
+      return (
+        React.createElement("span", {className: "text-chat-entry-timestamp"}, 
+          date.toLocaleTimeString(language,
+                                   {hour: "numeric", minute: "numeric",
+                                   hour12: false})
+        )
+      );
+    },
+
     render: function() {
       var classes = React.addons.classSet({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
+        "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       return (
         React.createElement("div", {className: classes}, 
-          React.createElement("p", null, this.props.message)
+          React.createElement("p", null, this.props.message), 
+          React.createElement("span", {className: "text-chat-arrow"}), 
+          this.props.showTimestamp ? this._renderTimestamp() : null
         )
       );
     }
   });
 
   var TextChatRoomName = React.createClass({displayName: "TextChatRoomName",
     mixins: [React.addons.PureRenderMixin],
 
@@ -62,16 +86,20 @@ loop.shared.views.chat = (function(mozL1
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({displayName: "TextChatEntriesView",
     mixins: [
       React.addons.PureRenderMixin,
       sharedMixins.AudioMixin
     ],
 
+    statics: {
+      ONE_MINUTE: 60
+    },
+
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       messageList: React.PropTypes.array.isRequired
     },
 
     getInitialState: function() {
       return {
         receivedMessageCount: 0
@@ -109,16 +137,19 @@ loop.shared.views.chat = (function(mozL1
           } catch (ex) {
             console.error("TextChatEntriesView.componentDidUpdate exception", ex);
           }
         }.bind(this));
       }
     },
 
     render: function() {
+      /* Keep track of the last printed timestamp. */
+      var lastTimestamp = 0;
+
       if (!this.props.messageList.length) {
         return null;
       }
 
       return (
         React.createElement("div", {className: "text-chat-entries"}, 
           React.createElement("div", {className: "text-chat-scroller"}, 
             
@@ -136,33 +167,90 @@ loop.shared.views.chat = (function(mozL1
                             dispatcher: this.props.dispatcher, 
                             showContextTitle: true, 
                             thumbnail: entry.extraData.thumbnail, 
                             url: entry.extraData.location, 
                             useDesktopPaths: false})
                         )
                       );
                     default:
-                      console.error("Unsupported contentType", entry.contentType);
+                      console.error("Unsupported contentType",
+                                    entry.contentType);
                       return null;
                   }
                 }
 
+                /* For SENT messages there is no received timestamp. */
+                var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
+
+                var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
+                var shouldShowTimestamp = this._shouldShowTimestamp(i,
+                                                                    timeDiff);
+
+                if (shouldShowTimestamp) {
+                  lastTimestamp = timestamp;
+                }
+
                 return (
-                  React.createElement(TextChatEntry, {
-                    contentType: entry.contentType, 
-                    key: i, 
-                    message: entry.message, 
-                    type: entry.type})
-                );
+                  React.createElement(TextChatEntry, {contentType: entry.contentType, 
+                                 key: i, 
+                                 message: entry.message, 
+                                 showTimestamp: shouldShowTimestamp, 
+                                 timestamp: timestamp, 
+                                 type: entry.type})
+                  );
               }, this)
             
           )
         )
       );
+    },
+
+    /**
+     * Decide to show timestamp or not on a message.
+     * If the time difference between two consecutive messages is bigger than
+     * one minute or if message types are different.
+     *
+     * @param {number} idx       Index of message in the messageList.
+     * @param {boolean} timeDiff If difference between consecutive messages is
+     *                           bigger than one minute.
+     */
+    _shouldShowTimestamp: function(idx, timeDiff) {
+      if (!idx) {
+        return true;
+      }
+
+      /* If consecutive messages are from different senders */
+      if (this.props.messageList[idx].type !==
+          this.props.messageList[idx - 1].type) {
+        return true;
+      }
+
+      return timeDiff;
+    },
+
+    /**
+     * Determines if difference between the two timestamp arguments
+     * is bigger that 60 (1 minute)
+     *
+     * Timestamps are using ISO8601 format.
+     *
+     * @param {string} currTime Timestamp of message yet to be rendered.
+     * @param {string} prevTime Last timestamp printed in the chat view.
+     */
+    _isOneMinDelta: function(currTime, prevTime) {
+      var date1 = new Date(currTime);
+      var date2 = new Date(prevTime);
+      var delta = date1 - date2;
+
+      if (delta / 1000 >= this.constructor.ONE_MINUTE) {
+        return true;
+      }
+
+      return false;
     }
   });
 
   /**
    * Displays a text chat entry input box for sending messages.
    *
    * @property {loop.Dispatcher} dispatcher
    * @property {Boolean} showPlaceholder    Set to true to show the placeholder message.
@@ -209,17 +297,18 @@ loop.shared.views.chat = (function(mozL1
 
       // Don't send empty messages.
       if (!this.state.messageDetail) {
         return;
       }
 
       this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: this.state.messageDetail
+        message: this.state.messageDetail,
+        sentTimestamp: (new Date()).toISOString()
       }));
 
       // Reset the form to empty, ready for the next message.
       this.setState({ messageDetail: "" });
     },
 
     render: function() {
       if (!this.props.textChatEnabled) {
@@ -304,11 +393,12 @@ loop.shared.views.chat = (function(mozL1
             textChatEnabled: this.state.textChatEnabled})
         )
       );
     }
   });
 
   return {
     TextChatEntriesView: TextChatEntriesView,
+    TextChatEntry: TextChatEntry,
     TextChatView: TextChatView
   };
 })(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -16,30 +16,54 @@ loop.shared.views.chat = (function(mozL1
    * Renders an individual entry for the text chat entries view.
    */
   var TextChatEntry = React.createClass({
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
       contentType: React.PropTypes.string.isRequired,
       message: React.PropTypes.string.isRequired,
+      showTimestamp: React.PropTypes.bool.isRequired,
+      timestamp: React.PropTypes.string.isRequired,
       type: React.PropTypes.string.isRequired
     },
 
+    /**
+     * Pretty print timestamp. From time in milliseconds to HH:MM
+     * (or L10N equivalent).
+     *
+     */
+    _renderTimestamp: function() {
+      var date = new Date(this.props.timestamp);
+      var language = mozL10n.language ? mozL10n.language.code
+                                      : mozL10n.getLanguage();
+
+      return (
+        <span className="text-chat-entry-timestamp">
+          {date.toLocaleTimeString(language,
+                                   {hour: "numeric", minute: "numeric",
+                                   hour12: false})}
+        </span>
+      );
+    },
+
     render: function() {
       var classes = React.addons.classSet({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
+        "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       return (
         <div className={classes}>
           <p>{this.props.message}</p>
+          <span className="text-chat-arrow" />
+          {this.props.showTimestamp ? this._renderTimestamp() : null}
         </div>
       );
     }
   });
 
   var TextChatRoomName = React.createClass({
     mixins: [React.addons.PureRenderMixin],
 
@@ -62,16 +86,20 @@ loop.shared.views.chat = (function(mozL1
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({
     mixins: [
       React.addons.PureRenderMixin,
       sharedMixins.AudioMixin
     ],
 
+    statics: {
+      ONE_MINUTE: 60
+    },
+
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       messageList: React.PropTypes.array.isRequired
     },
 
     getInitialState: function() {
       return {
         receivedMessageCount: 0
@@ -109,16 +137,19 @@ loop.shared.views.chat = (function(mozL1
           } catch (ex) {
             console.error("TextChatEntriesView.componentDidUpdate exception", ex);
           }
         }.bind(this));
       }
     },
 
     render: function() {
+      /* Keep track of the last printed timestamp. */
+      var lastTimestamp = 0;
+
       if (!this.props.messageList.length) {
         return null;
       }
 
       return (
         <div className="text-chat-entries">
           <div className="text-chat-scroller">
             {
@@ -136,33 +167,90 @@ loop.shared.views.chat = (function(mozL1
                             dispatcher={this.props.dispatcher}
                             showContextTitle={true}
                             thumbnail={entry.extraData.thumbnail}
                             url={entry.extraData.location}
                             useDesktopPaths={false} />
                         </div>
                       );
                     default:
-                      console.error("Unsupported contentType", entry.contentType);
+                      console.error("Unsupported contentType",
+                                    entry.contentType);
                       return null;
                   }
                 }
 
+                /* For SENT messages there is no received timestamp. */
+                var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
+
+                var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
+                var shouldShowTimestamp = this._shouldShowTimestamp(i,
+                                                                    timeDiff);
+
+                if (shouldShowTimestamp) {
+                  lastTimestamp = timestamp;
+                }
+
                 return (
-                  <TextChatEntry
-                    contentType={entry.contentType}
-                    key={i}
-                    message={entry.message}
-                    type={entry.type} />
-                );
+                  <TextChatEntry contentType={entry.contentType}
+                                 key={i}
+                                 message={entry.message}
+                                 showTimestamp={shouldShowTimestamp}
+                                 timestamp={timestamp}
+                                 type={entry.type} />
+                  );
               }, this)
             }
           </div>
         </div>
       );
+    },
+
+    /**
+     * Decide to show timestamp or not on a message.
+     * If the time difference between two consecutive messages is bigger than
+     * one minute or if message types are different.
+     *
+     * @param {number} idx       Index of message in the messageList.
+     * @param {boolean} timeDiff If difference between consecutive messages is
+     *                           bigger than one minute.
+     */
+    _shouldShowTimestamp: function(idx, timeDiff) {
+      if (!idx) {
+        return true;
+      }
+
+      /* If consecutive messages are from different senders */
+      if (this.props.messageList[idx].type !==
+          this.props.messageList[idx - 1].type) {
+        return true;
+      }
+
+      return timeDiff;
+    },
+
+    /**
+     * Determines if difference between the two timestamp arguments
+     * is bigger that 60 (1 minute)
+     *
+     * Timestamps are using ISO8601 format.
+     *
+     * @param {string} currTime Timestamp of message yet to be rendered.
+     * @param {string} prevTime Last timestamp printed in the chat view.
+     */
+    _isOneMinDelta: function(currTime, prevTime) {
+      var date1 = new Date(currTime);
+      var date2 = new Date(prevTime);
+      var delta = date1 - date2;
+
+      if (delta / 1000 >= this.constructor.ONE_MINUTE) {
+        return true;
+      }
+
+      return false;
     }
   });
 
   /**
    * Displays a text chat entry input box for sending messages.
    *
    * @property {loop.Dispatcher} dispatcher
    * @property {Boolean} showPlaceholder    Set to true to show the placeholder message.
@@ -209,17 +297,18 @@ loop.shared.views.chat = (function(mozL1
 
       // Don't send empty messages.
       if (!this.state.messageDetail) {
         return;
       }
 
       this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: this.state.messageDetail
+        message: this.state.messageDetail,
+        sentTimestamp: (new Date()).toISOString()
       }));
 
       // Reset the form to empty, ready for the next message.
       this.setState({ messageDetail: "" });
     },
 
     render: function() {
       if (!this.props.textChatEnabled) {
@@ -304,11 +393,12 @@ loop.shared.views.chat = (function(mozL1
             textChatEnabled={this.state.textChatEnabled} />
         </div>
       );
     }
   });
 
   return {
     TextChatEntriesView: TextChatEntriesView,
+    TextChatEntry: TextChatEntry,
     TextChatView: TextChatView
   };
 })(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -35,16 +35,18 @@ browser.jar:
   content/browser/loop/shared/img/sad.png                       (content/shared/img/sad.png)
   content/browser/loop/shared/img/icon_32.png                   (content/shared/img/icon_32.png)
   content/browser/loop/shared/img/icon_64.png                   (content/shared/img/icon_64.png)
   content/browser/loop/shared/img/spinner.svg                   (content/shared/img/spinner.svg)
   # XXX could get rid of the png spinner usages and replace them with the svg
   # one?
   content/browser/loop/shared/img/spinner.png                   (content/shared/img/spinner.png)
   content/browser/loop/shared/img/spinner@2x.png                (content/shared/img/spinner@2x.png)
+  content/browser/loop/shared/img/chatbubble-arrow-left.svg     (content/shared/img/chatbubble-arrow-left.svg)
+  content/browser/loop/shared/img/chatbubble-arrow-right.svg    (content/shared/img/chatbubble-arrow-right.svg)
   content/browser/loop/shared/img/audio-inverse-14x14.png       (content/shared/img/audio-inverse-14x14.png)
   content/browser/loop/shared/img/audio-inverse-14x14@2x.png    (content/shared/img/audio-inverse-14x14@2x.png)
   content/browser/loop/shared/img/facemute-14x14.png            (content/shared/img/facemute-14x14.png)
   content/browser/loop/shared/img/facemute-14x14@2x.png         (content/shared/img/facemute-14x14@2x.png)
   content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
   content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
   content/browser/loop/shared/img/mute-inverse-14x14.png        (content/shared/img/mute-inverse-14x14.png)
   content/browser/loop/shared/img/mute-inverse-14x14@2x.png     (content/shared/img/mute-inverse-14x14@2x.png)
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -1336,32 +1336,39 @@ describe("loop.OTSdkDriver", function ()
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.DataChannelsAvailable({
             available: true
           }));
       });
 
       it("should dispatch `ReceivedTextChatMessage` when a text message is received", function() {
         var fakeChannel = _.extend({}, Backbone.Events);
+        var data = '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT +
+                   '","message":"Are you there?","receivedTimestamp": "2015-06-25T00:29:14.197Z"}';
+        var clock = sinon.useFakeTimers();
 
         subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
 
         session.trigger("signal:readyForDataChannel");
 
         // Now send the message.
         fakeChannel.trigger("message", {
-          data: '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT + '","message":"Are you there?"}'
+          data: data
         });
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.ReceivedTextChatMessage({
             contentType: CHAT_CONTENT_TYPES.TEXT,
-            message: "Are you there?"
+            message: "Are you there?",
+            receivedTimestamp: "1970-01-01T00:00:00.000Z"
           }));
+
+        /* Restore the time. */
+        clock.restore();
       });
     });
 
     describe("exception", function() {
       describe("Unable to publish (GetUserMedia)", function() {
         it("should destroy the publisher", function() {
           sdk.trigger("exception", {
             code: OT.ExceptionCodes.UNABLE_TO_PUBLISH,
--- a/browser/components/loop/test/shared/textChatStore_test.js
+++ b/browser/components/loop/test/shared/textChatStore_test.js
@@ -65,24 +65,29 @@ describe("loop.store.TextChatStore", fun
   });
 
   describe("#receivedTextChatMessage", function() {
     it("should add the message to the list", function() {
       var message = "Hello!";
 
       store.receivedTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: message
+        message: message,
+        extraData: undefined,
+        sentTimestamp: "2015-06-24T23:58:53.848Z",
+        receivedTimestamp: "1970-01-01T00:00:00.000Z"
       });
 
       expect(store.getStoreState("messageList")).eql([{
         type: CHAT_MESSAGE_TYPES.RECEIVED,
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: message,
-        extraData: undefined
+        extraData: undefined,
+        sentTimestamp: "2015-06-24T23:58:53.848Z",
+        receivedTimestamp: "1970-01-01T00:00:00.000Z"
       }]);
     });
 
     it("should not add messages for unknown content types", function() {
       store.receivedTextChatMessage({
         contentType: "invalid type",
         message: "Hi"
       });
@@ -113,26 +118,30 @@ describe("loop.store.TextChatStore", fun
 
       sinon.assert.calledOnce(fakeSdkDriver.sendTextChatMessage);
       sinon.assert.calledWithExactly(fakeSdkDriver.sendTextChatMessage, messageData);
     });
 
     it("should add the message to the list", function() {
       var messageData = {
         contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: "It's awesome!"
+        message: "It's awesome!",
+        sentTimestamp: "2015-06-24T23:58:53.848Z",
+        receivedTimestamp: "2015-06-24T23:58:53.848Z"
       };
 
       store.sendTextChatMessage(messageData);
 
       expect(store.getStoreState("messageList")).eql([{
         type: CHAT_MESSAGE_TYPES.SENT,
         contentType: messageData.contentType,
         message: messageData.message,
-        extraData: undefined
+        extraData: undefined,
+        sentTimestamp: "2015-06-24T23:58:53.848Z",
+        receivedTimestamp: "2015-06-24T23:58:53.848Z"
       }]);
     });
 
     it("should dipatch a LoopChatMessageAppended event", function() {
       store.sendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: "Hello!"
       });
@@ -150,17 +159,19 @@ describe("loop.store.TextChatStore", fun
         roomOwner: "Mark",
         roomUrl: "fake"
       }));
 
       expect(store.getStoreState("messageList")).eql([{
         type: CHAT_MESSAGE_TYPES.SPECIAL,
         contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
         message: "Let's share!",
-        extraData: undefined
+        extraData: undefined,
+        sentTimestamp: undefined,
+        receivedTimestamp: undefined
       }]);
     });
 
     it("should add the context to the list", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "Let's share!",
         roomOwner: "Mark",
         roomUrl: "fake",
@@ -171,21 +182,25 @@ describe("loop.store.TextChatStore", fun
         }]
       }));
 
       expect(store.getStoreState("messageList")).eql([
         {
           type: CHAT_MESSAGE_TYPES.SPECIAL,
           contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
           message: "Let's share!",
-          extraData: undefined
+          extraData: undefined,
+          sentTimestamp: undefined,
+          receivedTimestamp: undefined
         }, {
           type: CHAT_MESSAGE_TYPES.SPECIAL,
           contentType: CHAT_CONTENT_TYPES.CONTEXT,
           message: "A wonderful event",
+          sentTimestamp: undefined,
+          receivedTimestamp: undefined,
           extraData: {
             location: "http://wonderful.invalid",
             thumbnail: "fake"
           }
         }
       ]);
     });
 
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -6,21 +6,21 @@ describe("loop.shared.views.TextChatView
 
   var expect = chai.expect;
   var sharedActions = loop.shared.actions;
   var sharedViews = loop.shared.views;
   var TestUtils = React.addons.TestUtils;
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
   var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
 
-  var dispatcher, fakeSdkDriver, sandbox, store;
+  var dispatcher, fakeSdkDriver, sandbox, store, fakeClock;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
-    sandbox.useFakeTimers();
+    fakeClock = sandbox.useFakeTimers();
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
 
     fakeSdkDriver = {
       sendTextChatMessage: sinon.stub()
     };
 
@@ -115,31 +115,184 @@ describe("loop.shared.views.TextChatView
           message: "Hello!"
         }]
       });
 
       sinon.assert.notCalled(view.play);
     });
   });
 
+  describe("TextChatEntry", function() {
+    var view;
+
+    function mountTestComponent(extraProps) {
+      var props = _.extend({
+        dispatcher: dispatcher
+      }, extraProps);
+      return TestUtils.renderIntoDocument(
+        React.createElement(loop.shared.views.chat.TextChatEntry, props));
+    }
+
+    it("should not render a timestamp", function() {
+      view = mountTestComponent({
+        showTimestamp: false,
+        timestamp: "2015-06-23T22:48:39.738Z"
+      });
+      var node = view.getDOMNode();
+
+      expect(node.querySelector(".text-chat-entry-timestamp")).to.eql(null);
+    });
+
+    it("should render a timestamp", function() {
+      view = mountTestComponent({
+        showTimestamp: true,
+        timestamp: "2015-06-23T22:48:39.738Z"
+      });
+      var node = view.getDOMNode();
+
+      expect(node.querySelector(".text-chat-entry-timestamp")).to.not.eql(null);
+    });
+  });
+
+  describe("TextChatEntriesView", function() {
+    var view, node;
+
+    function mountTestComponent(extraProps) {
+      var props = _.extend({
+        dispatcher: dispatcher
+      }, extraProps);
+      return TestUtils.renderIntoDocument(
+        React.createElement(loop.shared.views.chat.TextChatEntriesView, props));
+    }
+
+    beforeEach(function() {
+      store.setStoreState({ textChatEnabled: true });
+    });
+
+    it("should show timestamps if there are different senders", function() {
+      view = mountTestComponent({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!"
+        }, {
+          type: CHAT_MESSAGE_TYPES.SENT,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Is it me you're looking for?"
+        }]
+      });
+      node = view.getDOMNode();
+
+      expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
+          .to.eql(2);
+    });
+
+    it("should show timestamps if they are 1 minute apart (SENT)", function() {
+      view = mountTestComponent({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.SENT,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!",
+          sentTimestamp: "2015-06-25T17:53:55.357Z"
+        }, {
+          type: CHAT_MESSAGE_TYPES.SENT,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Is it me you're looking for?",
+          sentTimestamp: "2015-06-25T17:54:55.357Z"
+        }]
+      });
+      node = view.getDOMNode();
+
+      expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
+          .to.eql(2);
+    });
+
+    it("should show timestamps if they are 1 minute apart (RECV)", function() {
+      view = mountTestComponent({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!",
+          receivedTimestamp: "2015-06-25T17:53:55.357Z"
+        }, {
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Is it me you're looking for?",
+          receivedTimestamp: "2015-06-25T17:54:55.357Z"
+        }]
+      });
+      node = view.getDOMNode();
+
+      expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
+          .to.eql(2);
+    });
+
+    it("should not show timestamps from msgs sent in the same minute", function() {
+      view = mountTestComponent({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!"
+        }, {
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Is it me you're looking for?"
+        }]
+      });
+      node = view.getDOMNode();
+
+      expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
+          .to.eql(1);
+    });
+  });
+
   describe("TextChatView", function() {
     var view;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({
         dispatcher: dispatcher
       }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.shared.views.chat.TextChatView, props));
     }
 
     beforeEach(function() {
       store.setStoreState({ textChatEnabled: true });
     });
 
+    it("should show timestamps from msgs sent more than 1 min apart", function() {
+      view = mountTestComponent();
+
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Hello!",
+        sentTimestamp: "1970-01-01T00:02:00.000Z",
+        receivedTimestamp: "1970-01-01T00:02:00.000Z"
+      });
+
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Is it me you're looking for?",
+        sentTimestamp: "1970-01-01T00:03:00.000Z",
+        receivedTimestamp: "1970-01-01T00:03:00.000Z"
+      });
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Is it me you're looking for?",
+        sentTimestamp: "1970-01-01T00:02:00.000Z",
+        receivedTimestamp: "1970-01-01T00:02:00.000Z"
+      });
+
+      var node = view.getDOMNode();
+
+      expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
+          .to.eql(2);
+    });
+
     it("should not display anything if no messages and text chat not enabled and showAlways is false", function() {
       store.setStoreState({ textChatEnabled: false });
 
       view = mountTestComponent({
         showAlways: false
       });
 
       expect(view.getDOMNode()).eql(null);
@@ -167,16 +320,62 @@ describe("loop.shared.views.TextChatView
       view = mountTestComponent();
 
       var node = view.getDOMNode();
 
       expect(node.querySelector(".text-chat-box")).not.eql(null);
       expect(node.querySelector(".text-chat-entries")).eql(null);
     });
 
+    it("should render message entries when message were sent/ received", function() {
+      view = mountTestComponent();
+
+      store.receivedTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Hello!"
+      });
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Is it me you're looking for?"
+      });
+
+      var node = view.getDOMNode();
+      expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
+
+      var entries = node.querySelectorAll(".text-chat-entry");
+      expect(entries.length).to.eql(2);
+      expect(entries[0].classList.contains("received")).to.eql(true);
+      expect(entries[1].classList.contains("received")).to.not.eql(true);
+    });
+
+    it("should add `sent` CSS class selector to msg of type SENT", function() {
+      var node = mountTestComponent().getDOMNode();
+
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Foo",
+        timestamp: 0
+      });
+
+      expect(node.querySelector(".sent")).to.not.eql(null);
+    });
+
+    it("should add `received` CSS class selector to msg of type RECEIVED",
+       function() {
+         var node = mountTestComponent().getDOMNode();
+
+         store.receivedTextChatMessage({
+           contentType: CHAT_CONTENT_TYPES.TEXT,
+           message: "Foo",
+           timestamp: 0
+         });
+
+         expect(node.querySelector(".received")).to.not.eql(null);
+     });
+
     it("should render a room name special entry", function() {
       view = mountTestComponent({
         showRoomName: true
       });
 
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "A wonderful surprise!",
         roomOwner: "Chris",
@@ -228,17 +427,18 @@ describe("loop.shared.views.TextChatView
         key: "Enter",
         which: 13
       });
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWithExactly(dispatcher.dispatch,
         new sharedActions.SendTextChatMessage({
           contentType: CHAT_CONTENT_TYPES.TEXT,
-          message: "Hello!"
+          message: "Hello!",
+          sentTimestamp: "1970-01-01T00:00:00.000Z"
         }));
     });
 
     it("should not dispatch SendTextChatMessage when the message is empty", function() {
       view = mountTestComponent();
 
       var entryNode = view.getDOMNode().querySelector(".text-chat-box > form > input");
 
--- a/browser/components/loop/ui/fake-l10n.js
+++ b/browser/components/loop/ui/fake-l10n.js
@@ -17,10 +17,15 @@ navigator.mozL10n = document.mozL10n = {
 
     // upcase the first letter
     var readableStringId = stringId.replace(/^./, function(match) {
       "use strict";
       return match.toUpperCase();
     }).replace(/_/g, " ");  // and convert _ chars to spaces
 
     return "" + readableStringId + (vars ? ";" + JSON.stringify(vars) : "");
+  },
+
+  /* For timestamp formatting reasons. */
+  language: {
+    code: "en-US"
   }
 };
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -82,17 +82,17 @@
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var mockSDK = _.extend({
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
-        message: message
+        message: message.message
       }));
     }
   }, Backbone.Events);
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
    * an active store, they'd interfere with each other.
    *
@@ -304,40 +304,56 @@
       // use the fallback thumbnail
     }]
   }));
 
   textChatStore.setStoreState({textChatEnabled: true});
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Rheet!"
+    message: "Rheet!",
+    sentTimestamp: "2015-06-23T22:21:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Hi there"
+    message: "Hi there",
+    receivedTimestamp: "2015-06-23T22:21:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Hello",
+    receivedTimestamp: "2015-06-23T23:24:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Check out this menu from DNA Pizza:" +
     " http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
-    "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%"
+    "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
-    "linewrappingissuesifthecssiswrong"
+    "linewrappingissuesifthecssiswrong",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "That avocado monkey-brains pie sounds tasty!"
+    message: "That avocado monkey-brains pie sounds tasty!",
+    receivedTimestamp: "2015-06-23T22:25:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "What time should we meet?"
+    message: "What time should we meet?",
+    sentTimestamp: "2015-06-23T22:27:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Cool",
+    sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
     feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -82,17 +82,17 @@
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var mockSDK = _.extend({
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
-        message: message
+        message: message.message
       }));
     }
   }, Backbone.Events);
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
    * an active store, they'd interfere with each other.
    *
@@ -304,40 +304,56 @@
       // use the fallback thumbnail
     }]
   }));
 
   textChatStore.setStoreState({textChatEnabled: true});
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Rheet!"
+    message: "Rheet!",
+    sentTimestamp: "2015-06-23T22:21:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Hi there"
+    message: "Hi there",
+    receivedTimestamp: "2015-06-23T22:21:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Hello",
+    receivedTimestamp: "2015-06-23T23:24:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Check out this menu from DNA Pizza:" +
     " http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
-    "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%"
+    "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
-    "linewrappingissuesifthecssiswrong"
+    "linewrappingissuesifthecssiswrong",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "That avocado monkey-brains pie sounds tasty!"
+    message: "That avocado monkey-brains pie sounds tasty!",
+    receivedTimestamp: "2015-06-23T22:25:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "What time should we meet?"
+    message: "What time should we meet?",
+    sentTimestamp: "2015-06-23T22:27:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Cool",
+    sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
     feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -1,12 +1,14 @@
 /* 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/. */
 
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
 var gPrivacyPane = {
 
   /**
    * Whether the use has selected the auto-start private browsing mode in the UI.
    */
   _autoStartPrivateBrowsing: false,
 
   /**
@@ -98,16 +100,18 @@ var gPrivacyPane = {
     setEventListener("privateBrowsingAutoStart", "command",
                      gPrivacyPane.updateAutostart);
     setEventListener("cookieExceptions", "command",
                      gPrivacyPane.showCookieExceptions);
     setEventListener("showCookiesButton", "command",
                      gPrivacyPane.showCookies);
     setEventListener("clearDataSettings", "command",
                      gPrivacyPane.showClearPrivateDataSettings);
+
+    document.getElementById("searchesSuggestion").hidden = !AppConstants.NIGHTLY_BUILD;
   },
 
   // HISTORY MODE
 
   /**
    * The list of preferences which affect the initial history mode settings.
    * If the auto start private browsing mode pref is active, the initial
    * history mode would be set to "Don't remember anything".
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -253,12 +253,13 @@
             onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
             accesskey="&locbar.bookmarks.accesskey;"
             preference="browser.urlbar.suggest.bookmark"/>
   <checkbox id="openpageSuggestion" label="&locbar.openpage.label;"
             onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
             accesskey="&locbar.openpage.accesskey;"
             preference="browser.urlbar.suggest.openpage"/>
   <checkbox id="searchesSuggestion" label="&locbar.searches.label;"
+            hidden="true"
             onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
             accesskey="&locbar.searches.accesskey;"
             preference="browser.urlbar.suggest.searches"/>
 </groupbox>
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -124,16 +124,35 @@ let gSyncPane = {
       return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
     }),
 
     this.updateWeavePrefs();
 
     this._initProfileImageUI();
   },
 
+  _toggleComputerNameControls: function(editMode) {
+    let textbox = document.getElementById("fxaSyncComputerName");
+    textbox.className = editMode ? "" : "plain";
+    textbox.disabled = !editMode;
+    document.getElementById("fxaChangeDeviceName").hidden = editMode;
+    document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode;
+    document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode;
+  },
+
+  _updateComputerNameValue: function(save) {
+    let textbox = document.getElementById("fxaSyncComputerName");
+    if (save) {
+      Weave.Service.clientsEngine.localName = textbox.value;
+    }
+    else {
+      textbox.value = Weave.Service.clientsEngine.localName;
+    }
+  },
+
   _setupEventListeners: function() {
     function setEventListener(aId, aEventType, aCallback)
     {
       document.getElementById(aId)
               .addEventListener(aEventType, aCallback.bind(gSyncPane));
     }
 
     setEventListener("noAccountSetup", "click", function (aEvent) {
@@ -156,16 +175,27 @@ let gSyncPane = {
     });
     setEventListener("syncEnginesList", "select", function () {
       if (this.selectedCount)
         this.clearSelection();
     });
     setEventListener("syncComputerName", "change", function (e) {
       gSyncUtils.changeName(e.target);
     });
+    setEventListener("fxaChangeDeviceName", "click", function () {
+      this._toggleComputerNameControls(true);
+    });
+    setEventListener("fxaCancelChangeDeviceName", "click", function () {
+      this._toggleComputerNameControls(false);
+      this._updateComputerNameValue(false);
+    });
+    setEventListener("fxaSaveChangeDeviceName", "click", function () {
+      this._toggleComputerNameControls(false);
+      this._updateComputerNameValue(true);
+    });
     setEventListener("unlinkDevice", "click", function () {
       gSyncPane.startOver(true);
       return false;
     });
     setEventListener("tosPP-normal-ToS", "click", gSyncPane.openToS);
     setEventListener("tosPP-normal-PP", "click", gSyncPane.openPrivacyPolicy);
     setEventListener("loginErrorUpdatePass", "click", function () {
       gSyncPane.updatePass();
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -309,24 +309,40 @@
                     preference="engine.addons"/>
           <checkbox label="&engine.prefs.label;"
                     accesskey="&engine.prefs.accesskey;"
                     preference="engine.prefs"/>
         </vbox>
         <spacer/>
       </hbox>
     </groupbox>
-    <hbox align="center">
-      <label accesskey="&syncDeviceName.accesskey;"
-             control="syncComputerName">
-        &syncDeviceName.label;
-      </label>
-      <textbox id="fxaSyncComputerName"
-                flex="1"/>
-    </hbox>
+    <vbox>
+      <caption>
+        <label accesskey="&syncDeviceName.accesskey;"
+               control="fxaSyncComputerName">
+          &fxaSyncDeviceName.label;
+        </label>
+      </caption>
+      <hbox id="fxaDeviceName">
+        <hbox flex="1">
+          <textbox id="fxaSyncComputerName" class="plain"
+                   disabled="true" flex="1"/>
+        </hbox>
+        <hbox>
+          <button id="fxaChangeDeviceName"
+                  label="&changeSyncDeviceName.label;"/>
+          <button id="fxaCancelChangeDeviceName"
+                  label="&cancelChangeSyncDeviceName.label;"
+                  hidden="true"/>
+          <button id="fxaSaveChangeDeviceName"
+                  label="&saveChangeSyncDeviceName.label;"
+                  hidden="true"/>
+        </hbox>
+      </hbox>
+    </vbox>
     <spacer flex="1"/>
     <vbox id="tosPP-small">
       <label id="tosPP-small-ToS" class="text-link">
         &prefs.tosLink.label;
       </label>
       <label id="tosPP-small-PP" class="text-link">
         &fxaPrivacyNotice.link.label;
       </label>
--- a/browser/components/preferences/in-content/tests/browser_privacypane_1.js
+++ b/browser/components/preferences/in-content/tests/browser_privacypane_1.js
@@ -14,13 +14,10 @@ function test() {
   loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
 
   run_test_subset([
     test_pane_visibility,
     test_dependent_elements,
     test_dependent_cookie_elements,
     test_dependent_clearonclose_elements,
     test_dependent_prefs,
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]);
 }
--- a/browser/components/preferences/in-content/tests/browser_privacypane_3.js
+++ b/browser/components/preferences/in-content/tests/browser_privacypane_3.js
@@ -13,13 +13,10 @@ function test() {
   loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
 
   run_test_subset([
     test_custom_retention("rememberHistory", "remember"),
     test_custom_retention("rememberHistory", "custom"),
     test_custom_retention("rememberForms", "remember"),
     test_custom_retention("rememberForms", "custom"),
     test_historymode_retention("remember", "remember"),
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]);
 }
--- a/browser/components/preferences/in-content/tests/browser_privacypane_4.js
+++ b/browser/components/preferences/in-content/tests/browser_privacypane_4.js
@@ -22,13 +22,10 @@ function test() {
     test_custom_retention("acceptThirdPartyMenu", "custom", "always")
     ], [
     test_custom_retention("keepCookiesUntil", "remember", 1),
     test_custom_retention("keepCookiesUntil", "custom", 2),
     test_custom_retention("keepCookiesUntil", "custom", 0),
     test_custom_retention("alwaysClear", "remember"),
     test_custom_retention("alwaysClear", "custom"),
     test_historymode_retention("remember", "remember"),
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]));
 }
--- a/browser/components/preferences/in-content/tests/browser_privacypane_5.js
+++ b/browser/components/preferences/in-content/tests/browser_privacypane_5.js
@@ -7,20 +7,21 @@ function test() {
   let rootDir = getRootDirectory(gTestPath);
   let jar = getJar(rootDir);
   if (jar) {
     let tmpdir = extractJarToTmp(jar);
     rootDir = "file://" + tmpdir.path + '/';
   }
   loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
 
-  run_test_subset([
+  let tests = [
     test_locbar_suggestion_retention("history", true),
     test_locbar_suggestion_retention("bookmark", true),
-    test_locbar_suggestion_retention("searches", true),
     test_locbar_suggestion_retention("openpage", false),
     test_locbar_suggestion_retention("history", true),
     test_locbar_suggestion_retention("history", false),
+  ];
 
-    // reset all preferences to their default values once we're done
-    reset_preferences
-  ]);
+  if (AppConstants.NIGHTLY_BUILD)
+    tests.push(test_locbar_suggestion_retention("searches", true)),
+
+  run_test_subset(tests);
 }
--- a/browser/components/preferences/in-content/tests/browser_privacypane_8.js
+++ b/browser/components/preferences/in-content/tests/browser_privacypane_8.js
@@ -22,13 +22,10 @@ function test() {
     // history mode should now be custom; set history mode to dontremember
     test_historymode_retention("dontremember", "custom"),
 
     // history mode should remain custom; set history mode to remember
     test_historymode_retention("remember", "custom"),
 
     // history mode should now be remember
     test_historymode_retention("remember", "remember"),
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]);
 }
--- a/browser/components/preferences/in-content/tests/privacypane_tests_perwindow.js
+++ b/browser/components/preferences/in-content/tests/privacypane_tests_perwindow.js
@@ -310,36 +310,43 @@ function test_locbar_suggestion_retentio
     ok(elem, "Suggest " + suggestion + " checkbox should exist.");
     elem.click();
 
     is(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), autocomplete,
        "browser.urlbar.autocomplete.enabled pref should be " + autocomplete);
   };
 }
 
+const gPrefCache = new Map();
+
+function cache_preferences(win) {
+  let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
+  for (let pref of prefs)
+    gPrefCache.set(pref.name, pref.value);
+}
+
 function reset_preferences(win) {
   let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
-  for (let i = 0; i < prefs.length; ++i)
-    if (prefs[i].hasUserValue)
-      prefs[i].reset();
+  for (let pref of prefs)
+    pref.value = gPrefCache.get(pref.name);
 }
 
 let testRunner;
 function run_test_subset(subset) {
   Services.prefs.setBoolPref("browser.preferences.instantApply", true);
   dump("subset: " + [x.name for (x of subset)].join(",") + "\n");
 
   waitForExplicitFinish();
   registerCleanupFunction(function() {
     // Reset pref to its default
     Services.prefs.clearUserPref("browser.preferences.instantApply");
   });
 
   testRunner = {
-    tests: subset,
+    tests: [cache_preferences, ...subset, reset_preferences],
     counter: 0,
     runNext: function() {
       if (this.counter == this.tests.length) {
         finish();
       } else {
         let self = this;
         setTimeout(function() {
           runTestOnPrivacyPrefPane(self.tests[self.counter++]);
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -1,14 +1,15 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
 
 var gPrivacyPane = {
 
   /**
    * Whether the use has selected the auto-start private browsing mode in the UI.
    */
   _autoStartPrivateBrowsing: false,
 
@@ -64,16 +65,18 @@ var gPrivacyPane = {
     this.initializeHistoryMode();
     this.updateHistoryModePane();
     this.updatePrivacyMicroControls();
     this.initAutoStartPrivateBrowsingReverter();
 #ifdef NIGHTLY_BUILD
     this._initTrackingProtection();
 #endif
     this._initAutocomplete();
+
+    document.getElementById("searchesSuggestion").hidden = !AppConstants.NIGHTLY_BUILD;
   },
 
   // HISTORY MODE
 
   /**
    * The list of preferences which affect the initial history mode settings.
    * If the auto start private browsing mode pref is active, the initial
    * history mode would be set to "Don't remember anything".
--- a/browser/components/preferences/privacy.xul
+++ b/browser/components/preferences/privacy.xul
@@ -279,16 +279,17 @@
                   onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
                   accesskey="&locbar.bookmarks.accesskey;"
                   preference="browser.urlbar.suggest.bookmark"/>
         <checkbox id="openpageSuggestion" label="&locbar.openpage.label;"
                   onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
                   accesskey="&locbar.openpage.accesskey;"
                   preference="browser.urlbar.suggest.openpage"/>
         <checkbox id="searchesSuggestion" label="&locbar.searches.label;"
+                  hidden="true"
                   onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
                   accesskey="&locbar.searches.accesskey;"
                   preference="browser.urlbar.suggest.searches"/>
       </vbox>
     </groupbox>
 
   </prefpane>
 
--- a/browser/components/preferences/tests/browser_privacypane_1.js
+++ b/browser/components/preferences/tests/browser_privacypane_1.js
@@ -15,13 +15,10 @@ function test() {
   loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
 
   run_test_subset([
     test_pane_visibility,
     test_dependent_elements,
     test_dependent_cookie_elements,
     test_dependent_clearonclose_elements,
     test_dependent_prefs,
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]);
 }
--- a/browser/components/preferences/tests/browser_privacypane_3.js
+++ b/browser/components/preferences/tests/browser_privacypane_3.js
@@ -14,13 +14,10 @@ function test() {
   loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
 
   run_test_subset([
     test_custom_retention("rememberHistory", "remember"),
     test_custom_retention("rememberHistory", "custom"),
     test_custom_retention("rememberForms", "remember"),
     test_custom_retention("rememberForms", "custom"),
     test_historymode_retention("remember", "remember"),
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]);
 }
--- a/browser/components/preferences/tests/browser_privacypane_4.js
+++ b/browser/components/preferences/tests/browser_privacypane_4.js
@@ -23,13 +23,10 @@ function test() {
     test_custom_retention("acceptThirdPartyMenu", "custom", "always")
     ], [
     test_custom_retention("keepCookiesUntil", "remember", 1),
     test_custom_retention("keepCookiesUntil", "custom", 2),
     test_custom_retention("keepCookiesUntil", "custom", 0),
     test_custom_retention("alwaysClear", "remember"),
     test_custom_retention("alwaysClear", "custom"),
     test_historymode_retention("remember", "remember"),
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]));
 }
--- a/browser/components/preferences/tests/browser_privacypane_5.js
+++ b/browser/components/preferences/tests/browser_privacypane_5.js
@@ -7,20 +7,21 @@ function test() {
   let rootDir = getRootDirectory(gTestPath);
   let jar = getJar(rootDir);
   if (jar) {
     let tmpdir = extractJarToTmp(jar);
     rootDir = "file://" + tmpdir.path + '/';
   }
   loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
 
-  run_test_subset([
+  let tests = [
     test_locbar_suggestion_retention("history", true),
     test_locbar_suggestion_retention("bookmark", true),
-    test_locbar_suggestion_retention("searches", true),
     test_locbar_suggestion_retention("openpage", false),
     test_locbar_suggestion_retention("history", true),
     test_locbar_suggestion_retention("history", false),
+  ];
 
-    // reset all preferences to their default values once we're done
-    reset_preferences
-  ]);
+  if (AppConstants.NIGHTLY_BUILD)
+    tests.push(test_locbar_suggestion_retention("searches", true)),
+
+  run_test_subset(tests);
 }
--- a/browser/components/preferences/tests/browser_privacypane_8.js
+++ b/browser/components/preferences/tests/browser_privacypane_8.js
@@ -23,13 +23,10 @@ function test() {
     // history mode should now be custom; set history mode to dontremember
     test_historymode_retention("dontremember", "custom"),
 
     // history mode should remain custom; set history mode to remember
     test_historymode_retention("remember", "custom"),
 
     // history mode should now be remember
     test_historymode_retention("remember", "remember"),
-
-    // reset all preferences to their default values once we're done
-    reset_preferences
   ]);
 }
--- a/browser/components/preferences/tests/privacypane_tests_perwindow.js
+++ b/browser/components/preferences/tests/privacypane_tests_perwindow.js
@@ -316,32 +316,39 @@ function test_locbar_suggestion_retentio
     ok(elem, "Suggest " + suggestion + " checkbox should exist.");
     elem.click();
 
     is(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), autocomplete,
        "browser.urlbar.autocomplete.enabled pref should be " + autocomplete);
   };
 }
 
+const gPrefCache = new Map();
+
+function cache_preferences(win) {
+  let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
+  for (let pref of prefs)
+    gPrefCache.set(pref.name, pref.value);
+}
+
 function reset_preferences(win) {
-  let prefs = win.document.getElementsByTagName("preference");
-  for (let i = 0; i < prefs.length; ++i)
-    if (prefs[i].hasUserValue)
-      prefs[i].reset();
+  let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
+  for (let pref of prefs)
+    pref.value = gPrefCache.get(pref.name);
 }
 
 let testRunner;
 function run_test_subset(subset) {
   let instantApplyOrig = Services.prefs.getBoolPref("browser.preferences.instantApply");
   Services.prefs.setBoolPref("browser.preferences.instantApply", true);
 
   waitForExplicitFinish();
 
   testRunner = {
-    tests: subset,
+    tests: [cache_preferences, ...subset, reset_preferences],
     counter: 0,
     runNext: function() {
       if (this.counter == this.tests.length) {
         // cleanup
         Services.prefs.setBoolPref("browser.preferences.instantApply", instantApplyOrig);
         finish();
       } else {
         let self = this;
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -82,16 +82,17 @@ support-files =
   doc_minified_bogus_map.html
   doc_native-event-handler.html
   doc_no-page-sources.html
   doc_pause-exceptions.html
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_pretty-print-3.html
   doc_pretty-print-on-paused.html
+  doc_promise-get-allocation-stack.html
   doc_promise.html
   doc_random-javascript.html
   doc_recursion-stack.html
   doc_same-line-functions.html
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_scope-variable-4.html
@@ -341,17 +342,21 @@ skip-if = e10s && debug
 skip-if = e10s && debug
 [browser_dbg_pretty-print-12.js]
 skip-if = e10s && debug
 [browser_dbg_pretty-print-13.js]
 skip-if = e10s && debug
 [browser_dbg_pretty-print-on-paused.js]
 skip-if = e10s && debug
 [browser_dbg_progress-listener-bug.js]
-skip-if = e10a && debug
+skip-if = e10s && debug
+[browser_dbg_promises-allocation-stack.js]
+skip-if = e10s && debug
+[browser_dbg_promises-chrome-allocation-stack.js]
+skip-if = e10s && debug
 [browser_dbg_reload-preferred-script-01.js]
 skip-if = e10s && debug
 [browser_dbg_reload-preferred-script-02.js]
 skip-if = e10s && debug
 [browser_dbg_reload-preferred-script-03.js]
 skip-if = e10s && debug
 [browser_dbg_reload-same-script.js]
 skip-if = e10s && debug
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_promises-allocation-stack.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can get a stack to a promise's allocation point.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_promise-get-allocation-stack.html";
+const { PromisesFront } = devtools.require("devtools/server/actors/promises");
+let events = devtools.require("sdk/event/core");
+
+function test() {
+  Task.spawn(function* () {
+    DebuggerServer.init();
+    DebuggerServer.addBrowserActors();
+
+    const [ tab,, panel ] = yield initDebugger(TAB_URL);
+
+    let client = new DebuggerClient(DebuggerServer.connectPipe());
+    yield connect(client);
+
+    let { tabs } = yield listTabs(client);
+    let targetTab = findTab(tabs, TAB_URL);
+    yield attachTab(client, targetTab);
+
+    yield testGetAllocationStack(client, targetTab, tab);
+
+    yield close(client);
+    yield closeDebuggerAndFinish(panel);
+  }).then(null, error => {
+    ok(false, "Got an error: " + error.message + "\n" + error.stack);
+  });
+}
+
+function* testGetAllocationStack(client, form, tab) {
+  let front = PromisesFront(client, form);
+
+  yield front.attach();
+  yield front.listPromises();
+
+  // Get the grip for promise p
+  let onNewPromise = new Promise(resolve => {
+    events.on(front, "new-promises", promises => {
+      for (let p of promises) {
+        if (p.preview.ownProperties.name &&
+            p.preview.ownProperties.name.value === "p") {
+          resolve(p);
+        }
+      }
+    });
+  });
+
+  callInTab(tab, "makePromises");
+
+  let grip = yield onNewPromise;
+  ok(grip, "Found our promise p");
+
+  let objectClient = new ObjectClient(client, grip);
+  ok(objectClient, "Got Object Client");
+
+  yield new Promise(resolve => {
+    objectClient.getPromiseAllocationStack(response => {
+      ok(response.allocationStack.length, "Got promise allocation stack.");
+
+      for (let stack of response.allocationStack) {
+        is(stack.source.url, TAB_URL, "Got correct source URL.");
+        is(stack.functionDisplayName, "makePromises",
+           "Got correct function display name.");
+        is(typeof stack.line, "number", "Expect stack line to be a number.");
+        is(typeof stack.column, "number",
+           "Expect stack column to be a number.");
+      }
+
+      resolve();
+    });
+  });
+
+  yield front.detach();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_promises-chrome-allocation-stack.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can get a stack to a promise's allocation point in the chrome
+ * process.
+ */
+
+"use strict";
+
+const SOURCE_URL = "browser_dbg_promises-chrome-allocation-stack.js";
+const { PromisesFront } = devtools.require("devtools/server/actors/promises");
+let events = devtools.require("sdk/event/core");
+
+const STACK_DATA = [
+  { functionDisplayName: "test/</<" },
+  { functionDisplayName: "testGetAllocationStack" },
+];
+
+function test() {
+  Task.spawn(function* () {
+    requestLongerTimeout(10);
+
+    DebuggerServer.init();
+    DebuggerServer.addBrowserActors();
+    DebuggerServer.allowChromeProcess = true;
+
+    let client = new DebuggerClient(DebuggerServer.connectPipe());
+    yield connect(client);
+    let chrome = yield client.getProcess();
+    let [, tabClient] = yield attachTab(client, chrome.form);
+    yield tabClient.attachThread();
+
+    yield testGetAllocationStack(client, chrome.form, () => {
+      let p = new Promise(() => {});
+      p.name = "p";
+      let q = p.then();
+      q.name = "q";
+      let r = p.then(null, () => {});
+      r.name = "r";
+    });
+
+    yield close(client);
+    finish();
+  }).then(null, error => {
+    ok(false, "Got an error: " + error.message + "\n" + error.stack);
+  });
+}
+
+function* testGetAllocationStack(client, form, makePromises) {
+  let front = PromisesFront(client, form);
+
+  yield front.attach();
+  yield front.listPromises();
+
+  // Get the grip for promise p
+  let onNewPromise = new Promise(resolve => {
+    events.on(front, "new-promises", promises => {
+      for (let p of promises) {
+        if (p.preview.ownProperties.name &&
+            p.preview.ownProperties.name.value === "p") {
+          resolve(p);
+        }
+      }
+    });
+  });
+
+  makePromises();
+
+  let grip = yield onNewPromise;
+  ok(grip, "Found our promise p");
+
+  let objectClient = new ObjectClient(client, grip);
+  ok(objectClient, "Got Object Client");
+
+  yield new Promise(resolve => {
+    objectClient.getPromiseAllocationStack(response => {
+      ok(response.allocationStack.length, "Got promise allocation stack.");
+
+      for (let i = 0; i < STACK_DATA.length; i++) {
+        let data = STACK_DATA[i];
+        let stack = response.allocationStack[i];
+
+        ok(stack.source.url.startsWith("chrome:"), "Got a chrome source URL");
+        ok(stack.source.url.endsWith(SOURCE_URL), "Got correct source URL.");
+        is(stack.functionDisplayName, data.functionDisplayName,
+           "Got correct function display name.");
+        is(typeof stack.line, "number", "Expect stack line to be a number.");
+        is(typeof stack.column, "number",
+           "Expect stack column to be a number.");
+      }
+
+      resolve();
+    });
+  });
+
+  yield front.detach();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_promise-get-allocation-stack.html
@@ -0,0 +1,24 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Promise test page</title>
+  </head>
+
+  <body>
+    <script type="text/javascript">
+      function makePromises() {
+        var p = new Promise(() => {});
+        p.name = "p";
+        var q = p.then();
+        q.name = "q";
+        var r = p.then(null, () => {});
+        r.name = "r";
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -15,17 +15,18 @@ Services.prefs.setBoolPref("devtools.deb
 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 let { Promise: promise } = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { require } = devtools;
 let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
 let { BrowserToolboxProcess } = Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", {});
 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
-let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
+let { DebuggerClient, ObjectClient } =
+  Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
 let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 let EventEmitter = require("devtools/toolkit/event-emitter");
 const { promiseInvoke } = require("devtools/async-utils");
 let TargetFactory = devtools.TargetFactory;
 let Toolbox = devtools.Toolbox;
 
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/";
 const FRAME_SCRIPT_URL = getRootDirectory(gTestPath) + "code_frame-script.js";
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -29,16 +29,17 @@ support-files =
 [browser_target_support.js]
 [browser_two_tabs.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_getpanelwhenready.js]
 [browser_toolbox_highlight.js]
 [browser_toolbox_hosts.js]
 [browser_toolbox_hosts_size.js]
 [browser_toolbox_minimize.js]
+skip-if = true # Bug 1177463 - Temporarily hide the minimize button
 [browser_toolbox_options.js]
 [browser_toolbox_options_disable_buttons.js]
 [browser_toolbox_options_disable_cache-01.js]
 skip-if = e10s # Bug 1030318
 [browser_toolbox_options_disable_cache-02.js]
 skip-if = e10s # Bug 1030318
 [browser_toolbox_options_disable_js.js]
 skip-if = e10s # Bug 1030318
--- a/browser/devtools/markupview/test/helper_events_test_runner.js
+++ b/browser/devtools/markupview/test/helper_events_test_runner.js
@@ -10,18 +10,16 @@ function* runEventPopupTests() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
 
   yield inspector.markup.expandAll();
 
   for (let {selector, expected} of TEST_DATA) {
     yield checkEventsForNode(selector, expected, inspector);
   }
 
-  gBrowser.removeCurrentTab();
-
   // Wait for promises to avoid leaks when running this as a single test.
   // We need to do this because we have opened a bunch of popups and don't them
   // to affect other test runs when they are GCd.
   yield promiseNextTick();
 }
 
 /**
  * Generator function that takes a selector and expected results and returns
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -30,17 +30,16 @@ const REQUESTS_WATERFALL_SAFE_BOUNDS = 9
 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
-const DEFAULT_HTTP_VERSION = "HTTP/1.1";
 const REQUEST_TIME_DECIMALS = 2;
 const HEADERS_SIZE_DECIMALS = 3;
 const CONTENT_SIZE_DECIMALS = 2;
 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
   "ecmascript": "js",
   "javascript": "js",
   "x-javascript": "js"
 };
@@ -2636,17 +2635,17 @@ NetworkDetailsView.prototype = {
     if (aData.status) {
       $("#headers-summary-status-circle").setAttribute("code", aData.fromCache ? "cached" : aData.status);
       $("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText);
       $("#headers-summary-status").removeAttribute("hidden");
     } else {
       $("#headers-summary-status").setAttribute("hidden", "true");
     }
 
-    if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
+    if (aData.httpVersion) {
       $("#headers-summary-version-value").setAttribute("value", aData.httpVersion);
       $("#headers-summary-version").removeAttribute("hidden");
     } else {
       $("#headers-summary-version").setAttribute("hidden", "true");
     }
   },
 
   /**
--- a/browser/devtools/shared/inplace-editor.js
+++ b/browser/devtools/shared/inplace-editor.js
@@ -229,20 +229,21 @@ function InplaceEditor(aOptions, aEvent)
     this._advanceChars = aCharCode => aCharCode in advanceCharcodes;
   }
 
   // Hide the provided element and add our editor.
   this.originalDisplay = this.elt.style.display;
   this.elt.style.display = "none";
   this.elt.parentNode.insertBefore(this.input, this.elt);
 
+  this.input.focus();
+
   if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
     this.input.select();
   }
-  this.input.focus();
 
   if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") {
     this._maybeSuggestCompletion(true);
   }
 
   this.input.addEventListener("blur", this._onBlur, false);
   this.input.addEventListener("keypress", this._onKeyPress, false);
   this.input.addEventListener("input", this._onInput, false);
@@ -954,17 +955,18 @@ InplaceEditor.prototype = {
         if (this.stopOnShiftTab) {
           direction = null;
         } else {
           direction = FOCUS_BACKWARD;
         }
       }
       if ((this.stopOnReturn &&
            aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) ||
-          (this.stopOnTab && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB)) {
+          (this.stopOnTab && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
+           !aEvent.shiftKey)) {
         direction = null;
       }
 
       // Now we don't want to suggest anything as we are moving out.
       this._preventSuggestions = true;
       // But we still want to show suggestions for css values. i.e. moving out
       // of css property input box in forward direction
       if (this.contentType == CONTENT_TYPES.CSS_PROPERTY &&
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -2705,31 +2705,29 @@ RuleEditor.prototype = {
         title: CssLogic.l10n("rule.selectorHighlighter.tooltip")
       });
       selectorHighlighter.addEventListener("click", () => {
         this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector);
       });
     }
 
     this.selectorText = createChild(this.selectorContainer, "span", {
-      class: "ruleview-selector theme-fg-color3"
+      class: "ruleview-selector theme-fg-color3",
+      tabindex: this.isSelectorEditable ? "0" : "-1",
     });
 
     if (this.isSelectorEditable) {
       this.selectorContainer.addEventListener("click", aEvent => {
         // Clicks within the selector shouldn't propagate any further.
         aEvent.stopPropagation();
       }, false);
 
       editableField({
         element: this.selectorText,
         done: this._onSelectorDone,
-        stopOnShiftTab: true,
-        stopOnTab: true,
-        stopOnReturn: true
       });
     }
 
     this.openBrace = createChild(header, "span", {
       class: "ruleview-ruleopen",
       textContent: " {"
     });
 
@@ -2996,36 +2994,38 @@ RuleEditor.prototype = {
     }
   },
 
   /**
    * Called when the selector's inplace editor is closed.
    * Ignores the change if the user pressed escape, otherwise
    * commits it.
    *
-   * @param {string} aValue
+   * @param {string} value
    *        The value contained in the editor.
-   * @param {boolean} aCommit
+   * @param {boolean} commit
    *        True if the change should be applied.
+   * @param {number} direction
+   *        The move focus direction number.
    */
-  _onSelectorDone: function(aValue, aCommit) {
-    if (!aCommit || this.isEditing || aValue === "" ||
-        aValue === this.rule.selectorText) {
+  _onSelectorDone: function(value, commit, direction) {
+    if (!commit || this.isEditing || value === "" ||
+        value === this.rule.selectorText) {
       return;
     }
 
     let ruleView = this.ruleView;
     let elementStyle = ruleView._elementStyle;
     let element = elementStyle.element;
     let supportsUnmatchedRules =
       this.rule.domRule.supportsModifySelectorUnmatched;
 
     this.isEditing = true;
 
-    this.rule.domRule.modifySelector(element, aValue).then(response => {
+    this.rule.domRule.modifySelector(element, value).then(response => {
       this.isEditing = false;
 
       if (!supportsUnmatchedRules) {
         if (response) {
           this.ruleView.refreshPanel();
         }
         return;
       }
@@ -3047,20 +3047,44 @@ RuleEditor.prototype = {
       this.element.parentNode.replaceChild(editor.element, this.element);
 
       // Remove highlight for modified selector
       if (ruleView.highlightedSelector &&
           ruleView.highlightedSelector == this.rule.selectorText) {
         ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
           ruleView.highlightedSelector);
       }
+
+      this._moveSelectorFocus(newRule, direction);
     }).then(null, err => {
       this.isEditing = false;
       promiseWarn(err);
     });
+  },
+
+  /**
+   * Handle moving the focus change after pressing tab and return from the
+   * selector inplace editor. The focused element after a tab or return keypress
+   * is lost because the rule editor is replaced.
+   *
+   * @param {Rule} rule
+   *        The Rule object.
+   * @param {number} direction
+   *        The move focus direction number.
+   */
+  _moveSelectorFocus: function(rule, direction) {
+    if (!direction || direction == Ci.nsIFocusManager.MOVEFOCUS_BACKWARD) {
+      return;
+    }
+
+    if (rule.textProps.length > 0) {
+      rule.textProps[0].editor.nameSpan.click();
+    } else {
+      this.propertyList.click();
+    }
   }
 };
 
 /**
  * Create a TextPropertyEditor.
  *
  * @param {RuleEditor} aRuleEditor
  *        The rule editor that owns this TextPropertyEditor.
--- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js
@@ -2,70 +2,79 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test selector value is correctly displayed when committing the inplace editor
 // with ENTER, ESC, SHIFT+TAB and TAB
 
-let PAGE_CONTENT = [
-  '<style type="text/css">',
-  '  #testid {',
-  '    text-align: center;',
-  '  }',
-  '</style>',
-  '<div id="testid" class="testclass">Styled Node</div>',
+let TEST_URI = [
+  "<style type='text/css'>",
+  "  #testid1 {",
+  "    text-align: center;",
+  "  }",
+  "  #testid2 {",
+  "    text-align: center;",
+  "  }",
+  "  #testid3 {",
+  "  }",
+  "</style>",
+  "<div id='testid1'>Styled Node</div>",
+  "<div id='testid2'>Styled Node</div>",
+  "<div id='testid3'>Styled Node</div>",
 ].join("\n");
 
 const TEST_DATA = [
   {
-    node: "#testid",
+    node: "#testid1",
     value: ".testclass",
     commitKey: "VK_ESCAPE",
     modifiers: {},
-    expected: "#testid"
+    expected: "#testid1",
+
   },
   {
-    node: "#testid",
-    value: ".testclass",
+    node: "#testid1",
+    value: ".testclass1",
     commitKey: "VK_RETURN",
     modifiers: {},
-    expected: ".testclass"
+    expected: ".testclass1"
   },
   {
-    node: "#testid",
-    value: ".testclass",
+    node: "#testid2",
+    value: ".testclass2",
     commitKey: "VK_TAB",
     modifiers: {},
-    expected: ".testclass"
+    expected: ".testclass2"
   },
   {
-    node: "#testid",
-    value: ".testclass",
+    node: "#testid3",
+    value: ".testclass3",
     commitKey: "VK_TAB",
     modifiers: {shiftKey: true},
-    expected: ".testclass"
+    expected: ".testclass3"
   }
 ];
 
 add_task(function*() {
-  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
-  let {toolbox, inspector, view} = yield openRuleView();
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, view } = yield openRuleView();
 
   info("Iterating over the test data");
   for (let data of TEST_DATA) {
     yield runTestData(inspector, view, data);
   }
 });
 
 function* runTestData(inspector, view, data) {
   let {node, value, commitKey, modifiers, expected} = data;
 
-  info("Updating " + node + " to " + value + " and committing with " + commitKey + ". Expecting: " + expected);
+  info("Updating " + node + " to " + value + " and committing with " +
+       commitKey + ". Expecting: " + expected);
 
   info("Selecting the test element");
   yield selectNode(node, inspector);
 
   let idRuleEditor = getRuleViewRuleEditor(view, 1);
 
   info("Focusing an existing selector name in the rule-view");
   let editor = yield focusEditableField(idRuleEditor.selectorText);
@@ -73,21 +82,37 @@ function* runTestData(inspector, view, d
       "The selector editor got focused");
 
   info("Enter the new selector value: " + value);
   editor.input.value = value;
 
   info("Entering the commit key " + commitKey + " " + modifiers);
   EventUtils.synthesizeKey(commitKey, modifiers);
 
+  let activeElement = view.doc.activeElement;
+
   if (commitKey === "VK_ESCAPE") {
     is(idRuleEditor.rule.selectorText, expected,
         "Value is as expected: " + expected);
-    is(idRuleEditor.isEditing, false, "Selector is not being edited.")
-  } else {
-    yield once(view, "ruleview-changed");
-    ok(getRuleViewRule(view, expected),
-        "Rule with " + name + " selector exists.");
+    is(idRuleEditor.isEditing, false, "Selector is not being edited.");
+    is(idRuleEditor.selectorText, activeElement,
+       "Focus is on selector span.");
+    return;
   }
 
-  info("Resetting page content");
-  content.document.body.innerHTML = PAGE_CONTENT;
+  yield once(view, "ruleview-changed");
+
+  ok(getRuleViewRule(view, expected),
+     "Rule with " + expected + " selector exists.");
+
+  if (modifiers.shiftKey) {
+    idRuleEditor = getRuleViewRuleEditor(view, 0);
+  }
+
+  let rule = idRuleEditor.rule;
+  if (rule.textProps.length > 0) {
+    is(inplaceEditor(rule.textProps[0].editor.nameSpan).input, activeElement,
+       "Focus is on the first property name span.");
+  } else {
+    is(inplaceEditor(idRuleEditor.newPropSpan).input, activeElement,
+       "Focus is on the new property span.");
+  }
 }
--- a/browser/devtools/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js
+++ b/browser/devtools/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js
@@ -5,55 +5,59 @@
 
 /* Description of the test:
  * We are loading a file with the following CSP:
  *     'reflected-xss filter'
  * This directive is not supported, hence we confirm that
  * the according message is displayed in the web console.
  */
 
-const EXPECTED_RESULT = "Not supporting directive 'reflected-xss'. Directive and values will be ignored.";
-const TEST_FILE = "http://example.com/browser/browser/devtools/webconsole/test/" +
-                  "test_bug1045902_console_csp_ignore_reflected_xss_message.html";
+"use strict";
+
+const EXPECTED_RESULT = "Not supporting directive 'reflected-xss'. Directive " +
+                        "and values will be ignored.";
+const TEST_FILE = "http://example.com/browser/browser/devtools/webconsole/" +
+                  "test/test_bug1045902_console_csp_ignore_reflected_xss_" +
+                  "message.html";
 
 let hud = undefined;
 
-let TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring reflected XSS (bug 1045902)";
+let TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring " +
+               "reflected XSS (bug 1045902)";
 
 let test = asyncTest(function* () {
   let { browser } = yield loadTab(TEST_URI);
 
   hud = yield openConsole();
 
   yield loadDocument(browser);
   yield testViolationMessage();
 
   hud = null;
 });
 
-
 function loadDocument(browser) {
   let deferred = promise.defer();
 
-  hud.jsterm.clearOutput()
+  hud.jsterm.clearOutput();
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     deferred.resolve();
   }, true);
   content.location = TEST_FILE;
 
   return deferred.promise;
 }
 
 function testViolationMessage() {
-  let deferred = promise.defer();
   let aOutputNode = hud.outputNode;
 
   return waitForSuccess({
-      name: "Confirming that CSP logs messages to the console when 'reflected-xss' directive is used!",
+      name: "Confirming that CSP logs messages to the console when " +
+            "'reflected-xss' directive is used!",
       validator: function() {
-        console.log(hud.outputNode.textContent);
+        console.log(aOutputNode.textContent);
         let success = false;
-        success = hud.outputNode.textContent.indexOf(EXPECTED_RESULT) > -1;
+        success = aOutputNode.textContent.indexOf(EXPECTED_RESULT) > -1;
         return success;
       }
     });
 }
--- a/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
+++ b/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
@@ -5,18 +5,20 @@
 
 // Tests if the JSTerm sandbox is updated when the user navigates from one
 // domain to another, in order to avoid permission denied errors with a sandbox
 // created for a different origin.
 
 "use strict";
 
 let test = asyncTest(function* () {
-  const TEST_URI1 = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
-  const TEST_URI2 = "http://example.org/browser/browser/devtools/webconsole/test/test-console.html";
+  const TEST_URI1 = "http://example.com/browser/browser/devtools/webconsole/" +
+                    "test/test-console.html";
+  const TEST_URI2 = "http://example.org/browser/browser/devtools/webconsole/" +
+                    "test/test-console.html";
 
   yield loadTab(TEST_URI1);
   let hud = yield openConsole();
 
   hud.jsterm.clearOutput();
   hud.jsterm.execute("window.location.href");
 
   info("wait for window.location.href");
--- a/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js
+++ b/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js
@@ -50,17 +50,18 @@ let test = asyncTest(function* () {
   message.scrollIntoView();
 
   yield waitForContextMenu(menu, message, () => {
     let isHidden = menu.querySelector(CONTEXT_MENU_ID).hidden;
     ok(isHidden, CONTEXT_MENU_ID + " is hidden");
   });
 
   hud.jsterm.clearOutput();
-  content.location.reload(); // Reloading will produce network logging
+  // Reloading will produce network logging
+  content.location.reload();
 
   // Test that the "Copy Link Location" command is enabled and works
   // as expected for any network-related message.
   // This command should copy only the URL.
   [result] = yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "test-console.html",
@@ -75,20 +76,25 @@ let test = asyncTest(function* () {
 
   goUpdateCommand(COMMAND_NAME);
   ok(isEnabled(), COMMAND_NAME + " is enabled");
 
   info("expected clipboard value: " + message.url);
 
   let deferred = promise.defer();
 
-  waitForClipboard((aData) => { return aData.trim() == message.url; },
-    () => { goDoCommand(COMMAND_NAME); },
-    () => { deferred.resolve(null); },
-    () => { deferred.reject(null); });
+  waitForClipboard((aData) => {
+    return aData.trim() == message.url;
+  }, () => {
+    goDoCommand(COMMAND_NAME);
+  }, () => {
+    deferred.resolve(null);
+  }, () => {
+    deferred.reject(null);
+  });
 
   yield deferred.promise;
 
   // Test that the "Copy Link Location" menu item is visible for network-related
   // messages.
   message.scrollIntoView();
 
   yield waitForContextMenu(menu, message, () => {
--- a/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js
+++ b/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js
@@ -3,17 +3,18 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Test that makes sure messages are not considered repeated when console.log()
 // is invoked with different objects, see bug 865288.
 
 "use strict";
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-repeated-messages.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                 "test/test-repeated-messages.html";
 
 let test = asyncTest(function* () {
   yield loadTab(TEST_URI);
   let hud = yield openConsole();
 
   info("waiting for 3 console.log objects");
 
   hud.jsterm.clearOutput(true);
@@ -41,18 +42,17 @@ let test = asyncTest(function* () {
     let msg = msgs[i];
     let clickable = msg.querySelector(".message-body a");
     ok(clickable, "clickable object #" + i);
 
     msg.scrollIntoView(false);
     yield clickObject(clickable, i);
   }
 
-  function* clickObject(obj, i)
-  {
+  function* clickObject(obj, i) {
     executeSoon(() => {
       EventUtils.synthesizeMouse(obj, 2, 2, {}, hud.iframeWindow);
     });
 
     let varView = yield hud.jsterm.once("variablesview-fetched");
     ok(varView, "variables view fetched #" + i);
 
     yield findVariableViewProperties(varView, [
--- a/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
+++ b/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
@@ -3,20 +3,20 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Check that the variables view sidebar can be closed by pressing Escape in the
 // web console.
 
 "use strict";
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                 "test/test-eval-in-stackframe.html";
 
-function test()
-{
+function test() {
   let hud;
 
   Task.spawn(runner).then(finishTest);
 
   function* runner() {
     let {tab} = yield loadTab(TEST_URI);
     hud = yield openConsole(tab);
     let jsterm = hud.jsterm;
--- a/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
+++ b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
@@ -3,22 +3,24 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Check that users can inspect objects logged from cross-domain iframes -
 // bug 869003.
 
 "use strict";
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-869003-top-window.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                 "test/test-bug-869003-top-window.html";
 
 let test = asyncTest(function* () {
   // This test is slightly more involved: it opens the web console, then the
   // variables view for a given object, it updates a property in the view and
-  // checks the result. We can get a timeout with debug builds on slower machines.
+  // checks the result. We can get a timeout with debug builds on slower
+  // machines.
   requestLongerTimeout(2);
 
   yield loadTab("data:text/html;charset=utf8,<p>hello");
   let hud = yield openConsole();
 
   content.location = TEST_URI;
 
   let [result] = yield waitForMessages({
@@ -38,17 +40,17 @@ let test = asyncTest(function* () {
   let body = msg.querySelector(".message-body");
   ok(body, "message body");
 
   let clickable = result.clickableElements[0];
   ok(clickable, "clickable object found");
   ok(body.textContent.includes('{ hello: "world!",'), "message text check");
 
   executeSoon(() => {
-    EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow)
+    EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
   });
 
   let aVar = yield hud.jsterm.once("variablesview-fetched");
   ok(aVar, "variables view fetched");
   ok(aVar._variablesView, "variables view object");
 
   [result] = yield findVariableViewProperties(aVar, [
     { name: "hello", value: "world!" },
--- a/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
+++ b/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
@@ -49,17 +49,16 @@ let test = asyncTest(function* () {
     toolboxDestroyed.resolve(null);
   });
 
   // Get out of the web console initialization.
   executeSoon(() => {
     EventUtils.synthesizeKey("w", { accelKey: true });
   });
 
-
   yield promise.all([tabClosed.promise, toolboxDestroyed.promise,
                      tabSelected.promise]);
   info("promise.all resolved. start testing the Browser Console");
 
   hud = yield HUDService.toggleBrowserConsole();
   ok(hud, "Browser Console opened");
 
   let deferred = promise.defer();
--- a/browser/devtools/webconsole/test/browser_cached_messages.js
+++ b/browser/devtools/webconsole/test/browser_cached_messages.js
@@ -1,28 +1,30 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-// Test to see if the cached messages are displayed when the console UI is opened.
+// Test to see if the cached messages are displayed when the console UI is
+// opened.
+
+"use strict";
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-webconsole-error-observer.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                 "test/test-webconsole-error-observer.html";
 
-function test()
-{
+function test() {
   waitForExplicitFinish();
 
   expectUncaughtException();
 
   loadTab(TEST_URI).then(testOpenUI);
 }
 
-function testOpenUI(aTestReopen)
-{
+function testOpenUI(aTestReopen) {
   openConsole().then((hud) => {
     waitForMessages({
       webconsole: hud,
       messages: [
         {
           text: "log Bazzle",
           category: CATEGORY_WEBDEV,
           severity: SEVERITY_LOG,
--- a/browser/devtools/webconsole/test/browser_console.js
+++ b/browser/devtools/webconsole/test/browser_console.js
@@ -1,16 +1,19 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Test the basic features of the Browser Console, bug 587757.
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html?" + Date.now();
+"use strict";
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                 "test/test-console.html?" + Date.now();
 
 const TEST_XHR_ERROR_URI = `http://example.com/404.html?${Date.now()}`;
 
 "use strict";
 
 let test = asyncTest(function*() {
   yield loadTab(TEST_URI);
 
@@ -22,18 +25,17 @@ let test = asyncTest(function*() {
   EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, window);
 
   hud = yield opened;
   ok(hud, "browser console opened");
 
   yield consoleOpened(hud);
 });
 
-function consoleOpened(hud)
-{
+function consoleOpened(hud) {
   hud.jsterm.clearOutput(true);
 
   expectUncaughtException();
   executeSoon(() => {
     foobarExceptionBug587757();
   });
 
   // Add a message from a chrome window.
@@ -48,17 +50,19 @@ function consoleOpened(hud)
   // Check for network requests.
   let xhr = new XMLHttpRequest();
   xhr.onload = () => console.log("xhr loaded, status is: " + xhr.status);
   xhr.open("get", TEST_URI, true);
   xhr.send();
 
   // Check for xhr error.
   let xhrErr = new XMLHttpRequest();
-  xhrErr.onload = () => console.log("xhr error loaded, status is: " + xhrErr.status);
+  xhrErr.onload = () => {
+    console.log("xhr error loaded, status is: " + xhrErr.status);
+  };
   xhrErr.open("get", TEST_XHR_ERROR_URI, true);
   xhrErr.send();
 
   return waitForMessages({
     webconsole: hud,
     messages: [
       {
         name: "chrome window console.log() is displayed",
--- a/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
+++ b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -6,18 +6,17 @@
 // Check that exceptions from scripts loaded with the addon-sdk loader are
 // opened correctly in View Source from the Browser Console.
 // See bug 866950.
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 866950";
 
-function test()
-{
+function test() {
   requestLongerTimeout(2);
 
   let webconsole, browserconsole;
 
   Task.spawn(runner).then(finishTest);
 
   function* runner() {
     let {tab} = yield loadTab(TEST_URI);
@@ -25,17 +24,18 @@ function test()
     ok(webconsole, "web console opened");
 
     browserconsole = yield HUDService.toggleBrowserConsole();
     ok(browserconsole, "browser console opened");
 
     // Cause an exception in a script loaded with the addon-sdk loader.
     let toolbox = gDevTools.getToolbox(webconsole.target);
     let oldPanels = toolbox._toolPanels;
-    toolbox._toolPanels = {}; // non-iterable
+    // non-iterable
+    toolbox._toolPanels = {};
 
     function fixToolbox() {
       toolbox._toolPanels = oldPanels;
     }
 
     info("generate exception and wait for message");
 
     executeSoon(() => {
--- a/browser/devtools/webconsole/test/browser_console_clear_on_reload.js
+++ b/browser/devtools/webconsole/test/browser_console_clear_on_reload.js
@@ -5,17 +5,18 @@
 
 // Check that clear output on page reload works - bug 705921.
 // Check that clear output and page reload remove the sidebar - bug 971967.
 
 "use strict";
 
 let test = asyncTest(function*() {
   const PREF = "devtools.webconsole.persistlog";
-  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                   "test/test-console.html";
 
   Services.prefs.setBoolPref(PREF, false);
   registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
 
   yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
   ok(hud, "Web Console opened");
--- a/browser/devtools/webconsole/test/browser_console_click_focus.js
+++ b/browser/devtools/webconsole/test/browser_console_click_focus.js
@@ -2,17 +2,18 @@
 /* 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/. */
 
 // Tests that the input field is focused when the console is opened.
 
 "use strict";
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                 "test/test-console.html";
 
 let test = asyncTest(function*() {
   yield loadTab(TEST_URI);
   let hud = yield openConsole();
 
   let [result] = yield waitForMessages({
     webconsole: hud,
     messages: [{
@@ -27,27 +28,27 @@ let test = asyncTest(function*() {
   ok(outputItem, "found a logged message");
 
   let inputNode = hud.jsterm.inputNode;
   ok(inputNode.getAttribute("focused"), "input node is focused, first");
 
   let lostFocus = () => {
     inputNode.removeEventListener("blur", lostFocus);
     info("input node lost focus");
-  }
+  };
 
   inputNode.addEventListener("blur", lostFocus);
 
   document.getElementById("urlbar").click();
 
   ok(!inputNode.getAttribute("focused"), "input node is not focused");
 
   EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
 
-  ok(inputNode.getAttribute("focused"), "input node is focused, second time")
+  ok(inputNode.getAttribute("focused"), "input node is focused, second time");
 
   // test click-drags are not focusing the input element.
   EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4},
     outputItem);
   EventUtils.sendMouseEvent({type: "click", clientX: 15, clientY: 5},
     outputItem);
 
   todo(!inputNode.getAttribute("focused"), "input node is not focused after drag");
--- a/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
+++ b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
@@ -9,20 +9,21 @@
 
 function onNewMessage(aEvent, aNewMessages) {
   for (let msg of aNewMessages) {
     // Messages that shouldn't be output contain the substring FAIL_TEST
     if (msg.node.textContent.includes("FAIL_TEST")) {
       ok(false, "Message shouldn't have been output: " + msg.node.textContent);
     }
   }
-};
+}
 
 add_task(function*() {
-  let storage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(Ci.nsIConsoleAPIStorage);
+  let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+  let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
   storage.clearEvents();
 
   let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
   console.log("bug861338-log-cached");
 
   let hud = yield HUDService.toggleBrowserConsole();
 
   yield waitForMessages({
@@ -142,18 +143,19 @@ add_task(function*() {
   yield findVariableViewProperties(varView, [{
     name: "bug851231prop",
     value: "bug851231value",
   }], { webconsole: hud });
 
   yield HUDService.toggleBrowserConsole();
 });
 
-add_task(function* test_prefix() {
-  let storage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(Ci.nsIConsoleAPIStorage);
+add_task(function* testPrefix() {
+  let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+  let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
   storage.clearEvents();
 
   let {ConsoleAPI} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
   let consoleOptions = {
     maxLogLevel: "error",
     prefix: "Log Prefix",
   };
   let console2 = new ConsoleAPI(consoleOptions);
@@ -172,18 +174,19 @@ add_task(function* test_prefix() {
     }],
   });
 
   hud.jsterm.clearOutput(true);
   hud.ui.off("new-messages", onNewMessage);
   yield HUDService.toggleBrowserConsole();
 });
 
-add_task(function* test_maxLogLevelPref_missing() {
-  let storage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(Ci.nsIConsoleAPIStorage);
+add_task(function* testMaxLogLevelPrefMissing() {
+  let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+  let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
   storage.clearEvents();
 
   let {ConsoleAPI} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
   let consoleOptions = {
     maxLogLevel: "error",
     maxLogLevelPref: "testing.maxLogLevel",
   };
   let console = new ConsoleAPI(consoleOptions);
@@ -210,18 +213,19 @@ add_task(function* test_maxLogLevelPref_
     }],
   });
 
   hud.jsterm.clearOutput(true);
   hud.ui.off("new-messages", onNewMessage);
   yield HUDService.toggleBrowserConsole();
 });
 
-add_task(function* test_maxLogLevelPref() {
-  let storage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(Ci.nsIConsoleAPIStorage);
+add_task(function* testMaxLogLevelPref() {
+  let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+  let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
   storage.clearEvents();
 
   let {ConsoleAPI} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
   let consoleOptions = {
     maxLogLevel: "error",
     maxLogLevelPref: "testing.maxLogLevel",
   };
 
--- a/browser/devtools/webconsole/test/browser_console_copy_command.js
+++ b/browser/devtools/webconsole/test/browser_console_copy_command.js
@@ -1,52 +1,58 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Tests that the `copy` console helper works as intended.
 
+"use strict";
+
 let gWebConsole, gJSTerm;
 
-let TEXT =  "Lorem ipsum dolor sit amet, consectetur adipisicing " +
+let TEXT = "Lorem ipsum dolor sit amet, consectetur adipisicing " +
     "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " +
     "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
     "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " +
     "dolor in reprehenderit in voluptate velit esse cillum dolore eu " +
     "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " +
     "proident, sunt in culpa qui officia deserunt mollit anim id est laborum." +
     new Date();
 
 let ID = "select-me";
 
 add_task(function* init() {
   yield loadTab("data:text/html;charset=utf-8," +
                 "<body>" +
                 "  <div>" +
                 "    <h1>Testing copy command</h1>" +
                 "    <p>This is some example text</p>" +
-                "    <p id='select-me'>"+TEXT+"</p>" +
+                "    <p id='select-me'>" + TEXT + "</p>" +
                 "  </div>" +
                 "  <div><p></p></div>" +
                 "</body>");
 
   gWebConsole = yield openConsole();
   gJSTerm = gWebConsole.jsterm;
 });
 
-add_task(function* test_copy() {
+add_task(function* testCopy() {
   let RANDOM = Math.random();
   let string = "Text: " + RANDOM;
   let obj = {a: 1, b: "foo", c: RANDOM};
 
-  let samples = [[RANDOM, RANDOM],
-                 [JSON.stringify(string), string],
-                 [obj.toSource(),  JSON.stringify(obj, null, "  ")],
-                 ["$('#" + ID + "')", content.document.getElementById(ID).outerHTML]
+  let samples = [
+                  [RANDOM, RANDOM],
+                  [JSON.stringify(string), string],
+                  [obj.toSource(), JSON.stringify(obj, null, "  ")],
+                  [
+                    "$('#" + ID + "')",
+                    content.document.getElementById(ID).outerHTML
+                  ]
                 ];
   for (let [source, reference] of samples) {
     let deferredResult = promise.defer();
 
     SimpleTest.waitForClipboard(
       "" + reference,
       () => {
         let command = "copy(" + source + ")";
--- a/browser/devtools/webconsole/test/browser_console_copy_entire_message_context_menu.js
+++ b/browser/devtools/webconsole/test/browser_console_copy_entire_message_context_menu.js
@@ -6,18 +6,18 @@
 // Test copying of the entire console message when right-clicked
 // with no other text selected. See Bug 1100562.
 
 function test() {
   let hud;
   let outputNode;
   let contextMenu;
 
-  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
-  const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/"));
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+                   "test/test-console.html";
 
   Task.spawn(runner).then(finishTest);
 
   function* runner() {
     const {tab} = yield loadTab(TEST_URI);
     hud = yield openConsole(tab);
     outputNode = hud.outputNode;
     contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu");
@@ -37,28 +37,32 @@ function test() {
         severity: SEVERITY_LOG,
       }]
     });
 
     outputNode.focus();
     let message = [...results.matched][0];
     message.scrollIntoView();
 
-    yield waitForContextMenu(contextMenu, message, copyFromPopup, testContextMenuCopy);
+    yield waitForContextMenu(contextMenu, message, copyFromPopup,
+                             testContextMenuCopy);
 
     function copyFromPopup() {
       let copyItem = contextMenu.querySelector("#cMenu_copy");
       copyItem.doCommand();
 
-      let controller = top.document.commandDispatcher.getControllerForCommand("cmd_copy");
+      let controller = top.document.commandDispatcher
+                                   .getControllerForCommand("cmd_copy");
       is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
     }
 
     function testContextMenuCopy() {
-      waitForClipboard((str) => { return message.textContent.trim() == str.trim(); },
-        () => { goDoCommand("cmd_copy") },
-        () => {}, () => {}
+      waitForClipboard((str) => {
+        return message.textContent.trim() == str.trim();
+      }, () => {
+        goDoCommand("cmd_copy");
+      }, () => {}, () => {}
       );
     }
 
     yield closeConsole(tab);
   }
-}
\ No newline at end of file
+}
--- a/browser/devtools/webconsole/test/browser_console_dead_objects.js
+++ b/browser/devtools/webconsole/test/browser_console_dead_objects.js
@@ -11,43 +11,43 @@
 // - closes the tab,
 // - tries to use the object that was pointing to the now-defunct content
 // document. This is the dead object.
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf8,<p>dead objects!";
 
-function test()
-{
+function test() {
   let hud = null;
 
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref("devtools.chrome.enabled");
   });
 
   Task.spawn(runner).then(finishTest);
 
   function* runner() {
     Services.prefs.setBoolPref("devtools.chrome.enabled", true);
-    let {tab} = yield loadTab(TEST_URI);
+    yield loadTab(TEST_URI);
 
     info("open the browser console");
 
     hud = yield HUDService.toggleBrowserConsole();
     ok(hud, "browser console opened");
 
     let jsterm = hud.jsterm;
 
     jsterm.clearOutput();
 
     // Add the reference to the content document.
     yield jsterm.execute("Cu = Components.utils;" +
                   "Cu.import('resource://gre/modules/Services.jsm');" +
-                  "chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');" +
+                  "chromeWindow = Services.wm.getMostRecentWindow('" +
+                  "navigator:browser');" +
                   "foobarzTezt = chromeWindow.content.document;" +
                   "delete chromeWindow");
 
     gBrowser.removeCurrentTab();
 
     let msg = yield jsterm.execute("foobarzTezt");
 
     isnot(hud.outputNode.textContent.indexOf("[object DeadObject]"), -1,
--- a/browser/devtools/webconsole/test/browser_console_error_source_click.js
+++ b/browser/devtools/webconsole/test/browser_console_error_source_click.js
@@ -1,29 +1,29 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Check that JS errors and CSS warnings open view source when their source link
 // is clicked in the Browser Console. See bug 877778.
 
+"use strict";
+
 const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 877778 " +
                  "<button onclick='foobar.explode()' " +
                  "style='test-color: green-please'>click!</button>";
-function test()
-{
+function test() {
   let hud;
 
   loadTab(TEST_URI).then(() => {
     HUDService.toggleBrowserConsole().then(browserConsoleOpened);
   });
 
-  function browserConsoleOpened(aHud)
-  {
+  function browserConsoleOpened(aHud) {
     hud = aHud;
     ok(hud, "browser console opened");
 
     let button = content.document.querySelector("button");
     ok(button, "button element found");
 
     info("generate exception and wait for the message");
     executeSoon(() => {
@@ -43,18 +43,17 @@ function test()
           text: "Unknown property 'test-color'",
           category: CATEGORY_CSS,
           severity: SEVERITY_WARNING,
         },
       ],
     }).then(onMessageFound);
   }
 
-  function onMessageFound(results)
-  {
+  function onMessageFound(results) {
     let viewSource = hud.viewSource;
     let viewSourceCalled = false;
     hud.viewSourceInDebugger = () => viewSourceCalled = true;
 
     for (let result of results) {
       viewSourceCalled = false;
 
       let msg = [...results[0].matched][0];
--- a/browser/devtools/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js
+++ b/browser/devtools/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js
@@ -1,105 +1,110 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 /*
- * Bug 922161 - hide Browser Console JS input field if devtools.chrome.enabled is false
+ * Bug 922161 - Hide Browser Console JS input field if devtools.chrome.enabled
+ * is false.
  * when devtools.chrome.enabled then
  *   -browser console jsterm should be enabled
  *   -browser console object inspector properties should be set.
  *   -webconsole jsterm should be enabled
  *   -webconsole object inspector properties should be set.
  *
  * when devtools.chrome.enabled == false then
  *   -browser console jsterm should be disabled
  *   -browser console object inspector properties should not be set.
  *   -webconsole jsterm should be enabled
  *   -webconsole object inspector properties should be set.
  */
 
+"use strict";
+
 function testObjectInspectorPropertiesAreNotSet(variablesView) {
   is(variablesView.eval, null, "vview.eval is null");
   is(variablesView.switch, null, "vview.switch is null");
   is(variablesView.delete, null, "vview.delete is null");
 }
 
 function* getVariablesView(hud) {
   function openVariablesView(event, vview) {
     deferred.resolve(vview._variablesView);
   }
 
   let deferred = promise.defer();
   hud.jsterm.clearOutput();
-  hud.jsterm.execute('new Object()');
+  hud.jsterm.execute("new Object()");
 
   let [message] = yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "Object",
       category: CATEGORY_OUTPUT,
     }],
-  })
+  });
 
   hud.jsterm.once("variablesview-fetched", openVariablesView);
 
   let anchor = [...message.matched][0].querySelector("a");
 
   executeSoon(() =>
     EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow)
   );
 
   return deferred.promise;
 }
 
 function testJSTermIsVisible(hud) {
-  let inputContainer = hud.ui.window.document.querySelector(".jsterm-input-container");
+  let inputContainer = hud.ui.window.document
+                                    .querySelector(".jsterm-input-container");
   isnot(inputContainer.style.display, "none", "input is visible");
 }
 
 function testObjectInspectorPropertiesAreSet(variablesView) {
   isnot(variablesView.eval, null, "vview.eval is set");
   isnot(variablesView.switch, null, "vview.switch is set");
   isnot(variablesView.delete, null, "vview.delete is set");
 }
 
 function testJSTermIsNotVisible(hud) {
-  let inputContainer = hud.ui.window.document.querySelector(".jsterm-input-container");
+  let inputContainer = hud.ui.window.document
+                                    .querySelector(".jsterm-input-container");
   is(inputContainer.style.display, "none", "input is not visible");
 }
 
 function* testRunner() {
   let browserConsole, webConsole, variablesView;
 
   Services.prefs.setBoolPref("devtools.chrome.enabled", true);
 
   browserConsole = yield HUDService.toggleBrowserConsole();
   variablesView = yield getVariablesView(browserConsole);
   testJSTermIsVisible(browserConsole);
   testObjectInspectorPropertiesAreSet(variablesView);
 
   let {tab: browserTab} = yield loadTab("data:text/html;charset=utf8,hello world");
   webConsole = yield openConsole(browserTab);
   variablesView = yield getVariablesView(webConsole);
-  testJSTermIsVisible(webConsole)
-  testObjectInspectorPropertiesAreSet(variablesView)
+  testJSTermIsVisible(webConsole);
+  testObjectInspectorPropertiesAreSet(variablesView);
   yield closeConsole(browserTab);
 
   yield HUDService.toggleBrowserConsole();
   Services.prefs.setBoolPref("devtools.chrome.enabled", false);
 
   browserConsole = yield HUDService.toggleBrowserConsole();
   variablesView = yield getVariablesView(browserConsole);
   testJSTermIsNotVisible(browserConsole);
   testObjectInspectorPropertiesAreNotSet(variablesView);
 
   webConsole = yield openConsole(browserTab);
   variablesView = yield getVariablesView(webConsole);
-  testJSTermIsVisible(webConsole)
-  testObjectInspectorPropertiesAreSet(variablesView)
+  testJSTermIsVisible(webConsole);
+  testObjectInspectorPropertiesAreSet(variablesView);
   yield closeConsole(browserTab);
 }
 
 function test() {
   Task.spawn(testRunner).then(finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_console_history_persist.js
+++ b/browser/devtools/webconsole/test/browser_console_history_persist.js
@@ -3,90 +3,102 @@
  * 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/. */
 
 // Test that console command input is persisted across toolbox loads.
 // See Bug 943306.
 
 "use strict";
 
-const TEST_URI = "data:text/html;charset=utf-8,Web Console test for persisting history - bug 943306";
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+                 "persisting history - bug 943306";
 const INPUT_HISTORY_COUNT = 10;
 
 let test = asyncTest(function* () {
-  info ("Setting custom input history pref to " + INPUT_HISTORY_COUNT);
-  Services.prefs.setIntPref("devtools.webconsole.inputHistoryCount", INPUT_HISTORY_COUNT);
+  info("Setting custom input history pref to " + INPUT_HISTORY_COUNT);
+  Services.prefs.setIntPref("devtools.webconsole.inputHistoryCount",
+                            INPUT_HISTORY_COUNT);
 
   // First tab: run a bunch of commands and then make sure that you can
   // navigate through their history.
   yield loadTab(TEST_URI);
   let hud1 = yield openConsole();
-  is (JSON.stringify(hud1.jsterm.history), "[]", "No history on first tab initially");
+  is(JSON.stringify(hud1.jsterm.history), "[]",
+     "No history on first tab initially");
   yield populateInputHistory(hud1);
-  is (JSON.stringify(hud1.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
-    "First tab has populated history");
+  is(JSON.stringify(hud1.jsterm.history),
+     '["0","1","2","3","4","5","6","7","8","9"]',
+     "First tab has populated history");
 
   // Second tab: Just make sure that you can navigate through the history
   // generated by the first tab.
   yield loadTab(TEST_URI);
   let hud2 = yield openConsole();
-  is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
-    "Second tab has populated history");
+  is(JSON.stringify(hud2.jsterm.history),
+     '["0","1","2","3","4","5","6","7","8","9"]',
+     "Second tab has populated history");
   yield testNaviatingHistoryInUI(hud2);
-  is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9",""]',
-    "An empty entry has been added in the second tab due to history perusal");
+  is(JSON.stringify(hud2.jsterm.history),
+     '["0","1","2","3","4","5","6","7","8","9",""]',
+     "An empty entry has been added in the second tab due to history perusal");
 
   // Third tab: Should have the same history as first tab, but if we run a
   // command, then the history of the first and second shouldn't be affected
   yield loadTab(TEST_URI);
   let hud3 = yield openConsole();
-  is (JSON.stringify(hud3.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
-    "Third tab has populated history");
+  is(JSON.stringify(hud3.jsterm.history),
+     '["0","1","2","3","4","5","6","7","8","9"]',
+     "Third tab has populated history");
 
   // Set input value separately from execute so UP arrow accurately navigates history.
   hud3.jsterm.setInputValue('"hello from third tab"');
   hud3.jsterm.execute();
 
-  is (JSON.stringify(hud1.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
-    "First tab history hasn't changed due to command in third tab");
-  is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9",""]',
-    "Second tab history hasn't changed due to command in third tab");
-  is (JSON.stringify(hud3.jsterm.history), '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
-    "Third tab has updated history (and purged the first result) after running a command");
+  is(JSON.stringify(hud1.jsterm.history),
+     '["0","1","2","3","4","5","6","7","8","9"]',
+     "First tab history hasn't changed due to command in third tab");
+  is(JSON.stringify(hud2.jsterm.history),
+     '["0","1","2","3","4","5","6","7","8","9",""]',
+     "Second tab history hasn't changed due to command in third tab");
+  is(JSON.stringify(hud3.jsterm.history),
+     '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
+     "Third tab has updated history (and purged the first result) after " +
+     "running a command");
 
   // Fourth tab: Should have the latest command from the third tab, followed
   // by the rest of the history from the first tab.
   yield loadTab(TEST_URI);
   let hud4 = yield openConsole();
-  is (JSON.stringify(hud4.jsterm.history), '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
-    "Fourth tab has most recent history");
+  is(JSON.stringify(hud4.jsterm.history),
+     '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
+     "Fourth tab has most recent history");
 
   yield hud4.jsterm.clearHistory();
-  is (JSON.stringify(hud4.jsterm.history), '[]',
-    "Clearing history for a tab works");
+  is(JSON.stringify(hud4.jsterm.history), '[]',
+     "Clearing history for a tab works");
 
   yield loadTab(TEST_URI);
   let hud5 = yield openConsole();
-  is (JSON.stringify(hud5.jsterm.history), '[]',
-    "Clearing history carries over to a new tab");
+  is(JSON.stringify(hud5.jsterm.history), '[]',
+     "Clearing history carries over to a new tab");
 
-  info ("Clearing custom input history pref");
+  info("Clearing custom input history pref");
   Services.prefs.clearUserPref("devtools.webconsole.inputHistoryCount");
 });
 
 /**
  * Populate the history by running the following commands:
  *  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  */
 function* populateInputHistory(hud) {
   let jsterm = hud.jsterm;
-  let {inputNode} = jsterm;
 
   for (let i = 0; i < INPUT_HISTORY_COUNT; i++) {
-    // Set input value separately from execute so UP arrow accurately navigates history.
+    // Set input value separately from execute so UP arrow accurately navigates
+    // history.
     jsterm.setInputValue(i);
     jsterm.execute();
   }
 }
 
 /**
  * Check pressing up results in history traversal like:
  *  [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -36,16 +36,20 @@
 <!ENTITY engine.passwords.accesskey "P">
 <!ENTITY engine.prefs.label         "Preferences">
 <!ENTITY engine.prefs.accesskey     "S">
 <!ENTITY engine.addons.label        "Add-ons">
 <!ENTITY engine.addons.accesskey    "A">
 
 <!-- Device Settings -->
 <!ENTITY syncDeviceName.label       "Device Name:">
+<!ENTITY fxaSyncDeviceName.label       "Device Name">
+<!ENTITY changeSyncDeviceName.label "Change Device Name...">
+<!ENTITY cancelChangeSyncDeviceName.label "Cancel">
+<!ENTITY saveChangeSyncDeviceName.label "Save">
 <!ENTITY syncDeviceName.accesskey   "c">
 <!ENTITY unlinkDevice.label           "Unlink This Device">
 
 <!-- Footer stuff -->
 <!ENTITY prefs.tosLink.label        "Terms of Service">
 <!ENTITY prefs.ppLink.label         "Privacy Policy">
 
 <!-- Firefox Accounts stuff -->
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -658,16 +658,22 @@
 #toolbox-dock-side  > image {
   background-image: url("chrome://browser/skin/devtools/dock-side@2x.png");
 }
 
 #toolbox-dock-window > image {
   background-image: url("chrome://browser/skin/devtools/undock@2x.png");
 }
 
+#toolbox-dock-bottom-minimize {
+  /* Bug 1177463 - The minimize button is currently hidden until we agree on
+     the UI for it, and until bug 1173849 is fixed too. */
+  display: none;
+}
+
 #toolbox-dock-bottom-minimize > image {
   background-image: url("chrome://browser/skin/devtools/dock-bottom-minimize@2x.png");
 }
 
 #toolbox-dock-bottom-minimize.minimized > image {
   background-image: url("chrome://browser/skin/devtools/dock-bottom-maximize@2x.png");
 }
 
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -382,8 +382,21 @@ description > html|a {
   margin: 5px 0 0 0;
   animation: fadein 3000ms;
 }
 
 @keyframes fadein {
   from { opacity: 0; }
   to   { opacity: 1; }
 }
+
+/**
+ * Sync
+ */
+
+#fxaDeviceName {
+  margin: 14px 0px;
+}
+
+#fxaSyncComputerName.plain {
+  background-color: transparent;
+  opacity: 1;
+}
--- a/build/valgrind/mach_commands.py
+++ b/build/valgrind/mach_commands.py
@@ -1,16 +1,15 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import os
-import re
 import subprocess
 
 from mach.decorators import (
     Command,
     CommandArgument,
     CommandProvider,
 )
 from mozbuild.base import (
--- a/dom/bindings/mach_commands.py
+++ b/dom/bindings/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import os
 import sys
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
--- a/dom/nfc/gonk/NfcService.cpp
+++ b/dom/nfc/gonk/NfcService.cpp
@@ -448,17 +448,17 @@ NfcService::ReceiveSocketData(
 
 void
 NfcService::OnConnectSuccess(int aIndex)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   switch (aIndex) {
     case LISTEN_SOCKET: {
-        nsCString value("nfcd:-a ");
+        nsCString value("nfcd:-S -a ");
         value.Append(mListenSocketName);
         if (NS_WARN_IF(property_set("ctl.start", value.get()) < 0)) {
           OnConnectError(STREAM_SOCKET);
         }
       }
       break;
     case STREAM_SOCKET:
       /* nothing to do */
--- a/ipc/nfc/NfcConnector.cpp
+++ b/ipc/nfc/NfcConnector.cpp
@@ -18,17 +18,17 @@ NfcConnector::NfcConnector(const nsACStr
 { }
 
 NfcConnector::~NfcConnector()
 { }
 
 nsresult
 NfcConnector::CreateSocket(int& aFd) const
 {
-  aFd = socket(AF_LOCAL, SOCK_STREAM, 0);
+  aFd = socket(AF_LOCAL, SOCK_SEQPACKET, 0);
   if (aFd < 0) {
     NS_WARNING("Could not open NFC socket!");
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
--- a/layout/tools/reftest/mach_commands.py
+++ b/layout/tools/reftest/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import mozpack.path as mozpath
 import os
 import re
 import sys
 import warnings
 import which
 
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1682,16 +1682,17 @@ public class BrowserApp extends GeckoApp
                 final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
                 // TODO: handle choose what to Sync.
                 State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
                 fxAccount = AndroidFxAccount.addAndroidAccount(this,
                         email,
                         getProfile().getName(),
                         FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT,
                         FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT,
+                        FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT,
                         state,
                         AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
             } catch (Exception e) {
                 Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
                 if (callback == null) {
                     callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
                 }
             }
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -884,16 +884,17 @@ sync_java_files = [
     'fxa/login/State.java',
     'fxa/login/StateFactory.java',
     'fxa/login/TokensAndKeysState.java',
     'fxa/receivers/FxAccountDeletedReceiver.java',
     'fxa/receivers/FxAccountDeletedService.java',
     'fxa/receivers/FxAccountUpgradeReceiver.java',
     'fxa/sync/FxAccountGlobalSession.java',
     'fxa/sync/FxAccountNotificationManager.java',
+    'fxa/sync/FxAccountProfileService.java',
     'fxa/sync/FxAccountSchedulePolicy.java',
     'fxa/sync/FxAccountSyncAdapter.java',
     'fxa/sync/FxAccountSyncDelegate.java',
     'fxa/sync/FxAccountSyncService.java',
     'fxa/sync/FxAccountSyncStatusHelper.java',
     'fxa/sync/SchedulePolicy.java',
     'fxa/tasks/FxAccountCodeResender.java',
     'fxa/tasks/FxAccountCreateAccountTask.java',
--- a/mobile/android/base/db/BrowserProvider.java
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -1306,16 +1306,24 @@ public class BrowserProvider extends Sha
             beginWrite(db);
             return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
         }
 
         debug("Marking bookmarks as deleted for URI: " + uri);
 
         ContentValues values = new ContentValues();
         values.put(Bookmarks.IS_DELETED, 1);
+        values.put(Bookmarks.POSITION, 0);
+        values.putNull(Bookmarks.PARENT);
+        values.putNull(Bookmarks.URL);
+        values.putNull(Bookmarks.TITLE);
+        values.putNull(Bookmarks.DESCRIPTION);
+        values.putNull(Bookmarks.KEYWORD);
+        values.putNull(Bookmarks.TAGS);
+        values.putNull(Bookmarks.FAVICON_ID);
 
         // Doing this UPDATE (or the DELETE above) first ensures that the
         // first operation within this transaction is a write.
         // The cleanup call below will do a SELECT first, and thus would
         // require the transaction to be upgraded from a reader to a writer.
         final int updated = updateBookmarks(uri, values, selection, selectionArgs);
         try {
             cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
--- a/mobile/android/base/fxa/FxAccountConstants.java
+++ b/mobile/android/base/fxa/FxAccountConstants.java
@@ -11,24 +11,34 @@ public class FxAccountConstants {
   public static final String ACCOUNT_TYPE = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE;
 
   // Must be a client ID allocated with "canGrant" privileges!
   public static final String OAUTH_CLIENT_ID_FENNEC = "3332a18d142636cb";
 
   public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1";
   public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5";
   public static final String DEFAULT_OAUTH_SERVER_ENDPOINT = "https://oauth.accounts.firefox.com/v1";
+  public static final String DEFAULT_PROFILE_SERVER_ENDPOINT = "https://profile.accounts.firefox.com/v1";
 
   public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://stable.dev.lcip.org/auth/v1";
   public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5";
   public static final String STAGE_OAUTH_SERVER_ENDPOINT = "https://oauth-stable.dev.lcip.org/v1";
+  public static final String STAGE_PROFILE_SERVER_ENDPOINT = "https://latest.dev.lcip.org/profile/v1";
+
+  // Action to update on cached profile information.
+  public static final String ACCOUNT_PROFILE_AVATAR_UPDATED_ACTION = "org.mozilla.gecko.fxa.profile.cached";
 
   // You must be at least 13 years old, on the day of creation, to create a Firefox Account.
   public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 13;
 
+  // Key for avatar URI in profile JSON.
+  public static final String KEY_PROFILE_JSON_AVATAR = "avatar";
+  // Key for username in profile JSON.
+  public static final String KEY_PROFILE_JSON_USERNAME = "username";
+
   // You must wait 15 minutes after failing an age check before trying to create a different account.
   public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;
 
   public static final String USER_AGENT = "Firefox-Android-FxAccounts/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_DISPLAYNAME + ")";
 
   public static final String ACCOUNT_PICKLE_FILENAME = "fxa.account.json";
 
   /**
--- a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
@@ -69,16 +69,17 @@ abstract public class FxAccountAbstractS
   public static final String EXTRA_YEAR = "year";
   public static final String EXTRA_MONTH = "month";
   public static final String EXTRA_DAY = "day";
   public static final String EXTRA_EXTRAS = "extras";
 
   public static final String JSON_KEY_AUTH = "auth";
   public static final String JSON_KEY_SERVICES = "services";
   public static final String JSON_KEY_SYNC = "sync";
+  public static final String JSON_KEY_PROFILE = "profile";
 
   public FxAccountAbstractSetupActivity() {
     super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT);
   }
 
   protected FxAccountAbstractSetupActivity(int resume) {
     super(resume);
   }
@@ -95,25 +96,30 @@ abstract public class FxAccountAbstractS
   protected EditText passwordEdit;
   protected Button showPasswordButton;
   protected TextView remoteErrorTextView;
   protected Button button;
   protected ProgressBar progressBar;
 
   private String authServerEndpoint;
   private String syncServerEndpoint;
+  private String profileServerEndpoint;
 
   protected String getAuthServerEndpoint() {
     return authServerEndpoint;
   }
 
   protected String getTokenServerEndpoint() {
     return syncServerEndpoint;
   }
 
+  protected String getProfileServerEndpoint() {
+    return profileServerEndpoint;
+  }
+
   protected void createShowPasswordButton() {
     showPasswordButton.setOnClickListener(new OnClickListener() {
       @Override
       public void onClick(View v) {
         boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod;
         setPasswordButtonShown(!isShown);
       }
     });
@@ -342,32 +348,34 @@ abstract public class FxAccountAbstractS
     public void handleSuccess(LoginResponse result) {
       Logger.info(LOG_TAG, "Got success response; adding Android account.");
 
       // We're on the UI thread, but it's okay to create the account here.
       AndroidFxAccount fxAccount;
       try {
         final String profile = Constants.DEFAULT_PROFILE;
         final String tokenServerURI = getTokenServerEndpoint();
+        final String profileServerURI = getProfileServerEndpoint();
         // It is crucial that we use the email address provided by the server
         // (rather than whatever the user entered), because the user's keys are
         // wrapped and salted with the initial email they provided to
         // /create/account. Of course, we want to pass through what the user
         // entered locally as much as possible, so we create the Android account
         // with their entered email address, etc.
         // The passwordStretcher should have seen this email address before, so
         // we shouldn't be calculating the expensive stretch twice.
         byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
         byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
         State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken);
         fxAccount = AndroidFxAccount.addAndroidAccount(getApplicationContext(),
             email,
             profile,
             serverURI,
             tokenServerURI,
+            profileServerURI,
             state,
             this.authoritiesToSyncAutomaticallyMap);
         if (fxAccount == null) {
           throw new RuntimeException("Could not add Android account.");
         }
 
         if (selectedEngines != null) {
           Logger.info(LOG_TAG, "User has selected engines; storing to prefs.");
@@ -466,16 +474,17 @@ abstract public class FxAccountAbstractS
     }
 
     // This sets defaults as well as extracting from extras, so it's not conditional.
     updateServersFromIntentExtras(getIntent());
 
     if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
       FxAccountUtils.pii(LOG_TAG, "Using auth server: " + authServerEndpoint);
       FxAccountUtils.pii(LOG_TAG, "Using sync server: " + syncServerEndpoint);
+      FxAccountUtils.pii(LOG_TAG, "Using profile server: " + profileServerEndpoint);
     }
 
     updateCustomServerView();
   }
 
   @Override
   public void onResume() {
     super.onResume();
@@ -525,16 +534,17 @@ abstract public class FxAccountAbstractS
     intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
     startActivityForResult(intent, requestCode);
   }
 
   protected void updateServersFromIntentExtras(Intent intent) {
     // Start with defaults.
     this.authServerEndpoint = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT;
     this.syncServerEndpoint = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT;
+    this.profileServerEndpoint = FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT;
 
     if (intent == null) {
       Logger.warn(LOG_TAG, "Intent is null; ignoring and using default servers.");
       return;
     }
 
     final String extrasString = intent.getStringExtra(EXTRA_EXTRAS);
 
@@ -549,23 +559,27 @@ abstract public class FxAccountAbstractS
       services = extras.getObject(JSON_KEY_SERVICES);
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Got exception parsing extras; ignoring and using default servers.");
       return;
     }
 
     String authServer = extras.getString(JSON_KEY_AUTH);
     String syncServer = services == null ? null : services.getString(JSON_KEY_SYNC);
+    String profileServer = services == null ? null : services.getString(JSON_KEY_PROFILE);
 
     if (authServer != null) {
       this.authServerEndpoint = authServer;
     }
     if (syncServer != null) {
       this.syncServerEndpoint = syncServer;
     }
+    if (profileServer != null) {
+      this.profileServerEndpoint = profileServer;
+    }
 
     if (FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServerEndpoint) &&
         !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServerEndpoint)) {
       // We really don't want to hard-code assumptions about server
       // configurations into client code in such a way that if and when the
       // situation is relaxed, the client code stops valid usage. Instead, we
       // warn. This configuration should present itself as an auth exception at
       // Sync time.
--- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
@@ -22,31 +22,35 @@ import org.mozilla.gecko.fxa.authenticat
 import org.mozilla.gecko.fxa.login.Married;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
 import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import android.accounts.Account;
+import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Handler;
 import android.preference.CheckBoxPreference;
 import android.preference.EditTextPreference;
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceScreen;
+import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.widget.Toast;
 
 
 /**
  * A fragment that displays the status of an AndroidFxAccount.
  * <p>
@@ -57,30 +61,32 @@ public class FxAccountStatusFragment
     extends PreferenceFragment
     implements OnPreferenceClickListener, OnPreferenceChangeListener {
   private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
 
   /**
    * If a device claims to have synced before this date, we will assume it has never synced.
    */
   private static final Date EARLIEST_VALID_SYNCED_DATE;
+
   static {
     final Calendar c = GregorianCalendar.getInstance();
     c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
     EARLIEST_VALID_SYNCED_DATE = c.getTime();
   }
 
   // When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
   // before trying to sync. Should we kill off the fragment before the sync
   // request happens, that's okay: the runnable will run if the UI thread is
   // still around to service it, and since we're not updating any UI, we'll just
   // schedule the sync as usual. See also comment below about garbage
   // collection.
   private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
   private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000;
+  private static final long PROFILE_FETCH_RETRY_INTERVAL_IN_MILLISECONDS = 60 * 1000;
 
   // By default, the auth/account server preference is only shown when the
   // account is configured to use a custom server. In debug mode, this is set.
   private static boolean ALWAYS_SHOW_AUTH_SERVER = false;
 
   // By default, the Sync server preference is only shown when the account is
   // configured to use a custom Sync server. In debug mode, this is set.
   private static boolean ALWAYS_SHOW_SYNC_SERVER = false;
@@ -129,16 +135,22 @@ public class FxAccountStatusFragment
   // Member variable so that re-posting pushes back the already posted instance.
   // This Runnable references the fxAccount above, but it is not specific to a
   // single account. (That is, it does not capture a single account instance.)
   protected Runnable requestSyncRunnable;
 
   // Runnable to update last synced time.
   protected Runnable lastSyncedTimeUpdateRunnable;
 
+  // Runnable to retry fetching profile information.
+  protected Runnable profileFetchRunnable;
+
+  // Broadcast Receiver to update profile Information.
+  protected FxAccountProfileInformationReceiver accountProfileInformationReceiver;
+
   protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate();
 
   protected Preference ensureFindPreference(String key) {
     Preference preference = findPreference(key);
     if (preference == null) {
       throw new IllegalStateException("Could not find preference with key: " + key);
     }
     return preference;
@@ -309,16 +321,17 @@ public class FxAccountStatusFragment
   }
 
   protected Bundle getExtrasForAccount() {
     final Bundle extras = new Bundle();
     final ExtendedJSONObject o = new ExtendedJSONObject();
     o.put(FxAccountAbstractSetupActivity.JSON_KEY_AUTH, fxAccount.getAccountServerURI());
     final ExtendedJSONObject services = new ExtendedJSONObject();
     services.put(FxAccountAbstractSetupActivity.JSON_KEY_SYNC, fxAccount.getTokenServerURI());
+    services.put(FxAccountAbstractSetupActivity.JSON_KEY_PROFILE, fxAccount.getProfileServerURI());
     o.put(FxAccountAbstractSetupActivity.JSON_KEY_SERVICES, services);
     extras.putString(FxAccountAbstractSetupActivity.EXTRA_EXTRAS, o.toJSONString());
     return extras;
   }
 
   protected void setCheckboxesEnabled(boolean enabled) {
     bookmarksPreference.setEnabled(enabled);
     historyPreference.setEnabled(enabled);
@@ -479,16 +492,25 @@ public class FxAccountStatusFragment
   public void onPause() {
     super.onPause();
     FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
 
     // Focus lost, remove scheduled update if any.
     if (lastSyncedTimeUpdateRunnable != null) {
       handler.removeCallbacks(lastSyncedTimeUpdateRunnable);
     }
+
+    if (profileFetchRunnable != null) {
+      handler.removeCallbacks(profileFetchRunnable);
+    }
+
+    // Focus lost, unregister broadcast receiver.
+    if (accountProfileInformationReceiver != null) {
+      LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(accountProfileInformationReceiver);
+    }
   }
 
   protected void hardRefresh() {
     // This is the only way to guarantee that the EditText dialogs created by
     // EditTextPreferences are re-created. This works around the issue described
     // at http://androiddev.orkitra.com/?p=112079.
     final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
     statusScreen.removeAll();
@@ -500,25 +522,17 @@ public class FxAccountStatusFragment
   protected void refresh() {
     // refresh is called from our onResume, which can happen before the owning
     // Activity tells us about an account (via our public
     // refresh(AndroidFxAccount) method).
     if (fxAccount == null) {
       throw new IllegalArgumentException("fxAccount must not be null");
     }
 
-    if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) {
-      if (AppConstants.Versions.feature11Plus) {
-        profilePreference.setIcon(getResources().getDrawable(R.drawable.sync_avatar_default));
-      }
-      profilePreference.setTitle(fxAccount.getAndroidAccount().name);
-    } else {
-      emailPreference.setTitle(fxAccount.getEmail());
-    }
-
+    updateProfileInformation();
     updateAuthServerPreference();
     updateSyncServerPreference();
 
     try {
       // There are error states determined by Android, not the login state
       // machine, and we have a chance to present these states here. We handle
       // them specially, since we can't surface these states as part of syncing,
       // because they generally stop syncs from happening regularly. Right now
@@ -580,16 +594,72 @@ public class FxAccountStatusFragment
     if (currentlySyncing) {
       syncNowPreference.setTitle(R.string.fxaccount_status_syncing);
     } else {
       syncNowPreference.setTitle(R.string.fxaccount_status_sync_now);
     }
     scheduleAndUpdateLastSyncedTime();
   }
 
+  private void updateProfileInformation() {
+    if (!AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) {
+      // Life is so much simpler.
+      emailPreference.setTitle(fxAccount.getEmail());
+      return;
+    }
+
+    final ExtendedJSONObject cachedProfileJSON = fxAccount.getCachedProfileJSON();
+    if (cachedProfileJSON != null) {
+      // Update profile information from the cached Json.
+      updateProfileInformation(cachedProfileJSON);
+      return;
+    }
+
+    // Update the profile title with email as the fallback.
+    // Profile icon by default use the default avatar as the fallback.
+    profilePreference.setTitle(fxAccount.getEmail());
+
+    // Register a local broadcast receiver to get profile cached notification.
+    final IntentFilter intentFilter = new IntentFilter();
+    intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_AVATAR_UPDATED_ACTION);
+    accountProfileInformationReceiver = new FxAccountProfileInformationReceiver();
+    LocalBroadcastManager.getInstance(getActivity()).registerReceiver(accountProfileInformationReceiver, intentFilter);
+
+    // Fetch the profile from the server.
+    fxAccount.maybeUpdateProfileJSON(false);
+
+    // Schedule an runnable to retry fetching profile.
+    profileFetchRunnable = new ProfileFetchUpdateRunnable();
+    handler.postDelayed(profileFetchRunnable, PROFILE_FETCH_RETRY_INTERVAL_IN_MILLISECONDS);
+  }
+
+  /**
+   * Update profile information from json on UI thread.
+   *
+   * @param profileJson json fetched from server.
+   */
+  protected void updateProfileInformation(final ExtendedJSONObject profileJson) {
+    // Remove the scheduled runnable for fetching the profile information.
+    if (profileFetchRunnable != null) {
+      handler.removeCallbacks(profileFetchRunnable);
+    }
+
+    // Read the profile information from json and Update the UI elements.
+    ThreadUtils.postToUiThread(new Runnable() {
+      @Override
+      public void run() {
+        // Icon update from java is not supported prior to API 11, skip the avatar update for older device.
+        if (AppConstants.Versions.feature11Plus) {
+          profilePreference.setIcon(getResources().getDrawable(R.drawable.sync_avatar_default));
+        }
+        profilePreference.setTitle(fxAccount.getAndroidAccount().name);
+      }
+    });
+  }
+
   private void scheduleAndUpdateLastSyncedTime() {
     final String lastSynced = getLastSyncedString(fxAccount.getLastSyncedTimestamp());
     syncNowPreference.setSummary(lastSynced);
     handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS);
   }
 
   protected void updateAuthServerPreference() {
     final String authServer = fxAccount.getAccountServerURI();
@@ -756,16 +826,39 @@ public class FxAccountStatusFragment
   protected class LastSyncTimeUpdateRunnable implements Runnable  {
     @Override
     public void run() {
       scheduleAndUpdateLastSyncedTime();
     }
   }
 
   /**
+   * The Runnable that schedules a future to fetch profile information.
+   */
+  protected class ProfileFetchUpdateRunnable implements Runnable  {
+    @Override
+    public void run() {
+      updateProfileInformation();
+    }
+  }
+
+  /**
+   * Broadcast receiver to receive updates for the cached profile action.
+   */
+  public class FxAccountProfileInformationReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      if (intent.getAction().equals(FxAccountConstants.ACCOUNT_PROFILE_AVATAR_UPDATED_ACTION)) {
+        // We should have a cached profile json here.
+        updateProfileInformation(fxAccount.getCachedProfileJSON());
+      }
+    }
+  }
+
+  /**
    * A separate listener to separate debug logic from main code paths.
    */
   protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
     @Override
     public boolean onPreferenceClick(Preference preference) {
       final String key = preference.getKey();
       if ("debug_refresh".equals(key)) {
         Logger.info(LOG_TAG, "Refreshing.");
--- a/mobile/android/base/fxa/authenticator/AccountPickler.java
+++ b/mobile/android/base/fxa/authenticator/AccountPickler.java
@@ -64,16 +64,17 @@ public class AccountPickler {
   public static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp";
 
   public static final String KEY_ACCOUNT_VERSION = "account_version";
   public static final String KEY_ACCOUNT_TYPE = "account_type";
   public static final String KEY_EMAIL = "email";
   public static final String KEY_PROFILE = "profile";
   public static final String KEY_IDP_SERVER_URI = "idpServerURI";
   public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
+  public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
 
   public static final String KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = "authoritiesToSyncAutomaticallyMap";
 
   // Deprecated, but maintained for migration purposes.
   public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
 
   public static final String KEY_BUNDLE = "bundle";
 
@@ -94,16 +95,17 @@ public class AccountPickler {
     o.put(KEY_PICKLE_TIMESTAMP, now);
 
     o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
     o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
     o.put(KEY_EMAIL, account.getEmail());
     o.put(KEY_PROFILE, account.getProfile());
     o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
     o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
+    o.put(KEY_PROFILE_SERVER_URI, account.getProfileServerURI());
 
     final ExtendedJSONObject p = new ExtendedJSONObject();
     for (Entry<String, Boolean> pair : account.getAuthoritiesToSyncAutomaticallyMap().entrySet()) {
       p.put(pair.getKey(), pair.getValue());
     }
     o.put(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP, p);
 
     // TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
@@ -181,17 +183,17 @@ public class AccountPickler {
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e);
       return null;
     }
 
     final AndroidFxAccount account;
     try {
       account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile,
-          params.authServerURI, params.tokenServerURI, params.state,
+          params.authServerURI, params.tokenServerURI, params.profileServerURI, params.state,
           params.authoritiesToSyncAutomaticallyMap,
           params.accountVersion,
           true, params.bundle);
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e);
       return null;
     }
 
@@ -215,16 +217,17 @@ public class AccountPickler {
   private static class UnpickleParams {
     private Long pickleVersion;
 
     private int accountVersion;
     private String email;
     private String profile;
     private String authServerURI;
     private String tokenServerURI;
+    private String profileServerURI;
     private final Map<String, Boolean> authoritiesToSyncAutomaticallyMap = new HashMap<>();
 
     private ExtendedJSONObject bundle;
     private State state;
 
     private UnpickleParams() {
     }
 
@@ -290,16 +293,24 @@ public class AccountPickler {
     private void unpickleV1(final ExtendedJSONObject json)
         throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
 
       this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION);
       this.email = json.getString(KEY_EMAIL);
       this.profile = json.getString(KEY_PROFILE);
       this.authServerURI = json.getString(KEY_IDP_SERVER_URI);
       this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI);
+      this.profileServerURI = json.getString(KEY_PROFILE_SERVER_URI);
+
+      // Fallback to default value when profile server URI was not pickled.
+      if (this.profileServerURI == null) {
+        this.profileServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(this.authServerURI)
+            ? FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT
+            : FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
+      }
 
       // We get the default value for everything except syncing browser data.
       this.authoritiesToSyncAutomaticallyMap.put(BrowserContract.AUTHORITY, json.getBoolean(KEY_IS_SYNCING_ENABLED));
 
       this.bundle = json.getObject(KEY_BUNDLE);
       if (bundle == null) {
         throw new IllegalStateException("Pickle bundle is null.");
       }
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -22,28 +22,34 @@ import org.mozilla.gecko.background.comm
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.fxa.login.StateFactory;
+import org.mozilla.gecko.fxa.sync.FxAccountProfileService;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.setup.Constants;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
 import android.util.Log;
+import android.support.v4.content.LocalBroadcastManager;
 
 /**
  * A Firefox Account that stores its details and state as user data attached to
  * an Android Account instance.
  * <p>
  * Account user data is accessible only to the Android App(s) that own the
  * Account type. Account user data is not removed when the App's private data is
  * cleared.
@@ -54,62 +60,79 @@ public class AndroidFxAccount {
   public static final int CURRENT_SYNC_PREFS_VERSION = 1;
   public static final int CURRENT_RL_PREFS_VERSION = 1;
 
   // When updating the account, do not forget to update AccountPickler.
   public static final int CURRENT_ACCOUNT_VERSION = 3;
   public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
   public static final String ACCOUNT_KEY_PROFILE = "profile";
   public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
+  private static final String ACCOUNT_KEY_PROFILE_SERVER = "profileServerURI";
 
   public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI";       // Sync-specific.
   public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
+  public static final String ACCOUNT_KEY_PROFILE_AVATAR = "avatar";
 
   public static final int CURRENT_BUNDLE_VERSION = 2;
   public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
   public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
   public static final String BUNDLE_KEY_STATE = "state";
 
+  // Account authentication token type for fetching account profile.
+  public static final String PROFILE_OAUTH_TOKEN_TYPE = "oauth::profile";
+
   // Services may request OAuth tokens from the Firefox Account dynamically.
   // Each such token is prefixed with "oauth::" and a service-dependent scope.
   // Such tokens should be destroyed when the account is removed from the device.
   // This list collects all the known "oauth::" token types in order to delete them when necessary.
   private static final List<String> KNOWN_OAUTH_TOKEN_TYPES;
+
   static {
     final List<String> list = new ArrayList<>();
     if (AppConstants.MOZ_ANDROID_READING_LIST_SERVICE) {
       list.add(ReadingListConstants.AUTH_TOKEN_TYPE);
     }
+    if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) {
+      list.add(PROFILE_OAUTH_TOKEN_TYPE);
+    }
     KNOWN_OAUTH_TOKEN_TYPES = Collections.unmodifiableList(list);
   }
 
   public static final Map<String, Boolean> DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP;
   static {
     final HashMap<String, Boolean> m = new HashMap<String, Boolean>();
     // By default, Firefox Sync is enabled.
     m.put(BrowserContract.AUTHORITY, true);
     DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = Collections.unmodifiableMap(m);
   }
 
   private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp";
+  public static final String PREF_KEY_LAST_PROFILE_FETCH_TIME = "lastProfilefetchTime";
+  public static final String PREF_KEY_NUMBER_OF_PROFILE_FETCH = "numProfileFetch";
+
+  // Max wait time between successful profile avatar network fetch.
+  public static final long PROFILE_FETCH_RETRY_BACKOFF_DELTA_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
+  // Max attempts allowed for retrying profile avatar network fetch.
+  public static final int MAX_PROFILE_FETCH_RETRIES = 5;
 
   protected final Context context;
   protected final AccountManager accountManager;
   protected final Account account;
 
   /**
    * A cache associating Account name (email address) to a representation of the
    * account's internal bundle.
    * <p>
    * The cache is invalidated entirely when <it>any</it> new Account is added,
    * because there is no reliable way to know that an Account has been removed
    * and then re-added.
    */
   protected static final ConcurrentHashMap<String, ExtendedJSONObject> perAccountBundleCache =
       new ConcurrentHashMap<>();
+  private ExtendedJSONObject profileJson;
 
   public static void invalidateCaches() {
     perAccountBundleCache.clear();
   }
 
   /**
    * Create an Android Firefox Account instance backed by an Android Account
    * instance.
@@ -280,16 +303,20 @@ public class AndroidFxAccount {
   public String getAccountServerURI() {
     return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER);
   }
 
   public String getTokenServerURI() {
     return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER);
   }
 
+  public String getProfileServerURI() {
+    return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE_SERVER);
+  }
+
   public String getOAuthServerURI() {
     // Allow testing against stage.
     if (FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(getAccountServerURI())) {
       return FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT;
     } else {
       return FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT;
     }
   }
@@ -365,30 +392,32 @@ public class AndroidFxAccount {
   }
 
   public static AndroidFxAccount addAndroidAccount(
       Context context,
       String email,
       String profile,
       String idpServerURI,
       String tokenServerURI,
+      String profileServerURI,
       State state,
       final Map<String, Boolean> authoritiesToSyncAutomaticallyMap)
           throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
-    return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, state,
+    return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, profileServerURI, state,
         authoritiesToSyncAutomaticallyMap,
         CURRENT_ACCOUNT_VERSION, false, null);
   }
 
   public static AndroidFxAccount addAndroidAccount(
       Context context,
       String email,
       String profile,
       String idpServerURI,
       String tokenServerURI,
+      String profileServerURI,
       State state,
       final Map<String, Boolean> authoritiesToSyncAutomaticallyMap,
       final int accountVersion,
       final boolean fromPickle,
       ExtendedJSONObject bundle)
           throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
     if (email == null) {
       throw new IllegalArgumentException("email must not be null");
@@ -397,32 +426,36 @@ public class AndroidFxAccount {
       throw new IllegalArgumentException("profile must not be null");
     }
     if (idpServerURI == null) {
       throw new IllegalArgumentException("idpServerURI must not be null");
     }
     if (tokenServerURI == null) {
       throw new IllegalArgumentException("tokenServerURI must not be null");
     }
+    if (profileServerURI == null) {
+      throw new IllegalArgumentException("profileServerURI must not be null");
+    }
     if (state == null) {
       throw new IllegalArgumentException("state must not be null");
     }
 
     // TODO: Add migration code.
     if (accountVersion != CURRENT_ACCOUNT_VERSION) {
       throw new IllegalStateException("Could not create account of version " + accountVersion +
           ". Current version is " + CURRENT_ACCOUNT_VERSION + ".");
     }
 
     // Android has internal restrictions that require all values in this
     // bundle to be strings. *sigh*
     Bundle userdata = new Bundle();
     userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION);
     userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI);
     userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI);
+    userdata.putString(ACCOUNT_KEY_PROFILE_SERVER, profileServerURI);
     userdata.putString(ACCOUNT_KEY_PROFILE, profile);
 
     if (bundle == null) {
       bundle = new ExtendedJSONObject();
       // TODO: How to upgrade?
       bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
     }
     bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
@@ -629,16 +662,51 @@ public class AndroidFxAccount {
 
     // Update intent with tokens and service URI.
     intent.putExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY, getOAuthServerURI());
     // Deleted broadcasts are package-private, so there's no security risk include the tokens in the extras
     intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS, tokens.toArray(new String[tokens.size()]));
     return intent;
   }
 
+  private void setLastProfileFetchTimestampAndAttempts(long now, int attempts) {
+    try {
+      getSyncPrefs().edit().putLong(PREF_KEY_LAST_PROFILE_FETCH_TIME, now).commit();
+      getSyncPrefs().edit().putInt(PREF_KEY_NUMBER_OF_PROFILE_FETCH, attempts);
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception setting last profile fetch time & attempts; ignoring.", e);
+    }
+  }
+
+  private long getLastProfileFetchTimestamp() {
+    final long neverFetched = -1L;
+    try {
+      return getSyncPrefs().getLong(PREF_KEY_LAST_PROFILE_FETCH_TIME, neverFetched);
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception getting last profile fetch time; ignoring.", e);
+      return neverFetched;
+    }
+  }
+
+  private int getNumberOfProfileFetch() {
+    final int neverFetched = 0;
+    try {
+      return getSyncPrefs().getInt(PREF_KEY_NUMBER_OF_PROFILE_FETCH, neverFetched);
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception getting number of profile fetch; ignoring.", e);
+      return neverFetched;
+    }
+  }
+
+  private boolean canScheduleProfileFetch() {
+    final int attempts = getNumberOfProfileFetch();
+    final long delta = System.currentTimeMillis() - getLastProfileFetchTimestamp();
+    return delta > PROFILE_FETCH_RETRY_BACKOFF_DELTA_IN_MILLISECONDS || attempts < MAX_PROFILE_FETCH_RETRIES;
+  }
+
   public void setLastSyncedTimestamp(long now) {
     try {
       getSyncPrefs().edit().putLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, now).commit();
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Got exception setting last synced time; ignoring.", e);
     }
   }
 
@@ -651,44 +719,165 @@ public class AndroidFxAccount {
       return neverSynced;
     }
   }
 
   // Debug only!  This is dangerous!
   public void unsafeTransitionToDefaultEndpoints() {
     unsafeTransitionToStageEndpoints(
         FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT,
-        FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT);
+        FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT,
+        FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
     }
 
   // Debug only!  This is dangerous!
   public void unsafeTransitionToStageEndpoints() {
     unsafeTransitionToStageEndpoints(
         FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT,
-        FxAccountConstants.STAGE_TOKEN_SERVER_ENDPOINT);
+        FxAccountConstants.STAGE_TOKEN_SERVER_ENDPOINT,
+        FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT);
   }
 
-  protected void unsafeTransitionToStageEndpoints(String authServerEndpoint, String tokenServerEndpoint) {
+  protected void unsafeTransitionToStageEndpoints(String authServerEndpoint, String tokenServerEndpoint, String profileServerEndpoint) {
     try {
       getReadingListPrefs().edit().clear().commit();
     } catch (UnsupportedEncodingException | GeneralSecurityException e) {
       // Ignore.
     }
     try {
       getSyncPrefs().edit().clear().commit();
     } catch (UnsupportedEncodingException | GeneralSecurityException e) {
       // Ignore.
     }
     State state = getState();
     setState(state.makeSeparatedState());
     accountManager.setUserData(account, ACCOUNT_KEY_IDP_SERVER, authServerEndpoint);
     accountManager.setUserData(account, ACCOUNT_KEY_TOKEN_SERVER, tokenServerEndpoint);
+    accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_SERVER, profileServerEndpoint);
     ContentResolver.setIsSyncable(account, BrowserContract.READING_LIST_AUTHORITY, 1);
   }
 
+  // Helper function to create intent for profile avatar updated event.
+  private Intent getProfileAvatarUpdatedIntent() {
+    final Intent profileCachedIntent = new Intent();
+    profileCachedIntent.setAction(FxAccountConstants.ACCOUNT_PROFILE_AVATAR_UPDATED_ACTION);
+    return profileCachedIntent;
+  }
+
+  /**
+   * Returns the cached profile JSON object if available or null.
+   *
+   * @return profile JSON Object.
+   */
+  public ExtendedJSONObject getCachedProfileJSON() {
+    if (profileJson == null) {
+      // Try to retrieve and parse the json string from account manager.
+      final String profileJsonString = accountManager.getUserData(account, ACCOUNT_KEY_PROFILE_AVATAR);
+      if (profileJsonString != null) {
+        Logger.info(LOG_TAG, "Cached Profile information retrieved from AccountManager.");
+        try {
+          profileJson = ExtendedJSONObject.parseJSONObject(profileJsonString);
+        } catch (Exception e) {
+          Logger.error(LOG_TAG, "Failed to parse profile json; ignoring.", e);
+        }
+      }
+    }
+    return profileJson;
+  }
+
+  /**
+   * Fetches the profile json from the server and updates the local cache.
+   *
+   * <p>
+   * On successful fetch and cache, LocalBroadcastManager is used to notify the receivers asynchronously.
+   * </p>
+   *
+   * @param isForceFetch boolean to isForceFetch fetch from the server.
+   */
+  public void maybeUpdateProfileJSON(final boolean isForceFetch) {
+    final ExtendedJSONObject profileJson = getCachedProfileJSON();
+    final Intent profileAvatarUpdatedIntent = getProfileAvatarUpdatedIntent();
+
+    if (!isForceFetch && profileJson != null && !profileJson.keySet().isEmpty()) {
+      // Second line of defense, cache may have been updated in between.
+      Logger.info(LOG_TAG, "Profile already cached.");
+      LocalBroadcastManager.getInstance(context).sendBroadcast(profileAvatarUpdatedIntent);
+      return;
+    }
+
+    if (!isForceFetch && !canScheduleProfileFetch()) {
+      // Rate limiting repeated attempts to fetch the profile information.
+      Logger.info(LOG_TAG, "Too many attempts to fetch the profile information.");
+      return;
+    }
+
+    ThreadUtils.postToBackgroundThread(new Runnable() {
+      @Override
+      public void run() {
+        // Fetch profile information from server.
+        String authToken;
+        try {
+          authToken = accountManager.blockingGetAuthToken(account, AndroidFxAccount.PROFILE_OAUTH_TOKEN_TYPE, true);
+          if (authToken == null) {
+            throw new RuntimeException("Couldn't get oauth token!  Aborting profile fetch.");
+          }
+        } catch (Exception e) {
+          Logger.error(LOG_TAG, "Error fetching profile information; ignoring.", e);
+          return;
+        }
+
+        Logger.info(LOG_TAG, "Intent service launched to fetch profile.");
+        final Intent intent = new Intent(context, FxAccountProfileService.class);
+        intent.putExtra(FxAccountProfileService.KEY_AUTH_TOKEN, authToken);
+        intent.putExtra(FxAccountProfileService.KEY_PROFILE_SERVER_URI, getProfileServerURI());
+        intent.putExtra(FxAccountProfileService.KEY_RESULT_RECEIVER, new ProfileResultReceiver(profileAvatarUpdatedIntent));
+        context.startService(intent);
+
+        // Update the profile fetch time and attempts, resetting the attempts if last fetch was over a day old.
+        final int attempts = getNumberOfProfileFetch();
+        final long now = System.currentTimeMillis();
+        final long delta = now - getLastProfileFetchTimestamp();
+        setLastProfileFetchTimestampAndAttempts(now, delta < PROFILE_FETCH_RETRY_BACKOFF_DELTA_IN_MILLISECONDS ? attempts + 1 : 1);
+      }
+    });
+  }
+
+  private class ProfileResultReceiver extends ResultReceiver {
+    private final Intent profileAvatarUpdatedIntent;
+
+    public ProfileResultReceiver(Intent broadcastIntent) {
+      super(new Handler());
+      this.profileAvatarUpdatedIntent = broadcastIntent;
+    }
+
+    @Override
+    protected void onReceiveResult(int resultCode, Bundle bundle) {
+      super.onReceiveResult(resultCode, bundle);
+      switch (resultCode) {
+        case Activity.RESULT_OK:
+          try {
+            final String resultData = bundle.getString(FxAccountProfileService.KEY_RESULT_STRING);
+            profileJson = ExtendedJSONObject.parseJSONObject(resultData);
+            accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_AVATAR, resultData);
+            Logger.pii(LOG_TAG, "Profile fetch successful." + resultData);
+            LocalBroadcastManager.getInstance(context).sendBroadcast(profileAvatarUpdatedIntent);
+          } catch (Exception e) {
+            Logger.error(LOG_TAG, "Failed to parse profile json; ignoring.", e);
+          }
+          break;
+        case Activity.RESULT_CANCELED:
+          Logger.warn(LOG_TAG, "Failed to fetch profile; ignoring.");
+          break;
+        default:
+          Logger.warn(LOG_TAG, "Invalid Result code received; ignoring.");
+          break;
+      }
+    }
+  }
+
   /**
    * Take the lock to own updating any Firefox Account's internal state.
    *
    * We use a <code>Semaphore</code> rather than a <code>ReentrantLock</code>
    * because the callback that needs to release the lock may not be invoked on
    * the thread that initially acquired the lock. Be aware!
    */
   protected static final Semaphore sLock = new Semaphore(1, true /* fair */);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountProfileService.java
@@ -0,0 +1,91 @@
+/* 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/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import android.app.Activity;
+import android.app.IntentService;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException;
+import org.mozilla.gecko.background.fxa.profile.FxAccountProfileClient10;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class FxAccountProfileService extends IntentService {
+  private static final String LOG_TAG = "FxAccountProfileService";
+  private static final Executor EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
+  public static final String KEY_AUTH_TOKEN = "auth_token";
+  public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
+  public static final String KEY_RESULT_RECEIVER = "resultReceiver";
+  public static final String KEY_RESULT_STRING = "RESULT_STRING";
+
+  public FxAccountProfileService() {
+    super("FxAccountProfileService");
+  }
+
+  @Override
+  protected void onHandleIntent(Intent intent) {
+    final String authToken = intent.getStringExtra(KEY_AUTH_TOKEN);
+    final String profileServerURI = intent.getStringExtra(KEY_PROFILE_SERVER_URI);
+    final ResultReceiver resultReceiver = intent.getParcelableExtra(KEY_RESULT_RECEIVER);
+
+    if (authToken == null || authToken.length() == 0) {
+      Logger.warn(LOG_TAG, "Invalid Auth Token");
+      sendResult("Invalid Auth Token", resultReceiver, Activity.RESULT_CANCELED);
+      return;
+    }
+
+    if (profileServerURI == null || profileServerURI.length() == 0) {
+      Logger.warn(LOG_TAG, "Invalid profile Server Endpoint");
+      sendResult("Invalid profile Server Endpoint", resultReceiver, Activity.RESULT_CANCELED);
+      return;
+    }
+
+    // This delegate fetches the profile avatar json.
+    FxAccountProfileClient10.RequestDelegate<ExtendedJSONObject> delegate = new FxAccountAbstractClient.RequestDelegate<ExtendedJSONObject>() {
+      @Override
+      public void handleError(Exception e) {
+        Logger.error(LOG_TAG, "Error fetching Account profile.", e);
+        sendResult("Error fetching Account profile.", resultReceiver, Activity.RESULT_CANCELED);
+      }
+
+      @Override
+      public void handleFailure(FxAccountAbstractClientException.FxAccountAbstractClientRemoteException e) {
+        Logger.warn(LOG_TAG, "Failed to fetch Account profile.", e);
+        sendResult("Failed to fetch Account profile.", resultReceiver, Activity.RESULT_CANCELED);
+      }
+
+      @Override
+      public void handleSuccess(ExtendedJSONObject result) {
+        if (result != null){
+          Logger.pii(LOG_TAG, "Profile Server response : " + result.toJSONString());
+          sendResult(result.toJSONString(), resultReceiver, Activity.RESULT_OK);
+        }
+      }
+    };
+
+    FxAccountProfileClient10 client = new FxAccountProfileClient10(profileServerURI, EXECUTOR_SERVICE);
+    try {
+      client.profile(authToken, delegate);
+    } catch (Exception e) {
+      Logger.error(LOG_TAG, "Got exception fetching profile.", e);
+      delegate.handleError(e);
+    }
+  }
+
+  private void sendResult(final String result, final ResultReceiver resultReceiver, final int code) {
+    if (resultReceiver != null) {
+      final Bundle bundle = new Bundle();
+      bundle.putString(KEY_RESULT_STRING, result);
+      resultReceiver.send(code, bundle);
+    }
+  }
+}
--- a/mobile/android/base/sync/MigrationSentinelSyncStage.java
+++ b/mobile/android/base/sync/MigrationSentinelSyncStage.java
@@ -44,16 +44,17 @@ public class MigrationSentinelSyncStage 
     private static final String SENTINEL_KEY_EMAIL = "email";
     private static final String SENTINEL_KEY_UID = "uid";
     private static final String SENTINEL_KEY_VERIFIED = "verified";
     private static final String SENTINEL_KEY_PASSWORD = "password";
     private static final String SENTINEL_KEY_PREFS = "prefs";
 
     private static final String PREFS_KEY_AUTH_SERVER_ENDPOINT = "identity.fxaccounts.auth.uri";
     private static final String PREFS_KEY_TOKEN_SERVER_ENDPOINT = "services.sync.tokenServerURI";
+    private static final String PREFS_KEY_PROFILE_SERVER_ENDPOINT = "services.fxaccounts.profile.uri";
 
     private final GlobalSession session;
     private long fetchTimestamp = -1L;
 
     MigrationChecker(GlobalSession session) {
       this.session = session;
     }
 
@@ -65,26 +66,28 @@ public class MigrationSentinelSyncStage 
       }
     }
 
     private boolean migrate(String email,
                             String uid,
                             boolean verified,
                             String password,
                             String authServerURI,
-                            String tokenServerURI) throws Exception {
+                            String tokenServerURI,
+                            String profileServerURI) throws Exception {
       final String profile = "default";
 
       final State state = new MigratedFromSync11(email, uid, verified, password);
 
       final AndroidFxAccount fxAccount = AndroidFxAccount.addAndroidAccount(session.context,
           email,
           profile,
           authServerURI,
           tokenServerURI,
+          profileServerURI,
           state,
           AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
 
       if (fxAccount == null) {
         Logger.warn(LOG_TAG, "Could not add Android account named like: " + Utils.obfuscateEmail(email));
         return false;
       } else {
         return true;
@@ -118,39 +121,45 @@ public class MigrationSentinelSyncStage 
       final String uid = sentinel.getString(SENTINEL_KEY_UID);
       final boolean verified = sentinel.getBoolean(SENTINEL_KEY_VERIFIED);
       // We never expect this to be set, but we're being forward thinking here.
       final String password = sentinel.getString(SENTINEL_KEY_PASSWORD);
 
       final ExtendedJSONObject prefs = payload.getObject(SENTINEL_KEY_PREFS);
       String authServerURI = null;
       String tokenServerURI = null;
+      String profileServerURI = null;
       if (prefs != null) {
         authServerURI = prefs.getString(PREFS_KEY_AUTH_SERVER_ENDPOINT);
         tokenServerURI = prefs.getString(PREFS_KEY_TOKEN_SERVER_ENDPOINT);
+        profileServerURI = prefs.getString(PREFS_KEY_PROFILE_SERVER_ENDPOINT);
       }
       if (authServerURI == null) {
         authServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT;
       }
       if (tokenServerURI == null) {
         tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT;
       }
+      if (profileServerURI == null) {
+        profileServerURI = FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT;
+      }
 
       Logger.info(LOG_TAG, "Migration sentinel contained valid credentials.");
       if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
         FxAccountUtils.pii(LOG_TAG, "payload: " + payload.toJSONString());
         FxAccountUtils.pii(LOG_TAG, "email: " + email);
         FxAccountUtils.pii(LOG_TAG, "uid: " + uid);
         FxAccountUtils.pii(LOG_TAG, "verified: " + verified);
         FxAccountUtils.pii(LOG_TAG, "password: " + password);
         FxAccountUtils.pii(LOG_TAG, "authServer: " + authServerURI);
         FxAccountUtils.pii(LOG_TAG, "tokenServerURI: " + tokenServerURI);
+        FxAccountUtils.pii(LOG_TAG, "profileServerURI: " + profileServerURI);
       }
 
-      if (migrate(email, uid, verified, password, authServerURI, tokenServerURI)) {
+      if (migrate(email, uid, verified, password, authServerURI, tokenServerURI, profileServerURI)) {
         session.callback.informMigrated(session);
         onMigrated();
       } else {
         onError(null, "Could not add Android account.");
       }
     }
 
     private void onMigrated() {
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
 import logging
 import os
 
 import mozpack.path as mozpath
 
 from mozbuild.base import (
@@ -18,28 +18,30 @@ from mozbuild.base import (
 from mozbuild.util import (
     FileAvoidWrite,
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
+    SubCommand,
 )
 
 SUCCESS = '''
 You should be ready to build with Gradle and import into IntelliJ!  Test with
 
     ./mach gradle build
 
 and in IntelliJ select File > Import project... and choose
 
     {topobjdir}/mobile/android/gradle
 '''
 
+
 @CommandProvider
 class MachCommands(MachCommandBase):
     @Command('gradle', category='devenv',
         description='Run gradle.',
         conditions=[conditions.is_android])
     @CommandArgument('args', nargs=argparse.REMAINDER)
     def gradle(self, args):
         # Avoid logging the command
@@ -49,17 +51,16 @@ class MachCommands(MachCommandBase):
         if code:
             return code
 
         return self.run_process(['./gradlew'] + args,
             pass_thru=True, # Allow user to run gradle interactively.
             ensure_exit_code=False, # Don't throw on non-zero exit code.
             cwd=mozpath.join(self.topobjdir, 'mobile', 'android', 'gradle'))
 
-
     @Command('gradle-install', category='devenv',
         description='Install gradle environment.',
         conditions=[conditions.is_android])
     def gradle_install(self, quiet=False):
         import mozpack.manifests
         m = mozpack.manifests.InstallManifest()
 
         def srcdir(dst, src):
@@ -157,8 +158,109 @@ class MachCommands(MachCommandBase):
             ensure_exit_code=False, # Don't throw on non-zero exit code.
             cwd=mozpath.join(self.topsrcdir, 'mobile', 'android'))
 
         if not quiet:
             if not code:
                 print(SUCCESS.format(topobjdir=self.topobjdir))
 
         return code
+
+
+@CommandProvider
+class PackageFrontend(MachCommandBase):
+    """Fetch and install binary artifacts from Mozilla automation."""
+
+    @Command('artifact', category='post-build',
+        description='Use pre-built artifacts to build Fennec.',
+        conditions=[
+            conditions.is_android,  # mobile/android only for now.
+            conditions.is_hg,  # mercurial only for now.
+        ])
+    def artifact(self):
+        '''Download, cache, and install pre-built binary artifacts to build Fennec.
+
+        Invoke |mach artifact| before each |mach package| to freshen your installed
+        binary libraries.  That is, package using
+
+        mach artifact install && mach package
+
+        to download, cache, and install binary artifacts from Mozilla automation,
+        replacing whatever may be in your object directory.  Use |mach artifact last|
+        to see what binary artifacts were last used.
+
+        Never build libxul again!
+        '''
+        pass
+
+    def _make_artifacts(self, tree=None, job=None):
+        self.log_manager.terminal_handler.setLevel(logging.INFO)
+
+        self._activate_virtualenv()
+        self.virtualenv_manager.install_pip_package('pylru==1.0.9')
+        self.virtualenv_manager.install_pip_package('taskcluster==0.0.16')
+
+        state_dir = self._mach_context.state_dir
+        cache_dir = os.path.join(state_dir, 'package-frontend')
+
+        import which
+        hg = which.which('hg')
+
+        # Absolutely must come after the virtualenv is populated!
+        from mozbuild.artifacts import Artifacts
+        artifacts = Artifacts(tree, job, log=self.log, cache_dir=cache_dir, hg=hg)
+        return artifacts
+
+    @SubCommand('artifact', 'install',
+        'Install a good pre-built artifact.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')  # TODO: switch to central as this stabilizes.
+    @CommandArgument('--job', metavar='JOB', choices=['android-api-11'],
+        help='Build job.',
+        default='android-api-11')  # TODO: fish job from build configuration.
+    @CommandArgument('source', metavar='SRC', nargs='?', type=str,
+        help='Where to fetch and install artifacts from.  Can be omitted, in '
+            'which case the current hg repository is inspected; an hg revision; '
+            'a remote URL; or a local file.',
+        default=None)
+    def artifact_install(self, source=None, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        return artifacts.install_from(source, self.distdir)
+
+    @SubCommand('artifact', 'last',
+        'Print the last pre-built artifact installed.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')
+    @CommandArgument('--job', metavar='JOB', type=str,
+        help='Build job.',
+        default='android-api-11')
+    def artifact_print_last(self, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        artifacts.print_last()
+        return 0
+
+    @SubCommand('artifact', 'print-cache',
+        'Print local artifact cache for debugging.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')
+    @CommandArgument('--job', metavar='JOB', type=str,
+        help='Build job.',
+        default='android-api-11')
+    def artifact_print_cache(self, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        artifacts.print_cache()
+        return 0
+
+    @SubCommand('artifact', 'clear-cache',
+        'Delete local artifacts and reset local artifact cache.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')
+    @CommandArgument('--job', metavar='JOB', type=str,
+        help='Build job.',
+        default='android-api-11')
+    def artifact_clear_cache(self, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        artifacts.clear_cache()
+        return 0
--- a/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
@@ -10,16 +10,21 @@
                 android:resource="@xml/fxaccount_authenticator" />
         </service>
 
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
         </service>
 
+        <service
+            android:exported="false"
+            android:name="org.mozilla.gecko.fxa.sync.FxAccountProfileService" >
+        </service>
+
         <!-- Firefox Sync. -->
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.fxa.sync.FxAccountSyncService" >
             <intent-filter >
                 <action android:name="android.content.SyncAdapter" />
             </intent-filter>
 
--- a/mobile/android/tests/background/junit3/src/fxa/TestAccountLoader.java
+++ b/mobile/android/tests/background/junit3/src/fxa/TestAccountLoader.java
@@ -53,16 +53,17 @@ public class TestAccountLoader extends A
   // as test accounts and deleted in tearDown.
   private static final String TEST_USERNAME = "testAccount@mozilla.com";
   private static final String TEST_ACCOUNTTYPE = FxAccountConstants.ACCOUNT_TYPE;
 
   private static final String TEST_SYNCKEY = "testSyncKey";
   private static final String TEST_SYNCPASSWORD = "testSyncPassword";
 
   private static final String TEST_TOKEN_SERVER_URI = "testTokenServerURI";
+  private static final String TEST_PROFILE_SERVER_URI = "testProfileServerURI";
   private static final String TEST_AUTH_SERVER_URI = "testAuthServerURI";
   private static final String TEST_PROFILE = "testProfile";
 
   public TestAccountLoader() {
     super(TEST_ACCOUNTTYPE, TEST_USERNAME);
   }
 
   static {
@@ -170,18 +171,18 @@ public class TestAccountLoader extends A
       if (syncAccount != null) {
         TestSyncAccounts.deleteAccount(this, accountManager, syncAccount);
       }
     }
 
     // This account will get cleaned up in tearDown.
     final State state = new Separated(TEST_USERNAME, "uid", false); // State choice is arbitrary.
     final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context,
-        TEST_USERNAME, TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, state,
-        AndroidSyncTestCaseWithAccounts.TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED);
+        TEST_USERNAME, TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, TEST_PROFILE_SERVER_URI,
+        state, AndroidSyncTestCaseWithAccounts.TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED);
     assertNotNull(account);
     assertFirefoxAccount(getLoaderResultSynchronously(loader));
   }
 
   protected void assertFirefoxAccount(Account account) {
     assertNotNull(account);
     assertEquals(FxAccountConstants.ACCOUNT_TYPE, account.type);
   }
--- a/mobile/android/tests/background/junit3/src/fxa/authenticator/TestAccountPickler.java
+++ b/mobile/android/tests/background/junit3/src/fxa/authenticator/TestAccountPickler.java
@@ -13,16 +13,17 @@ import org.mozilla.gecko.sync.ExtendedJS
 import org.mozilla.gecko.sync.Utils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.test.RenamingDelegatingContext;
 
 public class TestAccountPickler extends AndroidSyncTestCaseWithAccounts {
   private static final String TEST_TOKEN_SERVER_URI = "tokenServerURI";
+  private static final String TEST_PROFILE_SERVER_URI = "profileServerURI";
   private static final String TEST_AUTH_SERVER_URI = "serverURI";
   private static final String TEST_PROFILE = "profile";
   private final static String FILENAME_PREFIX = "TestAccountPickler-";
   private final static String PICKLE_FILENAME = "pickle";
 
   private final static String TEST_ACCOUNTTYPE = FxAccountConstants.ACCOUNT_TYPE;
 
   // Test account names must start with TEST_USERNAME in order to be recognized
@@ -50,17 +51,17 @@ public class TestAccountPickler extends 
   public void tearDown() {
     super.tearDown();
     this.context.deleteFile(PICKLE_FILENAME);
   }
 
   public AndroidFxAccount addTestAccount() throws Exception {
     final State state = new Separated(TEST_USERNAME, "uid", false); // State choice is arbitrary.
     final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context, TEST_USERNAME,
-        TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, state,
+        TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, TEST_PROFILE_SERVER_URI, state,
         AndroidSyncTestCaseWithAccounts.TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED);
     assertNotNull(account);
     assertNotNull(account.getProfile());
     assertTrue(testAccountsExist()); // Sanity check.
     this.account = account.getAndroidAccount(); // To remove in tearDown() if we throw.
     return account;
   }
 
@@ -76,16 +77,17 @@ public class TestAccountPickler extends 
 
     assertEquals(AndroidFxAccount.CURRENT_ACCOUNT_VERSION, o.getIntegerSafely(AccountPickler.KEY_ACCOUNT_VERSION).intValue());
     assertEquals(FxAccountConstants.ACCOUNT_TYPE, o.getString(AccountPickler.KEY_ACCOUNT_TYPE));
 
     assertEquals(TEST_USERNAME, o.getString(AccountPickler.KEY_EMAIL));
     assertEquals(TEST_PROFILE, o.getString(AccountPickler.KEY_PROFILE));
     assertEquals(TEST_AUTH_SERVER_URI, o.getString(AccountPickler.KEY_IDP_SERVER_URI));
     assertEquals(TEST_TOKEN_SERVER_URI, o.getString(AccountPickler.KEY_TOKEN_SERVER_URI));
+    assertEquals(TEST_PROFILE_SERVER_URI, o.getString(AccountPickler.KEY_PROFILE_SERVER_URI));
 
     assertNotNull(o.getObject(AccountPickler.KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP));
     assertNotNull(o.get(AccountPickler.KEY_BUNDLE));
   }
 
   public void testPickleAndUnpickle() throws Exception {
     final AndroidFxAccount inputAccount = addTestAccount();
 
--- a/mobile/android/tests/browser/robocop/testBrowserProvider.java
+++ b/mobile/android/tests/browser/robocop/testBrowserProvider.java
@@ -615,16 +615,36 @@ public class testBrowserProvider extends
             int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
                                            BrowserContract.Bookmarks._ID + " = ?",
                                            new String[] { String.valueOf(id) });
 
             mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
 
             Cursor c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
             mAsserter.is(c.moveToFirst(), true, "Deleted bookmark was only marked as deleted");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), null,
+                    "Deleted bookmark title is null");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), null,
+                    "Deleted bookmark URL is null");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), null,
+                    "Deleted bookmark tags is null");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), null,
+                    "Deleted bookmark keyword is null");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), null,
+                    "Deleted bookmark description is null");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), String.valueOf(0),
+                    "Deleted bookmark has correct position");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)), null,
+                    "Deleted bookmark parent ID is null");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.IS_DELETED)), String.valueOf(1),
+                    "Deleted bookmark has correct is-deleted state");
+            mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON_ID)), null,
+                    "Deleted bookmark Favicon ID is null");
+            mAsserter.isnot(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.GUID)), null,
+                    "Deleted bookmark GUID is not null");
             c.close();
 
             deleted = mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
                                        BrowserContract.Bookmarks._ID + " = ?",
                                        new String[] { String.valueOf(id) });
 
             mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
 
--- a/python/compare-locales/mach_commands.py
+++ b/python/compare-locales/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
 from mozbuild.base import (
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -1,20 +1,18 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
-import glob
 import logging
 import mozpack.path as mozpath
 import os
-import sys
 
 from mozbuild.base import (
     MachCommandBase,
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
@@ -49,16 +47,17 @@ class MachCommands(MachCommandBase):
         default=False,
         action='store_true',
         help='Stop running tests after the first error or failure.')
     @CommandArgument('tests', nargs='+',
         metavar='TEST',
         help='Tests to run. Each test can be a single file or a directory.')
     def python_test(self, tests, verbose=False, stop=False):
         self._activate_virtualenv()
+        import glob
 
         # Python's unittest, and in particular discover, has problems with
         # clashing namespaces when importing multiple test modules. What follows
         # is a simple way to keep environments separate, at the price of
         # launching Python multiple times. This also runs tests via mozunit,
         # which produces output in the format Mozilla infrastructure expects.
         return_code = 0
         files = []
--- a/python/mozboot/mozboot/mach_commands.py
+++ b/python/mozboot/mozboot/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 from mach.decorators import (
     CommandProvider,
     Command,
 )
 
 
 @CommandProvider
--- a/python/mozbuild/dumbmake/dumbmake.py
+++ b/python/mozbuild/dumbmake/dumbmake.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 from collections import OrderedDict
 from itertools import groupby
 from operator import itemgetter
 from os.path import dirname
 
 WHITESPACE_CHARACTERS = ' \t'
 
--- a/python/mozbuild/mozbuild/action/buildlist.py
+++ b/python/mozbuild/mozbuild/action/buildlist.py
@@ -2,17 +2,17 @@
 # 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/.
 
 '''A generic script to add entries to a file
 if the entry does not already exist.
 
 Usage: buildlist.py <filename> <entry> [<entry> ...]
 '''
-from __future__ import print_function
+from __future__ import absolute_import, print_function
 
 import sys
 import os
 
 from mozbuild.util import lock_file
 
 def addEntriesToListFile(listFile, entries):
   """Given a file |listFile| containing one entry per line,
--- a/python/mozbuild/mozbuild/action/cl.py
+++ b/python/mozbuild/mozbuild/action/cl.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import ctypes
 import os
 import sys
 
 from mozprocess.processhandler import ProcessHandlerMixin
 from mozbuild.makeutil import Makefile
 
 CL_INCLUDES_PREFIX = os.environ.get("CL_INCLUDES_PREFIX", "Note: including file:")
--- a/python/mozbuild/mozbuild/action/convert_def_file.py
+++ b/python/mozbuild/mozbuild/action/convert_def_file.py
@@ -1,15 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # Convert Windows-style export files into a single Unix-style linker
 # script, applying any necessary preprocessing.
 
+from __future__ import absolute_import
+
 import itertools
 import re
 import sys
 from StringIO import StringIO
 
 from mozbuild.preprocessor import Preprocessor
 from mozbuild.util import FileAvoidWrite
 
--- a/python/mozbuild/mozbuild/action/file_generate.py
+++ b/python/mozbuild/mozbuild/action/file_generate.py
@@ -1,17 +1,18 @@
 # 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/.
 
 # Given a Python script and arguments describing the output file, and
 # the arguments that can be used to generate the output file, call the
 # script's |main| method with appropriate arguments.
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
+
 import argparse
 import imp
 import os
 import sys
 import traceback
 
 from mozbuild.util import FileAvoidWrite
 
--- a/python/mozbuild/mozbuild/action/generate_browsersearch.py
+++ b/python/mozbuild/mozbuild/action/generate_browsersearch.py
@@ -26,24 +26,24 @@ particular search plugins by name. Here,
 which we have seen a region-specific default plugin.
 
 6. Generate a JSON representation of the above information, and write the result
 to browsersearch.json in the locale-specific raw resource directory
 e.g. raw/browsersearch.json, raw-pt-rBR/browsersearch.json.
 '''
 
 from __future__ import (
+    absolute_import,
     print_function,
     unicode_literals,
 )
 
 import argparse
 import codecs
 import json
-import re
 import sys
 import os
 
 from mozbuild.dotproperties import (
     DotProperties,
 )
 from mozbuild.util import (
     FileAvoidWrite,
--- a/python/mozbuild/mozbuild/action/generate_suggestedsites.py
+++ b/python/mozbuild/mozbuild/action/generate_suggestedsites.py
@@ -20,17 +20,17 @@ containing the respective properties i.e
 for a 'mozilla' identifier, we'll look for keys like:
 browser.suggestedsites.mozilla.url, browser.suggestedsites.mozilla.title, etc.
 
 4. Generate a JSON representation of each site, join them in a JSON array, and
 write the result to suggestedsites.json on the locale-specific raw resource
 directory e.g. raw/suggestedsites.json, raw-pt-rBR/suggestedsites.json.
 '''
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
 
 import argparse
 import json
 import sys
 import os
 
 from mozbuild.dotproperties import (
     DotProperties,
--- a/python/mozbuild/mozbuild/action/jar_maker.py
+++ b/python/mozbuild/mozbuild/action/jar_maker.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import sys
 
 import mozbuild.jar
 
 
 def main(args):
     return mozbuild.jar.main(args)
 
--- a/python/mozbuild/mozbuild/action/package_geckolibs_aar.py
+++ b/python/mozbuild/mozbuild/action/package_geckolibs_aar.py
@@ -3,17 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 '''
 Script to produce an Android ARchive (.aar) containing the compiled
 Gecko library binaries.  The AAR file is intended for use by local
 developers using Gradle.
 '''
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
 
 import argparse
 import hashlib
 import os
 import shutil
 import sys
 import zipfile
 
--- a/python/mozbuild/mozbuild/action/preprocessor.py
+++ b/python/mozbuild/mozbuild/action/preprocessor.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import sys
 
 from mozbuild.preprocessor import Preprocessor
 
 
 def main(args):
   pp = Preprocessor()
   pp.handleCommandLine(args, True)
--- a/python/mozbuild/mozbuild/action/process_install_manifest.py
+++ b/python/mozbuild/mozbuild/action/process_install_manifest.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
 import sys
 from mozpack.copier import FileCopier
 from mozpack.manifests import InstallManifest
 
 
 COMPLETE = 'From {dest}: Kept {existing} existing; Added/updated {updated}; ' \
--- a/python/mozbuild/mozbuild/action/webidl.py
+++ b/python/mozbuild/mozbuild/action/webidl.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import sys
 
 from mozwebidlcodegen import BuildSystemWebIDL
 
 
 def main(argv):
     """Perform WebIDL code generation required by the build system."""
     manager = BuildSystemWebIDL.from_environment().manager
--- a/python/mozbuild/mozbuild/action/xpccheck.py
+++ b/python/mozbuild/mozbuild/action/xpccheck.py
@@ -1,18 +1,20 @@
 # 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/.
 
-'''A generic script to verify all test files are in the 
+'''A generic script to verify all test files are in the
 corresponding .ini file.
 
 Usage: xpccheck.py <directory> [<directory> ...]
 '''
 
+from __future__ import absolute_import
+
 import sys
 import os
 from glob import glob
 import manifestparser
 
 def getIniTests(testdir):
   mp = manifestparser.ManifestParser(strict=False)
   mp.read(os.path.join(testdir, 'xpcshell.ini'))
--- a/python/mozbuild/mozbuild/action/xpidl-process.py
+++ b/python/mozbuild/mozbuild/action/xpidl-process.py
@@ -2,16 +2,18 @@
 # 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/.
 
 # This script is used to generate an output header and xpt file for
 # input IDL file(s). It's purpose is to directly support the build
 # system. The API will change to meet the needs of the build system.
 
+from __future__ import absolute_import
+
 import argparse
 import os
 import sys
 
 from io import BytesIO
 
 from buildconfig import topsrcdir
 from header import print_header
--- a/python/mozbuild/mozbuild/action/zip.py
+++ b/python/mozbuild/mozbuild/action/zip.py
@@ -1,15 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # This script creates a zip file, but will also strip any binaries
 # it finds before adding them to the zip.
 
+from __future__ import absolute_import
+
 from mozpack.files import FileFinder
 from mozpack.copier import Jarrer
 from mozpack.errors import errors
 
 import argparse
 import mozpack.path as mozpath
 import sys
 
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/artifacts.py
@@ -0,0 +1,398 @@
+# 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/.
+
+'''
+Fetch build artifacts from a Firefox tree.
+
+This provides an (at-the-moment special purpose) interface to download Android
+artifacts from Mozilla's Task Cluster.
+
+This module performs the following steps:
+
+* find a candidate hg parent revision using the local pushlog.  The local
+  pushlog is maintained by mozext locally and updated on every pull.
+
+* map the candidate parent to candidate Task Cluster tasks and artifact
+  locations.  Pushlog entries might not correspond to tasks (yet), and those
+  tasks might not produce the desired class of artifacts.
+
+* fetch fresh Task Cluster artifacts and purge old artifacts, using a simple
+  Least Recently Used cache.
+
+The bulk of the complexity is in managing and persisting several caches.  If
+we found a Python LRU cache that pickled cleanly, we could remove a lot of
+this code!  Sadly, I found no such candidate implementations, so we pickle
+pylru caches manually.
+
+None of the instances (or the underlying caches) are safe for concurrent use.
+A future need, perhaps.
+
+This module requires certain modules be importable from the ambient Python
+environment.  |mach artifact| ensures these modules are available, but other
+consumers will need to arrange this themselves.
+'''
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import functools
+import logging
+import operator
+import os
+import pickle
+import re
+import shutil
+import subprocess
+import urlparse
+import zipfile
+
+import pylru
+import taskcluster
+
+from mozbuild.util import (
+    ensureParentDir,
+    FileAvoidWrite,
+)
+
+MAX_CACHED_PARENTS = 100  # Number of parent changesets to cache candidate pushheads for.
+NUM_PUSHHEADS_TO_QUERY_PER_PARENT = 50  # Number of candidate pushheads to cache per parent changeset.
+
+MAX_CACHED_TASKS = 400  # Number of pushheads to cache Task Cluster task data for.
+
+# Number of downloaded artifacts to cache.  Each artifact can be very large,
+# so don't make this to large!  TODO: make this a size (like 500 megs) rather than an artifact count.
+MAX_CACHED_ARTIFACTS = 6
+
+# TODO: handle multiple artifacts with the same filename.
+# TODO: handle installing binaries from different types of artifacts (.tar.bz2, .dmg, etc).
+# Keep the keys of this map in sync with the |mach artifact| --job options.
+JOB_DETAILS = {
+    # 'android-api-9': {'re': re.compile('public/build/fennec-(.*)\.android-arm\.apk')},
+    'android-api-11': {'re': re.compile('public/build/geckolibs-(.*)\.aar')},
+    # 'linux': {'re': re.compile('public/build/firefox-(.*)\.linux-i686\.tar\.bz2')},
+    # 'linux64': {'re': re.compile('public/build/firefox-(.*)\.linux-x86_64\.tar\.bz2')},
+    # 'macosx64': {'re': re.compile('public/build/firefox-(.*)\.mac\.dmg')},
+}
+
+
+def cachedmethod(cachefunc):
+    '''Decorator to wrap a class or instance method with a memoizing callable that
+    saves results in a (possibly shared) cache.
+    '''
+    def decorator(method):
+        def wrapper(self, *args, **kwargs):
+            mapping = cachefunc(self)
+            if mapping is None:
+                return method(self, *args, **kwargs)
+            key = (method.__name__, args, tuple(sorted(kwargs.items())))
+            try:
+                value = mapping[key]
+                return value
+            except KeyError:
+                pass
+            result = method(self, *args, **kwargs)
+            mapping[key] = result
+            return result
+        return functools.update_wrapper(wrapper, method)
+    return decorator
+
+
+class CacheManager(object):
+    '''Maintain an LRU cache.  Provide simple persistence, including support for
+    loading and saving the state using a "with" block.  Allow clearing the cache
+    and printing the cache for debugging.
+
+    Provide simple logging.
+    '''
+
+    def __init__(self, cache_dir, cache_name, cache_size, cache_callback=None, log=None):
+        self._cache = pylru.lrucache(cache_size, callback=cache_callback)
+        self._cache_filename = os.path.join(cache_dir, cache_name + '-cache.pickle')
+        self._log = log
+
+    def log(self, *args, **kwargs):
+        if self._log:
+            self._log(*args, **kwargs)
+
+    def load_cache(self):
+        try:
+            items = pickle.load(open(self._cache_filename, 'rb'))
+            for key, value in items:
+                self._cache[key] = value
+        except Exception as e:
+            # Corrupt cache, perhaps?  Sadly, pickle raises many different
+            # exceptions, so it's not worth trying to be fine grained here.
+            # We ignore any exception, so the cache is effectively dropped.
+            self.log(logging.INFO, 'artifact',
+                {'filename': self._cache_filename, 'exception': repr(e)},
+                'Ignoring exception unpickling cache file {filename}: {exception}')
+            pass
+
+    def dump_cache(self):
+        ensureParentDir(self._cache_filename)
+        pickle.dump(list(reversed(list(self._cache.items()))), open(self._cache_filename, 'wb'), -1)
+
+    def clear_cache(self):
+        with self:
+            self._cache.clear()
+
+    def print_cache(self):
+        with self:
+            for item in self._cache.items():
+                self.log(logging.INFO, 'artifact',
+                    {'item': item},
+                    '{item}')
+
+    def print_last_item(self, args, sorted_kwargs, result):
+        # By default, show nothing.
+        pass
+
+    def print_last(self):
+        # We use the persisted LRU caches to our advantage.  The first item is
+        # most recent.
+        with self:
+            item = next(self._cache.items(), None)
+            if item is not None:
+                (name, args, sorted_kwargs), result = item
+                self.print_last_item(args, sorted_kwargs, result)
+            else:
+                self.log(logging.WARN, 'artifact',
+                    {},
+                    'No last cached item found.')
+
+    def __enter__(self):
+        self.load_cache()
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.dump_cache()
+
+
+class PushHeadCache(CacheManager):
+    '''Map parent hg revisions to candidate pushheads.'''
+
+    def __init__(self, hg, cache_dir, log=None):
+        # It's not unusual to pull hundreds of changesets at once, and perhaps
+        # |hg up| back and forth a few times.
+        CacheManager.__init__(self, cache_dir, 'pushheads', MAX_CACHED_PARENTS, log=log)
+        self._hg = hg
+
+    @cachedmethod(operator.attrgetter('_cache'))
+    def pushheads(self, tree, parent):
+        pushheads = subprocess.check_output([self._hg, 'log',
+            '--template', '{node}\n',
+            '-r', 'last(pushhead("{tree}") & ::"{parent}", {num})'.format(
+                tree=tree, parent=parent, num=NUM_PUSHHEADS_TO_QUERY_PER_PARENT)])
+        pushheads = pushheads.strip().split('\n')
+        return pushheads
+
+
+class TaskCache(CacheManager):
+    '''Map candidate pushheads to Task Cluster task IDs and artifact URLs.'''
+
+    def __init__(self, cache_dir, log=None):
+        CacheManager.__init__(self, cache_dir, 'artifact_url', MAX_CACHED_TASKS, log=log)
+        self._index = taskcluster.Index()
+        self._queue = taskcluster.Queue()
+
+    @cachedmethod(operator.attrgetter('_cache'))
+    def artifact_url(self, tree, job, rev):
+        try:
+            artifact_re = JOB_DETAILS[job]['re']
+        except KeyError:
+            self.log(logging.INFO, 'artifact',
+                {'job': job},
+                'Unknown job {job}')
+            raise KeyError("Unknown job")
+
+        # Bug 1175655: it appears that the Task Cluster index only takes
+        # 12-char hex hashes.
+        key = '{rev}.{tree}.{job}'.format(rev=rev[:12], tree=tree, job=job)
+        try:
+            namespace = 'buildbot.revisions.{key}'.format(key=key)
+            task = self._index.findTask(namespace)
+        except Exception:
+            # Not all revisions correspond to pushes that produce the job we
+            # care about; and even those that do may not have completed yet.
+            raise ValueError('Task for {key} does not exist (yet)!'.format(key=key))
+        taskId = task['taskId']
+
+        # TODO: Make this not Android-only by matching a regular expression.
+        artifacts = self._queue.listLatestArtifacts(taskId)['artifacts']
+
+        def names():
+            for artifact in artifacts:
+                name = artifact['name']
+                if artifact_re.match(name):
+                    yield name
+
+        # TODO: Handle multiple artifacts, taking the latest one.
+        for name in names():
+            # We can easily extract the task ID and the build ID from the URL.
+            url = self._queue.buildUrl('getLatestArtifact', taskId, name)
+            return url
+        raise ValueError('Task for {key} existed, but no artifacts found!'.format(key=key))
+
+    def print_last_item(self, args, sorted_kwargs, result):
+        tree, job, rev = args
+        self.log(logging.INFO, 'artifact',
+            {'rev': rev},
+            'Last installed binaries from hg parent revision {rev}')
+
+
+class ArtifactCache(CacheManager):
+    '''Fetch Task Cluster artifact URLs and purge least recently used artifacts from disk.'''
+
+    def __init__(self, cache_dir, log=None):
+        # TODO: instead of storing N artifact packages, store M megabytes.
+        CacheManager.__init__(self, cache_dir, 'fetch', MAX_CACHED_ARTIFACTS, cache_callback=self.delete_file, log=log)
+        self._cache_dir = cache_dir
+
+    def delete_file(self, key, value):
+        try:
+            os.remove(value)
+            self.log(logging.INFO, 'artifact',
+                {'filename': value},
+                'Purged artifact {filename}')
+        except IOError:
+            pass
+
+    @cachedmethod(operator.attrgetter('_cache'))
+    def fetch(self, url, force=False):
+        args = ['wget', url]
+
+        if not force:
+            args[1:1] = ['--timestamping']
+
+        proc = subprocess.Popen(args, cwd=self._cache_dir)
+        status = None
+        # Leave it to the subprocess to handle Ctrl+C. If it terminates as
+        # a result of Ctrl+C, proc.wait() will return a status code, and,
+        # we get out of the loop. If it doesn't, like e.g. gdb, we continue
+        # waiting.
+        while status is None:
+            try:
+                status = proc.wait()
+            except KeyboardInterrupt:
+                pass
+
+        if status != 0:
+            raise Exception('Process executed with non-0 exit code: %s' % args)
+
+        return os.path.abspath(os.path.join(self._cache_dir, os.path.basename(url)))
+
+    def print_last_item(self, args, sorted_kwargs, result):
+        url, = args
+        self.log(logging.INFO, 'artifact',
+            {'url': url},
+            'Last installed binaries from url {url}')
+        self.log(logging.INFO, 'artifact',
+            {'filename': result},
+            'Last installed binaries from local file {filename}')
+
+
+class Artifacts(object):
+    '''Maintain state to efficiently fetch build artifacts from a Firefox tree.'''
+
+    def __init__(self, tree, job, log=None, cache_dir='.', hg='hg'):
+        self._tree = tree
+        self._job = job
+        self._log = log
+        self._hg = hg
+        self._cache_dir = cache_dir
+
+        self._pushhead_cache = PushHeadCache(self._hg, self._cache_dir, log=self._log)
+        self._task_cache = TaskCache(self._cache_dir, log=self._log)
+        self._artifact_cache = ArtifactCache(self._cache_dir, log=self._log)
+
+    def log(self, *args, **kwargs):
+        if self._log:
+            self._log(*args, **kwargs)
+
+    def install_from_file(self, filename, distdir):
+        self.log(logging.INFO, 'artifact',
+            {'filename': filename},
+            'Installing from {filename}')
+
+        # Copy all .so files to dist/bin, avoiding modification where possible.
+        ensureParentDir(os.path.join(distdir, 'bin', '.dummy'))
+
+        with zipfile.ZipFile(filename) as zf:
+            for info in zf.infolist():
+                if not info.filename.endswith('.so'):
+                    continue
+                n = os.path.join(distdir, 'bin', os.path.basename(info.filename))
+                fh = FileAvoidWrite(n, mode='r')
+                shutil.copyfileobj(zf.open(info), fh)
+                fh.write(zf.open(info).read())
+                file_existed, file_updated = fh.close()
+                self.log(logging.INFO, 'artifact',
+                    {'updating': 'Updating' if file_updated else 'Not updating', 'filename': n},
+                    '{updating} {filename}')
+        return 0
+
+    def install_from_url(self, url, distdir):
+        self.log(logging.INFO, 'artifact',
+            {'url': url},
+            'Installing from {url}')
+        with self._artifact_cache as artifact_cache:  # The with block handles persistence.
+            filename = artifact_cache.fetch(url)
+        return self.install_from_file(filename, distdir)
+
+    def install_from_hg(self, revset, distdir):
+        if not revset:
+            revset = '.'
+        if len(revset) != 40:
+            revset = subprocess.check_output([self._hg, 'log', '--template', '{node}\n', '-r', revset]).strip()
+            if len(revset.split('\n')) != 1:
+                raise ValueError('hg revision specification must resolve to exactly one commit')
+
+        self.log(logging.INFO, 'artifact',
+            {'revset': revset},
+            'Installing from {revset}')
+
+        url = None
+        with self._task_cache as task_cache, self._pushhead_cache as pushhead_cache:
+            # with blocks handle handle persistence.
+            for pushhead in pushhead_cache.pushheads(self._tree, revset):
+                try:
+                    url = task_cache.artifact_url(self._tree, self._job, pushhead)
+                    break
+                except ValueError:
+                    pass
+        if url:
+            return self.install_from_url(url, distdir)
+        return 1
+
+    def install_from(self, source, distdir):
+        if source and os.path.isfile(source):
+            return self.install_from_file(source, distdir)
+        elif source and urlparse.urlparse(source).scheme:
+            return self.install_from_url(source, distdir)
+        else:
+            return self.install_from_hg(source, distdir)
+
+    def print_last(self):
+        self.log(logging.INFO, 'artifact',
+            {},
+            'Printing last used artifact details.')
+        self._pushhead_cache.print_last()
+        self._task_cache.print_last()
+        self._artifact_cache.print_last()
+
+    def clear_cache(self):
+        self.log(logging.INFO, 'artifact',
+            {},
+            'Deleting cached artifacts and caches.')
+        self._pushhead_cache.clear_cache()
+        self._task_cache.clear_cache()
+        self._artifact_cache.clear_cache()
+
+    def print_cache(self):
+        self.log(logging.INFO, 'artifact',
+            {},
+            'Printing cached artifacts and caches.')
+        self._pushhead_cache.print_cache()
+        self._task_cache.print_cache()
+        self._artifact_cache.print_cache()
--- a/python/mozbuild/mozbuild/backend/android_eclipse.py
+++ b/python/mozbuild/mozbuild/backend/android_eclipse.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import itertools
 import os
 import time
 import types
 import xml.dom.minidom as minidom
 import xml.etree.ElementTree as ET
 
--- a/python/mozbuild/mozbuild/backend/base.py
+++ b/python/mozbuild/mozbuild/backend/base.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 from abc import (
     ABCMeta,
     abstractmethod,
 )
 
 import errno
 import os
--- a/python/mozbuild/mozbuild/backend/common.py
+++ b/python/mozbuild/mozbuild/backend/common.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import itertools
 import json
 import os
 import re
 
 import mozpack.path as mozpath
 import mozwebidlcodegen
--- a/python/mozbuild/mozbuild/backend/configenvironment.py
+++ b/python/mozbuild/mozbuild/backend/configenvironment.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import os
 import sys
 
 from collections import Iterable
 from types import StringTypes
 
 import mozpack.path as mozpath
 
--- a/python/mozbuild/mozbuild/backend/cpp_eclipse.py
+++ b/python/mozbuild/mozbuild/backend/cpp_eclipse.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import errno
 import random
 import os
 import subprocess
 import types
 import xml.etree.ElementTree as ET
 from .common import CommonBackend
 
--- a/python/mozbuild/mozbuild/backend/mach_commands.py
+++ b/python/mozbuild/mozbuild/backend/mach_commands.py
@@ -1,24 +1,20 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
-import glob
-import logging
 import os
 import sys
 import subprocess
 import which
 
-from mozbuild.backend.cpp_eclipse import CppEclipseBackend
-
 from mozbuild.base import (
     MachCommandBase,
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
@@ -98,16 +94,17 @@ class MachCommands(MachCommandBase):
             gradle_dir = None
             if self.is_gradle_project_already_imported():
                 gradle_dir = self.get_gradle_project_path()
             else:
                 gradle_dir = self.get_gradle_import_path()
             process = subprocess.check_call(studio + [gradle_dir])
 
     def get_eclipse_workspace_path(self):
+        from mozbuild.backend.cpp_eclipse import CppEclipseBackend
         return CppEclipseBackend.get_workspace_path(self.topsrcdir, self.topobjdir)
 
     def get_visualstudio_workspace_path(self):
         return os.path.join(self.topobjdir, 'msvc', 'mozilla.sln')
 
     def get_gradle_project_path(self):
         return os.path.join(self.topobjdir, 'mobile', 'android', 'gradle')
 
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -1,16 +1,14 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
-import errno
-import json
 import logging
 import os
 import re
 import types
 
 from collections import (
     defaultdict,
     namedtuple,
--- a/python/mozbuild/mozbuild/backend/visualstudio.py
+++ b/python/mozbuild/mozbuild/backend/visualstudio.py
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # This file contains a build backend for generating Visual Studio project
 # files.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import errno
 import os
 import re
 import types
 import uuid
 
 from xml.dom import getDOMImplementation
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import json
 import logging
 import mozpack.path as mozpath
 import multiprocessing
 import os
 import subprocess
 import sys
@@ -272,17 +272,17 @@ class MozbuildObject(ProcessExecutionMix
         return self.config_environment.substs
 
     @property
     def distdir(self):
         return os.path.join(self.topobjdir, 'dist')
 
     @property
     def bindir(self):
-        import mozinfo
+        from . import mozinfo
         if mozinfo.os == "mac":
             return os.path.join(self.topobjdir, 'dist', self.substs['MOZ_MACBUNDLE_NAME'], 'Contents', 'Resources')
         return os.path.join(self.topobjdir, 'dist', 'bin')
 
     @property
     def includedir(self):
         return os.path.join(self.topobjdir, 'dist', 'include')
 
@@ -759,16 +759,32 @@ class MachCommandConditions(object):
 
     @staticmethod
     def is_android(cls):
         """Must have an Android build."""
         if hasattr(cls, 'substs'):
             return cls.substs.get('MOZ_WIDGET_TOOLKIT') == 'android'
         return False
 
+    @staticmethod
+    def is_hg(cls):
+        """Must have a mercurial source checkout."""
+        if hasattr(cls, 'substs'):
+            top_srcdir = cls.substs.get('top_srcdir')
+            return top_srcdir and os.path.isdir(os.path.join(top_srcdir, '.hg'))
+        return False
+
+    @staticmethod
+    def is_git(cls):
+        """Must have a git source checkout."""
+        if hasattr(cls, 'substs'):
+            top_srcdir = cls.substs.get('top_srcdir')
+            return top_srcdir and os.path.isdir(os.path.join(top_srcdir, '.git'))
+        return False
+
 
 class PathArgument(object):
     """Parse a filesystem path argument and transform it in various ways."""
 
     def __init__(self, arg, topsrcdir, topobjdir, cwd=None):
         self.arg = arg
         self.topsrcdir = topsrcdir
         self.topobjdir = topobjdir
--- a/python/mozbuild/mozbuild/codecoverage/packager.py
+++ b/python/mozbuild/mozbuild/codecoverage/packager.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
 
 import argparse
 import sys
 
 from mozpack.files import FileFinder
 from mozpack.copier import Jarrer
 
 def package_gcno_tree(root, output_file):
--- a/python/mozbuild/mozbuild/compilation/codecomplete.py
+++ b/python/mozbuild/mozbuild/compilation/codecomplete.py
@@ -1,14 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # This modules provides functionality for dealing with code completion.
 
+from __future__ import absolute_import
+
 import os
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
--- a/python/mozbuild/mozbuild/compilation/warnings.py
+++ b/python/mozbuild/mozbuild/compilation/warnings.py
@@ -1,15 +1,15 @@
 # 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/.
 
 # This modules provides functionality for dealing with compiler warnings.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import errno
 import json
 import os
 import re
 
 from mozbuild.util import hash_file
 
--- a/python/mozbuild/mozbuild/config_status.py
+++ b/python/mozbuild/mozbuild/config_status.py
@@ -1,17 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # Combined with build/autoconf/config.status.m4, ConfigStatus is an almost
 # drop-in replacement for autoconf 2.13's config.status, with features
 # borrowed from autoconf > 2.5, and additional features.
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
 
 import logging
 import os
 import sys
 
 from optparse import OptionParser
 
 from mach.logging import LoggingManager
--- a/python/mozbuild/mozbuild/configure/check_debug_ranges.py
+++ b/python/mozbuild/mozbuild/configure/check_debug_ranges.py
@@ -1,16 +1,18 @@
 # 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/.
 
 # This script returns the number of items for the DW_AT_ranges corresponding
 # to a given compilation unit. This is used as a helper to find a bug in some
 # versions of GNU ld.
 
+from __future__ import absolute_import
+
 import subprocess
 import sys
 import re
 
 def get_range_for(compilation_unit, debug_info):
     '''Returns the range offset for a given compilation unit
        in a given debug_info.'''
     name = ranges = ''
--- a/python/mozbuild/mozbuild/configure/libstdcxx.py
+++ b/python/mozbuild/mozbuild/configure/libstdcxx.py
@@ -8,16 +8,18 @@
 # with 8 bits per element. For example, GLIBCXX_3.4.10 becomes
 # 3 << 16 | 4 << 8 | 10 = 197642. This format is easy to use
 # in the C preprocessor.
 
 # We find out both the host and target versions. Since the output
 # will be used from shell, we just print the two assignments and evaluate
 # them from shell.
 
+from __future__ import absolute_import
+
 import os
 import subprocess
 import re
 
 re_for_ld = re.compile('.*\((.*)\).*')
 
 def parse_readelf_line(x):
     """Return the version from a readelf line that looks like:
--- a/python/mozbuild/mozbuild/controller/building.py
+++ b/python/mozbuild/mozbuild/controller/building.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import getpass
 import json
 import logging
 import os
 import subprocess
 import sys
 import time
--- a/python/mozbuild/mozbuild/controller/clobber.py
+++ b/python/mozbuild/mozbuild/controller/clobber.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
 
 r'''This module contains code for managing clobbering of the tree.'''
 
 import os
 import sys
 
 from mozfile.mozfile import rmtree
 from textwrap import TextWrapper
--- a/python/mozbuild/mozbuild/doctor.py
+++ b/python/mozbuild/mozbuild/doctor.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import os
 import subprocess
 import sys
 
 import psutil
 
 from distutils.util import strtobool
 from distutils.version import LooseVersion
--- a/python/mozbuild/mozbuild/dotproperties.py
+++ b/python/mozbuild/mozbuild/dotproperties.py
@@ -1,21 +1,19 @@
 # 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/.
 
 # This file contains utility functions for reading .properties files, like
 # region.properties.
 
-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import codecs
 import re
-import os
 import sys
 
 if sys.version_info[0] == 3:
     str_type = str
 else:
     str_type = basestring
 
 class DotProperties:
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -9,17 +9,17 @@
 r"""This module contains the data structure (context) holding the configuration
 from a moz.build. The data emitted by the frontend derives from those contexts.
 
 It also defines the set of variables and functions available in moz.build.
 If you are looking for the absolute authority on what moz.build files can
 contain, you've come to the right place.
 """
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import os
 
 from collections import (
     Counter,
     OrderedDict,
 )
 from mozbuild.util import (
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -10,21 +10,18 @@ structures are defined in this module.
 All data structures of interest are children of the TreeMetadata class.
 
 Logic for populating these data structures is not defined in this class.
 Instead, what we have here are dumb container classes. The emitter module
 contains the code for converting executed mozbuild files into these data
 structures.
 """
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
-import os
-
-from collections import OrderedDict
 from mozbuild.util import (
     shell_quote,
     StrictOrderingOnAppendList,
 )
 import mozpack.path as mozpath
 from .context import FinalTargetValue
 
 from ..util import (
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -1,16 +1,15 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import itertools
-import json
 import logging
 import os
 import traceback
 import sys
 import time
 
 from collections import defaultdict, OrderedDict
 from mach.mixin.logging import LoggingMixin
--- a/python/mozbuild/mozbuild/frontend/gyp_reader.py
+++ b/python/mozbuild/mozbuild/frontend/gyp_reader.py
@@ -1,18 +1,18 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
+
 import gyp
 import sys
 import time
 import os
-from itertools import chain
 import mozpack.path as mozpath
 from mozpack.files import FileFinder
 from .sandbox import alphabetical_sorted
 from .context import (
     SourcePath,
     TemplateContext,
     VARIABLES,
 )
--- a/python/mozbuild/mozbuild/frontend/mach_commands.py
+++ b/python/mozbuild/mozbuild/frontend/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 from collections import defaultdict
 import os
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
--- a/python/mozbuild/mozbuild/frontend/reader.py
+++ b/python/mozbuild/mozbuild/frontend/reader.py
@@ -11,17 +11,17 @@ In terms of code architecture, the main 
 starts with a root mozbuild file. It creates a new execution environment for
 this file, which is represented by the Sandbox class. The Sandbox class is used
 to fill a Context, representing the output of an individual mozbuild file. The
 
 The BuildReader contains basic logic for traversing a tree of mozbuild files.
 It does this by examining specific variables populated during execution.
 """
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import ast
 import inspect
 import logging
 import os
 import sys
 import textwrap
 import time
--- a/python/mozbuild/mozbuild/frontend/sandbox.py
+++ b/python/mozbuild/mozbuild/frontend/sandbox.py
@@ -12,17 +12,17 @@ code and is used to fill a Context insta
 the execution.
 
 Code in this module takes a different approach to exception handling compared
 to what you'd see elsewhere in Python. Arguments to built-in exceptions like
 KeyError are machine parseable. This machine-friendly data is used to present
 user-friendly error messages in the case of errors.
 """
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import os
 import sys
 import weakref
 
 from mozbuild.util import ReadOnlyDict
 from .context import Context
 from mozpack.files import FileFinder
--- a/python/mozbuild/mozbuild/html_build_viewer.py
+++ b/python/mozbuild/mozbuild/html_build_viewer.py
@@ -1,15 +1,15 @@
 # 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/.
 
 # This module contains code for running an HTTP server to view build info.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import BaseHTTPServer
 import json
 import os
 
 
 class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     def do_GET(self):
--- a/python/mozbuild/mozbuild/jar.py
+++ b/python/mozbuild/mozbuild/jar.py
@@ -3,16 +3,18 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 '''jarmaker.py provides a python class to package up chrome content by
 processing jar.mn files.
 
 See the documentation for jar.mn on MDC for further details on the format.
 '''
 
+from __future__ import absolute_import
+
 import sys
 import os
 import errno
 import re
 import logging
 from time import localtime
 from MozZipFile import ZipFile
 from cStringIO import StringIO
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -1,16 +1,15 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
-import itertools
 import json
 import logging
 import operator
 import os
 import subprocess
 import sys
 
 import mozpack.path as mozpath
--- a/python/mozbuild/mozbuild/makeutil.py
+++ b/python/mozbuild/mozbuild/makeutil.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import os
 import re
 from types import StringTypes
 from collections import Iterable
 
 
 class Makefile(object):
     '''Provides an interface for writing simple makefiles
--- a/python/mozbuild/mozbuild/milestone.py
+++ b/python/mozbuild/mozbuild/milestone.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
 import os
 import re
 import sys
 
 
 def get_milestone_ab_with_num(milestone):
--- a/python/mozbuild/mozbuild/mozconfig.py
+++ b/python/mozbuild/mozbuild/mozconfig.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import filecmp
 import os
 import re
 import subprocess
 
 from collections import defaultdict
 from mach.mixin.process import ProcessExecutionMixin
--- a/python/mozbuild/mozbuild/mozinfo.py
+++ b/python/mozbuild/mozbuild/mozinfo.py
@@ -1,15 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # This module produces a JSON file that provides basic build info and
 # configuration metadata.
 
+from __future__ import absolute_import
+
 import os
 import re
 import json
 import mozbuild.mozconfig as mozconfig
 
 def build_dict(config, env=os.environ):
     """
     Build a dict containing data about the build configuration from
--- a/python/mozbuild/mozbuild/preprocessor.py
+++ b/python/mozbuild/mozbuild/preprocessor.py
@@ -19,17 +19,16 @@ unary :
 value :
   [0-9]+ # integer
   | 'defined(' \w+ ')'
   | \w+  # string identifier or value;
 """
 
 import sys
 import os
-import os.path
 import re
 from optparse import OptionParser
 import errno
 from makeutil import Makefile
 
 # hack around win32 mangling our line endings
 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65443
 if sys.platform == "win32":
--- a/python/mozbuild/mozbuild/pythonutil.py
+++ b/python/mozbuild/mozbuild/pythonutil.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import os
 import sys
 
 
 def iter_modules_in_path(*paths):
     paths = [os.path.abspath(os.path.normcase(p)) + os.sep
              for p in paths]
     for name, module in sys.modules.items():
--- a/python/mozbuild/mozbuild/testing.py
+++ b/python/mozbuild/mozbuild/testing.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import json
 import os
 
 import mozpack.path as mozpath
 
 from .base import MozbuildObject
 from .util import OrderedDefaultDict
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -1,19 +1,18 @@
 # 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/.
 
 # This file contains miscellaneous utility functions that don't belong anywhere
 # in particular.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import collections
-import copy
 import difflib
 import errno
 import functools
 import hashlib
 import itertools
 import os
 import stat
 import sys
@@ -114,21 +113,22 @@ class FileAvoidWrite(StringIO):
     it. When we close the file object, if the content in the in-memory buffer
     differs from what is on disk, then we write out the new content. Otherwise,
     the original file is untouched.
 
     Instances can optionally capture diffs of file changes. This feature is not
     enabled by default because it a) doesn't make sense for binary files b)
     could add unwanted overhead to calls.
     """
-    def __init__(self, filename, capture_diff=False):
+    def __init__(self, filename, capture_diff=False, mode='rU'):
         StringIO.__init__(self)
         self.name = filename
         self._capture_diff = capture_diff
         self.diff = None
+        self.mode = mode
 
     def close(self):
         """Stop accepting writes, compare file contents, and rewrite if needed.
 
         Returns a tuple of bools indicating what action was performed:
 
             (file existed, file updated)
 
@@ -137,17 +137,17 @@ class FileAvoidWrite(StringIO):
         of the result.
         """
         buf = self.getvalue()
         StringIO.close(self)
         existed = False
         old_content = None
 
         try:
-            existing = open(self.name, 'rU')
+            existing = open(self.name, self.mode)
             existed = True
         except IOError:
             pass
         else:
             try:
                 old_content = existing.read()
                 if old_content == buf:
                     return True, False
--- a/python/mozbuild/mozbuild/virtualenv.py
+++ b/python/mozbuild/mozbuild/virtualenv.py
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # This file contains code for populating the virtualenv environment for
 # Mozilla's build system. It is typically called as part of configure.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import distutils.sysconfig
 import os
 import shutil
 import subprocess
 import sys
 import warnings
 
--- a/python/mozbuild/mozpack/chrome/flags.py
+++ b/python/mozbuild/mozpack/chrome/flags.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import re
 from distutils.version import LooseVersion
 from mozpack.errors import errors
 from collections import OrderedDict
 
 
 class Flag(object):
     '''
--- a/python/mozbuild/mozpack/chrome/manifest.py
+++ b/python/mozbuild/mozpack/chrome/manifest.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import re
 import os
 from urlparse import urlparse
 import mozpack.path as mozpath
 from mozpack.chrome.flags import Flags
 from mozpack.errors import errors
 
 
--- a/python/mozbuild/mozpack/copier.py
+++ b/python/mozbuild/mozpack/copier.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import os
 import stat
 
 from mozpack.errors import errors
 from mozpack.files import (
     BaseFile,
     Dest,
 )
--- a/python/mozbuild/mozpack/errors.py
+++ b/python/mozbuild/mozpack/errors.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import sys
 from contextlib import contextmanager
 
 
 class ErrorMessage(Exception):
     '''Exception type raised from errors.error() and errors.fatal()'''
 
 
--- a/python/mozbuild/mozpack/executables.py
+++ b/python/mozbuild/mozpack/executables.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import os
 import struct
 import subprocess
 from mozpack.errors import errors
 
 MACHO_SIGNATURES = [
     0xfeedface,  # mach-o 32-bits big endian
     0xcefaedfe,  # mach-o 32-bits little endian
--- a/python/mozbuild/mozpack/files.py
+++ b/python/mozbuild/mozpack/files.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import errno
 import os
 import platform
 import shutil
 import stat
 import subprocess
 import uuid
 import mozbuild.makeutil as makeutil
--- a/python/mozbuild/mozpack/hg.py
+++ b/python/mozbuild/mozpack/hg.py
@@ -22,16 +22,18 @@
 # modification of the file, and distribution when not combined with
 # mozbuild.)
 #
 # If you modify this code, you may extend this exception to your
 # version of the code, but you are not obliged to do so. If you
 # do not wish to do so, delete this exception statement from your
 # version.
 
+from __future__ import absolute_import
+
 import mercurial.error as error
 import mercurial.hg as hg
 import mercurial.ui as hgui
 
 from .files import (
     BaseFinder,
     MercurialFile,
 )
--- a/python/mozbuild/mozpack/manifests.py
+++ b/python/mozbuild/mozpack/manifests.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 from contextlib import contextmanager
 import json
 
 from .files import (
     AbsoluteSymlinkFile,
     ExistingFile,
     File,
--- a/python/mozbuild/mozpack/mozjar.py
+++ b/python/mozbuild/mozpack/mozjar.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 from io import BytesIO
 import struct
 import zlib
 import os
 from zipfile import (
     ZIP_STORED,
     ZIP_DEFLATED,
 )
--- a/python/mozbuild/mozpack/packager/__init__.py
+++ b/python/mozbuild/mozpack/packager/__init__.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 from mozbuild.preprocessor import Preprocessor
 import re
 import os
 from mozpack.errors import errors
 from mozpack.chrome.manifest import (
     Manifest,
     ManifestChrome,
     ManifestInterfaces,
--- a/python/mozbuild/mozpack/packager/formats.py
+++ b/python/mozbuild/mozpack/packager/formats.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 from mozpack.chrome.manifest import (
     Manifest,
     ManifestInterfaces,
     ManifestChrome,
     ManifestBinaryComponent,
     ManifestResource,
 )
 from urlparse import urlparse
--- a/python/mozbuild/mozpack/packager/l10n.py
+++ b/python/mozbuild/mozpack/packager/l10n.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 '''
 Replace localized parts of a packaged directory with data from a langpack
 directory.
 '''
 
 import os
 import mozpack.path as mozpath
 from mozpack.packager.formats import (
--- a/python/mozbuild/mozpack/packager/unpack.py
+++ b/python/mozbuild/mozpack/packager/unpack.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import mozpack.path as mozpath
 from mozpack.files import (
     FileFinder,
     DeflatedFile,
     ManifestFile,
 )
 from mozpack.chrome.manifest import (
     parse_manifest,
--- a/python/mozbuild/mozpack/path.py
+++ b/python/mozbuild/mozpack/path.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import posixpath
 import os
 import re
 
 '''
 Like os.path, with a reduced set of functions, and with normalized path
 separators (always use forward slashes).
 Also contains a few additional utilities not found in os.path.
--- a/python/mozbuild/mozpack/unify.py
+++ b/python/mozbuild/mozpack/unify.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 from mozpack.files import (
     BaseFinder,
     JarFinder,
     ExecutableFile,
     BaseFile,
     GeneratedFile,
 )
 from mozpack.executables import (
--- a/services/common/tests/mach_commands.py
+++ b/services/common/tests/mach_commands.py
@@ -1,20 +1,15 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
-import logging
 import mozpack.path as mozpath
-import os
-import sys
-import warnings
-import which
 
 from mozbuild.base import (
     MachCommandBase,
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
--- a/testing/luciddream/mach_commands.py
+++ b/testing/luciddream/mach_commands.py
@@ -1,19 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # Integrates luciddream test runner with mach.
 
+from __future__ import absolute_import
+
 import os
-import re
-import sys
-
-import mozpack.path as mozpath
 
 from mozbuild.base import (
     MachCommandBase,
     MachCommandConditions as conditions,
     MozbuildObject,
 )
 
 from mach.decorators import (
--- a/testing/mach_commands.py
+++ b/testing/mach_commands.py
@@ -1,25 +1,23 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import os
-import pprint
 import sys
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
-from autotry import AutoTry
 from mozbuild.base import MachCommandBase
 
 
 UNKNOWN_TEST = '''
 I was unable to find tests in the argument(s) given.
 
 You need to specify a test directory, filename, test suite name, or
 abbreviation.
@@ -450,16 +448,18 @@ class PushToTry(MachCommandBase):
 
         The command requires either its own mercurial extension ("push-to-try",
         installable from mach mercurial-setup) or a git repo using git-cinnabar
         (available at https://github.com/glandium/git-cinnabar).
         """
 
         from mozbuild.testing import TestResolver
         from mozbuild.controller.building import BuildDriver
+        from autotry import AutoTry
+        import pprint
 
         print("mach try is under development, please file bugs blocking 1149670.")
 
         builds, platforms = self.validate_args(paths, tests, builds, platforms)
         resolver = self._spawn(TestResolver)
 
         at = AutoTry(self.topsrcdir, resolver, self._mach_context)
         if at.find_uncommited_changes():
--- a/testing/marionette/mach_commands.py
+++ b/testing/marionette/mach_commands.py
@@ -1,15 +1,14 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
-import imp
 import os
 import sys
 import argparse
 
 from mozlog.structured import commandline
 
 from mozbuild.base import (
     MachCommandBase,
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 from argparse import Namespace
 from collections import defaultdict
 from itertools import chain
 import logging
 import os
 import shutil
 import sys
--- a/testing/mozbase/mozfile/mozfile/__init__.py
+++ b/testing/mozbase/mozfile/mozfile/__init__.py
@@ -1,5 +1,7 @@
 # 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/.
 
-from mozfile import *
+from __future__ import absolute_import
+
+from .mozfile import *
--- a/testing/mozbase/mozfile/mozfile/mozfile.py
+++ b/testing/mozbase/mozfile/mozfile/mozfile.py
@@ -1,16 +1,18 @@
 # -*- coding: utf-8 -*-
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # We don't import all modules at the top for performance reasons. See Bug 1008943
 
+from __future__ import absolute_import
+
 from contextlib import contextmanager
 import errno
 import os
 import stat
 import time
 import warnings
 
 __all__ = ['extract_tarball',
--- a/testing/mozbase/mozinfo/mozinfo/__init__.py
+++ b/testing/mozbase/mozinfo/mozinfo/__init__.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 """
 interface to transform introspected system information to a format palatable to
 Mozilla
 
 Module variables:
 
 .. attribute:: bits
 
@@ -46,11 +48,12 @@ Module variables:
 
    * :attr:`bits`
    * :attr:`os`
    * :attr:`processor`
    * :attr:`version`
 
 """
 
-import mozinfo
-from mozinfo import *
+from . import mozinfo
+from .mozinfo import *
+
 __all__ = mozinfo.__all__
--- a/testing/mozbase/mozinfo/mozinfo/mozinfo.py
+++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py
@@ -3,16 +3,18 @@
 # 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/.
 
 # TODO: it might be a good idea of adding a system name (e.g. 'Ubuntu' for
 # linux) to the information; I certainly wouldn't want anyone parsing this
 # information and having behaviour depend on it
 
+from __future__ import absolute_import
+
 import os
 import platform
 import re
 import sys
 
 # keep a copy of the os module since updating globals overrides this
 _os = os
 
--- a/testing/mozbase/mozprocess/mozprocess/__init__.py
+++ b/testing/mozbase/mozprocess/mozprocess/__init__.py
@@ -1,5 +1,7 @@
 # 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/.
 
-from processhandler import *
+from __future__ import absolute_import
+
+from .processhandler import *
--- a/testing/mozbase/mozprocess/mozprocess/pid.py
+++ b/testing/mozbase/mozprocess/mozprocess/pid.py
@@ -1,15 +1,16 @@
 #!/usr/bin/env python
 
 # 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/.
 
-import os
+from __future__ import absolute_import
+
 import mozinfo
 import shlex
 import subprocess
 import sys
 
 # determine the platform-specific invocation of `ps`
 if mozinfo.isWin:
     psarg='ax'
@@ -67,17 +68,17 @@ def running_processes(name, psarg=psarg,
             retval.append((int(process['PID']), command))
     return retval
 
 def get_pids(name):
     """Get all the pids matching name"""
 
     if mozinfo.isWin:
         # use the windows-specific implementation
-        import wpk
+        from . import wpk
         return wpk.get_pids(name)
     else:
         return [pid for pid,_ in running_processes(name)]
 
 if __name__ == '__main__':
     pids = set()
     for i in sys.argv[1:]:
         pids.update(get_pids(i))
--- a/testing/mozbase/mozprocess/mozprocess/processhandler.py
+++ b/testing/mozbase/mozprocess/mozprocess/processhandler.py
@@ -1,15 +1,15 @@
 # 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/.
 
+from __future__ import absolute_import
+
 import os
-import re
-import select
 import signal
 import subprocess
 import sys
 import threading
 import time
 import traceback
 from Queue import Queue, Empty
 from datetime import datetime
@@ -20,18 +20,18 @@ MOZPROCESS_DEBUG = os.getenv("MOZPROCESS
 
 # We dont use mozinfo because it is expensive to import, see bug 933558.
 isWin = os.name == "nt"
 isPosix = os.name == "posix" # includes MacOS X
 
 if isWin:
     import ctypes, ctypes.wintypes, msvcrt
     from ctypes import sizeof, addressof, c_ulong, byref, WinError, c_longlong
-    import winprocess
-    from qijo import JobObjectAssociateCompletionPortInformation,\
+    from . import winprocess
+    from .qijo import JobObjectAssociateCompletionPortInformation,\
     JOBOBJECT_ASSOCIATE_COMPLETION_PORT, JobObjectExtendedLimitInformation,\
     JOBOBJECT_BASIC_LIMIT_INFORMATION, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, IO_COUNTERS
 
 class ProcessHandlerMixin(object):
     """
     A class for launching and manipulating local processes.
 
     :param cmd: command to run. May be a string or a list. If specified as a list, the first element will be interpreted as the command, and all additional elements will be interpreted as arguments to that command.
--- a/testing/mozbase/mozprocess/mozprocess/qijo.py
+++ b/testing/mozbase/mozprocess/mozprocess/qijo.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE, addressof, c_size_t, c_ulong
 from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LARGE_INTEGER
 
 LPVOID = c_void_p
 LPDWORD = POINTER(DWORD)
 SIZE_T = c_size_t
 ULONG_PTR = POINTER(c_ulong)
 
--- a/testing/mozbase/mozprocess/mozprocess/winprocess.py
+++ b/testing/mozbase/mozprocess/mozprocess/winprocess.py
@@ -29,19 +29,21 @@
 # THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
 # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
-from ctypes import c_void_p, POINTER, sizeof, Structure, Union, windll, WinError, WINFUNCTYPE, c_ulong
-from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD, ULONG
-from qijo import QueryInformationJobObject
+from __future__ import absolute_import
+
+from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE, c_ulong
+from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD
+from .qijo import QueryInformationJobObject
 
 LPVOID = c_void_p
 LPBYTE = POINTER(BYTE)
 LPDWORD = POINTER(DWORD)
 LPBOOL = POINTER(BOOL)
 LPULONG = POINTER(c_ulong)
 
 def ErrCheckBool(result, func, args):
--- a/testing/mozbase/mozprocess/mozprocess/wpk.py
+++ b/testing/mozbase/mozprocess/mozprocess/wpk.py
@@ -1,12 +1,14 @@
 # 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/.
 
+from __future__ import absolute_import
+
 from ctypes import sizeof, windll, addressof, create_unicode_buffer
 from ctypes.wintypes import DWORD, HANDLE
 
 PROCESS_TERMINATE = 0x0001
 PROCESS_QUERY_INFORMATION = 0x0400
 PROCESS_VM_READ = 0x0010
 
 def get_pids(process_name):
--- a/testing/mozharness/mozharness.json
+++ b/testing/mozharness/mozharness.json
@@ -1,4 +1,4 @@
 {
     "repo": "https://hg.mozilla.org/build/mozharness",
-    "revision": "946d3352e137"
+    "revision": "4135b4ce5aa1"
 }
--- a/testing/talos/mach_commands.py
+++ b/testing/talos/mach_commands.py
@@ -1,15 +1,15 @@
 # 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/.
 
 # Integrates Talos mozharness with mach
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import os
 import sys
 import json
 import which
 import socket
 
 from mozbuild.base import (
--- a/testing/taskcluster/mach_commands.py
+++ b/testing/taskcluster/mach_commands.py
@@ -1,35 +1,27 @@
 # -*- coding: utf-8 -*-
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+from __future__ import absolute_import
+
 import os
-import os.path
 import json
 import copy
-import datetime
 import sys
-import urllib2
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
-from taskcluster_graph.commit_parser import parse_commit
-from taskcluster_graph.slugid import slugid
-from taskcluster_graph.slugidjar import SlugidJar
-from taskcluster_graph.from_now import json_time_from_now, current_json_time
-from taskcluster_graph.templates import Templates
-
-import taskcluster_graph.build_task
 
 ROOT = os.path.dirname(os.path.realpath(__file__))
 GECKO = os.path.realpath(os.path.join(ROOT, '..', '..'))
 DOCKER_ROOT = os.path.join(ROOT, '..', 'docker')
 MOZHARNESS_CONFIG = os.path.join(GECKO, 'testing', 'mozharness', 'mozharness.json')
 
 # XXX: If/when we have the taskcluster queue use construct url instead
 ARTIFACT_URL = 'https://queue.taskcluster.net/v1/task/{}/artifacts/{}'
@@ -60,16 +52,17 @@ def docker_image(name):
     version = open(os.path.join(DOCKER_ROOT, name, 'VERSION')).read().strip()
 
     if os.path.isfile(repository_path):
         repository = open(repository_path).read().strip()
 
     return '{}/{}:{}'.format(repository, name, version)
 
 def get_task(task_id):
+    import urllib2
     return json.load(urllib2.urlopen("https://queue.taskcluster.net/v1/task/" + task_id))
 
 
 def gaia_info():
     '''
     Fetch details from in tree gaia.json (which links this version of
     gecko->gaia) and construct the usual base/head/ref/rev pairing...
     '''
@@ -137,16 +130,23 @@ class DecisionTask(object):
     @CommandArgument('--comment',
         required=True,
         help='Commit message for this revision')
     @CommandArgument('--owner',
         required=True,
         help='email address of who owns this graph')
     @CommandArgument('task', help="Path to decision task to run.")
     def run_task(self, **params):
+        from taskcluster_graph.slugidjar import SlugidJar
+        from taskcluster_graph.from_now import (
+            json_time_from_now,
+            current_json_time,
+        )
+        from taskcluster_graph.templates import Templates
+
         templates = Templates(ROOT)
         # Template parameters used when expanding the graph
         parameters = dict(gaia_info().items() + {
             'source': 'http://todo.com/soon',
             'project': params['project'],
             'comment': params['comment'],
             'url': params['url'],
             'revision': params['revision'],
@@ -188,16 +188,25 @@ class Graph(object):
         required=False,
         default=0)
     @CommandArgument('--owner',
         required=True,
         help='email address of who owns this graph')
     @CommandArgument('--extend-graph',
         action="store_true", dest="ci", help='Omit create graph arguments')
     def create_graph(self, **params):
+        from taskcluster_graph.commit_parser import parse_commit
+        from taskcluster_graph.slugid import slugid
+        from taskcluster_graph.from_now import (
+            json_time_from_now,
+            current_json_time,
+        )
+        from taskcluster_graph.templates import Templates
+        import taskcluster_graph.build_task
+
         project = params['project']
         message = params.get('message', '') if project == 'try' else DEFAULT_TRY
 
         # Message would only be blank when not created from decision task
         if project == 'try' and not message:
             sys.stderr.write(
                     "Must supply commit message when creating try graph. " \
                     "Example: --message='try: -b do -p all -u all'"
@@ -406,16 +415,19 @@ class CIBuild(object):
     @CommandArgument('--mozharness-rev',
         help='Commit revision to use from mozharness repository')
     @CommandArgument('--owner',
         default='foobar@mozilla.com',
         help='email address of who owns this graph')
     @CommandArgument('build_task',
         help='path to build task definition')
     def create_ci_build(self, **params):
+        from taskcluster_graph.templates import Templates
+        import taskcluster_graph.build_task
+
         templates = Templates(ROOT)
         # TODO handle git repos
         head_repository = params['head_repository']
         if not head_repository:
             head_repository = get_hg_url()
 
         head_rev = params['head_rev']
         if not head_rev:
--- a/testing/web-platform/mach_commands.py
+++ b/testing/web-platform/mach_commands.py
@@ -1,15 +1,15 @@
 # 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/.
 
 # Integrates the web-platform-tests test runner with mach.
 
-from __future__ import unicode_literals, print_function
+from __future__ import absolute_import, unicode_literals, print_function
 
 import os
 import sys
 
 from mozbuild.base import (
     MachCommandBase,
     MachCommandConditions as conditions,
     MozbuildObject,
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -1,21 +1,20 @@
 # 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/.
 
 # Integrates the xpcshell test runner with mach.
 
-from __future__ import unicode_literals, print_function
+from __future__ import absolute_import, unicode_literals, print_function
 
 import argparse
 import os
 import shutil
 import sys
-import urllib2
 
 from mozlog import structured
 
 from mozbuild.base import (
     MachCommandBase,
     MozbuildObject,
     MachCommandConditions as conditions,
 )
@@ -317,16 +316,18 @@ class B2GXPCShellRunner(MozbuildObject):
         if build_path not in sys.path:
             sys.path.append(build_path)
 
         self.tests_dir = os.path.join(self.topobjdir, '_tests')
         self.xpcshell_dir = os.path.join(self.tests_dir, 'xpcshell')
         self.bin_dir = os.path.join(self.distdir, 'bin')
 
     def _download_busybox(self, b2g_home, emulator):
+        import urllib2
+
         target_device = 'generic'
         if emulator == 'x86':
             target_device = 'generic_x86'
         system_bin = os.path.join(b2g_home, 'out', 'target', 'product', target_device, 'system', 'bin')
         busybox_path = os.path.join(system_bin, 'busybox')
 
         if os.path.isfile(busybox_path):
             return busybox_path
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -23,16 +23,17 @@ const PREF_DELAY =                  [ "d
 const PREF_BEHAVIOR =               [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
 const PREF_FILTER_JS =              [ "filter.javascript",      true ];
 const PREF_MAXRESULTS =             [ "maxRichResults",         25 ];
 const PREF_RESTRICT_HISTORY =       [ "restrict.history",       "^" ];
 const PREF_RESTRICT_BOOKMARKS =     [ "restrict.bookmark",      "*" ];
 const PREF_RESTRICT_TYPED =         [ "restrict.typed",         "~" ];
 const PREF_RESTRICT_TAG =           [ "restrict.tag",           "+" ];
 const PREF_RESTRICT_SWITCHTAB =     [ "restrict.openpage",      "%" ];
+const PREF_RESTRICT_SEARCHES =      [ "restrict.searces",       "$" ];
 const PREF_MATCH_TITLE =            [ "match.title",            "#" ];
 const PREF_MATCH_URL =              [ "match.url",              "@" ];
 
 const PREF_SUGGEST_HISTORY =        [ "suggest.history",        true ];
 const PREF_SUGGEST_BOOKMARK =       [ "suggest.bookmark",       true ];
 const PREF_SUGGEST_OPENPAGE =       [ "suggest.openpage",       true ];
 const PREF_SUGGEST_HISTORY_ONLYTYPED = [ "suggest.history.onlyTyped", false ];
 const PREF_SUGGEST_SEARCHES =       [ "suggest.searches",       true ];
@@ -398,16 +399,17 @@ XPCOMUtils.defineLazyGetter(this, "Prefs
     store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
     store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
     store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
     store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
     store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
     store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
     store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
     store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB);
+    store.restrictSearchesToken = prefs.get(...PREF_RESTRICT_SEARCHES);
     store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE);
     store.matchURLToken = prefs.get(...PREF_MATCH_URL);
     store.suggestHistory = prefs.get(...PREF_SUGGEST_HISTORY);
     store.suggestBookmark = prefs.get(...PREF_SUGGEST_BOOKMARK);
     store.suggestOpenpage = prefs.get(...PREF_SUGGEST_OPENPAGE);
     store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED);
     store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES);
 
@@ -444,17 +446,18 @@ XPCOMUtils.defineLazyGetter(this, "Prefs
 
     store.tokenToBehaviorMap = new Map([
       [ store.restrictHistoryToken, "history" ],
       [ store.restrictBookmarkToken, "bookmark" ],
       [ store.restrictTagToken, "tag" ],
       [ store.restrictOpenPageToken, "openpage" ],
       [ store.matchTitleToken, "title" ],
       [ store.matchURLToken, "url" ],
-      [ store.restrictTypedToken, "typed" ]
+      [ store.restrictTypedToken, "typed" ],
+      [ store.restrictSearchesToken, "searches" ],
     ]);
 
     // Synchronize suggest.* prefs with autocomplete.enabled every time
     // autocomplete.enabled is changed.
     if (data == PREF_BRANCH + PREF_ENABLED[0]) {
       syncEnabledPref();
     }
   }
@@ -832,20 +835,25 @@ Search.prototype = {
     if (!this.pending)
       return;
 
     // Start fetching search suggestions a little earlier than Prefs.delay since
     // they're remote and will probably take longer to arrive.
     if (this.hasBehavior("searches")) {
       this._searchSuggestionController =
         PlacesSearchAutocompleteProvider.getSuggestionController(
-          this._originalSearchString,
+          this._searchTokens.join(" "),
           this._inPrivateWindow,
           Prefs.maxRichResults
         );
+      if (this.hasBehavior("restrict")) {
+        // We're done if we're restricting to search suggestions.
+        yield this._consumeAllSearchSuggestions();
+        return;
+      }
     }
 
     yield this._sleep(Math.round(Prefs.delay / 2));
     if (!this.pending)
       return;
 
     for (let [query, params] of queries) {
       let hasResult = yield conn.executeCached(query, params, this._onResultRow.bind(this));
@@ -872,16 +880,20 @@ Search.prototype = {
         yield conn.executeCached(query, params, this._onResultRow.bind(this));
         if (!this.pending)
           return;
       }
     }
 
     // If we still don't have enough results, fill the remaining space with
     // search suggestions.
+    yield this._consumeAllSearchSuggestions();
+  }),
+
+  _consumeAllSearchSuggestions: Task.async(function* () {
     if (this._searchSuggestionController && this.pending) {
       yield this._searchSuggestionController.fetchCompletePromise;
       while (this.pending && this._maybeAddSearchSuggestionMatch());
     }
   }),
 
   _matchKnownUrl: function* (conn, queries) {
     // Hosts have no "/" in them.
--- a/toolkit/components/places/tests/unifiedcomplete/test_searchSuggestions.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_searchSuggestions.js
@@ -1,13 +1,14 @@
 Cu.import("resource://gre/modules/FormHistory.jsm");
 
 const ENGINE_NAME = "engine-suggestions.xml";
 const SERVER_PORT = 9000;
 const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_RESTRICT_TOKEN = "$";
 
 // Set this to some other function to change how the server converts search
 // strings into suggestions.
 let suggestionsFromSearchString = searchStr => {
   let suffixes = ["foo", "bar"];
   return suffixes.map(s => searchStr + " " + s);
 };
 
@@ -140,8 +141,103 @@ add_task(function* suffixMatch() {
       style: ["action", "searchengine"],
       icon: "",
     }],
   });
 
   suggestionsFromSearchString = oldFn;
   yield cleanup();
 });
+
+add_task(function* restrictToken() {
+  Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+  // Add a visit and a bookmark.  Actually, make the bookmark visited too so
+  // that it's guaranteed, with its higher frecency, to appear above the search
+  // suggestions.
+  yield PlacesTestUtils.addVisits([
+    {
+      uri: NetUtil.newURI("http://example.com/hello-visit"),
+      title: "hello visit",
+    },
+    {
+      uri: NetUtil.newURI("http://example.com/hello-bookmark"),
+      title: "hello bookmark",
+    },
+  ]);
+
+  yield addBookmark({
+    uri: NetUtil.newURI("http://example.com/hello-bookmark"),
+    title: "hello bookmark",
+  });
+
+  // Do an unrestricted search to make sure everything appears in it, including
+  // the visit and bookmark.
+  let searchStr = "hello";
+  yield check_autocomplete({
+    search: searchStr,
+    matches: [
+      {
+        uri: NetUtil.newURI("http://example.com/hello-visit"),
+        title: "hello visit",
+      },
+      {
+        uri: NetUtil.newURI("http://example.com/hello-bookmark"),
+        title: "hello bookmark",
+        style: ["bookmark"],
+      },
+      {
+        uri: makeActionURI(("searchengine"), {
+          engineName: ENGINE_NAME,
+          input: searchStr,
+          searchQuery: searchStr,
+          searchSuggestion: "hello foo",
+        }),
+        title: ENGINE_NAME,
+        style: ["action", "searchengine"],
+        icon: "",
+      },
+      {
+        uri: makeActionURI(("searchengine"), {
+          engineName: ENGINE_NAME,
+          input: searchStr,
+          searchQuery: searchStr,
+          searchSuggestion: "hello bar",
+        }),
+        title: ENGINE_NAME,
+        style: ["action", "searchengine"],
+        icon: "",
+      },
+    ],
+  });
+
+  // Now do a restricted search to make sure only suggestions appear.
+  searchStr = SUGGEST_RESTRICT_TOKEN + " hello";
+  yield check_autocomplete({
+    search: searchStr,
+    matches: [
+      {
+        uri: makeActionURI(("searchengine"), {
+          engineName: ENGINE_NAME,
+          input: searchStr,
+          searchQuery: searchStr,
+          searchSuggestion: "hello foo",
+        }),
+        title: ENGINE_NAME,
+        style: ["action", "searchengine"],
+        icon: "",
+      },
+      {
+        uri: makeActionURI(("searchengine"), {
+          engineName: ENGINE_NAME,
+          input: searchStr,
+          searchQuery: searchStr,
+          searchSuggestion: "hello bar",
+        }),
+        title: ENGINE_NAME,
+        style: ["action", "searchengine"],
+        icon: "",
+      }
+    ],
+  });
+
+  yield cleanup();
+});
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -2453,17 +2453,31 @@ ObjectClient.prototype = {
   }, {
     before: function(aPacket) {
       if (this._grip.class !== "Promise") {
         throw new Error("getDependentPromises is only valid for promise " +
           "grips.");
       }
       return aPacket;
     }
-  })
+  }),
+
+  /**
+   * Request the stack to the promise's allocation point.
+   */
+  getPromiseAllocationStack: DebuggerClient.requester({
+    type: "allocationStack"
+  }, {
+    before: function(aPacket) {
+      if (this._grip.class !== "Promise") {
+        throw new Error("getAllocationStack is only valid for promise grips.");
+      }
+      return aPacket;
+    }
+  }),
 };
 
 /**
  * A PropertyIteratorClient provides a way to access to property names and
  * values of an object efficiently, slice by slice.
  * Note that the properties can be sorted in the backend,
  * this is controled while creating the PropertyIteratorClient
  * from ObjectClient.enumProperties.
--- a/toolkit/devtools/server/actors/object.js
+++ b/toolkit/devtools/server/actors/object.js
@@ -539,32 +539,98 @@ ObjectActor.prototype = {
                         "object grips with a 'Promise' class." };
     }
 
     let rawPromise = this.obj.unsafeDereference();
     let promises = PromiseDebugging.getDependentPromises(rawPromise).map(p =>
       this.hooks.createValueGrip(this.obj.makeDebuggeeValue(p)));
 
     return { promises };
+  },
+
+  /**
+   * Handle a protocol request to get the allocation stack of a promise.
+   */
+  onAllocationStack: function() {
+    if (this.obj.class != "Promise") {
+      return { error: "objectNotPromise",
+               message: "'allocationStack' request is only valid for " +
+                        "object grips with a 'Promise' class." };
+    }
+
+    let rawPromise = this.obj.unsafeDereference();
+    let stack = PromiseDebugging.getAllocationStack(rawPromise);
+    let allocationStacks = [];
+
+    while (stack) {
+      if (stack.source) {
+        let source = this._getSourceOriginalLocation(stack);
+
+        if (source) {
+          allocationStacks.push(source);
+        }
+      }
+      stack = stack.parent;
+    }
+
+    return Promise.all(allocationStacks).then(stacks => {
+      return { allocationStack: stacks };
+    });
+  },
+
+  /**
+   * Helper function for onAllocationStack which fetches the source location
+   * for a SavedFrame stack.
+   * @param SavedFrame stack
+   *        The promise allocation stack frame
+   * @return object
+   *         Returns an object containing the source location of the SavedFrame
+   *         stack.
+   */
+  _getSourceOriginalLocation: function(stack) {
+    let source;
+
+    // Catch any errors if the source actor cannot be found
+    try {
+      source = this.hooks.sources().getSourceActorByURL(stack.source);
+    } catch(e) {}
+
+    if (!source) {
+      return null;
+    }
+
+    return this.hooks.sources().getOriginalLocation(new GeneratedLocation(
+      source,
+      stack.line,
+      stack.column
+    )).then((originalLocation) => {
+      return {
+        source: originalLocation.originalSourceActor.form(),
+        line: originalLocation.originalLine,
+        column: originalLocation.originalColumn,
+        functionDisplayName: stack.functionDisplayName
+      };
+    });
   }
 };
 
 ObjectActor.prototype.requestTypes = {
   "definitionSite": ObjectActor.prototype.onDefinitionSite,
   "parameterNames": ObjectActor.prototype.onParameterNames,
   "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties,
   "enumProperties": ObjectActor.prototype.onEnumProperties,
   "prototype": ObjectActor.prototype.onPrototype,
   "property": ObjectActor.prototype.onProperty,
   "displayString": ObjectActor.prototype.onDisplayString,
   "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
   "decompile": ObjectActor.prototype.onDecompile,
   "release": ObjectActor.prototype.onRelease,
   "scope": ObjectActor.prototype.onScope,
-  "dependentPromises": ObjectActor.prototype.onDependentPromises
+  "dependentPromises": ObjectActor.prototype.onDependentPromises,
+  "allocationStack": ObjectActor.prototype.onAllocationStack
 };
 
 /**
  * Creates an actor to iterate over an object's property names and values.
  *
  * @param objectActor ObjectActor
  *        The object actor.
  * @param options Object
--- a/toolkit/devtools/server/actors/promises.js
+++ b/toolkit/devtools/server/actors/promises.js
@@ -2,20 +2,23 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const protocol = require("devtools/server/protocol");
 const { method, RetVal, Arg, types } = protocol;
 const { expectState, ActorPool } = require("devtools/server/actors/common");
-const { ObjectActor, createValueGrip } = require("devtools/server/actors/object");
+const { ObjectActor,
+        createValueGrip } = require("devtools/server/actors/object");
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 
+/* global events */
+
 // Teach protocol.js how to deal with legacy actor types
 types.addType("ObjectActor", {
   write: actor => actor.grip(),
   read: grip => grip
 });
 
 /**
  * The Promises Actor provides support for getting the list of live promises and
@@ -81,16 +84,24 @@ let PromisesActor = protocol.ActorClass(
     this.dbg.addDebuggees();
 
     this._navigationLifetimePool = this._createActorPool();
     this.conn.addActorPool(this._navigationLifetimePool);
 
     this._newPromises = [];
     this._promisesSettled = [];
 
+    this.dbg.findScripts().forEach(s => {
+      this.parent.sources.createSourceActors(s.source);
+    });
+
+    this.dbg.onNewScript = s => {
+      this.parent.sources.createSourceActors(s.source);
+    };
+
     events.on(this.parent, "window-ready", this._onWindowReady);
 
     this.state = "attached";
   }, `attaching to the PromisesActor`), {
     request: {},
     response: {}
   }),
 
@@ -137,18 +148,17 @@ let PromisesActor = protocol.ActorClass(
     }
 
     let actor = new ObjectActor(promise, {
       getGripDepth: () => this._gripDepth,
       incrementGripDepth: () => this._gripDepth++,
       decrementGripDepth: () => this._gripDepth--,
       createValueGrip: v =>
         createValueGrip(v, this._navigationLifetimePool, this.objectGrip),
-      sources: () => DevToolsUtils.reportException("PromisesActor",
-        Error("sources not yet implemented")),
+      sources: () => this.parent.sources,
       createEnvironmentActor: () => DevToolsUtils.reportException(
         "PromisesActor", Error("createEnvironmentActor not yet implemented")),
       getGlobalDebugObject: () => DevToolsUtils.reportException(
         "PromisesActor", Error("getGlobalDebugObject not yet implemented")),
     });
 
     this._navigationLifetimePool.addActor(actor);
     this._navigationLifetimePool.objectActors.set(promise, actor);
@@ -195,27 +205,27 @@ let PromisesActor = protocol.ActorClass(
    *
    * @param array array
    *        The list of Promise ObjectActors to emit
    * @param string eventName
    *        The event name
    */
   _makePromiseEventHandler: function(array, eventName) {
     return promise => {
-      let actor = this._createObjectActorForPromise(promise)
+      let actor = this._createObjectActorForPromise(promise);
       let needsScheduling = array.length == 0;
 
       array.push(actor);
 
       if (needsScheduling) {
         DevToolsUtils.executeSoon(() => {
           events.emit(this, eventName, array.splice(0, array.length));
         });
       }
-    }
+    };
   },
 
   _onWindowReady: expectState("attached", function({ isTopLevel }) {
     if (!isTopLevel) {
       return;
     }
 
     this._navigationLifetimePool.cleanup();
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can get the list of Promise objects that have settled from the
+ * PromisesActor onPromiseSettled event handler.
+ */
+
+"use strict";
+
+const { PromisesFront } = devtools.require("devtools/server/actors/promises");
+
+let events = devtools.require("sdk/event/core");
+
+add_task(function*() {
+  let client = yield startTestDebuggerServer("promises-actor-test");
+  let chromeActors = yield getChromeActors(client);
+
+  ok(Promise.toString().contains("native code"), "Expect native DOM Promise");
+
+  yield testPromisesSettled(client, chromeActors,
+    v => new Promise(resolve => resolve(v)),
+    v => new Promise((resolve, reject) => reject(v)));
+
+  let response = yield listTabs(client);
+  let targetTab = findTab(response.tabs, "promises-actor-test");
+  ok(targetTab, "Found our target tab.");
+
+  yield testPromisesSettled(client, targetTab, v => {
+    const debuggee = DebuggerServer.getTestGlobal("promises-actor-test");
+    return debuggee.Promise.resolve(v);
+  }, v => {
+    const debuggee = DebuggerServer.getTestGlobal("promises-actor-test");
+    return debuggee.Promise.reject(v);
+  });
+
+  yield close(client);
+});
+
+function* testPromisesSettled(client, form, makeResolvePromise,
+    makeRejectPromise) {
+  let front = PromisesFront(client, form);
+  let resolution = "MyLittleSecret" + Math.random();
+
+  yield front.attach();
+  yield front.listPromises();
+
+  let onPromiseSettled = oncePromiseSettled(front, resolution, true, false);
+  let resolvedPromise = makeResolvePromise(resolution);
+  let foundResolvedPromise = yield onPromiseSettled;
+  ok(foundResolvedPromise, "Found our resolved promise");
+
+  onPromiseSettled = oncePromiseSettled(front, resolution, false, true);
+  let rejectedPromise = makeRejectPromise(resolution);
+  let foundRejectedPromise = yield onPromiseSettled;
+  ok(foundRejectedPromise, "Found our rejected promise");
+
+  yield front.detach();
+  // Appease eslint
+  void resolvedPromise;
+  void rejectedPromise;
+}
+
+function oncePromiseSettled(front, resolution, resolveValue, rejectValue) {
+  return new Promise(resolve => {
+    events.on(front, "promises-settled", promises => {
+      for (let p of promises) {
+        equal(p.type, "object", "Expect type to be Object");
+        equal(p.class, "Promise", "Expect class to be Promise");
+        equal(typeof p.promiseState.creationTimestamp, "number",
+          "Expect creation timestamp to be a number");
+        equal(typeof p.promiseState.timeToSettle, "number",
+          "Expect time to settle to be a number");
+
+        if (p.promiseState.state === "fulfilled" &&
+            p.promiseState.value === resolution) {
+          resolve(resolveValue);
+        } else if (p.promiseState.state === "rejected" &&
+                   p.promiseState.reason === resolution) {
+          resolve(rejectValue);
+        } else {
+          dump("Found non-target promise\n");
+        }
+      }
+    });
+  });
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -82,16 +82,17 @@ support-files =
 [test_eval-03.js]
 [test_eval-04.js]
 [test_eval-05.js]
 [test_originAttributesToCookieJar.js]
 [test_promises_actor_attach.js]
 [test_promises_actor_exist.js]
 [test_promises_actor_list_promises.js]
 [test_promises_actor_onnewpromise.js]
+[test_promises_actor_onpromisesettled.js]
 [test_promises_client_getdependentpromises.js]
 [test_promises_object_creationtimestamp.js]
 [test_promises_object_timetosettle-01.js]
 [test_promises_object_timetosettle-02.js]
 [test_protocol_abort.js]
 [test_protocol_async.js]
 [test_protocol_children.js]
 [test_protocol_formtype.js]
--- a/toolkit/mozapps/installer/upload-files.mk
+++ b/toolkit/mozapps/installer/upload-files.mk
@@ -408,16 +408,21 @@ DIST_FILES := $(filter-out $(SO_LIBRARIE
 NON_DIST_FILES += libmozglue.so $(MOZ_CHILD_PROCESS_NAME) $(ASSET_SO_LIBRARIES)
 
 ifdef MOZ_ENABLE_SZIP
 # These libraries are szipped in-place in the
 # assets/$(ANDROID_CPU_ARCH) directory.
 SZIP_LIBRARIES := $(ASSET_SO_LIBRARIES)
 endif
 
+ifndef COMPILE_ENVIRONMENT
+# Any Fennec binary libraries we download are already szipped.
+ALREADY_SZIPPED=1
+endif
+
 # Fennec's OMNIJAR_NAME can include a directory; for example, it might
 # be "assets/omni.ja". This path specifies where the omni.ja file
 # lives in the APK, but should not root the resources it contains
 # under assets/ (i.e., resources should not live at chrome://assets/).
 # packager.py writes /omni.ja in order to be consistent with the
 # layout expected by language repacks. Therefore, we move it to the
 # correct path here, in INNER_MAKE_PACKAGE. See comment about
 # OMNIJAR_NAME in configure.in.
@@ -454,17 +459,17 @@ INNER_MAKE_PACKAGE	= \
   make -C $(GECKO_APP_AP_PATH) gecko-nodeps.ap_ && \
   cp $(GECKO_APP_AP_PATH)/gecko-nodeps.ap_ $(_ABS_DIST)/gecko.ap_ && \
   ( (test ! -f $(GECKO_APP_AP_PATH)/R.txt && echo "*** Warning: The R.txt that is being packaged might not agree with the R.txt that was built. This is normal during l10n repacks.") || \
     diff $(GECKO_APP_AP_PATH)/R.txt $(GECKO_APP_AP_PATH)/gecko-nodeps/R.txt >/dev/null || \
     (echo "*** Error: The R.txt that was built and the R.txt that is being packaged are not the same. Rebuild mobile/android/base and re-package." && exit 1)) && \
   ( cd $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH) && \
     unzip -o $(_ABS_DIST)/gecko.ap_ && \
     rm $(_ABS_DIST)/gecko.ap_ && \
-    $(ZIP) $(if $(MOZ_ENABLE_SZIP),-0 )$(_ABS_DIST)/gecko.ap_ $(ASSET_SO_LIBRARIES) && \
+    $(ZIP) $(if $(ALREADY_SZIPPED),-0 ,$(if $(MOZ_ENABLE_SZIP),-0 ))$(_ABS_DIST)/gecko.ap_ $(ASSET_SO_LIBRARIES) && \
     $(ZIP) -r9D $(_ABS_DIST)/gecko.ap_ $(DIST_FILES) -x $(NON_DIST_FILES) $(SZIP_LIBRARIES) && \
     $(if $(filter-out ./,$(OMNIJAR_DIR)), \
       mkdir -p $(OMNIJAR_DIR) && mv $(OMNIJAR_NAME) $(OMNIJAR_DIR) && ) \
     $(ZIP) -0 $(_ABS_DIST)/gecko.ap_ $(OMNIJAR_DIR)$(OMNIJAR_NAME)) && \
   rm -f $(_ABS_DIST)/gecko.apk && \
   cp $(_ABS_DIST)/gecko.ap_ $(_ABS_DIST)/gecko.apk && \
   $(ZIP) -j0 $(_ABS_DIST)/gecko.apk $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/classes.dex && \
   cp $(_ABS_DIST)/gecko.apk $(_ABS_DIST)/gecko-unsigned-unaligned.apk && \
--- a/tools/docs/mach_commands.py
+++ b/tools/docs/mach_commands.py
@@ -1,24 +1,23 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import os
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
 from mozbuild.base import MachCommandBase
-from mozbuild.frontend.reader import BuildReader
 
 
 @CommandProvider
 class Documentation(MachCommandBase):
     """Helps manage in-tree documentation."""
 
     @Command('build-docs', category='build-dev',
         description='Generate documentation for the tree.')
--- a/tools/mach_commands.py
+++ b/tools/mach_commands.py
@@ -1,19 +1,18 @@
 # 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/.
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import sys
 import os
 import stat
 import platform
-import urllib2
 import errno
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
@@ -214,18 +213,18 @@ class PastebinProvider(object):
                      help='Specify your name for use with pastebin.mozilla.org')
     @CommandArgument('--duration', default='day',
                      choices=['d', 'day', 'm', 'month', 'f', 'forever'],
                      help='Keep for specified duration (default: %(default)s)')
     @CommandArgument('file', nargs='?', default=None,
                      help='Specify the file to upload to pastebin.mozilla.org')
 
     def pastebin(self, language, poster, duration, file):
-        import sys
         import urllib
+        import urllib2
 
         URL = 'https://pastebin.mozilla.org/'
 
         FILE_TYPES = [{'value': 'text', 'name': 'None', 'extension': 'txt'},
         {'value': 'bash', 'name': 'Bash', 'extension': 'sh'},
         {'value': 'c', 'name': 'C', 'extension': 'c'},
         {'value': 'cpp', 'name': 'C++', 'extension': 'cpp'},
         {'value': 'html4strict', 'name': 'HTML', 'extension': 'html'},
@@ -294,16 +293,18 @@ class PastebinProvider(object):
 
 @CommandProvider
 class FormatProvider(MachCommandBase):
     @Command('clang-format', category='misc',
         description='Run clang-format on current changes')
     @CommandArgument('--show', '-s', action = 'store_true',
         help = 'Show diff output on instead of applying changes')
     def clang_format(self, show=False):
+        import urllib2
+
         plat = platform.system()
         fmt = plat.lower() + "/clang-format-3.5"
         fmt_diff = "clang-format-diff-3.5"
 
         # We are currently using a modified version of clang-format hosted on people.mozilla.org.
         # This is a temporary work around until we upstream the necessary changes and we can use
         # a system version of clang-format. See bug 961541.
         if plat == "Windows":
--- a/tools/mercurial/mach_commands.py
+++ b/tools/mercurial/mach_commands.py
@@ -1,13 +1,13 @@
 # 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/.
 
-from __future__ import print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 
 import os
 import sys
 
 from mach.decorators import (
     CommandProvider,
     CommandArgument,
     Command,